假设在我的平台上 sizeof(int)==sizeof(void*)
,并且我有以下代码:
printf( "%p", rand() );
因为在%p
的位置传递了一个非有效指针的值,这会导致未定义行为吗?
假设在我的平台上 sizeof(int)==sizeof(void*)
,并且我有以下代码:
printf( "%p", rand() );
因为在%p
的位置传递了一个非有效指针的值,这会导致未定义行为吗?
sizeof(int) == sizeof(void*)
,但是该代码与 printf( "%p", (void*)rand() );
不等价。a0
到a7
)用于访问内存(地址寄存器),另外8个(d0
到d7
)用于算术运算(数据寄存器)。该体系结构的有效调用约定为:
d0
和d1
;其余在堆栈上传递。a0
和a1
;其余在堆栈上传递。void foo(int i, void *p)
,您将在d0
中传递i
,在a0
中传递p
。void bar(void *p, int i)
,您也将在d0
中传递i
,在a0
中传递p
。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
并从堆栈中检索参数。但是你没有在堆栈上传递任何参数!这将读取未定义的堆栈垃圾,当尝试像字符串指针一样打印它时,程序很可能会崩溃。C标准,7.21.6.1,《fprintf》函数仅规定参数 p
应为指向 void
的指针。
p
参数必须是void
类型的指针。
根据附录J.2,这是一个约束条件,违反约束条件会导致未定义行为。
(以下是我之前的推理,它太复杂了。)
该段落没有描述如何从 ...
中检索 void*
,但 C 标准本身提供的唯一方法是 7.16.1.1,《va_arg》宏,该宏警告我们
如果 type 与实际下一个参数的类型不兼容(根据默认参数提升进行推广),则行为未定义
如果你读 6.2.7,兼容类型和组合类型,则没有暗示 void*
和 int
应该是兼容的,无论它们的大小如何。所以,我认为由于 va_arg
是在标准C中实现 printf
的唯一方法,因此其行为是未定义的。
char
应该与signed char
或unsigned char
具有相同的范围、表示和行为”,但“无论做出什么选择,char
都是与其他两个类型不兼容的单独类型。” - Fred Foo...
中检索void*
。如果您在规范中写明“应为指向void的指针”,但传递了一个int
,则意味着您违反了约束并触发了UB。 - Raymond Chenva_args
,在整数和指针之间进行转换是允许的。根据我的C11最终草案,6.3.2.3.5
:“整数可以转换为任何指针类型。除非先前指定,否则结果是实现定义的…”(“先前指定”是空指针常量)。类型intptr_t
被特别提供作为一个整数类型,它将正确地工作。但这仍然没有回答有关打印无效指针的问题。但是实现定义不等于未定义。 - BoBTFish...
中,没有可能进行转换,因为参数在调用 va_arg
之前没有类型可转换 到。 - Fred Foo%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
%p
也用于控制从 va_args
中提取类型的方式。错误地从 va_args
中提取是未定义的行为。 - Flexo
p
”参数应该是一个指向void的指针。指针的值将以实现定义的方式转换为一系列打印字符。虽然void*
无法被解引用,因此不需要可解引用,但我认为这是实现定义的。 - BoBTFishprintf("%p", reinterpret_cast<void*>(rand()));
- MSaltersprintf("%p", (void *) rand());
。注意问题的标签是C
。 - ninjaljc++
和reinterpret-cast
标签吗?编辑:我刚刚检查了问题的编辑历史。抱歉 ninjalj,标签已经被更改然后又改回来了。 - BoBTFish