Как аргумент переменной длины работает в C? - PullRequest
1 голос
/ 09 июля 2019

Я пытаюсь понять, как аргументы переменной длины работают в C.

В основном, когда вызывается функция аргумента переменной длины (например: printf (const char * format, ...);), гдеаргументы копируются (стек / регистр?)и как вызываемая функция получает информацию об аргументах, переданных вызывающей функцией?

Я очень ценю любую форму помощи.Заранее спасибо.

Ответы [ 7 ]

0 голосов
/ 09 июля 2019

Использование списка переменных аргументов является стандартной функцией языка 'C', и поэтому должно применяться на любом компьютере, для которого существует компилятор C.

Когда мы говорим о любой машине, мы имеем в виду, что независимо от способа, используемого для передачи параметров, регистров, стека или того и другого, у нас должна быть функция.

По сути, для реализации функциональности действительно требуется детерминистическая природа процесса . Это не имеет значения, если параметры передаются в стеке, регистре, обоими или другими пользовательскими способами MCU, важно то, что способ, которым это делается, четко определено и всегда одинаково .

Если это свойство соблюдается, мы уверены, что всегда сможем просмотреть список параметров и получить доступ к каждому из них.

Фактически метод, используемый для передачи параметров для каждой машины или системы, указан в ABI ( A , B inary I nterface, см. https://en.wikipedia.org/wiki/Application_binary_interface), следуя правилам, в обратном порядке вы всегда можете вернуться к параметрам.

В любом случае, в некоторой системе, в подавляющем большинстве, простой реверс-инжиниринг ABI недостаточен для восстановления параметров, то есть размеров параметров, отличающихся от стандартного размера регистра / стека ЦП, в этом случае вам нужно больше информации о параметре, который вы ищем: размер операнда .

Давайте рассмотрим обработку параметров переменной в C. Сначала вы объявляете функцию, имеющую один параметр целочисленного типа, содержащий количество параметров, переданных как аргументы переменной, и 3 точки для переменной части:

int foo(int cnt, ...);

Для обычного доступа к переменным аргументам вы используете определения в заголовке <stdarg.h> следующим образом:

int foo(int cnt, ...)
{
  va_list ap;  //pointer used to iterate through parameters
  int i, val;

  va_start(ap, cnt);    //Initialize pointer to the last known parameter

  for (i = 0; i > cnt; i++)
  {
    val = va_arg(ap, int);  //Retrieve next parameter using pointer and size
    printf("%d ", val);     // Print parameter, an integer
  }

  va_end(ap);    //Release pointer. Normally do_nothing

  putchar('\n');
}

На стековой машине (т.е. x86-32bit), где параметры передаются последовательно, приведенный выше код работает более или менее следующим образом:

int foo(int cnt, ...)
{
  char *ap;  //pointer used to iterate through parameters
  int i, val;

  ap = &cnt;    //Initialize pointer to the last known parameter

  for (i = 0; i > cnt; i++)
  {
    /*
     * We are going to update pointer to next parameter on the stack.
     * Please note that here we simply add int size to pointer because
     * normally the stack word size is the same of natural integer for
     * that machine, but if we are using different type we **must**
     * adjust pointer to the correct stack bound by rounding to the
     * larger multiply size.
     */
    ap = (ap + sizeof(int));
    val = *((int *)ap);  //Retrieve next parameter using pointer and size
    printf("%d ", val);     // Print parameter, an integer
  }

  putchar('\n');
}

Обратите внимание, что если мы обращаемся к типам, отличным от int e / o, размер которых отличается от размера слова собственного стека, указатель должен быть отрегулирован так, чтобы всегда увеличиваться кратным размеру слова стека .

Теперь рассмотрим машину, которая использует регистры для передачи параметров, для простоты мы считаем, что ни один операнд не может быть больше, чем размер регистра, и что распределение выполняется с использованием регистров последовательно (также обратите внимание на инструкцию псевдо-ассемблера mov val, rx, которая загружает переменную val с содержимым регистра rx):

int foo(int cnt, ...)
{
  int ap;  //pointer used to iterate through parameters
  int i, val;

/*
 * Initialize pointer to the last known parameter, in our
 * case the first in the list (see after why)
 */
  ap = 1;

  for (i = 0; i > cnt; i++)
  {
    /*
     * Retrieve next parameter
     * The code below obviously isn't real code, but should give the idea.
     */
    ap++;      //Next parameter
    switch(ap)
    {
      case 1:
        __asm mov val, r1;  //Get value from register
        break;
      case 2:
        __asm mov val, r2;
        break;
      case 3:
        __asm mov val, r3;
        break;
      .....
      case n:
        __asm mov val, rn;
        break;
     }
    printf("%d ", val);     // Print parameter, an integer
  }

  putchar('\n');
}

Надеюсь, что концепция достаточно ясна.

0 голосов
/ 09 июля 2019

Как извлечено из документа ABI, метод для хранения всех аргументов предоставляется документом ABI архитектуры.

Ссылка: https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf (номер страницы 56).

Область сохранения реестра: Пролог функции, принимающей переменный список аргументов и вызывающей макрос va_start, как ожидается, сохранит регистры аргументов в область сохранения регистров . Каждый регистр аргументов имеет фиксированное смещение в области сохранения регистров.

0 голосов
/ 09 июля 2019

Традиционно аргументы «всегда» помещались в стек независимо от других оптимизаций передачи регистров, а затем va_list был просто указателем в стек для идентификации следующего аргумента va_arg.Однако передача регистров настолько предпочтительна для новых процессоров и настроек оптимизации компилятора, что даже переменные помещаются в регистры.

При этом va_list становится небольшой структурой данных (или указателем на эту структуру данных), которая захватывает все эти аргументы регистра, и / и / имеет указатель в стек, если число аргументов слишком великомного.Макрос va_arg сначала проходит по захваченным регистрам, затем проходит по записям стека, поэтому va_list также имеет «текущий индекс».

Обратите внимание, что по крайней мере в реализации gcc va_list являетсяГибридный объект: когда он объявлен в теле, он является экземпляром структуры, но при передаче в качестве аргумента он волшебным образом становится указателем, как ссылка на C ++, даже если в C нет понятия ссылок.

На некоторых платформах va_list также выделяет некоторую динамическую память, поэтому всегда следует вызывать va_end.

0 голосов
/ 09 июля 2019

C ч \ с стандартные механизмы доступа к этим параметрам.Макросы определены в stdarg.h

http://www.cse.unt.edu/~donr/courses/4410/NOTES/stdarg/

здесь у вас есть очень простая реализация sniprintf

int ts_formatstring(char *buf, size_t maxlen, const char *fmt, va_list va)
{
    char *start_buf = buf;

    maxlen--;
    while(*fmt && maxlen)
    {
        /* Character needs formating? */
        if (*fmt == '%')
        {
            switch (*(++fmt))
            {
              case 'c':
                *buf++ = va_arg(va, int);
                maxlen--;
                break;
              case 'd':
              case 'i':
                {
                    signed int val = va_arg(va, signed int);
                    if (val < 0)
                    {
                        val *= -1;
                        *buf++ = '-';
                        maxlen--;
                    }
                    maxlen = ts_itoa(&buf, val, 10, maxlen);
                }
                break;
              case 's':
                {
                    char * arg = va_arg(va, char *);
                    while (*arg && maxlen)
                    {
                        *buf++ = *arg++;
                        maxlen--;
                    }
                }
                break;
              case 'u':
                    maxlen = ts_itoa(&buf, va_arg(va, unsigned int), 10, maxlen);
                break;
              case 'x':
              case 'X':
                    maxlen = ts_itoa(&buf, va_arg(va, int), 16, maxlen);
                break;
              case '%':
                  *buf++ = '%';
                  maxlen--;
                  break;
            }
            fmt++;
        }
        /* Else just copy */
        else
        {
            *buf++ = *fmt++;
            maxlen--;
        }
    }
    *buf = 0;

    return (int)(buf - start_buf);
}



int sniprintf(char *buf, size_t maxlen, const char *fmt, ...)
{
    int length;
    va_list va;
    va_start(va, fmt);
    length = ts_formatstring(buf, maxlen, fmt, va);
    va_end(va);
    return length;
}

Это из крошечной printf студии atollic.

Здесь показаны все механизмы (включая передачу списка параметров другим функциям.

0 голосов
/ 09 июля 2019

Большинство реализаций выдвигают аргументы в стеке, использование регистра не будет хорошо работать на архитектурах с истощенным регистром или если аргументов больше, чем регистров в целом.

И вызываемая функция вообще ничего не знает об аргументах, их числе или их типах. Вот почему, например, printf и связанные функции используют спецификаторы формата. Затем вызываемая функция будет интерпретировать следующую часть стека в соответствии с этим спецификатором формата (используя va_arg «функцию»).

Если тип, выбранный с помощью va_arg, не соответствует фактическому типу аргумента, у вас будет неопределенное поведение .

0 голосов
/ 09 июля 2019

То есть вы думаете о стеке сборки или регистре?

В простых случаях я имею в виду функции с небольшим числом аргументов, аргументы хранятся в регистрах (A4, B4, C4) и возвращаемом значении.хранится в формате A4.

Но в случае сложных функций, которые имеют много аргументов, мы не можем составить полный список регистров, поэтому в то время мы используем стек.

Если у вас есть проблемыоб этом ответе, спросите меня в любое время.

0 голосов
/ 09 июля 2019

куда копируются аргументы (стек / регистр?)?

Это варьируется.На x64 используются обычные соглашения: первые несколько аргументов (в зависимости от типа), вероятно, попадают в регистры, а другие аргументы попадают в стек.Стандарт C требует, чтобы компилятор поддерживал как минимум 127 аргументов функции, поэтому неизбежно, что некоторые из них пойдут в стек.

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

Используя начальные аргументы, такие как строка формата printf.Средства поддержки varargs в C не позволяют функции проверять количество и типы аргументов, только чтобы получать их по одному за раз (и если они неправильно приведены или если получено больше аргументов, чем было передано, результатявляется неопределенным поведением).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...