20

假设在我的平台上sizeof(int)==sizeof(void*),我有这个代码:

printf( "%p", rand() );

由于传递了一个不是有效指针的值来代替,这是否会成为未定义的行为%p

4

4 回答 4

20

为了扩展@larsman 的答案(它说由于您违反了约束,因此行为未定义),这是一个实际的 C 实现 where sizeof(int) == sizeof(void*),但代码不等同于printf( "%p", (void*)rand() );

摩托罗拉 68000 处理器有 16 个寄存器用于一般计算,但它们并不等效。a0其中八个(通过命名a7)用于访问内存(地址寄存器),另外八个(d0通过d7)用于算术(数据寄存器)。此架构的有效调用约定是

  1. 将前两个整数参数传入d0and d1; 将其余的传递到堆栈上。
  2. 将前两个指针参数传入a0and a1; 将其余的传递到堆栈上。
  3. 传递堆栈上的所有其他类型,无论大小。
  4. 无论类型如何,在堆栈上传递的参数都是从右向左推送的。
  5. 基于堆栈的参数在 4 字节边界上对齐。

这是一个完全合法的调用约定,类似于许多现代处理器使用的调用约定。

例如,要调用函数,您void foo(int i, void *p)将传入和传入id0pa0

请注意,要调用该函数,您void bar(void *p, int i)还需要传入和传入id0pa0

在这些规则下,printf("%p", rand())将传递格式字符串 ina0和随机数参数 in d0。另一方面,printf("%p", (void*)rand())将传递格式字符串 ina0和随机指针参数 in a1

va_list结构如下所示:

struct va_list {
    int d0;
    int d1;
    int a0;
    int a1;
    char *stackParameters;
    int intsUsed;
    int pointersUsed;
};

前四个成员使用寄存器的相应条目值进行初始化。通过、 和传递stackParameters的第一个基于堆栈的参数的指向被初始化为命名参数的数量,它们分别是整数和指针。...intsUsedpointersUsed

宏是一个编译器内在函数,它根据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())将传递格式字符串 ina0和随机整数 in d0。但是当printf函数执行时,它会看到%p格式并执行 a va_arg(ap, void*),它将使用get_pointer_arg和读取参数 froma1而不是d0. 由于a1未初始化,它包含垃圾。您生成的随机数将被忽略。

进一步举个例子,如果你有printf("%p %i %s", rand(), 0, "hello");这将被称为如下:

  • a0= 格式字符串的地址(第一个指针参数)
  • a1= 字符串地址"hello"(第二个指针参数)
  • d0= 随机数(第一个整数参数)
  • d1= 0(第二个整数参数)

When the printf function executes, it reads the format string from a0 as expected. When it sees the %p it will retrieve the pointer from a1 and print it, so you get the address of the string "hello". Then it will see the %i and retrieve the parameter from d0, so it prints a random number. Finally, it sees the %s and retrieves the parameter from the stack. But you didn't pass any parameters on the stack! This will read undefined stack garbage, which will most likely crash your program when it tries to print it as if it were a string pointer.

于 2012-07-27T14:28:49.487 回答
13

C 标准,7.21.6.1,fprintf函数,只是声明

p 参数应是指向 的指针void

根据附录 J.2,这是一个约束,违反约束会导致 UB。

(以下是我之前的推理为什么应该是UB,这太复杂了。)

该段没有描述如何从void*中检索...,但 C 标准本身为此目的提供的唯一方法是 7.16.1.1,va_arg宏,它警告我们

如果type与实际下一个参数的类型不兼容(根据默认参数提升),则行为未定义

如果您阅读 6.2.7,兼容类型和复合类型,那么无论它们的大小如何,都没有暗示void*并且int应该兼容。所以,我想说,因为这是在标准 Cva_arg中实现的唯一方法,所以行为是未定义的。printf

于 2012-07-27T13:11:08.340 回答
5

是的,它是未定义的。从 C++11、3.7.4.2/4 开始:

使用无效指针值(包括将其传递给释放函数)的效果是未定义的。

带脚注:

在某些实现中,它会导致系统生成的运行时错误。

于 2012-07-27T13:04:21.050 回答
-2

%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
于 2012-07-27T13:06:22.800 回答