Как реализованы переменные аргументы в gcc?

int max(int n, ...)

Я использую соглашение о вызовах cdecl, когда вызывающий объект очищает переменную после возврата вызываемого объекта.

Мне интересно узнать, как работают макросы va_end, va_start и va_arg?

Передает ли вызывающая сторона адрес массива аргументов в качестве второго аргумента функции max?


person Bruce    schedule 11.09.2012    source источник
comment
В этой записи блога amd64 и va_arg содержится отличное обсуждение того, как va_arg набор функций для переменных аргументов может различаться в зависимости от архитектуры машины и соглашений о вызовах, ABI, которые используются с конкретными процессорами. Современные процессоры с большим количеством доступных регистров, чем в старой архитектуре x86, позволяют передавать аргументы как в регистрах, так и в стеке.   -  person Richard Chambers    schedule 15.03.2018
comment
Связанный: stackoverflow.com/questions/5272703/   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 30.08.2019
comment
К сожалению, все ответы на этот вопрос в большей или меньшей степени неверны. (Они смутно точны для ABI, определенных давным-давно, но соглашение о вызовах намного сложнее для любого ABI, определенного за последние 20 с лишним лет, и включает в себя размещение данных в регистрах, а также в стеке. Да, даже для процессоров CISC. .) Я бы написал ответ получше, но это заняло бы у меня весь день, а сегодня мне нужно выполнить свою настоящую работу :-P   -  person zwol    schedule 23.07.2020


Ответы (4)


Если вы посмотрите на то, как язык C хранит параметры в стеке, вам станет ясно, как работают макросы:

Higher memory address    Last parameter
                         Penultimate parameter
                         ....
                         Second parameter
Lower memory address     First parameter
       StackPointer  ->  Return address

(обратите внимание, в зависимости от аппаратного обеспечения указатель стека может быть на одну строку ниже, а выше и ниже могут быть заменены местами)

Аргументы всегда хранятся так1, даже без параметра типа ....

Макрос va_start просто устанавливает указатель на первый параметр функции, например:

 void func (int a, ...)
 { 
   // va_start
   char *p = (char *) &a + sizeof a;
 }

что заставляет p указывать на второй параметр. Макрос va_arg делает это:

 void func (int a, ...)
 { 
   // va_start
   char *p = (char *) &a + sizeof a;

   // va_arg
   int i1 = *((int *)p);
   p += sizeof (int);

   // va_arg
   int i2 = *((int *)p);
   p += sizeof (int);

   // va_arg
   long i2 = *((long *)p);
   p += sizeof (long);
 }

Макрос va_end просто устанавливает значение p в NULL.

ПРИМЕЧАНИЯ:

  1. Оптимизирующие компиляторы и некоторые процессоры RISC хранят параметры в регистрах, а не используют стек. Наличие параметра ... отключило бы эту возможность и компилятору пришлось бы использовать стек.
person Skizz    schedule 11.09.2012
comment
Это действительно зависит от платформы, так как многие соглашения о вызовах (включая распространенные x64, PPC, ARM) передают большинство своих параметров в регистры. Многие платформы не помещают адрес возврата в стек, на одной или двух платформах стеки растут вверх, а не вниз, а некоторые соглашения о вызовах помещают аргументы в стек в обратном порядке. - person Dietrich Epp; 11.09.2012
comment
@DietrichEpp: я знаю. Но, надеюсь, он получит некоторые основы. Я сделал несколько заметок в ответе, чтобы отразить различные способы работы стеков. Тем не менее, чтобы охватить большинство различных способов, которыми компилятор реализует это, потребуется гораздо более длинный ответ. Простым способом было бы найти определения макросов и посмотреть, как они расширяются, и, надеюсь, не происходит никакой жуткой магии компилятора. - person Skizz; 11.09.2012
comment
переменные параметры работают только на 32-битных с соглашением о вызовах cdecl, которое передает параметры только в стеке и в обратном порядке, чтобы позволить вызывающей стороне решить, сколько параметров передать. Только вызывающая сторона настраивает стек вызовов и очищает его после возврата вызова, что является жизненно важным ключом к использованию вариативности. Другие 32-битные соглашения о вызовах либо используют сочетание регистров и стека, либо смешивают обязанности вызывающего/вызываемого в отношении настройки/очистки, что делает невозможным использование вариативных параметров. - person Remy Lebeau; 25.06.2016
comment
В 64-разрядной версии по-прежнему возможны вариативные параметры, но реализация va_arg() будет очень сложной. , требующий поддержки компилятора, а не только кода пользовательского режима. - person Remy Lebeau; 25.06.2016
comment
@RemyLebau Есть ли способ получить 64-битный gcc (gcc 5 или 6 в x86-64 Linux), чтобы использовать соглашения о вызовах Skizz, так хорошо описанные выше? Я спрашиваю, потому что хотел бы, чтобы мои ученики вручную реализовали вариационную функцию на C в качестве упражнения (т.е. без макросов va_*), чтобы получить практическое понимание передачи параметров через стек, но я этого не делаю. Не хочу, чтобы они устанавливали 32-битный gcc только для этого упражнения. - person Holger Peine; 02.09.2016
comment
Присутствие параметра ... отключило бы эту возможность и компилятору пришлось бы использовать стек. На самом деле нет, в x86-64 соглашения о вызовах Windows и System V по-прежнему передают аргументы в одних и тех же регистрах для вариативный или нет. Соглашение о вызовах Windows требует теневого пространства над адресом возврата перед аргументами стека (если они есть), поэтому вариационная функция может просто сбросить 4 регистра в теневое пространство и индексировать свои аргументы как массив. (Вызывающий должен дублировать аргументы FP в целых числах и регистрах XMM для функций с переменным числом переменных). - person Peter Cordes; 17.12.2017
comment
Таким образом, Windows оптимизирована для вариативных функций за счет обычных функций. Но x86-64 System V требует, чтобы вариативные функции были более сложными, чтобы выяснить, где находятся их аргументы. Реализация gcc сбрасывает в стек аргументы, передающие регистры (включая xmm, если al != 0, что указывает на то, что в регистрах есть некоторые аргументы FP), а затем обрабатывает это как непересекающийся массив для va_arg. Обычный современный код не вызывает невстроенные функции с переменным числом аргументов в узких циклах, а неупорядоченное выполнение в любом случае делает мертвые хранилища неиспользуемых регистров не очень дорогими. - person Peter Cordes; 17.12.2017
comment
@PeterCordes: Нет, операционная система не предъявляет никаких требований к тому, как параметры обрабатываются в приложении, единственные спецификации, которые определяет ОС, — это то, как параметры передаются самой ОС, компилятор определяет, как параметры реализуются в приложении. применение. То, как параметры обрабатываются в моем ответе, сильно зависит от архитектуры исходных процессоров 8088/6 и состояния разработки программного обеспечения в то время, в наши дни с гораздо более быстрыми процессорами и достижениями в SE это может быть сделано по-другому, но это то, что мы застряли с. - person Skizz; 23.12.2017
comment
И указывает ли GCC, как обрабатываются аргументы? Я спрашиваю только потому, что GCC является очень универсальным кросс-платформенным компилятором, и способ реализации аргументов, вероятно, больше определяется целевой архитектурой, чем компилятором, процессор 88000 будет иметь совсем другую стратегию, чем, например, Z80, но GCC сможет ориентироваться на любой из них с тем же исходным кодом. - person Skizz; 23.12.2017
comment
Все основные компиляторы C предпочитают следовать стандартным соглашениям о вызовах/ABI, поэтому вы можете вызывать библиотечные функции в библиотеках, скомпилированных другим компилятором на той же целевой платформе. В x86-64 эти соглашения о вызовах (x86-64 System V и соглашение x86-64 Windows) используют регистровые аргументы даже для функций с переменным числом аргументов. См. stackoverflow.com/questions/6212665 для примеров вызова printf. Так что нет, компилятор не может просто что-то придумать, и нет, ... не отключает передачу аргументов регистра. И да, конечно, разные цели имеют разные соглашения о вызовах. - person Peter Cordes; 23.12.2017
comment
В 32-разрядной версии x86 Windows одно из стандартных соглашений о вызовах (я думаю, __stdcall) заставляет вызываемого извлекать аргументы из стека (например, с ret 8 вместо ret). Но даже если это значение по умолчанию, функции с переменным числом переменных не используют это; они используют вызывающие абоненты (например, __cdecl). Таким образом, ... может повлиять на выбранное соглашение о вызовах, но не обязательно отключать передачу аргументов регистра. Дизайн ABI, в котором это имело место, возможен, но не является правилом. - person Peter Cordes; 23.12.2017
comment
@Skizz, почему код const int& rf= 3; va_list vl; va_start(vl, rf); имеет неопределенное поведение? - person alexander.sivak; 21.03.2021

Поскольку аргументы передаются в стек, va_ «функции» (в большинстве случаев они реализуются в виде макросов) просто манипулируют частным указателем стека. Этот частный указатель стека сохраняется из аргумента, переданного в va_start, а затем va_arg "извлекает" аргументы из "стека" по мере перебора параметров.

Допустим, вы вызываете функцию max с тремя параметрами, например:

max(a, b, c);

Внутри функции max стек в основном выглядит так:

      +-----+
      |  c  |
      |  b  |
      |  a  |
      | ret |
SP -> +-----+

SP - это настоящий указатель стека, и на самом деле в стеке находятся не a, b и c, а их значения. ret — это адрес возврата, на который следует перейти после выполнения функции.

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

      +-----+
      |  c  |
ap -> |  b  |
      |  a  |
      | ret |
SP -> +-----+

Когда вы используете va_arg(ap, int), он возвращает то, на что указывает указатель частного стека, а затем «извлекает» его, изменяя указатель частного стека, чтобы теперь он указывал на следующий аргумент. Теперь стек выглядит так:

      +-----+
ap -> |  c  |
      |  b  |
      |  a  |
      | ret |
SP -> +-----+

Это описание конечно упрощено, но принцип показывает.

person Some programmer dude    schedule 11.09.2012
comment
Конечно, va_arg не сможет вытолкнуть его из стека, если не соблюдает соглашение об очистке вызывающего абонента. - person Puppy; 11.09.2012
comment
@Joachim: Можете ли вы привести несколько иллюстраций или описать свой ответ более подробно. Я не могу визуализировать то, что вы говорите. - person Bruce; 11.09.2012
comment
@DeadMG Конечно, нет, поэтому я помещаю попсы в кавычки. :) - person Some programmer dude; 11.09.2012
comment
@Bruce Изменил мой ответ, надеюсь, теперь вы разберетесь в этом лучше. - person Some programmer dude; 11.09.2012
comment
Вот как это работает на 32-битной платформе. На 64-битной платформе одни аргументы передаются через регистры, другие — в стек, поэтому 64-битная реализация сложнее. - person Maxim Egorushkin; 11.09.2012
comment
@JoachimPileborg: Большое спасибо. Теперь я могу представить это гораздо лучше. - person Bruce; 11.09.2012

В общем, как я грущу target.def, когда прототип функции объявлен с помощью ( ,...) компилятор устанавливает дерево синтаксического анализа, помеченное флагом varargs и ссылками на типы именованных аргументов. Для строгого соответствия C каждый именованный аргумент должен получать любую дополнительную информацию, необходимую для настройки va_list, когда этот параметр является именованным полем va_start и в качестве возможного возврата в va_arg(), но большинство компиляторов просто генерируют эту информацию для последнего именованного аргумента. . Когда функция определена, ее генератор пролога отмечает, что был установлен флаг varargs, и добавляет код, необходимый для настройки любых скрытых полей, которые он добавляет к фрейму, который имеет известные смещения, на которые может ссылаться макрос va_start.

Когда он находит ссылку на эту функцию, он создает дополнительные деревья синтаксического анализа и генерации кода для каждого аргумента, представляющего ..., которые могут вводить дополнительные скрытые поля информации о типе времени выполнения, такие как границы массива, которые добавляются к настройкам полей для va_start. и va_arg для именованных аргументов. Это комбинированное дерево определяет, какой код генерируется для копирования значений параметров во фрейм, пролог устанавливает, что необходимо для va_start для создания va_list, начиная с произвольного или последнего именованного параметра, и каждый вызов va_arg() генерирует встроенный код, который ссылается на любые скрытые поля, специфичные для параметра, используемые для проверки во время компиляции ожидаемого возврата, являются назначением, совместимым с использованием компилируемого выражения, и выполняют любые необходимые продвижения/приведения аргументов. Сумма размеров значений именованных полей и размеров скрытых полей определяет, какое значение компилируется после вызова или в эпилоге функции для моделей очистки вызываемого объекта для корректировки кадра по возвращении.

Каждый из этих шагов имеет зависимости от процессора и соглашения о вызовах, инкапсулированные в файлы config/proc/proc.c и proc.h, которые переопределяют упрощенные определения по умолчанию va_start() и va_arg(), предполагающие, что каждый аргумент имеет фиксированный выделенный размер. некоторое расстояние выше первого именованного аргумента в стеке. Для некоторых платформ или языков кадры параметров, реализованные как отдельные malloc(), более желательны, чем стек фиксированного размера. Также обратите внимание, что эти варианты использования не являются потокобезопасными; небезопасно передавать ссылку va_list в другой поток без неуказанных средств обеспечения того, чтобы кадр параметра не стал недействительным из-за возврата функции или прерывания потока.

person M. Ziegast    schedule 10.06.2018
comment
Этот ответ, по-видимому, не касается вопроса, который на самом деле задал OP. - person zwol; 23.07.2020

person    schedule
comment
Спасибо за ваш ответ. Меня больше интересует, как настроен стек для вызываемого объекта (как его толкают) и как работают макросы? - person Bruce; 11.09.2012
comment
Похоже, это не отвечает на какой-либо вопрос, это просто блок кода, не имеющий особой связи ни с чем, что сказал ОП. - person zwol; 23.07.2020