Как работает передача аргументов? - PullRequest
10 голосов
/ 09 декабря 2010

Я хочу знать, как работает передача аргументов в функции в Си. Где хранятся значения и как они извлекаются? Как работает вариационная передача аргументов? Кроме того, поскольку это связано: как насчет возвращаемых значений?

У меня есть базовые представления о регистрах процессора и ассемблере, но недостаточно, чтобы я полностью понял ASM, который GCC выплевывает мне. Несколько простых аннотированных примеров будут высоко оценены.

Ответы [ 5 ]

18 голосов
/ 09 декабря 2010

Учитывая этот код:

int foo (int a, int b) {
  return a + b;
}

int main (void) {
  foo(3, 5);
  return 0;
}

Компиляция с gcc foo.c -S дает вывод сборки:

foo:
    pushl   %ebp
    movl    %esp, %ebp
    movl    12(%ebp), %eax
    movl    8(%ebp), %edx
    leal    (%edx,%eax), %eax
    popl    %ebp
    ret

main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    movl    $5, 4(%esp)
    movl    $3, (%esp)
    call    foo
    movl    $0, %eax
    leave
    ret

Таким образом, по сути, вызывающий объект (в данном случае main) сначала выделяет 8 байтов в стеке для размещения двух аргументов, затем помещает два аргумента в стек с соответствующими смещениями (4 и 0) и затем выдается инструкция call, которая передает управление в процедуру foo. Подпрограмма foo считывает свои аргументы из соответствующих смещений в стеке, восстанавливает ее и помещает возвращаемое значение в регистр eax, чтобы он был доступен вызывающей стороне.

5 голосов
/ 09 декабря 2010

Это зависит от платформы и является частью "ABI". На самом деле, некоторые компиляторы даже позволяют выбирать между различными соглашениями.

Например, Microsoft Visual Studio предлагает соглашение о вызовах __fastcall, в котором используются регистры. Другие платформы или соглашения о вызовах используют стек исключительно.

Вариативные аргументы работают очень похоже - они передаются через регистры или стек. В случае регистров они обычно располагаются в порядке возрастания, в зависимости от типа. Если у вас есть что-то вроде (int a, int b, float c, int d), ABI PowerPC может поместить a в r3, b в r4, d в r5 и c в fp1 (I забыл, где начинаются плавающие регистры, но вы поняли).

Возвращаемые значения, опять же, работают так же.

К сожалению, у меня не так много примеров, большая часть моей сборки находится в PowerPC, и все, что вы видите в сборке, это код, идущий прямо к r3, r4, r5, и помещающий возвращаемое значение также в r3.

3 голосов
/ 09 декабря 2010

Ваши вопросы - это больше, чем кто-либо может обоснованно попытаться ответить в SO сообщении, не говоря уже о том, что его реализация также определена.

Однако, если вы заинтересованы в ответе x86, могу предложитьПосмотрите эту лекцию Stanford CS107 под названием Парадигмы программирования , где все ответы на поставленные вами вопросы будут подробно объяснены (и весьма красноречиво) в первых 6–8 лекциях.

2 голосов
/ 06 октября 2018

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

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

Более свежие процессоры (например,как ARM или PowerPC), как правило, их соглашения о вызовах определяются поставщиком ЦП и совместимы между различными операционными системами.Исключением является x86: разные системы используют разные соглашения о вызовах.Раньше было гораздо больше соглашений о вызовах для 16-битного 8086 и 32-битного 80386, чем для x86_64 (хотя даже это не одно).32-битные x86-программы Windows иногда используют несколько соглашений о вызовах в одной и той же программе.

Некоторые наблюдения:

  • Пример операционной системы, которая поддерживает несколько различных ABI с различными соглашениями о вызовах одновременнонекоторые из которых следуют тем же соглашениям, что и другие ОС для той же архитектуры, - это Linux для x86_64.Он может содержать три различных основных интерфейса пользователя (i386, x32 и x86_64), два из которых совпадают с другими операционными системами для одного и того же ЦП, и несколько вариантов.
  • Исключение из правила, согласно которому одна система вызываетДля всего используется 16- и 32-разрядные версии MS Windows, которые унаследовали некоторые из распространенных соглашений о вызовах от MS-DOS.Windows C API использует другое соглашение о вызовах (STDCALL, первоначально FAR PASCAL), чем соглашение о вызовах «C» для той же платформы, а также поддерживает соглашения FORTRAN и FASTCALL.Все четыре выпускаются в вариантах NEAR и FAR в 16-битных ОС.Поэтому почти все программы Windows используют по крайней мере два различных соглашения в одной и той же программе.
  • Архитектуры с большим количеством регистров, включая классический RISC и почти все современные ISA, используют несколько из этих регистров для передачи и возврата аргументов функции.
  • Архитектуры с небольшим количеством регистров общего назначения или без них часто передают аргументы в стеке, на которые указывает указатель стека.Архитектуры CISC часто имеют инструкции для вызова и возврата, которые хранят адрес возврата в стеке.(Архитектуры RISC обычно хранят адрес возврата в «регистре связи», который вызываемый может сохранить / восстановить вручную, если это не конечная функция.)
  • Распространенный вариант - для оконечных вызовов, функций, чье возвращаемое значение равнотакже возвращаемое значение вызывающей стороны, чтобы перейти к следующей функции (чтобы она возвращалась к нашей родительской функции) вместо того, чтобы вызывать ее и затем возвращать после ее возврата.Размещение аргументов в нужных местах должно учитывать адрес возврата, уже находящийся в стеке, куда его поместит инструкция вызова.Это особенно верно для хвостовых рекурсивных вызовов, которые имеют одинаковый кадр стека при каждом вызове.Хвосто-рекурсивный вызов обычно эквивалентен циклу: обновите несколько регистров, которые изменились, затем вернитесь к точке входа.Им не нужно создавать новый фрейм стека или иметь свой собственный обратный адрес: вы можете просто обновить фрейм стека вызывающей стороны и использовать его адрес возврата в качестве хвостового вызова.то есть хвостовая рекурсия легко оптимизируется в цикл.
  • Некоторые архитектуры с несколькими регистрами, тем не менее, определяют альтернативное соглашение о вызовах, которое может передавать один или два аргумента в регистрах.Это было FASTCALL в MS-DOS и Windows.
  • Несколько старых ISA, таких как SPARC, имели специальный банк «оконных» регистров, так что у каждой функции был свой банк входных и выходных регистров.и когда он сделал вызов функции, выходные данные вызывающего абонента стали входными данными вызываемого абонента, и наоборот, когда пришло время возвращать значение.Современные суперскалярные конструкции считают, что это больше проблем, чем стоит.
  • Несколько очень старыхАрхитектуры использовали самоизменяющийся код в своих соглашениях о вызовах, и первое издание Искусство компьютерного программирования следовало этой модели для своего абстрактного языка.Он больше не работает на большинстве современных процессоров, которые имеют кэш инструкций.
  • Несколько других очень старых архитектур не имели стека и, как правило, не могли снова вызывать ту же функцию, вводя ее снова, пока она не вернулась.
  • Функция с большим количеством аргументов почти всегда помещает большинство из них в стек.
  • Функции C, которые помещают аргументы в стек, почти вынуждены толкать их в обратном порядке и заставлять вызывающую функцию очищатьстек.Вызываемая функция может даже не знать точно, сколько аргументов в стеке!То есть, если вы вызовете printf("%d\n", x);, компилятор поместит x, затем строку форматирования и адрес возврата в стек.Это гарантирует, что первый аргумент находится в известном смещении от указателя стека, а <varargs.h> содержит информацию, необходимую для его работы.
  • Большинство других языков и, следовательно, некоторые операционные системы, которые поддерживают компиляторы C, делают этонаоборот: аргументы выдвигаются слева направо.Вызываемая функция обычно очищает свой собственный стек стека.Раньше это называлось соглашением PASCAL в MS-DOS и сохранилось как соглашение STDCALL в Windows.Он не может поддерживать функции с переменным числом аргументов.(https://en.wikibooks.org/wiki/X86_Disassembly/Calling_Conventions)
  • Fortran и некоторые другие языки исторически передавали все аргументы по ссылке, что переводится в C как аргументы указателя. Компиляторы, которые могут нуждаться в взаимодействии с этими другими языками, часто поддерживают эти соглашения о внешних вызовах.
  • Поскольку основным источником ошибок было «разрушение стека», многие компиляторы теперь имеют способ добавлять канареечные значения (которые, как канарейка в угольной шахте, предупреждают вас, что происходит что-то опасное, если что-то случается сих) и другие средства обнаружения, когда код вмешивается в фрейм стека.
  • Другая форма вариации на разных платформах заключается в том, будет ли фрейм стека содержать всю информацию, необходимую для отладки или обработчика исключений для обратного отслеживания,или эта информация будет в отдельных метаданных (или вообще не будет присутствовать), что позволит упростить пролог / эпилог функции (-fomit-frame-pointer).

Вы можете получить кросс-компиляторы для выдачи кода с использованием различных вызововусловности и сравнитьem, с такими переключателями, как -S -target (на clang).

0 голосов
/ 09 декабря 2010

По сути, C передает аргументы, помещая их в стек.Для типов указателей указатель помещается в стек.

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

Возвращаемые значения возвращаются в регистре AX или их вариациях.

...