Когда main определяется без параметров, argc и argv все еще присутствуют в стеке?

Рассмотрим очень просто:

int main(void) {
    return 0;
}

Я скомпилировал его (с mingw32-gcc) и выполнил его как main.exe foo bar.

Теперь я ожидал какого-то сбоя или ошибки, вызванной основной функцией, явно объявленной как лишенная параметров life. Отсутствие ошибок привело к этому вопросу, и это действительно четыре вопроса.

  • Почему это работает? Ответ: Поскольку стандарт говорит так!

  • Являются ли входные параметры просто проигнорированными или стек, подготовленный с помощью argc и argv молча? Ответ: В этом конкретном случае готов стек.

  • Как я могу проверить выше? Ответ: См. более простой ответ.

  • Является ли эта платформа зависимой? Ответ: Да и нет.

Ответ 1

Я не знаю кросс-платформенного ответа на ваш вопрос. Но мне стало любопытно. Так что же нам делать? Посмотрите на стек!

Для первой итерации:

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

Ничего интересного здесь. За исключением одного: обе программы C имеют один и тот же вывод сборки

Это в основном имеет смысл; нам никогда не нужно нажимать/удалять что-либо из стека для main(), так как это первое, что стоит в стеке вызовов.

Итак, я написал эту программу:

int main(int argc, char *argv[]) {
   return argc;
}

И его asm:

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

Мы украли код "return argc" сверху и ввели его в asm этих двух программ. Когда мы компилируем и запускаем их, а затем вызываем echo $? (которые возвращают возвращаемое значение предыдущего процесса), мы получаем "правильный" ответ. Поэтому, когда я запускаю "./test a b c d", тогда $? дает мне "5" для обеих программ, хотя только один из них имеет argc/argv. Это говорит мне, что на моей платформе argc наверняка помещается в стек. Я бы поспорил, что аналогичный тест подтвердит это для argv.

Попробуйте это в Windows!

Ответ 2

Из стандарта C99:

5.1.2.2.1 Запуск программы

Функция, вызванная при запуске программы, называется main. Реализация не объявляет прототип для этой функции. Он определяется с типом возврата int и без Параметры:

int main(void) { /* ... */ }

или с двумя параметрами (называемыми здесь argc и argv, хотя любые имена могут быть используются, поскольку они являются локальными для функции, в которой они объявлены):

int main (int argc, char * argv []) {/*... */}

или эквивалент; или каким-либо другим способом реализации.

Ответ 3

В классическом C вы можете сделать что-то подобное:

void f() {}

f(5, 6);

Нет ничего, что помешало бы вам вызвать функцию с другим числом параметров, как предполагает ее определение. (Современные компиляторы, естественно, считают это вопиющей ошибкой и сильно сопротивляются собственно компиляции кода.)

То же самое происходит с вашей функцией main(). Библиотека времени C вызовет

main(argc, argv);

но тот факт, что ваша функция не готова принять эти два аргумента, не имеет никакого отношения к вызывающему.

Ответ 4

В большинстве компиляторов __argc и __argv существуют как глобальные переменные из библиотеки времени выполнения. Значения будут правильными.

В окнах они не будут корректными, если точка входа имеет подпись UTF-16, что также является единственным способом получения правильных аргументов команды на этой платформе. В этом случае они будут пусты, но это не ваш случай, и есть две альтернативные переменные ширины.

Ответ 5

  • Почему это работает: Как правило, аргументы функции передаются в определенных местах (обычно регистры или стек). Функция без аргументов никогда не проверяет их, поэтому их содержимое не имеет значения. Это зависит от соглашений о вызовах и именованиях, но см. № 4.

  • Стек обычно будет подготовлен. На платформах, где argv анализируется библиотекой времени выполнения, например DOS, компилятор может выбрать не ссылаться в коде, если ничего не использует argv, но это сложность, которую многие считают нужным. На других платформах argv подготовлен exec() до того, как ваша программа будет загружена.

  • В зависимости от платформы, но в системах Linux, например, вы можете фактически проверить содержимое argv в /proc/PID/cmdline независимо от того, используются они или нет. Многие платформы также предоставляют отдельные вызовы для поиска аргументов.

  • В соответствии со стандартом, указанным Тимом Шеффером, основному не нужно принимать аргументы. На большинстве платформ сами аргументы будут существовать, но main() без аргументов никогда не узнает о них.

Ответ 6

Есть несколько заметок.

Стандарт в основном говорит, что, скорее всего, главное: функция, не принимающая никаких аргументов, или функция, принимающая два аргумента или что-то еще!

См. например, мой ответ на этот вопрос.

Но ваш вопрос указывает на другие факты.

Почему это работает? Ответ: Потому что стандарт говорит так!

Это неверно. Он работает по другим причинам. Он работает из-за вызывающих соглашений.

Этими конвенциями могут быть: аргументы помещаются в стек, а вызывающий отвечает за очистку стека. Из-за этого, в фактическом коде asm, вызываемый может полностью игнорировать то, что находится в стеке. Вызов выглядит как

   push value1
   push value2
   call function
   add esp, 8

(примеры intel, просто чтобы остаться в мейнстриме).

Что делает функция с аргументами, нажимаемыми на стек, совершенно неинтересно, все будет работать нормально! И это действительно так, даже если соглашение вызова отличается, например.

   li  $a0, value
   li  $a1, value
   jal function

Если функция учитывает регистры $a0 и $a1 или нет, ничего не меняет.

Таким образом, вызываемый может игнорировать аргументы без вреда, cn полагает, что они не существуют, или они могут знать, что они существуют, но предпочитают игнорировать их (напротив, было бы проблематично, если вызываемый получает значения из стека или регистров, в то время как вызывающий абонент ничего не пропускал).

Вот почему все работает.

С точки зрения C, если мы находимся в системе, где код запуска вызывает main с двумя аргументами (int и char **) и ожидает значение возврата int, "правильный" прототип будет

 int main(int argc, char **argv) { }

Но предположим теперь, что мы не используем эти аргументы.

Правильнее сказать int main(void) или int main() (все еще в той же системе, где реализация вызывает main с двумя аргументами и ожидает значение int, как было сказано ранее)?

Действительно, стандарт не говорит, что мы должны делать. Правильный "prototype", который говорит, что у нас есть два аргумента, все еще тот, что был показан ранее.

Но с логической точки зрения, правильный способ сказать, что есть аргументы (мы знаем это), но мы их не интересуемся, это

 int main() { /* ... */ }

В этот ответ Я показал, что происходит, если мы передаем аргументы функции, объявленной как int func(), и что произойдет, если мы передадим аргументы объявленной функции как int func(void).

Во втором случае мы имеем ошибку, так как (void) явно говорит, что функция не имеет аргументов.

С main мы не сможем получить ошибку, так как у нас нет реального прототипа для аргументов, но стоит отметить, что gcc -std=c99 -pedantic не дает никаких предупреждений для int main() или для int main(void), и это означают, что 1) gcc не совместим с C99 даже с флагом std, или 2) оба способа являются стандартными. Скорее всего, это вариант 2.

Один из них явно соответствует стандарту (int main(void)), другой действительно int main(int argc, char **argv), но без явного указания аргументов, поскольку мы их не интересуем.

int main(void) работает даже тогда, когда существуют аргументы, из-за того, что я написал ранее. Но в нем говорится, что главное не принимает аргументов. Хотя во многих случаях, если мы можем написать int main(int argc, char **argv), тогда это значение false, а int main() должно быть предпочтительным.

Еще одна интересная вещь: если мы говорим, что main не возвращает значение (void main()) в системе, где реализация ожидает возвращаемого значения, мы получаем предупреждение. Это связано с тем, что вызывающий абонент ожидает, что он что-то сделает с ним, так что это "поведение w90 > ", если мы не вернем значение (это не означает, что явное return в случае main, но объявив main как возвращающий int).

Во многих кодах запуска я видел, как main вызывается одним из следующих способов:

  retval = main(_argc, _argv);
  retval = main(_argc, _argv, environ);
  retval = main(_argc, _argv, environ, apple); // apple specific stuff

Но могут существовать коды запуска, которые называет основной по-разному, например. retval = main(); в этом случае, чтобы показать это, мы можем использовать int main(void), а с другой стороны, с помощью int main(int argc, char **argv) будет компилироваться, но сделать сбой программы, если мы фактически используем аргументы (так как извлеченные значения будут мусорными).

Является ли эта платформа зависимой?

Как называется основной, зависит от платформы (специфическая реализация), как это допускается стандартами. "Предполагаемый" главный прототип - это консенсус и, как уже было сказано, если мы знаем, что есть аргументы, но мы их не будем использовать, мы должны использовать int main() как короткую форму для более длинного int main(int argc, char **argv), тогда как int main(void) означает что-то другое: ie main не принимает никаких аргументов (что неверно в системе, о которой мы думаем)