Подтвердить что ты не робот

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

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

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


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

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

4b9b3361

Ответ 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, которые в основном абстрагируют низкоуровневый доступ стекового кадра.

Ответ 2

Функции Variadic определяются стандартом, с очень небольшими явными ограничениями. Вот пример, снятый с 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 как обычный тип аргумента.

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