为了扩展@larsman 的答案(它说由于您违反了约束,行为未定义),这是一个实际的 C 实现,其中 sizeof(int) == sizeof(void*),但代码不等同于 printf( "%p", (void*)rand() );
摩托罗拉 68000 处理器有 16 个寄存器用于一般计算,但它们并不等效。其中八个(命名为a0 到a7)用于访问内存(地址寄存器),另外八个(d0 到d7)用于算术(数据寄存器)。此架构的有效调用约定是
- 传递
d0和d1中的前两个整数参数;将其余部分传递到堆栈中。
- 传递
a0和a1中的前两个指针参数;将其余部分传递到堆栈中。
- 传递堆栈中的所有其他类型,无论大小。
- 无论类型如何,在堆栈上传递的参数都是从右向左推送的。
- 基于堆栈的参数按 4 字节边界对齐。
这是一个完全合法的调用约定,类似于许多现代处理器使用的调用约定。
例如,要调用函数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 并从堆栈中检索参数。但是您没有在堆栈上传递任何参数!这将读取未定义的堆栈垃圾,当它试图打印它时,它很可能会使你的程序崩溃,就像它是一个字符串指针一样。