Некоторые особенности языка C начинались как хитрости, которые просто так сработали.
Одной из таких функций является наличие нескольких подписей для основных списков аргументов и списков аргументов переменной длины.
Программисты заметили, что они могут передавать функции дополнительные аргументы, и с их компилятором ничего плохого не происходит.
Это так, если соглашения о вызовах таковы, что:
- Вызывающая функция очищает аргументы.
- Крайние левые аргументы находятся ближе к вершине стека или к основанию кадра стека, так что ложные аргументы не делают адресацию недействительной.
Один набор соглашений о вызовах, который подчиняется этим правилам, - это передача параметров на основе стека, при которой вызывающий объект выталкивает аргументы, и они перемещаются справа налево:
;; pseudo-assembly-language
;; main(argc, argv, envp); call
push envp ;; rightmost argument
push argv ;;
push argc ;; leftmost argument ends up on top of stack
call main
pop ;; caller cleans up
pop
pop
В компиляторах, где используется этот тип соглашения о вызовах, ничего особенного не нужно делать для поддержки двух типов mainили даже дополнительных типов. mainможет быть функцией без аргументов, и в этом случае она не обращает внимания на элементы, которые были помещены в стек. Если это функция двух аргументов, то он находит argcи argvкак два самых верхних элемента стека. Если это зависящий от платформы вариант с тремя аргументами и указателем среды (обычное расширение), это тоже будет работать: он найдет этот третий аргумент как третий элемент сверху стека.
Таким образом, фиксированный вызов работает во всех случаях, позволяя связать один фиксированный модуль запуска с программой. Этот модуль можно было бы написать на C как функцию, похожую на эту:
/* I'm adding envp to show that even a popular platform-specific variant
can be handled. */
extern int main(int argc, char **argv, char **envp);
void __start(void)
{
/* This is the real startup function for the executable.
It performs a bunch of library initialization. */
/* ... */
/* And then: */
exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}
Другими словами, этот стартовый модуль всегда вызывает основную функцию с тремя аргументами. Если main не принимает аргументов или принимает только аргументы, int, char **он работает нормально, а также, если он не принимает аргументов, из-за соглашений о вызовах.
Если бы вам пришлось делать такие вещи в своей программе, это было бы непереносимым и считалось бы неопределенным поведением ISO C: объявление и вызов функции одним способом и определение ее другим. Но трюк с запуском компилятора не обязательно должен быть переносимым; он не руководствуется правилами переносимых программ.
Но предположим, что соглашения о вызовах таковы, что это не может работать таким образом. В таком случае компилятор должен относиться к нему mainспециально. Когда он замечает, что компилирует mainфункцию, он может сгенерировать код, совместимый, скажем, с вызовом с тремя аргументами.
То есть вы пишете это:
int main(void)
{
/* ... */
}
Но когда компилятор видит это, он по существу выполняет преобразование кода, так что функция, которую он компилирует, выглядит примерно так:
int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
/* ... */
}
за исключением того, что имена __argc_ignoreбуквально не существуют. Такие имена не вводятся в вашу область видимости, и не будет никаких предупреждений о неиспользуемых аргументах. Преобразование кода заставляет компилятор генерировать код с правильной связью, который знает, что он должен очистить три аргумента.
Другая стратегия реализации заключается в том, что компилятор или, возможно, компоновщик может самостоятельно сгенерировать __startфункцию (или как бы она ни называлась) или, по крайней мере, выбрать одну из нескольких предварительно скомпилированных альтернатив. В объектном файле может храниться информация о том, какая из поддерживаемых форм mainиспользуется. Компоновщик может просмотреть эту информацию и выбрать правильную версию модуля запуска, который содержит вызов main, совместимый с определением программы. Реализации C обычно имеют лишь небольшое количество поддерживаемых форм, mainпоэтому такой подход осуществим.
Компиляторы для языка C99 всегда должны обрабатывать mainдо некоторой степени специально, чтобы поддержать хак, что если функция завершается без returnоператора, поведение будет таким, как если бы она return 0была выполнена. Это, опять же, можно лечить преобразованием кода. Компилятор замечает, что вызываемая функция mainкомпилируется. Затем он проверяет, доступен ли конец тела. Если это так, он вставляетreturn 0;
mainметод в одной программеC(или, на самом деле, практически на любом языке с такой конструкцией).