Технически, как работят променливите функции? Как работи 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. Това е non sequitur. Дублиращи се отговори не създават дублиращи се въпроси.   -  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

Тези натискащи операции запълват стека:

              []   // 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/). Потребителите могат лесно да използват variadic функция погрешно, или защото не са прочели документацията, или са забравили да коригират форматиращия низ или списъка с аргументи, или защото са просто зли, или каквото и да било. Вижте също Атака за форматиране на низ.

C Изпълнение

В C и C++ променливите функции се използват заедно с интерфейса va_list. Въпреки че натискането в стека е присъщо на тези езици (в K+R C можете дори да декларирате функция напред, без да посочвате нейните аргументи, но все пак да я извикате с произволен брой и тип аргументи), четенето от такъв списък с неизвестни аргументи се свързва чрез 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 не може да се използва като конвенция за извикване на променлива функция. Дори ако авторът на variadic функция знае броя на аргументите, може би компилаторът не може да го знае. А стандартите позволяват използването на множество 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 ще го актуализира според типа на аргумента, който се извлича. (Ето защо аргументът тип на 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
da просто го помислих за интересна бележка за изпълнение. - 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 на win32 api е cdecl, дори ако другите функции на api са stdcall. - person ikh; 20.04.2014
comment
@ikh: Да, разбирам. Въпреки че не е невъзможно (извиканата функция може да изчисти от информацията, предадена във форматния низ), stdcall би означавало още по-големи проблеми със сигурността. - person Sebastian Mach; 21.04.2014
comment
@ikh stdcall и cdecl са Microsoftisms. Те не се използват, освен когато компилаторите се опитват да бъдат съвместими с 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 можете да декларирате функция без никаква информация за нейните аргументи; в такива случаи обаче всички извиквания на функцията трябва да предават аргументи, съвместими с тези в дефиницията на функцията, или това е недефинирано поведение (и такива функции не могат да бъдат varargs). И това е посочено като остаряла функция (с други думи, отхвърлена). - 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 да няма ефект, но единственият начин, който би могъл да работи с callee-clean конвенцията, би бил, ако повикващият знае или, че е отговорен за различни аргументи, или че трябва да позволи на callee знае количеството аргументи, които callee трябваше да изчисти. - person supercat; 23.02.2017