Ответ 1
Чтобы расширить ответ на @larsman (в котором говорится, что, поскольку вы нарушили ограничение, поведение undefined), здесь выполняется реальная реализация C, где sizeof(int) == sizeof(void*)
, но код не эквивалентен printf( "%p", (void*)rand() );
Процессор Motorola 68000 имеет 16 регистров, которые используются для общих вычислений, но они не эквивалентны. Восемь из них (с именем a0
через a7
) используются для доступа к памяти (регистры адресов), а остальные восемь (от d0
до d7
) используются для арифметических (регистров данных). Допустимым вызовом для этой архитектуры будет
- Передайте первые два целочисленных параметра в
d0
иd1
; передайте остальные в стек. - Передайте первые два параметра указателя в
a0
иa1
; передайте остальные в стек. - Передайте все остальные типы в стеке, независимо от размера.
- Параметры, переданные в стеке, перемещаются справа налево независимо от типа.
- Параметры на основе стека выровнены по 4-байтным границам.
Это совершенно законное соглашение о вызове, аналогичное соглашениям, используемым многими современными процессорами.
Например, чтобы вызвать функцию void foo(int i, void *p)
, вы пройдете i
в d0
и p
в a0
.
Обратите внимание, что для вызова функции void bar(void *p, int i)
вы также проходите i
в d0
и p
в a0
.
В соответствии с этими правилами printf("%p", rand())
передаст строку формата в a0
и параметр случайного числа в d0
. С другой стороны, printf("%p", (void*)rand())
передаст строку формата в a0
и параметр случайного указателя в a1
.
Структура va_list
будет выглядеть так:
struct va_list {
int d0;
int d1;
int a0;
int a1;
char *stackParameters;
int intsUsed;
int pointersUsed;
};
Первые четыре элемента инициализируются соответствующими входными значениями регистров. stackParameters
указывает на первые параметры на основе стека, переданные через ...
, а intsUsed
и pointersUsed
инициализируются числом именованных параметров, которые являются целыми числами и указателями соответственно.
Макрос va_arg
является встроенным компилятором, который генерирует другой код на основе ожидаемого типа параметра.
- Если тип параметра является указателем, то
va_arg(ap, T)
расширяется до(T*)get_pointer_arg(&ap)
. - Если тип параметра является целым числом, то
va_arg(ap, T)
расширяется до(T)get_integer_arg(&ap)
. - Если тип параметра является чем-то другим, то
va_arg(ap, T)
расширяется до*(T*)get_other_arg(&ap, sizeof(T))
.
Функция get_pointer_arg
выполняется следующим образом:
void *get_pointer_arg(va_list *ap)
{
void *p;
switch (ap->pointersUsed++) {
case 0: p = ap->a0; break;
case 1: p = ap->a1; break;
case 2: p = *(void**)get_other_arg(ap, sizeof(p)); break;
}
return p;
}
Функция get_integer_arg
выполняется следующим образом:
int get_integer_arg(va_list *ap)
{
int i;
switch (ap->intsUsed++) {
case 0: i = ap->d0; break;
case 1: i = ap->d1; break;
case 2: i = *(int*)get_other_arg(ap, sizeof(i)); break;
}
return i;
}
И функция get_other_arg
выглядит примерно так:
void *get_other_arg(va_list *ap, size_t size)
{
void *p = ap->stackParameters;
ap->stackParameters += ((size + 3) & ~3);
return p;
}
Как отмечалось ранее, вызов printf("%p", rand())
передал строку формата в a0
и случайное целое число в d0
. Но когда функция printf
выполняется, она увидит формат %p
и выполнит va_arg(ap, void*)
, который будет использовать get_pointer_arg
и прочитает параметр a1
вместо d0
. Поскольку a1
не был инициализирован, он содержит мусор. Выбранное случайное число игнорируется.
Взяв пример дальше, если у вас есть printf("%p %i %s", rand(), 0, "hello");
, это будет вызываться следующим образом:
-
a0
= адрес строки формата (первый параметр указателя) -
a1
= адрес строки"hello"
(второй параметр указателя) -
d0
= случайное число (первый целочисленный параметр) -
d1
= 0 (второй целочисленный параметр)
Когда функция printf
выполняется, она считывает строку формата из a0
, как ожидалось. Когда он увидит %p
, он будет извлекать указатель из a1
и печатать его, поэтому вы получите адрес строки "hello"
. Затем он увидит %i
и извлечет параметр из d0
, чтобы он печатал случайное число. Наконец, он видит %s
и извлекает параметр из стека. Но вы не передавали никаких параметров в стек! Это будет читать мусор стека undefined, который, скорее всего, сбой вашей программы, когда он попытается напечатать ее, как если бы это был указатель на строку.