Предположим, что на моей платформе sizeof(int)==sizeof(void*)
и у меня есть этот код:
printf( "%p", rand() );
Будет ли это поведение undefined из-за передачи значения, которое не является допустимым указателем вместо %p
?
Предположим, что на моей платформе sizeof(int)==sizeof(void*)
и у меня есть этот код:
printf( "%p", rand() );
Будет ли это поведение undefined из-за передачи значения, которое не является допустимым указателем вместо %p
?
Чтобы расширить ответ на @larsman (в котором говорится, что, поскольку вы нарушили ограничение, поведение undefined), здесь выполняется реальная реализация C, где sizeof(int) == sizeof(void*)
, но код не эквивалентен printf( "%p", (void*)rand() );
Процессор Motorola 68000 имеет 16 регистров, которые используются для общих вычислений, но они не эквивалентны. Восемь из них (с именем a0
через a7
) используются для доступа к памяти (регистры адресов), а остальные восемь (от d0
до d7
) используются для арифметических (регистров данных). Допустимым вызовом для этой архитектуры будет
d0
и d1
; передайте остальные в стек.a0
и a1
; передайте остальные в стек.Это совершенно законное соглашение о вызове, аналогичное соглашениям, используемым многими современными процессорами.
Например, чтобы вызвать функцию 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, который, скорее всего, сбой вашей программы, когда он попытается напечатать ее, как если бы это был указатель на строку.
C стандарт, 7.21.6.1, функция fprintf
, указывает только
p
Аргумент должен быть указателем наvoid
.
В приложении J.2 это ограничение, и нарушение ограничения вызывает UB.
(Ниже приведены мои предыдущие рассуждения, почему это должен быть UB, который был слишком сложным.)
Этот параграф не описывает, как void*
извлекается из ...
, но единственный способ, который сам стандарт C предлагает для этой цели, - это 7.16.1.1, макрос va_arg
, который предупреждает нас, что
, если тип несовместим с типом фактического следующего аргумента (в соответствии с поощрением по умолчанию), поведение undefined
Если вы читаете 6.2.7, Тип совместимого и составного типа, тогда нет намека на то, что void*
и int
должны быть совместимы, независимо от их размера. Итак, я бы сказал, что, поскольку va_arg
- единственный способ реализовать printf
в стандарте C, поведение undefined.
Да, это undefined. Из С++ 11, 3.7.4.2/4:
Эффект использования недопустимого значения указателя (включая передачу его функции удаления) undefined.
со сноской:
В некоторых реализациях он вызывает системную ошибку выполнения.
% p - это только спецификация выходного формата для printf. Ему не нужно разыгрывать или проверять указатель каким-либо образом, хотя некоторые компиляторы выдают предупреждение, если тип не является указателем:
int main(void)
{
int t = 5;
printf("%p\n", t);
}
Предупреждение о компиляции:
warning: format ‘%p’ expects argument of type ‘void*’, but argument 2 has type ‘int’ [-Wformat]
Выходы:
0x5