Когда main определен без параметров, будут ли argc и argv все еще присутствовать в стеке?
Рассмотрим очень простое:
int main(void) {
return 0;
}
Я скомпилировал его (с помощью mingw32-gcc) и выполнил как main.exe foo bar
,
Теперь я ожидал какой-то сбой или ошибку, вызванную основной функцией, явно объявленной как лишенная жизненных параметров. Отсутствие ошибок привело к этому вопросу, а это действительно четыре вопроса.
Почему это работает? Ответ: потому что стандарт так говорит!
Входные параметры просто игнорируются или стек готовится с помощью argc & argv без вывода сообщений? Ответ: в данном конкретном случае стек подготовлен.
Как я могу проверить выше? Ответ: см. Ответ rascher.
Эта платформа зависит? Ответ: да, и нет.
7 ответов
Я не знаю кроссплатформенного ответа на ваш вопрос. Но это сделало меня любопытным. Так что же нам делать? Посмотри на стопку!
Для первой итерации:
test.c
int main(void) {
return 0;
}
test2.c
int main(int argc, char *argv[]) {
return 0;
}
А теперь посмотрим на вывод сборки:
$ gcc -S -o test.s test.c
$ cat test.s
.file "test.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
movl $0, %eax
popl %ebp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits
Здесь нет ничего захватывающего. За исключением одного: обе программы на С имеют одинаковый вывод на ассемблере!
Это в основном имеет смысл; нам никогда не нужно выталкивать / выталкивать что-либо из стека для main(), так как это первое, что есть в стеке вызовов.
Итак, я написал эту программу:
int main(int argc, char *argv[]) {
return argc;
}
И его асм:
main:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
popl %ebp
ret
Это говорит нам о том, что "argc" находится в 8(%ebp)
Итак, теперь для еще двух программ на C:
int main(int argc, char *argv[]) {
__asm__("movl 8(%ebp), %eax\n\t"
"popl %ebp\n\t"
"ret");
/*return argc;*/
}
int main(void) {
__asm__("movl 8(%ebp), %eax\n\t"
"popl %ebp\n\t"
"ret");
/*return argc;*/
}
Мы украли код возврата argc сверху и вставили его в ассемблер этих двух программ. Когда мы скомпилируем и запустим их, а затем вызовем echo $?
(что повторяет возвращаемое значение предыдущего процесса) мы получаем "правильный" ответ. Поэтому, когда я запускаю "./test abcd", тогда $?
дает мне "5" для обеих программ - хотя только в одной определен argc / argv. Это говорит мне о том, что на моей платформе argc наверняка помещен в стек. Могу поспорить, что аналогичный тест подтвердит это для argv.
Попробуйте это на Windows!
Из стандарта C99:
5.1.2.2.1 Запуск программы
Функция, вызываемая при запуске программы, называется основной. Реализация не объявляет прототип для этой функции. Он должен быть определен с типом возврата int и без параметров:
int main(void) { /* ... */ }
или с двумя параметрами (именуемыми здесь как argc и argv, хотя могут использоваться любые имена, так как они являются локальными для функции, в которой они объявлены):
int main (int argc, char * argv []) {/ *... * /}
или эквивалент; или каким-либо другим способом, определяемым реализацией.
В классическом C вы можете сделать что-то похожее:
void f() {}
f(5, 6);
Ничто не мешает вам вызывать функцию с другим количеством параметров, как предполагает ее определение. (Современные компиляторы, естественно, считают это вопиющей ошибкой и будут сильно сопротивляться фактической компиляции кода.)
То же самое происходит с вашим main()
функция. Библиотека времени выполнения C вызовет
main(argc, argv);
но тот факт, что ваша функция не готова принять эти два аргумента, не имеет значения для вызывающей стороны.
В большинстве компиляторов __argc и __argv существуют как глобальные переменные из библиотеки времени выполнения. Значения будут правильными.
В Windows они не будут корректными, если точка входа имеет подпись UTF-16, что также является единственным способом получения правильных аргументов команды на этой платформе. В этом случае они будут пустыми, но это не ваш случай, и есть две альтернативные переменные widechar.
Есть несколько заметок, чтобы сделать.
Стандарт в основном говорит, что, скорее всего, main: функция, не имеющая аргументов, или функция, принимающая два аргумента, или что-то еще!
Смотрите, например, мой ответ на этот вопрос.
Но ваш вопрос указывает на другие факты.
Почему это работает? Ответ: потому что стандарт так говорит!
Это не правильно. Это работает по другим причинам. Это работает из-за соглашений о вызовах.
Это соглашение может быть следующим: аргументы помещаются в стек, а вызывающая сторона отвечает за очистку стека. Из-за этого в реальном asm-коде вызываемый может полностью игнорировать то, что находится в стеке. Звонок выглядит как
push value1
push value2
call function
add esp, 8
(примеры Intel, просто чтобы оставаться в мейнстриме).
То, что функция делает с аргументами, помещенными в стек, совершенно неинтересно, все будет работать нормально! И это действительно так, даже если соглашение о вызовах отличается, например,
li $a0, value
li $a1, value
jal function
Если функция учитывает регистры $a0 и $a1 или нет, ничего не меняется.
Таким образом, вызываемый объект может игнорировать без аргументов вреда, может полагать, что они не существуют, или он может знать, что они существуют, но предпочитает игнорировать их (наоборот, было бы проблематично, если вызываемый объект получает значения из стека или регистров, в то время как вызывающий объект ничего не прошло).
Вот почему все работает.
С точки зрения C, если мы находимся в системе, где код запуска вызывает main с двумя аргументами (int и char **) и ожидаем возвращаемое значение int, "правильный" прототип будет
int main(int argc, char **argv) { }
Но давайте теперь предположим, что мы не используем эти аргументы.
Правильнее сказать int main(void)
или же int main()
(все еще в той же системе, где реализация вызывает main с двумя аргументами и ожидает возвращаемое значение типа int, как сказано выше)?
Действительно стандарт не говорит, что мы должны делать. Правильный "прототип", который говорит, что у нас есть два аргумента, все еще показан выше.
Но с логической точки зрения правильный способ сказать, что есть аргументы (мы их знаем), но мы не заинтересованы в них,
int main() { /* ... */ }
В этом ответе я показал, что происходит, если мы передаем аргументы функции, объявленной как int func()
и что произойдет, если мы передадим аргументы функции, объявленной как int func(void)
,
Во втором случае мы имеем ошибку, так как (void)
явно говорит, что функция не имеет аргументов.
С main
мы не можем получить ошибку, так как у нас нет реального прототипа, обязывающего аргументы, но стоит отметить, что gcc -std=c99 -pedantic
не дает предупреждения для int main()
ни для int main(void)
, и это будет означать, что 1) gcc не соответствует C99 даже с std
flag, или 2) оба способа соответствуют стандарту. Скорее это вариант 2.
Один явно соответствует стандарту (int main(void)
) другой действительно int main(int argc, char **argv)
, но без явного высказывания аргументов, так как мы не заинтересованы в них.
int main(void)
работает, даже если существуют аргументы, из-за того, что я написал раньше. Но говорится, что главное не требует аргументов. Хотя во многих случаях, если мы можем написать int main(int argc, char **argv)
тогда это ложь, и int main()
должен быть предпочтительным вместо этого.
Еще одна интересная вещь, которую стоит отметить, это то, что если мы говорим, что main не возвращает значение (void main()
) в системе, где реализация ожидает возвращаемое значение, мы получаем предупреждение. Это связано с тем, что вызывающий объект ожидает, что он что-то с ним сделает, так что это "неопределенное поведение", если мы не возвращаем значение (что не означает помещения явного return
в main
случай, но декларирующий main
как возвращение int).
Во многих кодах запуска, которые я видел, main вызывается одним из следующих способов:
retval = main(_argc, _argv);
retval = main(_argc, _argv, environ);
retval = main(_argc, _argv, environ, apple); // apple specific stuff
Но могут существовать коды запуска, вызывающие main по-разному, например, retval = main()
; в этом случае, чтобы показать это, мы можем использовать int main(void)
и с другой стороны, используя int main(int argc, char **argv)
будет компилироваться, но вывести программу из строя, если мы на самом деле будем использовать аргументы (так как полученные значения будут мусором).
Эта платформа зависит?
То, как вызывается main, зависит от платформы (зависит от реализации), как это разрешено стандартами. "Предполагаемый" основной прототип является последовательностью, и, как уже было сказано, если мы знаем, что переданы аргументы, но мы не будем их использовать, мы должны использовать int main()
в краткой форме для дольше int main(int argc, char **argv)
, в то время как int main(void)
означает что-то другое: то есть main не принимает аргументов (это неверно в системе, о которой мы думаем)
Полностью согласен с ответом Грега Хьюджилла, но я не могу не добавить к нему несколько слов. Я думаю, это тоже можно объяснить вызовом конвенции _cdecl. из-за _cdecl вызывающая сторона несет ответственность за очистку стека, даже когда вызывается основное использование
main(argc, argv);
, вызываемый (вы) определяете основную функцию, например
int main(){...}
или же
int main(int argc, char *argv[]){...}
, это не имеет значения, вызывающий отправляет два аргумента, а затем выдвигает их сам (
add esp, $value
в коде assmeble), просто в первом случае вы теряете параметры: argc и argv. кстати, для глубокого понимания ответа Грега Хьюгилла вам могут потребоваться некоторые знания о компоновщике и загрузчике.
Почему это работает: Как правило, аргументы функции передаются в определенных местах (обычно регистры или стек). Функция без аргументов никогда не проверит их, поэтому их содержимое не имеет значения. Это зависит от соглашений о вызовах и именах, но см. #4.
Стек, как правило, будет подготовлен. На платформах, где argv анализируется библиотекой времени выполнения, например, DOS, компилятор может не ссылаться в коде, если ничего не использует argv, но это сложность, которую некоторые считают необходимой. На других платформах argv подготавливается exec() еще до загрузки вашей программы.
В зависимости от платформы, но в системах Linux, например, вы можете проверить содержимое argv в /proc/PID/cmdline, независимо от того, используются они или нет. Многие платформы также предоставляют отдельные вызовы для поиска аргументов.
Согласно стандарту, цитируемому Тимом Шеффером, main не нужно принимать аргументы. На большинстве платформ сами аргументы все еще будут существовать, но main() без аргументов никогда не узнает о них.