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 -pedantic
opcją 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?
main()
jako "rozpoczęcie programu"static
czas 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.Źle czytasz zdanie.
Standard DEFINIUJE słowo „start” na potrzeby pozostałej części standardu. Nie mówi, że żaden kod nie jest wykonywany przed
main
wywoł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
main
zostanie 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.
źródło
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()
źródło
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:
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 ++.
źródło
Sekcja 3.6 jako całość jest bardzo jasna na temat interakcji
main
i dynamicznych inicjalizacji. „Wyznaczony początek programu” nie jest używany nigdzie indziej i służy jedynie do opisu ogólnego celumain()
. Nie ma sensu interpretowanie tego jednego wyrażenia w normatywny sposób, który jest sprzeczny z bardziej szczegółowymi i jasnymi wymogami normy.źródło
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:
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.
źródło
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ę.
źródło
main () to funkcja użytkownika wywoływana przez bibliotekę wykonawczą C.
zobacz także: Unikanie głównego (punktu wejścia) w programie C.
źródło
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.
źródło
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.
źródło
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.
źródło
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:
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_main
są już w_start
, co dodającgcc -Wl,--verbose
wiemy, jest punktem wejścia, ponieważ skrypt linkera zawiera: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
_start
i 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_main
zgodnie z oczekiwaniami.Niestety
-static
sprawia, żebt
odmain
nie pokazuje tak dużo informacji:#0 main () at hello.c:3 #1 0x0000000000402560 in __libc_start_main () #2 0x0000000000401c3e in _start ()
Jeśli usuniemy
-static
i zaczniemy odstarti
, 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_user
wydaje 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
_start
i 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
main
jest 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:
main
. To daje kilka pomysłów: co dzieje się przed main w C ++?crti.o
które można zobaczyć,gcc --verbose main.c
a które ostatecznie zostaną dodane do końcowego łączaźródło