Цей затуманений код C стверджує, що працює без main (), але що він насправді робить?


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?? ");
}

Це побічно викликає main? як?


146
Визначені макроси розгортання починають говорити "основний". Це просто фокус. Нічого цікавого.
rghome

10
Ваш ланцюжок інструментів повинен мати можливість залишити попередньо оброблений код навколо у файлі - фактичний файл, який скомпільовано - де ви його побачите, справді має main ()

@rghome Чому б не опублікувати як відповідь? І це явно цікаво, враховуючи кількість голосів за.
Матсеманн

3
@Matsemann Вау! Я не помітив вищих голосів. Я міг би змінити це на відповідь, і якби коментар "за" - "голоси" відповідав "за", - це, безумовно, найкращий результат, але вже є детальна відповідь. Я думаю, сенс мого коментаря полягає в тому, що він насправді не цікавий, і тому він діє як альтернатива для людей, які не хочуть голосувати за відповідь. Дякуємо, що вказали на це.
rghome

Хлопці, лінкер визначає точку входу як інструмент операційної системи, а не саму мову. Ви навіть можете встановити нашу власну точку входу, і ви можете створити бібліотеку, яка також є виконуваною! unix.stackexchange.com/a/223415/37799
Ho1

Відповіді:


193

Мова C визначає середовище виконання у двох категоріях: автономна та розміщена . В обох середовищах виконання функція викликається середовищем для запуску програми.
У окремо стоячому середовищі функція запуску програми може бути визначена, тоді як у розміщеному середовищі вона повинна бути main. Жодна програма на C не може працювати без функції запуску програми у визначених середовищах.

У вашому випадку mainце приховано визначеннями препроцесора. begin()розшириться, до decode(a,n,i,m,a,t,e)якого далі буде розширено до 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)є параметризованим макросом із 7 параметрами. Список Заміни для цього макросу m##s##u##t. m, s, uі tє 4- м , 1- м , 3 і 2- м параметрами, що використовуються у списку заміщення.

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

Від відпочинку ніякої користі немає ( лише для затуманення ). Аргумент, переданий в decode" a , n , i , m , a, t, e", отже, ідентифікатори m, s, uі tзамінюються аргументами m, a, iі n, відповідно.

 m --> m  
 s --> a 
 u --> i 
 t --> n

11
@GrijeshChauhan усі компілятори C обробляють макроси, це вимагається всіма стандартами C, починаючи з C89.
jdarthenay

17
Це явно неправильно. У Linux я можу використовувати _start(). Або навіть більш низького рівня, я можу спробувати просто вирівняти старт моєї програми з адресою, на яку встановлюється IP після завантаження. main()є C Стандартної бібліотеки . Сама С не обмежує цього.
ljrk

1
@haccks Стандартна бібліотека визначає точку входу. Сама мова байдужа
ljrk

3
Ви можете пояснити, як decode(a,n,i,m,a,t,e)стати m##a##i##n? Чи замінює це символи? Чи можете ви надати посилання на документацію decodeфункції? Дякую.
AL

1
@AL Спочатку beginвизначається, що замінюється тим, decode(a,n,i,m,a,t,e)що визначено раніше. Ця функція приймає аргументи s,t,u,m,p,e,dта об'єднує їх у такій формі m##s##u##t( ##означає об'єднувати ). Тобто він ігнорує значення p, e та d. Коли ви "телефонуєте" decodeз s = a, t = n, u = i, m = m, він фактично замінює beginна main.
ljrk

71

Спробуйте використовувати gcc -E source.c, вихід закінчується на:

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

Отже, main()функція фактично генерується препроцесором.


37

Програма, про яку йде мова, дійсно викликає main()через розширення макросів, але ваше припущення помилкове - їй зовсім не потрібно телефонувати main()!

Власне кажучи, ви можете мати програму на С і мати змогу скомпілювати її, не маючи mainсимволу. main- це те, до чого c libraryочікує стрибок, після того, як він закінчить власну ініціалізацію. Зазвичай ви переходите mainз символу libc, відомого як _start. Завжди можна мати дуже дійсну програму, яка просто виконує збірку, не маючи основної. Погляньте на це:

/* 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 */
}

Складіть вищезазначене gcc -nostdlib without_main.cта перегляньте його друк Hello World!на екрані, просто зробивши системні виклики (переривання) у вбудованій збірці.

Для отримання додаткової інформації про цю конкретну проблему відвідайте блог ksplice

Іншим цікавим питанням є те, що ви також можете мати програму, яка компілюється, не маючи, щоб mainсимвол відповідав функції C Наприклад, ви можете мати наступне як дуже дійсну програму на С, що змушує компілятор скуголити лише тоді, коли ви піднімаєте рівень попереджень.

/* 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
};

Значення в масиві - це байти, які відповідають інструкціям, необхідним для друку Hello World на екрані. Щоб отримати докладніший звіт про те, як працює ця конкретна програма, погляньте на цю публікацію в блозі , де я її також прочитав спочатку.

Я хочу зробити останнє повідомлення про ці програми. Я не знаю, чи реєструються вони як дійсні програми C відповідно до специфікації мови C, але їх компіляція та запуск, безумовно, дуже можливий, навіть якщо вони порушують саму специфікацію.


1
Це назва _startчастини визначеного стандарту, чи це лише специфіка реалізації? Звичайно, ваш "основний як масив" є специфічним для архітектури. Також важливо, що не буде нерозумним, щоб трюк "основний як масив" зазнав збою під час виконання через обмеження безпеки (хоча це було б більш імовірно, якби ви не використовували constкваліфікатор, і все одно багато систем дозволяли б це).
mah

1
@mah: _startне входить до стандарту ELF, хоча AMD64 psABI містить посилання _startна 3.4 Ініціалізація процесу . Офіційно ELF знає лише адресу за адресою e_entryв заголовку ELF, _startце лише назва, яку вибрала реалізація.
ninjalj

1
@mah Також важливо, було б нерозумно, щоб трюк "основний як масив" провалився під час виконання через обмеження безпеки (хоча це було б більш імовірно, якби ви не використовували кваліфікатор const, і все одно багато систем дозволяли б це). Тільки якщо остаточний виконуваний файл якимось чином можна виділити як щось небезпечне - двійковий виконуваний файл є двійковим виконуваним файлом незалежно від того, як він там потрапив. І constце не матиме значення - ім’я символу в цьому двійковому виконуваному файлі main. Ні більше, ні менше. constє конструкцією C, яка нічого не означає під час виконання.
Andrew Henle,

1
@Stewart: це, безумовно, не вдається на ARMv6l (помилка сегментації). Але це повинно працювати на будь-якій архітектурі x86-64.
залишилося близько

@AndrewHenle двійковий виконуваний файл - це двійковий виконуваний файл, незалежно від того, як він там потрапив - не зовсім так. Бінарний виконуваний файл - це не одна крапка виконуваних інструкцій, це ретельно зіставлена ​​BLOB-частина розділів, частина з яких є інструкціями, частина з яких є даними лише для читання, а частина з них є даними, які потрібно ініціалізувати у дані для читання та запису. (Деякі) апаратні модулі безпеки можуть запобігати виконанню зі сторінок, не позначених як такі, і це хороша функція для запобігання, наприклад, переповненню стека, що призводить до виконання коду в стеку, але, на жаль, це іноді є законним або часто не ввімкненим.
mah

30

Хтось намагається поводитися як Чарівник. Він думає, що може нас обдурити. Але ми всі знаємо, c виконання програми починається з main().

int begin()Будуть замінені decode(a,n,i,m,a,t,e)на один прохід стадії препроцесора. Потім знову decode(a,n,i,m,a,t,e)буде замінено на m ## a ## i ## n. Як і при позиційній асоціації макровиклику, sволя має значення символу a. Аналогічно, uбуде замінено на "i" і tбуде замінено на "n". І, ось як, m##s##u##tстанеmain

Що стосується ##символу в розширенні макросів, він є оператором попередньої обробки і виконує вставку маркера. Коли макрос розгортається, два маркери по обидва боки кожного оператора '##' об'єднуються в один маркер, який потім замінює '##' та два оригінальні маркери в розширенні макроса.

Якщо ви не вірите мені, ви можете скомпілювати свій код із -Eпрапором. Це зупинить процес компіляції після попередньої обробки, і ви побачите результат вставки маркера.

gcc -E FILENAME.c

11

decode(a,b,c,d,[...])перетасовує перші чотири аргументи та об’єднує їх, щоб отримати новий ідентифікатор у порядку dacb. (Решта три аргументи ігноруються.) Наприклад, decode(a,n,i,m,[...])дає ідентифікатор main. Зверніть увагу, що саме beginце визначається як макрос.

Тому beginмакрос просто визначається як main.


2

У вашому прикладі main()функція насправді присутня, оскільки beginє макросом, який компілятор замінює decodeмакросом, який, у свою чергу, замінений виразом m ## s ## u ## t. Використовуючи розширення макросів ##, ви перейдете до слова mainз decode. Це слід:

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

Це просто хитрість main(), але використання назви main()функції введення програми не є обов’язковим для мови програмування C. Це залежить від вашої операційної системи та компонувальника як одного з її інструментів.

У Windows ви не завжди використовуєте main(), а, скоріше, WinMainабоwWinMain , хоча ви можете використовувати main(), навіть за допомогою інструментальної мережі Microsoft . У Linux можна користуватися _start.

Точка входу залежить від лінкера як інструменту операційної системи, а не від самої мови. Ви навіть можете встановити нашу власну точку входу, і ви можете створити бібліотеку, яка також є виконуваною !


@vaxquis Ви маєте рацію, але це часткова відповідь, яку я написав на комплімент / виправлення першої відповіді, яка пов'язує main()функцію з мовою програмування С, що є неправильним.
Хо1

@vaxquis Я припустив, що пояснення "main () функції не є важливим у програмах на С" буде частковою відповіддю. Я додав абзац, щоб відповідь була повною. - Ho1 16 хвилин тому
Ho1
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.