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

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

int main(void) {
    return 0;
}

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

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

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

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

  • Как я проверяю вышеупомянутое? Ответ: См. ответ rascher.

  • Этот иждивенец платформы? Ответ: Да, и нет.

21
задан manneorama 30 June 2010 в 06:31
поделиться

6 ответов

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

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

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!

20
ответ дан 29 November 2019 в 20:31
поделиться

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

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

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

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

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

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

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

10
ответ дан 29 November 2019 в 20:31
поделиться

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

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

3
ответ дан 29 November 2019 в 20:31
поделиться

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

void f() {}

f(5, 6);

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

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

main(argc, argv);

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

5
ответ дан 29 November 2019 в 20:31
поделиться

Есть кое-что, что нужно сделать.

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

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

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

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

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

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

   push value1
   push value2
   call function
   add esp, 8

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

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

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

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

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

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

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

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

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

Правильнее сказать int main (void) или int 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) работает, даже когда существуют аргументы, из-за того, что я писал раньше. Но в нем говорится, что main не аргументирует. Хотя во многих случаях, если мы можем написать 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 не принимает аргументов (это неверно в системе, о которой мы думаем )

3
ответ дан 29 November 2019 в 20:31
поделиться
  1. Почему это работает: Обычно аргументы функции передаются в определенных местах (обычно регистры или стек). Функция без аргументов никогда не будет их проверять, поэтому их содержимое не имеет значения. Это зависит от соглашений о вызовах и именах, но см. №4.

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

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

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

1
ответ дан 29 November 2019 в 20:31
поделиться
Другие вопросы по тегам:

Похожие вопросы: