Когда main определен без параметров, будут ли argc и argv все еще присутствовать в стеке? - PullRequest
21 голосов
/ 30 июня 2010

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

int main(void) {
    return 0;
}

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

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

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

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

  • Как проверить вышеуказанное? Ответ: См. Ответ Расчера.

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

Ответы [ 6 ]

20 голосов
/ 30 июня 2010

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

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

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

Мыукрал код возврата argc сверху и вставил его в ассемблер этих двух программ.Когда мы скомпилируем и запустим их, а затем вызовем echo $? (что отражает возвращаемое значение предыдущего процесса), мы получим «правильный» ответ.Поэтому, когда я запускаю «./test abcd», тогда $? дает мне «5» для обеих программ - хотя только в одной из них определен argc / argv.Это говорит мне о том, что на моей платформе argc наверняка помещен в стек.Могу поспорить, что аналогичный тест подтвердит это для argv.

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

10 голосов
/ 30 июня 2010

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

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

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

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

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

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

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

5 голосов
/ 30 июня 2010

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

void f() {}

f(5, 6);

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

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

main(argc, argv);

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

3 голосов
/ 30 июня 2010

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

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

2 голосов
/ 01 июля 2010

Есть несколько заметок, которые нужно сделать.

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

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

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

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

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

Этими соглашениями могут быть: аргументы помещаются в стек, а вызывающая сторона отвечает за очистку стека. Из-за этого в реальном 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, или 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).

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

  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 не принимает аргументов (что неверно в системе, о которой мы думаем)

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

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

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

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

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