%p格式符是否只用于有效指针?

20

假设在我的平台上 sizeof(int)==sizeof(void*),并且我有以下代码:

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

因为在%p的位置传递了一个非有效指针的值,这会导致未定义行为吗?


4
标准规定:如果转换说明符是无效的,则行为未定义。 - cnicutar
我猜这归结为指针到底有多特殊的问题。我非常好奇能否得到一个好的答案。 - Sergey Kalinichenko
p”参数应该是一个指向void的指针。指针的值将以实现定义的方式转换为一系列打印字符。虽然void*无法被解引用,因此不需要可解引用,但我认为这是实现定义的。 - BoBTFish
即使在理论上这是有效的,它仍然需要一个转换:printf("%p", reinterpret_cast<void*>(rand())); - MSalters
@MSalters:你是说 printf("%p", (void *) rand());。注意问题的标签是 C - ninjalj
@ninjalj 这个问题有 c++reinterpret-cast 标签吗?编辑:我刚刚检查了问题的编辑历史。抱歉 ninjalj,标签已经被更改然后又改回来了。 - BoBTFish
4个回答

20
为了补充 @larsman 的回答(它说由于您违反了限制,因此行为是未定义的),这里有一个实际的C实现,其中 sizeof(int) == sizeof(void*),但是该代码与 printf( "%p", (void*)rand() );不等价。
Motorola 68000处理器有16个用于通用计算的寄存器,但它们并不相等。其中8个(命名为a0a7)用于访问内存(地址寄存器),另外8个(d0d7)用于算术运算(数据寄存器)。该体系结构的有效调用约定为:
  1. 将前两个整数参数传递给d0d1;其余在堆栈上传递。
  2. 将前两个指针参数传递给a0a1;其余在堆栈上传递。
  3. 无论大小,所有其他类型都在堆栈上传递。
  4. 堆栈上传递的参数从右到左推入,而不考虑类型。
  5. 基于堆栈的参数在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 指向通过 ... 传递的第一个基于堆栈的参数,而 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()) 会将格式字符串传递到 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 并从堆栈中检索参数。但是你没有在堆栈上传递任何参数!这将读取未定义的堆栈垃圾,当尝试像字符串指针一样打印它时,程序很可能会崩溃。

13

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 的唯一方法,因此其行为是未定义的。


不知道在标准中该去哪里查找,但是将指针存储在足够大的整数类型中,然后再将其存储回来,这不是保证安全吗?而且讨论中的整数类型并不比指针更大,将整数转换为指针应该是安全的,对吧? - BoBTFish
@BoBTFish:7.16.1.1明确规定了非兼容类型的UB,而“足够大”并不是两种类型兼容的充分条件。例如,根据6.2.5,“char应该与signed charunsigned char具有相同的范围、表示和行为”,但“无论做出什么选择,char都是与其他两个类型不兼容的单独类型。” - Fred Foo
1
语言规范不需要描述如何从...中检索void*。如果您在规范中写明“应为指向void的指针”,但传递了一个int,则意味着您违反了约束并触发了UB。 - Raymond Chen
除了va_args,在整数和指针之间进行转换是允许的。根据我的C11最终草案,6.3.2.3.5:“整数可以转换为任何指针类型。除非先前指定,否则结果是实现定义的…”(“先前指定”是空指针常量)。类型intptr_t被特别提供作为一个整数类型,它将正确地工作。但这仍然没有回答有关打印无效指针的问题。但是实现定义不等于未定义。 - BoBTFish
@RaymondChen: 很有道理。查了一下相关资料,简化了回答。 - Fred Foo
@BoBTFish:是的,可以进行转换,但在 ... 中,没有可能进行转换,因为参数在调用 va_arg 之前没有类型可转换 - Fred Foo

5
是的,它是未定义的。来自C++11的3.7.4.2/4:
使用无效指针值(包括将其传递给解除分配函数)的效果是未定义的。
附注:
在某些实现中,它会导致系统生成的运行时故障。

但是“使用”指针值是什么意思呢?那不是涉及到解引用吗? - Fred Foo
2
@larsmans:不,解引用它被称为“dereferencing”,而不是“using”。使用对象的值意味着它出现在需要_rvalue_的表达式中;例如,作为函数参数。 - Mike Seymour

-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

1
%p 也用于控制从 va_args 中提取类型的方式。错误地从 va_args 中提取是未定义的行为。 - Flexo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接