Ten zaciemniony kod C twierdzi, że działa bez funkcji main (), ale co tak naprawdę robi?

84
#include <stdio.h>
#define decode(s,t,u,m,p,e,d) m##s##u##t
#define begin decode(a,n,i,m,a,t,e)

int begin()
{
    printf("Ha HA see how it is?? ");
}

Czy to pośrednio dzwoni main? w jaki sposób?

Rajeev Singh
źródło
146
Zdefiniowane makra rozwinięte zaczynają się nazywać „główne”. To tylko sztuczka. Nic interesującego.
rghome
10
Twój łańcuch narzędzi powinien mieć opcję pozostawienia wstępnie przetworzonego kodu w pliku - faktycznym pliku, który jest kompilowany - w miejscu, w którym go zobaczysz, rzeczywiście ma main ()
@rghome Dlaczego nie opublikować odpowiedzi? I to jest oczywiście interesujące, biorąc pod uwagę liczbę głosów za.
Matsemann
3
@Matsemann Wow! Nie zauważyłem głosów pozytywnych. Mógłbym to zmienić na odpowiedź, a jeśli głos za komentarz byłby głosem za odpowiedź, byłby to zdecydowanie mój najlepszy wynik, ale jest już szczegółowa odpowiedź. Myślę, że mój komentarz polega na tym, że nie jest on naprawdę interesujący i dlatego stanowi alternatywę dla osób, które nie chcą głosować za odpowiedzią. Dziękuję za wskazanie tego.
rghome
Chłopaki, to konsolidator jako narzędzie systemu operacyjnego ustawia punkt wejścia, a nie sam język. Możesz nawet ustawić własny punkt wejścia i możesz stworzyć bibliotekę, która jest również wykonywalna! unix.stackexchange.com/a/223415/37799
Ho1

Odpowiedzi:

193

Język C definiuje środowisko wykonawcze w dwóch kategoriach: wolnostojące i hostowane . W obu środowiskach wykonawczych środowisko wywołuje funkcję w celu uruchomienia programu.
W środowisku wolnostojącym funkcję uruchamiania programu można zdefiniować za pomocą implementacji, podczas gdy w środowisku hostowanym tak powinno być main. Żaden program w języku C nie może działać bez funkcji uruchamiania programu w określonych środowiskach.

W twoim przypadku mainjest ukryty przez definicje preprocesora. begin()rozszerzy się, do decode(a,n,i,m,a,t,e)którego dalej zostanie rozszerzony main.

int begin() -> int decode(a,n,i,m,a,t,e)() -> int m##a##i##n() -> int main() 

decode(s,t,u,m,p,e,d)to sparametryzowane makro z 7 parametrami. Lista zastępcza tego makra to m##s##u##t. m, s, ui tsą 4 p , 1 st , 3 rd i 2 ND parametr stosowany na liście zastępczej.

s, t, u, m, p, e, d
1  2  3  4  5  6  7

Reszta na nic się nie przyda ( tylko do zaciemniania ). Przekazany argument decodeto „ a , n , i , m , a, t, e”, więc identyfikatory m, s, ui tsą zastępowane odpowiednio argumentami m, a, ii n.

 m --> m  
 s --> a 
 u --> i 
 t --> n
haccks
źródło
11
@GrijeshChauhan wszystkie kompilatory C przetwarzają makra, jest to wymagane przez wszystkie standardy C od C89.
jdarthenay,
17
To jest po prostu błędne. W Linuksie mogę użyć _start(). Lub nawet na niższym poziomie mogę spróbować po prostu wyrównać początek mojego programu z adresem, na który ustawiany jest adres IP po uruchomieniu. main()jest biblioteką C Standard . Sam C nie nakłada na to ograniczeń.
ljrk
1
@haccks Biblioteka standardowa definiuje punkt wejścia. Sam język nie dba o to
ljrk
3
Czy możesz wyjaśnić, jak decode(a,n,i,m,a,t,e)się stało m##a##i##n? Czy zastępuje znaki? Czy możesz podać link do dokumentacji decodefunkcji? Dzięki.
AL
1
@AL First beginjest zdefiniowany do zastąpienia, przez decode(a,n,i,m,a,t,e)który zdefiniowano wcześniej. Ta funkcja pobiera argumenty s,t,u,m,p,e,di łączy je w tej formie m##s##u##t( ##oznacza konkatenację). Tzn. Ignoruje wartości p, e i d. A ty "połączenia" decodez y = A, T = N, U = l, m = M skutecznie zastępuje beginsię main.
ljrk
71

Spróbuj użyć gcc -E source.c, wyjście kończy się na:

int main()
{
    printf("Ha HA see how it is?? ");
}

Zatem main()funkcja jest faktycznie generowana przez preprocesor.

jdarthenay
źródło
37

Program, o którym mowa , wywołuje z main()powodu rozszerzenia makr, ale twoje założenie jest błędne - wcale nie musi wywoływać main()!

Ściśle mówiąc, możesz mieć program w C i być w stanie skompilować go bez mainsymbolu. mainjest czymś, do czego c libraryoczekuje skok po zakończeniu własnej inicjalizacji. Zwykle skaczesz mainz symbolu libc znanego jako _start. Zawsze można mieć bardzo poprawny program, który po prostu wykonuje asemblację, bez posiadania pliku main. Spójrz na to:

/* This must be compiled with the flag -nostdlib because otherwise the
 * linker will complain about multiple definitions of the symbol _start
 * (one here and one in glibc) and a missing reference to symbol main
 * (that the libc expects to be linked against).
 */

void
_start ()
{
    /* calling the write system call, with the arguments in this order:
     * 1. the stdout file descriptor
     * 2. the buffer we want to print (Here it's just a string literal).
     * 3. the amount of bytes we want to write.
     */
    asm ("int $0x80"::"a"(4), "b"(1), "c"("Hello world!\n"), "d"(13));
    asm ("int $0x80"::"a"(1), "b"(0)); /* calling exit syscall, with the argument to be 0 */
}

Skompiluj powyższe z gcc -nostdlib without_main.ci zobacz, jak wyświetla się Hello World!na ekranie, po prostu wywołując wywołania systemowe (przerwania) w asemblerze wbudowanym.

Aby uzyskać więcej informacji na temat tego konkretnego problemu, odwiedź blog ksplice

Inną interesującą kwestią jest to, że możesz również mieć program, który kompiluje się bez mainodpowiadania symbolowi funkcji C. Na przykład możesz mieć następujący program jako bardzo poprawny program w C, który spowoduje, że kompilator będzie jęczeć tylko po podniesieniu poziomu Ostrzeżenia.

/* These values are extracted from the decimal representation of the instructions
 * of a hello world program written in asm, that gdb provides.
 */
const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

Wartości w tablicy to bajty, które odpowiadają instrukcjom potrzebnym do wydrukowania Hello World na ekranie. Aby uzyskać bardziej szczegółowe informacje o tym, jak działa ten konkretny program, spójrz na ten wpis na blogu , w którym również go przeczytałem.

Chcę jeszcze raz zwrócić uwagę na te programy. Nie wiem, czy rejestrują się jako prawidłowe programy w C zgodnie ze specyfikacją języka C, ale ich kompilacja i uruchamianie jest z pewnością bardzo możliwe, nawet jeśli naruszają samą specyfikację.

NlightNFotis
źródło
1
Czy nazwa jest _startczęścią zdefiniowanego standardu, czy jest to tylko specyficzna dla implementacji? Z pewnością twój "main jako tablica" jest specyficzny dla architektury. Co więcej, nie byłoby nierozsądne, gdyby sztuczka „główna jako tablica” zawodziła w czasie wykonywania z powodu ograniczeń bezpieczeństwa (chociaż byłoby to bardziej prawdopodobne, gdybyś nie użył constkwalifikatora, a wiele systemów by na to pozwalało).
mah
1
@mah: _startnie jest w standardzie ELF, że 64-bitową psABI zawiera odniesienie do _startco 3,4 proces inicjalizacji . Oficjalnie ELF wie tylko o adresie e_entryw nagłówku ELF, _startto tylko nazwa wybrana przez implementację.
ninjalj
1
@mah Również ważne, nie byłoby nierozsądne, gdyby twoja sztuczka "główna jako tablica" zawiodła w czasie wykonywania z powodu ograniczeń bezpieczeństwa (chociaż byłoby to bardziej prawdopodobne, gdybyś nie użył kwalifikatora const, a wiele systemów pozwoliłoby to). Tylko jeśli końcowy plik wykonywalny jest w jakiś sposób rozpoznawalny jako coś niezabezpieczonego - binarny plik wykonywalny jest binarnym plikiem wykonywalnym bez względu na to, jak się tam dostał. I constto nie ma znaczenia - nazwa symbolu w tym binarnym pliku wykonywalnym to main. Nie więcej nie mniej. constjest konstrukcją C, która nic nie znaczy w czasie wykonywania.
Andrew Henle
1
@Stewart: z pewnością nie działa na ARMv6l (błąd segmentacji). Ale powinno działać na każdej architekturze x86-64.
leftaroundokoło
@AndrewHenle binarny plik wykonywalny jest binarnym plikiem wykonywalnym bez względu na to, jak się tam dostał - nie do końca prawda. Binarny plik wykonywalny nie jest pojedynczym blobem instrukcji wykonywalnych, jest to starannie zmapowany blob partycji, z których niektóre są instrukcjami, z których niektóre są danymi tylko do odczytu, a niektóre z nich są danymi do zainicjowania w dane do odczytu i zapisu. (Niektóre) sprzętowe MMU zabezpieczające mogą uniemożliwić wykonanie ze stron niezaznaczonych jako takie, i jest to dobra funkcja, która zapobiega na przykład przepełnieniom stosu prowadzącym do wykonania kodu na stosie, ale niestety czasami jest to legalne lub często nie jest włączone.
mah
30

Ktoś próbuje zachowywać się jak Mag. Myśli, że może nas oszukać. Ale wszyscy wiemy, że wykonanie programu c zaczyna się od main().

int begin()Zostaną zastąpione decode(a,n,i,m,a,t,e)jednym przejściu etapu preprocesora. Z drugiej strony decode(a,n,i,m,a,t,e)zostanie zastąpiony przez m ## a ## i ## n. Podobnie jak w przypadku pozycyjnego skojarzenia wywołania makra, swill ma wartość znaku a. Podobnie uzostanie zastąpione przez „i” i tzastąpione przez „n”. I tak m##s##u##tsię staniemain

Jeśli chodzi o ##symbol w rozwijaniu makra, jest to operator przetwarzania wstępnego i wykonuje wklejanie tokenu. Kiedy makro jest rozwinięte, dwa tokeny po obu stronach każdego operatora „##” są łączone w jeden token, który następnie zastępuje „##” i dwa oryginalne tokeny w rozwinięciu makra.

Jeśli mi nie wierzysz, możesz skompilować swój kod z -Eflagą. Zatrzyma proces kompilacji po wstępnym przetwarzaniu i możesz zobaczyć wynik wklejania tokenu.

gcc -E FILENAME.c
abhiarora
źródło
11

decode(a,b,c,d,[...])tasuje pierwsze cztery argumenty i łączy je w celu uzyskania nowego identyfikatora w określonej kolejności dacb. (Pozostałe trzy argumenty są ignorowane.) Na przykład decode(a,n,i,m,[...])podaje identyfikator main. Zwróć uwagę, że właśnie begintak zdefiniowano makro.

Dlatego beginmakro jest po prostu zdefiniowane jako main.

Frxstrem
źródło
2

W twoim przykładzie main()funkcja jest faktycznie obecna, ponieważ beginjest makrem, które kompilator zastępuje decodemakrem, które z kolei zastępuje wyrażeniem m ## s ## u ## t. Używając rozszerzenia makro ##, dotrzesz do słowa mainz decode. To jest ślad:

begin --> decode(a,n,i,m,a,t,e) --> m##parameter1##parameter3##parameter2 ---> main

To tylko sztuczka main(), ale używanie nazwy main()funkcji wejścia programu nie jest konieczne w języku programowania C. Zależy to od systemów operacyjnych i konsolidatora jako jednego z jego narzędzi.

W systemie Windows nie zawsze używasz main(), ale raczej WinMainlubwWinMain , chociaż możesz main(), nawet z łańcuchem narzędzi Microsoft . W Linuksie można użyć _start.

To zależy od konsolidatora jako narzędzia systemu operacyjnego, aby ustawić punkt wejścia, a nie sam język. Możesz nawet ustawić własny punkt wejścia i możesz stworzyć bibliotekę, która jest również wykonywalna !

Ho1
źródło
@vaxquis Masz rację, ale jest to częściowa odpowiedź, którą napisałem, aby skomplementować / poprawić pierwszą odpowiedź, która wiąże main()funkcję z językiem programowania C, co nie jest poprawne.
Ho1
@vaxquis Założyłem, że wyjaśnienie „funkcji main () nie jest niezbędne w programach C” byłoby częściową odpowiedzią. Dodałem akapit, aby uzupełnić odpowiedź. - Ho1 16 minut temu
Ho1