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



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



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






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




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

547   2  

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;
}

предположения примерно следующие.

  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 объект, как обычный тип аргумента.

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

Comments

    Ничего не найдено.