Ответ 1
Стандарты 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
-tokens и для каждого загружает еще одно значение из стека:
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++ функции variadic используются вместе с интерфейсом va_list
. Хотя вставка в стек является неотъемлемой частью этих языков (в K + RC вы можете даже объявить функцию вперед без указания ее аргументов, но при этом вызывать ее с любым числом и любыми аргументами), чтение из такого списка неизвестных аргументов сопряжено через va_...
-macros и va_list
-type, которые в основном абстрагируют низкоуровневый доступ стекового кадра.