Технически, как работают вариативные функции? Как работает printf?

Я знаю, что могу использовать va_arg для написания своих собственных функций с переменным числом аргументов, но как функции с переменным числом аргументов работают под капотом, то есть на уровне ассемблерных инструкций?

Например, как возможно, что printf принимает переменное количество аргументов?


* Нет правил без исключений. Языка C/C++ не существует, однако на этот вопрос можно ответить для обоих из них

* Примечание. Ответ изначально дан на Как может функция printf принимать числовые переменные параметры при их выводе?, но, похоже, это не относилось к вопрошавшему


person Sebastian Mach    schedule 16.04.2014    source источник
comment
@BЈовић: Это догадки; Текст буду уточнять.   -  person Sebastian Mach    schedule 16.04.2014
comment
Вопрос в том, как вариативные функции работают на технической основе; как это работает w.r.t. к оборудованию. И нет, это не обман. Вы понизили ответ? // редактировать: я удалил свой ответ в другой ветке.   -  person Sebastian Mach    schedule 16.04.2014
comment
Обычно человек не просто решает, что его вопрос должен быть частью c++-faq. тег. Это действительно часто спрашивают? Это хороший ответ на вопросы и ответы, поэтому спасибо за публикацию.   -  person Lightness Races in Orbit    schedule 16.04.2014
comment
@BЈовић: You just copy&pasted the answer. So, this question is duplicate of other. Это нелогично. Дублирующиеся ответы не делают дубликаты вопросов.   -  person Lightness Races in Orbit    schedule 16.04.2014
comment
@LightnessRacesinOrbit: понятно. Я ожидал чего-то плохого и должен был сделать домашнее задание лучше.   -  person Sebastian Mach    schedule 16.04.2014
comment
возможный дубликат Каков формат x86_64 структура va_list?   -  person Matthieu M.    schedule 16.04.2014
comment
@MatthieuM.: Я не уверен, достаточно ли это технически. Я уточню свой вопрос.   -  person Sebastian Mach    schedule 16.04.2014
comment
@phresnel: он кажется более техническим (или, по крайней мере, точным), чем ваш собственный ответ, хотя он специализирован для одной архитектуры.   -  person Matthieu M.    schedule 16.04.2014
comment
@MatthieuM.: Да, ваш комментарий заставил меня понять, что техническая неоднозначность, поэтому я теперь добавил уровень инструкций, который, как я понимаю, тоже нуждается в доработке. // Я не уверен, действительно ли это вопрос C или C++. Кажется, C служит только в качестве примера. Тем не менее, это может быть интересно программистам на C, ищущим просветления. Хм.   -  person Sebastian Mach    schedule 16.04.2014
comment
@phresnel: потенциально он может быть полезен за пределами C или C ++, однако я не знаю другого языка, который напрямую использует varargs.   -  person Matthieu M.    schedule 16.04.2014
comment
Я чувствую себя шизофреником, обсуждая это на нескольких уровнях себя. Но я думаю, что вы правы; двое из трех моих личностей считают, что теги C и C++ в порядке.   -  person Sebastian Mach    schedule 16.04.2014
comment
@MatthieuM.: Lua делает это с помощью механизмов, которые на 100% не связаны с механизмами C.   -  person Mooing Duck    schedule 16.04.2014


Ответы (2)


Стандарты C и C++ не предъявляют никаких требований к тому, как это должно работать. Соответствующий компилятор вполне может решить испускать цепочечные списки, std::stack<boost::any> или даже волшебную пони-пыль (согласно комментарию @ Xeo) под капотом.

Однако обычно он реализуется следующим образом, хотя такие преобразования, как встраивание или передача аргументов в регистры ЦП, могут не оставить ничего из обсуждаемого кода.

Также обратите внимание, что этот ответ конкретно описывает растущий вниз стек на изображениях ниже; кроме того, этот ответ является упрощением только для демонстрации схемы (см. https://en.wikipedia.org/wiki/Stack_frame).

Как можно вызвать функцию с нефиксированным числом аргументов

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

foobar("%d%d%d", 3,2,1);

Затем это компилируется в код на ассемблере, подобный этому (пример и схематично, фактический код может выглядеть иначе); обратите внимание, что аргументы передаются справа налево:

push 1
push 2
push 3
push "%d%d%d"
call foobar

Эти push-операции заполняют стек:

              []   // empty stack
-------------------------------
push 1:       [1]  
-------------------------------
push 2:       [1]
              [2]
-------------------------------
push 3:       [1]
              [2]
              [3]  // there is now 1, 2, 3 in the stack
-------------------------------
push "%d%d%d":[1]
              [2]
              [3]
              ["%d%d%d"]
-------------------------------
call foobar   ...  // foobar uses the same stack!

Нижний элемент стека называется «Вершина стека», часто сокращенно «TOS».

Функция foobar теперь будет обращаться к стеку, начиная с TOS, то есть строки формата, которая, как вы помните, была помещена последней. Представьте, что stack — это ваш указатель стека, stack[0] — это значение в TOS, stack[1] — это значение выше TOS и так далее:

format_string <- stack[0]

... а затем анализирует строку формата. При синтаксическом анализе он распознает %d-токенов и для каждого загружает еще одно значение из стека:

format_string <- stack[0]
offset <- 1
while (parsing):
    token = tokenize_one_more(format_string)
    if (needs_integer (token)):
        value <- stack[offset]
        offset = offset + 1
    ...

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

Безопасность

Эта зависимость от предоставленных пользователем аргументов также является одной из самых серьезных проблем с безопасностью (см. https://cwe.mitre.org/top25/). Пользователи могут легко использовать вариационную функцию неправильно, либо потому, что они не читали документацию, либо забыли настроить строку формата или список аргументов, либо потому, что они просто злые, или что-то еще. См. также Атака на формат строки.

C Реализация

В C и C++ функции с переменным числом переменных используются вместе с интерфейсом va_list. Хотя размещение в стеке свойственно этим языкам (в K+RC вы можете даже предварительно объявить функцию, не указывая ее аргументы, но по-прежнему вызывать ее с любым числом и типом аргументов), чтение из такого неизвестного списка аргументов осуществляется через интерфейс va_...-макросы и va_list-тип, которые в основном абстрагируют низкоуровневый доступ к кадрам стека.

person Sebastian Mach    schedule 16.04.2014
comment
Обратите внимание, что стандарт не предъявляет фактических требований к тому, как это работает. Для чего бы это ни стоило, он также может использовать волшебную пыль пони, чтобы заставить его работать. (Кроме того, я не минусовал.) - person Xeo; 16.04.2014
comment
@Xeo: Нет необходимости говорить, что в вашем случае этого не было :) Я добавлю заявление об отказе от ответственности и включу то, что вы правильно сказали. - person Sebastian Mach; 16.04.2014
comment
Просто для интереса тех, кто может прочитать это: это то, что делает возможными эксплойты форматной строки. Никогда не используйте строку пользовательского ввода в качестве строки формата в вызове printf! - person Cu3PO42; 16.04.2014
comment
stdcall нельзя использовать в качестве соглашения о вызове функции с переменным числом аргументов. Даже если автор вариационной функции знает количество аргументов, возможно, компилятор не может этого знать. И стандарты позволяют использовать несколько va_list, вызывая va_start умножение или используя va_copy, поэтому va_arg реализуется не pop, а путем прямого чтения стека (например, mov eax, [valist]). Таким образом, компилятор не может определить, сколько стека должно быть извлечено при компиляции вариативной функции - это знает только вызывающий. Таким образом, следует использовать cdecl. - person ikh; 16.04.2014
comment
Вероятно, стоит отметить, что многие компиляторы «обманывают», когда строка формата известна заранее и фактически не использует вариативную семантику. - person Vality; 16.04.2014
comment
Конечно, если стек растет вверх, а не вниз, все наоборот. И даже так, как вы описываете, это не совсем так. Аргументы на самом деле не появляются при доступе к ним. Обычно va_list определяет тип указателя, а va_arg обновляет его в соответствии с типом извлекаемого аргумента. (Вот почему аргумент type для va_arg должен соответствовать продвигаемому типу, а не тому типу, который вам действительно нужен.) - person James Kanze; 16.04.2014
comment
@ikh И stdcall, и cdecl являются чисто соглашениями Microsoft. Большинство других систем имеют только одно основное соглашение и передают все аргументы всем функциям одинаково. Те немногие, которые не используют (кроме Microsoft) стандартный механизм для указания соглашений о вызовах: extern "C" (или что-то другое вместо C). - person James Kanze; 16.04.2014
comment
-1: Это просто (и подробно) описывает, как работает стек для передачи фиксированного количества параметров. Ему удается упустить почти все существенные моменты того, как вызов функции Variadic с переменным числом аргументов фактически реализуется в большинстве архитектур: т. е. с указателем кадра или счетчиком аргументов< /i> в дополнение к указателю стека. без них вызываемая функция не знает, где находится нижняя часть кадра вызова. - person RBarryYoung; 16.04.2014
comment
@Vality: реализация printf должна иметь возможность обрабатывать произвольные вариативные аргументы, поэтому любой вызов printf должен использовать вариативную семантику. Обман, который вы описываете, может произойти, но только путем преобразования вызова printf в вызов какой-либо другой (невариативной) функции. Например, такой вызов, как printf("hello\n"), может быть оптимизирован до эквивалента puts("hello"). - person Keith Thompson; 16.04.2014
comment
@KeithThompson Да, это именно то, что я описывал, однако я думаю, что некоторые компиляторы идут дальше, преобразовывая printf с постоянной строкой формата в серию функций преобразования и помещают. Но я понимаю и согласен со всем, что вы сказали, я просто подумал, что это интересное примечание о реализации. - person Vality; 16.04.2014
comment
Ребята/девушки, я немного усовершенствовал свой ответ. Спасибо за помощь, надеюсь теперь лучше! - person Sebastian Mach; 17.04.2014
comment
@ikh: Но в stdcall аргументы передаются справа налево? В любом случае, я удалил этот раздел для ясности. - person Sebastian Mach; 17.04.2014
comment
@JamesKanze эм? stdcall и cdecl обычно используются в большинстве 32-битных систем. (хотя есть небольшие различия между одной системой и другой..) - person ikh; 19.04.2014
comment
@phresnel Да, и cdecl, и stdcall. - person ikh; 19.04.2014
comment
@ikh: я спросил, потому что вы написали stdcall cannot be used as the calling convention of variadic function. Even if the writer of variadic function knows the number of arguments, maybe compiler cannot know it., но для функций с переменным числом требуется передача справа налево (если только строка формата не будет последним аргументом) - person Sebastian Mach; 19.04.2014
comment
@phresnel Требуется не только передача справа налево, но и cleaning stack by caller. Как я уже сказал, компилятору слишком сложно или невозможно определить количество вариативных аргументов. - person ikh; 20.04.2014
comment
@phresnel Например, функция wsprintf API win32 равна cdecl, даже если другие функции API равны stdcall. - person ikh; 20.04.2014
comment
@ikh: Да, я вижу. Хотя это и не невозможно (вызванная функция может очищать информацию, переданную в строке формата), stdcall будет означать еще большие проблемы с безопасностью. - person Sebastian Mach; 21.04.2014
comment
@ikh stdcall и cdecl — это майкрософтизмы. Они не используются, за исключением случаев, когда компиляторы пытаются быть совместимыми с Microsoft. (И почему Microsoft сделала это таким образом, когда стандарт предоставляет стандартный способ сделать это, это вне меня.) - person James Kanze; 22.04.2014
comment
@JamesKanze Эм..? Хотя они не являются стандартными, они обычно используются в 32-битной системе x86. Например, при написании ассемблерной функции для связи с программой C, скомпилированной gcc, мы должны следовать cdecl, если не указано соглашение о вызовах. Конечно, он не может полностью равняться MS. - person ikh; 24.04.2014
comment
@ikh Я никогда не использовал их и не слышал о них под Linux; их не существовало, когда я разрабатывал на этой платформе. Обычно существует только одно соглашение о вызовах, используемое в конкретной архитектуре/ОС для определенного языка. Windows немного отличается в этом отношении, поскольку они навязывают соглашения Pascal при обращении к системной библиотеке C. - person James Kanze; 24.04.2014
comment
@ikh Итак, есть много разных имен, каждый компилятор использует свое подмножество и определяет те, которые он использует по-разному. В общем, любое использование таких имен требует спецификации компилятора, и многие из имен значимы только для одного компилятора. - person James Kanze; 25.04.2014
comment
И я только что заметил одно искажение в фактическом ответе: в C вы можете объявить функцию без какой-либо информации о ее аргументах; в таких случаях, однако, все вызовы функции должны передавать аргументы, совместимые с теми, что указаны в определении функции, иначе это будет неопределенное поведение (и такие функции не могут быть переменными). И это указано как устаревшая функция (другими словами, устаревшая). - person James Kanze; 25.04.2014
comment
@JamesKanze: Ой, заблуждение на моей стороне. Я отредактирую свой ответ. - person Sebastian Mach; 28.04.2014

Функции с переменным числом переменных определяются стандартом с очень небольшим количеством явных ограничений. Вот пример, взятый с сайта cplusplus.com.

/* va_start example */
#include <stdio.h>      /* printf */
#include <stdarg.h>     /* va_list, va_start, va_arg, va_end */

void PrintFloats (int n, ...)
{
  int i;
  double val;
  printf ("Printing floats:");
  va_list vl;
  va_start(vl,n);
  for (i=0;i<n;i++)
  {
    val=va_arg(vl,double);
    printf (" [%.2f]",val);
  }
  va_end(vl);
  printf ("\n");
}

int main ()
{
  PrintFloats (3,3.14159,2.71828,1.41421);
  return 0;
}

Предположения примерно такие.

  1. Должен быть (по крайней мере один) первый фиксированный именованный аргумент. ... на самом деле ничего не делает, кроме того, что говорит компилятору поступать правильно.
  2. Фиксированные аргументы предоставляют информацию о количестве переменных аргументов с помощью неуказанного механизма.
  3. Из фиксированного аргумента макрос va_start может вернуть объект, который позволяет извлекать аргументы. Тип va_list.
  4. Из объекта va_list va_arg можно выполнить итерацию по каждому вариативному аргументу и привести его значение к совместимому типу.
  5. В va_start могло произойти что-то странное, поэтому va_end снова все исправляет.

В самой обычной ситуации на основе стека va_list является просто указателем на аргументы, находящиеся в стеке, а va_arg увеличивает указатель, приводит его и разыменовывает в значение. Затем va_start инициализирует этот указатель с помощью какой-то простой арифметики (и внутренних знаний), а va_end ничего не делает. Здесь нет странного языка ассемблера, есть только некое внутреннее знание того, где что находится в стеке. Прочтите макросы в стандартных заголовках, чтобы узнать, что это такое.

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

Такие функции, как printf, работают именно так. Фиксированный аргумент — это строка формата, которая позволяет вычислить количество аргументов.

Такие функции, как vsprintf, передают объект va_list как обычный тип аргумента.

Если вам нужно больше или меньше деталей уровня, пожалуйста, добавьте к вопросу.

person david.pfx    schedule 18.05.2014
comment
... может иметь решающее значение в реализациях, которые обычно ожидают, что вызываемые функции очищают переданные аргументы при выходе. Стандарт C предписывает, что передача дополнительных аргументов чему-то вроде printf не имеет никакого эффекта, но единственный способ, который мог бы работать с соглашением об очистке вызываемого объекта, состоял бы в том, чтобы вызывающая сторона знала либо о том, что она отвечает за вариативные аргументы, либо о том, что она должна позволить вызываемый объект знает количество аргументов, которые вызываемый объект должен очистить. - person supercat; 23.02.2017