Этот запутанный код C утверждает, что он работает без main (), но что он на самом деле делает?

84

Это косвенно вызывает main? как?

Раджив Сингх
источник
146
Макросы, определенные для расширения, начинают называть «основной». Это просто уловка. Ничего интересного.
rghome
10
В вашей инструментальной цепочке должна быть возможность оставить предварительно обработанный код в файле - фактическом скомпилированном файле - там, где вы увидите, что он действительно имеет main ()
@rghome Почему бы не опубликовать ответ? И это явно интересно, учитывая количество положительных голосов.
Matsemann
3
@Matsemann Вау! Я не заметил голосов за. Я мог бы изменить его на ответ, и если бы голоса за комментарии были ответом за голоса, это был бы мой лучший результат, но уже есть подробный ответ. Я думаю, что смысл моего комментария в том, что он не совсем интересен и поэтому действует как альтернатива для людей, не желающих голосовать за ответ. Спасибо, что указали на это.
rghome
Ребята, точку входа должен устанавливать компоновщик как инструмент операционной системы, а не сам язык. Вы даже можете установить нашу собственную точку входа, и вы можете сделать библиотеку, которая также будет исполняемой! unix.stackexchange.com/a/223415/37799
Ho1

Ответы:

194

Язык C определяет среду выполнения в двух категориях: автономная и размещенная . В обеих средах выполнения функция вызывается средой для запуска программы.
В автономной среде функция запуска программы может быть определена, а в размещенной среде она должна быть main. Ни одна программа на C не может работать без функции запуска программы в определенных средах.

В вашем случае mainскрыто определениями препроцессора. begin()будет расширяться до decode(a,n,i,m,a,t,e)которого в дальнейшем будет расширяться до main.


decode(s,t,u,m,p,e,d)это параметризованный макрос с 7 параметрами. Список замены для этого макроса есть m##s##u##t. m, s, uи tявляются 4- м , 1- м , 3- м и 2- м параметрами, используемыми в списке замены.

Остальные бесполезны ( просто чтобы запутать ). Переданный аргумент decode- " a , n , i , m , a, t, e", поэтому идентификаторы m, s, uи tзаменяются аргументами m, a, iи n, соответственно.

хаки
источник
11
@GrijeshChauhan: все компиляторы C обрабатывают макросы, это требуется всеми стандартами C, начиная с C89.
jdarthenay
17
Это совершенно неправильно. В Linux я могу использовать _start(). Или даже на более низком уровне, я могу попытаться просто выровнять начало моей программы с адресом, на который устанавливается IP после загрузки. main()является C Стандартной библиотеки . Сам C не накладывает на это ограничений.
ljrk
1
@haccks Стандартная библиотека действительно определяет точку входа.
Самому
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. Когда вы «называете» decodes = a, t = n, u = i, m = m, он эффективно заменяется beginна main.
ljrk 06
71

Попробуйте использовать gcc -E source.c, вывод заканчивается на:

Таким образом, main()функция фактически создается препроцессором.

Jdarthenay
источник
37

Рассматриваемая программа действительно вызывается main()из-за расширения макроса, но ваше предположение ошибочно - она вообще не должна вызывать main()!

Строго говоря, у вас может быть программа на C и у вас есть возможность компилировать ее без mainсимвола. main- это то, к чему c libraryобъект ожидает перейти после завершения собственной инициализации. Обычно вы переходите mainс символа libc, известного как _start. Всегда можно иметь очень правильную программу, которая просто выполняет сборку, не имея файла main. Взгляните на это:

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

Для получения дополнительной информации об этой конкретной проблеме посетите блог ksplice.

Еще одна интересная проблема заключается в том, что у вас также может быть программа, которая компилируется без mainсоответствия символа функции C. Например, у вас может быть следующая очень допустимая программа на C, которая заставит компилятор ныть только при переходе на уровень предупреждений.

Значения в массиве представляют собой байты, которые соответствуют инструкциям, необходимым для вывода Hello World на экран. Для более подробного описания того, как работает эта конкретная программа, взгляните на это сообщение в блоге , где я также впервые прочитал его.

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

NlightNFotis
источник
1
Является ли имя _startчастью определенного стандарта или это просто зависит от реализации? Конечно, ваш «основной массив как массив» зависит от архитектуры. Также важно, что ваш трюк с «main as an array» не будет лишним во время выполнения из-за ограничений безопасности (хотя это было бы более вероятно, если бы вы не использовали constквалификатор, и все же многие системы разрешили бы это).
mah
1
@mah: _startне входит в стандарт ELF, хотя AMD64 psABI содержит ссылку _startна 3.4 Инициализация процесса . Официально ELF знает только адрес e_entryв заголовке ELF, _startэто просто имя, выбранное реализацией.
ninjalj
1
@mah Также важно, что ваш трюк «main as an array» не будет лишним во время выполнения из-за ограничений безопасности (хотя это было бы более вероятно, если бы вы не использовали квалификатор const, и все же многие системы разрешили бы Это). Только если конечный исполняемый файл каким-либо образом можно отличить как нечто небезопасное - двоичный исполняемый файл является двоичным исполняемым файлом независимо от того, как он туда попал. И constне имеет значения ни один бит - имя символа в этом двоичном исполняемом файле main. Ни больше ни меньше. constэто конструкция C, которая ничего не значит во время выполнения.
Эндрю Хенле
1
@Stewart: это определенно не работает на ARMv6l (ошибка сегментации). Но он должен работать на любой архитектуре x86-64.
leftaround примерно
@AndrewHenle - двоичный исполняемый файл - это двоичный исполняемый файл, независимо от того, как он туда попал - не совсем так. Двоичный исполняемый файл - это не отдельный блок исполняемых инструкций, это тщательно сопоставленный блок разделов, некоторые из которых являются инструкциями, некоторые - данными только для чтения, а некоторые - данными, которые должны быть инициализированы в данные для чтения и записи. (Некоторые) аппаратные MMU безопасности могут предотвращать выполнение со страниц, не отмеченных как таковые, и это хорошая функция для предотвращения, например, переполнения стека, приводящего к выполнению кода в стеке, но, к сожалению, иногда это законно или часто не включается.
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

Что касается ##символа в раскрытии макроса, это оператор предварительной обработки, который выполняет вставку токена. Когда макрос раскрывается, два токена по обе стороны от каждого оператора '##' объединяются в один токен, который затем заменяет '##' и два исходных токена в раскрытии макроса.

Если вы мне не верите, вы можете скомпилировать свой код с помощью -Eflag. Он остановит процесс компиляции после предварительной обработки, и вы сможете увидеть результат вставки токена.

Abhiarora
источник
11

decode(a,b,c,d,[...])перемешивает первые четыре аргумента и объединяет их, чтобы получить новый идентификатор в указанном порядке dacb. (Остальные три аргумента игнорируются.) Например, decode(a,n,i,m,[...])дает идентификатор main. Обратите внимание, что это то, как beginопределен макрос.

Следовательно, beginмакрос просто определяется как main.

Frxstrem
источник
2

В вашем примере main()функция фактически присутствует, потому что beginэто макрос, который компилятор заменяет decodeмакросом, который, в свою очередь, заменяется выражением m ## s ## u ## t. Используя расширение макроса ##, вы дойдете до слова mainиз decode. Это след:

Это просто уловка main(), но использование имени main()для функции входа в программу не обязательно в языке программирования C. Это зависит от ваших операционных систем и компоновщика как одного из его инструментов.

В Windows вы не всегда используете main(), а скорее WinMainилиwWinMain , хотя вы можете использовать main(), даже с набором инструментов Microsoft . В Linux можно использовать _start.

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

Ho1
источник
@vaxquis Вы правы, но это частичный ответ, который я написал, чтобы дополнить / исправить первый ответ, который связывает main()функцию с языком программирования C, что неверно.
Ho1
@vaxquis Я предположил, что объяснение того, что «функция main () не важна в программах на C», будет частичным ответом. Я добавил абзац, чтобы ответ был полным. - Ho1 16 минут назад
Ho1