【问题标题】:Is %p specifier only for valid pointers?%p 说明符是否仅适用于有效指针?
【发布时间】:2012-07-26 03:40:34
【问题描述】:

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

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

这是否会因为传递的值不是有效指针而不是 %p 而成为未定义行为?

【问题讨论】:

  • 标准确实声明如果转换规范无效,则行为未定义
  • 我想这归结为指针到底有多特别的问题。我很想看到一个好的答案。
  • "p 参数应为指向 void 的指针。指针的值以实现定义的方式转换为打印字符序列。"好吧,void* 无论如何都不能被取消引用,所以它不必是可取消引用的,但我认为它是定义的实现。
  • 即使这在理论上是有效的,它仍然需要演员:printf( "%p", reinterpret_cast<void*>(rand()) );
  • @MSalters:你的意思是printf("%p", (void *) rand());。注意问题被标记为C

标签: c++ pointers reinterpret-cast


【解决方案1】:

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 应该兼容。所以,我想说因为va_arg 是在标准C 中实现printf 的唯一方法,所以行为是未定义的。

【讨论】:

  • 不知道在标准中的何处查找,但不是将指针存储为足够大的整数类型,然后再将其存储回来就可以保证安全吗?而且由于讨论的整数类型不大于指针,所以将整数转换为指针应该是安全的,不是吗?
  • @BoBTFish: 7.16.1.1 明确地为不兼容的类型提供了 UB,并且“足够大”并不是两种类型兼容的充分条件。例如,在 6.2.5 之前,“char [shall] 具有与 signed charunsigned char 相同的范围、表示和行为”,但“[i]无论做出何种选择,char 都是与其他两种不同的类型,并且与任何一种都不兼容。”
  • 语言规范不需要描述如何从... 中检索void*。当规范说“应该是指向void”的指针时,您传递了int,这意味着您违反了约束并触发了UB。
  • 除了 va_args,整数和指针之间的转换允许的。从我的 c11 最终草案中,6.3.2.3.5:“整数可以转换为任何指针类型。除非前面指定,结果是实现定义的......”(“以前指定”是空指针常量)。 intptr_t 类型专门作为整数类型提供,正常工作。尽管如此,仍然没有回答有关打印无效指针的问题。但是定义的实现不是未定义的。
  • @RaymondChen:好点。查找参考并缩短答案。
【解决方案2】:

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

摩托罗拉 68000 处理器有 16 个寄存器用于一般计算,但它们并不等效。其中八个(命名为a0a7)用于访问内存(地址寄存器),另外八个(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 并从堆栈中检索参数。但是您没有在堆栈上传递任何参数!这将读取未定义的堆栈垃圾,当它试图打印它时,它很可能会使你的程序崩溃,就像它是一个字符串指针一样。

【讨论】:

    【解决方案3】:

    %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 中提取是未定义的行为。
    【解决方案4】:

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

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

    带脚注:

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

    【讨论】:

    • 但是什么是“使用”指针值?这不涉及取消引用吗?
    • @larsmans:不,取消引用它被称为“取消引用”,而不是“使用”。使用对象的值意味着它出现在需要 rvalue 的表达式中;例如,作为函数参数。
    猜你喜欢
    • 2019-10-15
    • 2012-05-14
    • 1970-01-01
    • 2011-10-08
    • 2011-10-19
    • 2019-06-11
    • 2014-03-03
    • 1970-01-01
    相关资源
    最近更新 更多