Технически, как работают вариативные функции? Как работает printf?
Я знаю, что могу использовать va_arg чтобы написать свои собственные вариационные функции, но как вариационные функции работают под капотом, т. е. на уровне инструкции по сборке?
например, как это возможно, что printf принимает переменное число аргументов?
* Нет правила без исключения. Нет языка C / C++, однако на этот вопрос можно ответить для них обоих
* Примечание: ответ первоначально дан как может функция printf может принимать переменные параметры в количестве при выводе их?, но, похоже, это не относится к вопрошающему
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".
The
foobarфункция теперь будет обращаться к стеку, начиная с TOS, т. е. строки формата, которая, как вы помните, была нажата последней. Представьте себеstack- это указатель стека ,stack[0]это значение в TOS,stack[1]один над ТОС, и так далее: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+R C вы можете даже вперед-объявить функцию без указания ее аргументов, но все равно вызывайте его с любым числом и добрыми аргументами), чтение из такого неизвестного списка аргументов сопряжено черезva_...-макросы иva_list- тип, который в основном абстрагирует низкоуровневый стек-фрейм доступ.
вариативные функции определяются стандартом, с очень небольшим количеством явных ограничений. Вот пример, поднятый из 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; }предположения примерно следующие.
- должен быть (по крайней мере, один) первый, фиксированный, именованный аргумент. Элемент
...на самом деле ничего не делает, кроме как сказать компилятору, чтобы делать правильные вещи.- фиксированные аргументы предоставляют информацию о том, сколько вариативных аргументов существует, с помощью механизм не определен.
- из фиксированного аргумента это возможно для
va_startмакрос для возврата объекта, который позволяет извлекать аргументы. Тип -va_list.- С
va_listобъект это возможно дляva_argчтобы перебирать каждый вариативный аргумент и принудить его значение к совместимому типу.- что-то странное могло произойти в
va_startтакva_endделает все правильно еще раз.в самом обычном ситуация на основе стека,
va_listэто просто указатель на аргументы, сидящие в стеке, иva_argувеличивает указатель, приводит его и разыменовывает на значение. Тогдаva_startинициализирует этот указатель некоторой простой арифметикой (и внутренним знанием) иva_endничего не делает. Там нет странного языка ассемблера, просто некоторые внутренние знания о том, где вещи лежат на стеке. Прочитайте макросы в стандартных заголовках, чтобы узнать, что это такое.некоторые компиляторы (MSVC) потребуется определенная последовательность вызовов, при которой вызывающий абонент будет освобождать стек, а не вызываемого абонента.
функции как
printfработать именно так. Фиксированный аргумент-это строка формата, которая позволяет вычислить количество аргументов.функции как
vsprintfпередатьva_listобъект, как обычный тип аргумента.Если вам нужно больше или меньше деталь, пожалуйста, добавьте в вопрос.
Comments