Czy main () naprawdę jest początkiem programu w C ++?

131

Sekcja 3.6.1 / 1 ze standardu C ++ zawiera:

Program powinien zawierać funkcję globalną zwaną main , która jest wyznaczonym początkiem programu.

Rozważmy teraz ten kod,

int square(int i) { return i*i; }
int user_main()
{ 
    for ( int i = 0 ; i < 10 ; ++i )
           std::cout << square(i) << endl;
    return 0;
}
int main_ret= user_main();
int main() 
{
        return main_ret;
}

Ten przykładowy kod robi to, co zamierzam, tj. Wypisuje kwadrat liczb całkowitych od 0 do 9, przed wejściem do main()funkcji, która ma być "startem" programu.

Skompilowałem go również z -pedanticopcją GCC 4.5.0. Nie daje żadnego błędu, ani nawet ostrzeżenia!

Więc moje pytanie brzmi:

Czy ten kod jest rzeczywiście zgodny ze standardem?

Jeśli jest zgodny ze standardem, czy nie unieważnia tego, co mówi norma? main()nie jest początkiem tego programu! user_main()wykonane przed main().

Rozumiem, że aby zainicjować zmienną globalną main_ret, use_main()wykonuje się ją najpierw, ale to zupełnie inna sprawa; Chodzi o to, że to nie unieważnia cytowany oświadczenie $ 3.6.1 / 1 z normą, a main()nie jest to początek programu; to jest w rzeczywistości koniec z tego programu!


EDYTOWAĆ:

Jak definiujesz słowo „start”?

Sprowadza się do zdefiniowania frazy „start programu” . Jak więc dokładnie to zdefiniujesz?

Nawaz
źródło

Odpowiedzi:

85

Nie, C ++ robi wiele rzeczy, aby „ustawić środowisko” przed wywołaniem main; jednakże main jest oficjalnym początkiem „określonej przez użytkownika” części programu C ++.

Niektórych ustawień środowiska nie można kontrolować (np. Początkowego kodu konfigurującego std :: cout; jednak część środowiska można kontrolować, jak statyczne bloki globalne (do inicjowania statycznych zmiennych globalnych). Pamiętaj, że ponieważ nie masz pełnego control przed main, nie masz pełnej kontroli nad kolejnością inicjalizacji bloków statycznych.

Po main, twój kod jest koncepcyjnie „w pełni kontrolowany” nad programem, w tym sensie, że możesz zarówno określić instrukcje, które mają być wykonane, jak i kolejność ich wykonywania. Wielowątkowość może zmienić kolejność wykonywania kodu; ale nadal masz kontrolę w C ++, ponieważ określiłeś, że sekcje kodu mają być wykonywane (prawdopodobnie) poza kolejnością.

Edwin Buck
źródło
9
+1 za to "Zauważ, że ponieważ nie masz pełnej kontroli przed main, nie masz pełnej kontroli nad kolejnością, w jakiej statyczne bloki są inicjowane. Po main, twój kod jest koncepcyjnie" w pełni kontrolowany "nad program w tym sensie, że można określić zarówno instrukcje do wykonania, jak i kolejność ich wykonywania ” . To również sprawia, że ​​oznaczam tę odpowiedź jako zaakceptowaną ... Myślę, że są to bardzo ważne punkty, które wystarczająco uzasadniają main()jako "rozpoczęcie programu"
Nawaz
13
@Nawaz: pamiętaj, że oprócz braku pełnej kontroli nad kolejnością inicjalizacji, nie masz kontroli nad błędami inicjalizacji: nie możesz przechwytywać wyjątków w zakresie globalnym.
André Caron,
@Nawaz: Co to są statyczne globalne bloki? czy możesz to wyjaśnić na prostym przykładzie? Dzięki
Destructor
@meet: Obiekty zadeklarowane na poziomie przestrzeni nazw mają staticczas przechowywania i jako takie, te obiekty należące do różnych jednostek tłumaczeniowych mogą być inicjowane w dowolnej kolejności (ponieważ kolejność nie jest określona przez standard). Nie jestem pewien, czy to odpowiada na twoje pytanie, chociaż tak mógłbym powiedzieć w kontekście tego tematu.
Nawaz
89

Źle czytasz zdanie.

Program powinien zawierać funkcję globalną zwaną main, która jest wyznaczonym początkiem programu.

Standard DEFINIUJE słowo „start” na potrzeby pozostałej części standardu. Nie mówi, że żaden kod nie jest wykonywany przed mainwywołaniem. Mówi, że początek programu jest uważany za funkcję main.

Twój program jest zgodny. Twój program nie został "uruchomiony" przed uruchomieniem main. Konstruktor jest wywoływany przed „uruchomieniem” programu zgodnie z definicją „start” w standardzie, ale nie ma to większego znaczenia. Dużo kodu jest wykonywany zanim mainzostanie kiedykolwiek nazywany w każdym programie, a nie tylko tego przykładem.

Dla celów dyskusji kod konstruktora jest wykonywany przed „uruchomieniem” programu i jest w pełni zgodny ze standardem.

Adam Davis
źródło
3
Przepraszamy, ale nie zgadzam się z twoją interpretacją tej klauzuli.
Wyścigi lekkości na orbicie
Myślę, że Adam Davis ma rację, „main” bardziej przypomina jakieś ograniczenia w kodowaniu.
laike9m
@LightnessRacesinOrbit Nigdy nie sprawdziłem, ale dla mnie to zdanie można logicznie sprowadzić do „funkcji globalnej o nazwie main jest wyznaczonym początkiem programu” (podkreślenie dodane). Jaka jest twoja interpretacja tego zdania?
Adam Davis,
1
@AdamDavis: Nie pamiętam, o co mi chodziło. Nie mogę teraz o żadnym wymyślić.
Wyścigi lekkości na orbicie,
Wygląda to na znacznie lepsze wyjaśnienie niż przyjęta odpowiedź.
BartoszKP
24

Twój program nie będzie się łączył i nie będzie działał, chyba że istnieje plik main. Jednak main () nie powoduje rozpoczęcia wykonywania programu, ponieważ obiekty na poziomie pliku mają konstruktory, które działają wcześniej i byłoby możliwe napisanie całego programu, który działa przez swój czas życia przed osiągnięciem funkcji main () i pozwoliłby samemu main puste ciało.

W rzeczywistości, aby to wymusić, musiałbyś mieć jeden obiekt, który jest zbudowany przed mainem i jego konstruktorem, aby wywołać cały przepływ programu.

Spójrz na to:

class Foo
{
public:
   Foo();

 // other stuff
};

Foo foo;

int main()
{
}

Przepływ twojego programu faktycznie wynikałby z Foo::Foo()

Dojną krową
źródło
13
+1. Ale pamiętaj, że jeśli masz wiele obiektów globalnych w różnych jednostkach tłumaczeniowych, szybko wpadniesz w kłopoty, ponieważ kolejność wywoływania konstruktorów jest niezdefiniowana. Możesz uciec od singletonów i leniwej inicjalizacji, ale w środowisku wielowątkowym rzeczy szybko stają się bardzo brzydkie. Jednym słowem, nie rób tego w prawdziwym kodzie.
Alexandre C.
3
Chociaż prawdopodobnie powinieneś nadać main () odpowiednią treść w swoim kodzie i pozwolić mu na uruchomienie wykonywania, koncepcja obiektów poza tym startem jest tym, na czym opiera się wiele bibliotek LD_PRELOAD.
CashCow
2
@Alex: Standard mówi, że nieokreślony, ale ze względów praktycznych kolejność łączy (zwykle w zależności od kompilatora) kontroluje kolejność inicjowania.
ThomasMcLeod
1
@Thomas: Na pewno nawet nie próbowałbym na tym polegać. Z pewnością nie próbowałbym też ręcznie kontrolować systemu kompilacji.
Alexandre C.
1
@Alex: nie jest to już takie ważne, ale kiedyś używaliśmy kolejności łączy do kontrolowania obrazu kompilacji, aby zmniejszyć fizyczne stronicowanie pamięci. Istnieją inne poboczne powody, dla których warto kontrolować kolejność inicjalizacji, nawet jeśli nie ma to wpływu na semantykę programu, na przykład testowanie porównania wydajności uruchamiania.
ThomasMcLeod
15

Również oznaczyłeś pytanie jako „C”, a więc mówiąc ściśle o C, inicjalizacja powinna się nie powieść, zgodnie z sekcją 6.7.8 „Inicjalizacja” normy ISO C99.

Najbardziej istotne w tym przypadku wydaje się ograniczenie nr 4, które mówi:

Wszystkie wyrażenia w inicjatorze obiektu, który ma statyczny czas trwania, powinny być wyrażeniami stałymi lub literałami łańcuchowymi.

Tak więc odpowiedź na Twoje pytanie brzmi, że kod nie jest zgodny ze standardem C.

Prawdopodobnie chciałbyś usunąć znacznik "C", gdybyś był zainteresowany tylko standardem C ++.

Remo.D
źródło
4
@ Remo.D czy możesz nam powiedzieć, co jest w tej sekcji. Nie każdy z nas ma standard C :).
UmmaGumma
2
Ponieważ jesteś taki wybredny: niestety, ANSI C jest przestarzała od 1989 r. ISO C90 lub C99 to odpowiednie normy, które należy przytoczyć.
Lundin
@Lundin: Nikt nigdy nie jest wystarczająco wybredny :) Czytałem ISO C99, ale jestem przekonany, że dotyczy to również C90.
Remo.D
@Strzał. Masz rację, dodałem zdanie, które moim zdaniem jest tutaj najbardziej odpowiednie.
Remo.D
3
@Remo: +1 za dostarczenie informacji, że jest nieprawidłowy C; nie wiedziałem tego. Zobacz, jak ludzie się uczą, czasami według planu, czasem przez przypadek!
Nawaz
10

Sekcja 3.6 jako całość jest bardzo jasna na temat interakcji maini dynamicznych inicjalizacji. „Wyznaczony początek programu” nie jest używany nigdzie indziej i służy jedynie do opisu ogólnego celu main(). Nie ma sensu interpretowanie tego jednego wyrażenia w normatywny sposób, który jest sprzeczny z bardziej szczegółowymi i jasnymi wymogami normy.

aschepler
źródło
9

Kompilator często musi dodać kod przed main (), aby był zgodny ze standardem. Ponieważ standard określa, że ​​inicjalizacja zmiennych globalnych / statycznych musi być wykonana przed wykonaniem programu. Jak wspomniano, to samo dotyczy konstruktorów obiektów umieszczonych w zasięgu pliku (globale).

W ten sposób oryginalne pytanie jest istotne dla C, jak również, ponieważ w programie C będzie nadal masz globalnych / statyczne inicjalizacji do zrobienia, zanim można uruchomić program.

Standardy zakładają, że zmienne te są inicjalizowane przez „magię”, ponieważ nie mówią, jak powinny być ustawione przed inicjalizacją programu. Myślę, że uważali to za coś poza zakresem standardu języka programowania.

Edycja: patrz na przykład ISO 9899: 1999 5.1.2:

Wszystkie obiekty ze statycznym czasem przechowywania powinny zostać zainicjalizowane (ustawione na wartości początkowe) przed uruchomieniem programu. Sposób i czas takiej inicjalizacji nie są poza tym określone.

Teoria stojąca za tym, jak ta „magia” miała zostać wykonana, sięga czasów narodzin C, kiedy był to język programowania przeznaczony do użytku tylko w systemie operacyjnym UNIX na komputerach z pamięcią RAM. Teoretycznie program byłby w stanie załadować wszystkie wstępnie zainicjowane dane z pliku wykonywalnego do pamięci RAM, w tym samym czasie, gdy sam program byłby ładowany do pamięci RAM.

Od tego czasu komputery i system operacyjny ewoluowały, a język C jest używany na znacznie szerszym obszarze, niż pierwotnie przewidywano. Nowoczesny system operacyjny PC ma adresy wirtualne itp., A wszystkie systemy wbudowane wykonują kod z pamięci ROM, a nie pamięci RAM. Jest więc wiele sytuacji, w których pamięci RAM nie można ustawić „automagicznie”.

Ponadto standard jest zbyt abstrakcyjny, aby cokolwiek wiedzieć o stosach, pamięci procesowej itp. Te rzeczy również należy zrobić przed uruchomieniem programu.

Dlatego prawie każdy program w C / C ++ ma jakiś kod inicjujący / „kopiujący”, który jest wykonywany przed wywołaniem main, aby zachować zgodność z regułami inicjalizacji standardów.

Na przykład systemy wbudowane mają zazwyczaj opcję o nazwie „uruchamianie niezgodne z ISO”, w której cała faza inicjalizacji jest pomijana ze względu na wydajność, a następnie kod faktycznie uruchamia się bezpośrednio z pliku main. Ale takie systemy nie są zgodne ze standardami, ponieważ nie można polegać na wartościach inicjalizacyjnych zmiennych globalnych / statycznych.

Lundin
źródło
4

Twój „program” po prostu zwraca wartość ze zmiennej globalnej. Cała reszta to kod inicjujący. W związku z tym obowiązuje standard - po prostu masz bardzo trywialny program i bardziej złożoną inicjalizację.

Zac Howland
źródło
2

Wydaje się, że spór o angielską semantykę. OP odnosi się do swojego bloku kodu najpierw jako „kod”, a później jako „program”. Użytkownik pisze kod, a następnie kompilator zapisuje program.

dSerk
źródło
1

main jest wywoływany po zainicjowaniu wszystkich zmiennych globalnych.

To, czego standard nie określa, to kolejność inicjalizacji wszystkich zmiennych globalnych wszystkich modułów i bibliotek połączonych statycznie.

vz0
źródło
0

Tak, main to „punkt wejścia” każdego programu w C ++, z wyjątkiem rozszerzeń specyficznych dla implementacji. Mimo to pewne rzeczy dzieją się przed main, zwłaszcza inicjalizacją globalną, taką jak dla main_ret.

Fred Nurk
źródło
0

Ubuntu 20.04 glibc 2.31 RTFS + GDB

glibc przeprowadza pewną konfigurację przed main, aby niektóre jego funkcje działały. Spróbujmy znaleźć kod źródłowy tego.

cześć, c

#include <stdio.h>

int main() {
    puts("hello");
    return 0;
}

Kompiluj i debuguj:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out

Teraz w GDB:

b main
r
bt -past-main

daje:

#0  main () at hello.c:3
#1  0x00007ffff7dc60b3 in __libc_start_main (main=0x555555555149 <main()>, argc=1, argv=0x7fffffffbfb8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffbfa8) at ../csu/libc-start.c:308
#2  0x000055555555508e in _start ()

Zawiera już linię wywołującego main: https://github.com/cirosantilli/glibc/blob/glibc-2.31/csu/libc-start.c#L308.

Funkcja ma miliard ifdef, jak można się spodziewać z poziomu starszego / ogólności glibc, ale niektóre kluczowe elementy, które wydają się dla nas działać, należy uprościć do:

# define LIBC_START_MAIN __libc_start_main

STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char **),
         int argc, char **argv,
{

      /* Initialize some stuff. */

      result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
  exit (result);
}

Wcześniej __libc_start_mainsą już w _start, co dodając gcc -Wl,--verbosewiemy, jest punktem wejścia, ponieważ skrypt linkera zawiera:

ENTRY(_start)

i dlatego jest faktycznie pierwszą instrukcją wykonywaną po zakończeniu dynamicznego ładowania.

Aby potwierdzić to w GDB, pozbywamy się dynamicznego modułu ładującego, kompilując z -static:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out

a następnie zatrzymaj GDB na pierwszej instrukcji wykonanej za pomocąstarti i wydrukuj pierwsze instrukcje :

starti
display/12i $pc

co daje:

=> 0x401c10 <_start>:   endbr64 
   0x401c14 <_start+4>: xor    %ebp,%ebp
   0x401c16 <_start+6>: mov    %rdx,%r9
   0x401c19 <_start+9>: pop    %rsi
   0x401c1a <_start+10>:        mov    %rsp,%rdx
   0x401c1d <_start+13>:        and    $0xfffffffffffffff0,%rsp
   0x401c21 <_start+17>:        push   %rax
   0x401c22 <_start+18>:        push   %rsp
   0x401c23 <_start+19>:        mov    $0x402dd0,%r8
   0x401c2a <_start+26>:        mov    $0x402d30,%rcx
   0x401c31 <_start+33>:        mov    $0x401d35,%rdi
   0x401c38 <_start+40>:        addr32 callq 0x4020d0 <__libc_start_main>

Po przeszukaniu źródła _starti skupieniu się na trafieniach x86_64 widzimy, że wydaje się to odpowiadać sysdeps/x86_64/start.S:58:


ENTRY (_start)
    /* Clearing frame pointer is insufficient, use CFI.  */
    cfi_undefined (rip)
    /* Clear the frame pointer.  The ABI suggests this be done, to mark
       the outermost frame obviously.  */
    xorl %ebp, %ebp

    /* Extract the arguments as encoded on the stack and set up
       the arguments for __libc_start_main (int (*main) (int, char **, char **),
           int argc, char *argv,
           void (*init) (void), void (*fini) (void),
           void (*rtld_fini) (void), void *stack_end).
       The arguments are passed via registers and on the stack:
    main:       %rdi
    argc:       %rsi
    argv:       %rdx
    init:       %rcx
    fini:       %r8
    rtld_fini:  %r9
    stack_end:  stack.  */

    mov %RDX_LP, %R9_LP /* Address of the shared library termination
                   function.  */
#ifdef __ILP32__
    mov (%rsp), %esi    /* Simulate popping 4-byte argument count.  */
    add $4, %esp
#else
    popq %rsi       /* Pop the argument count.  */
#endif
    /* argv starts just at the current stack top.  */
    mov %RSP_LP, %RDX_LP
    /* Align the stack to a 16 byte boundary to follow the ABI.  */
    and  $~15, %RSP_LP

    /* Push garbage because we push 8 more bytes.  */
    pushq %rax

    /* Provide the highest stack address to the user code (for stacks
       which grow downwards).  */
    pushq %rsp

#ifdef PIC
    /* Pass address of our own entry points to .fini and .init.  */
    mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP
    mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP

    mov main@GOTPCREL(%rip), %RDI_LP
#else
    /* Pass address of our own entry points to .fini and .init.  */
    mov $__libc_csu_fini, %R8_LP
    mov $__libc_csu_init, %RCX_LP

    mov $main, %RDI_LP
#endif

    /* Call the user's main function, and exit with its value.
       But let the libc call main.  Since __libc_start_main in
       libc.so is called very early, lazy binding isn't relevant
       here.  Use indirect branch via GOT to avoid extra branch
       to PLT slot.  In case of static executable, ld in binutils
       2.26 or above can convert indirect branch into direct
       branch.  */
    call *__libc_start_main@GOTPCREL(%rip)

co kończy się dzwonieniem __libc_start_mainzgodnie z oczekiwaniami.

Niestety -staticsprawia, że btod mainnie pokazuje tak dużo informacji:

#0  main () at hello.c:3
#1  0x0000000000402560 in __libc_start_main ()
#2  0x0000000000401c3e in _start ()

Jeśli usuniemy -statici zaczniemy od starti, otrzymamy zamiast tego:

=> 0x7ffff7fd0100 <_start>:     mov    %rsp,%rdi
   0x7ffff7fd0103 <_start+3>:   callq  0x7ffff7fd0df0 <_dl_start>
   0x7ffff7fd0108 <_dl_start_user>:     mov    %rax,%r12
   0x7ffff7fd010b <_dl_start_user+3>:   mov    0x2c4e7(%rip),%eax        # 0x7ffff7ffc5f8 <_dl_skip_args>
   0x7ffff7fd0111 <_dl_start_user+9>:   pop    %rdx

Przez grepowanie źródła _dl_start_userwydaje się pochodzić z sysdeps / x86_64 / dl-machine.h: L147

/* Initial entry point code for the dynamic linker.
   The C function `_dl_start' is the real entry point;
   its return value is the user program's entry point.  */
#define RTLD_START asm ("\n\
.text\n\
    .align 16\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
    movq %rsp, %rdi\n\
    call _dl_start\n\
_dl_start_user:\n\
    # Save the user entry point address in %r12.\n\
    movq %rax, %r12\n\
    # See if we were run as a command with the executable file\n\
    # name as an extra leading argument.\n\
    movl _dl_skip_args(%rip), %eax\n\
    # Pop the original argument count.\n\
    popq %rdx\n\

i przypuszczalnie jest to punkt wejścia dynamicznego modułu ładującego.

Jeśli przerwiemy _starti będziemy kontynuować, wydaje się, że kończy się to w tym samym miejscu, w którym używaliśmy -static, który następnie wywołuje __libc_start_main.

Kiedy zamiast tego spróbuję programu C ++:

hello.cpp

#include <iostream>

int main() {
    std::cout << "hello" << std::endl;
}

z:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o hello.out hello.cpp

wyniki są w zasadzie takie same, np. ślad w miejscu mainjest dokładnie taki sam.

Myślę, że kompilator C ++ po prostu wywołuje przechwyty, aby osiągnąć jakąkolwiek specyficzną funkcjonalność C ++, a rzeczy są dość dobrze uwzględnione w C / C ++.

DO ZROBIENIA:

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
źródło