只对有效指针%P符?(Is %p specifier only for valid pointers

2019-07-30 15:30发布

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

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

请问这个是不确定的,因为传递的值不到位的一个有效的指针的行为%p

Answer 1:

要在@ larsman的回答(它说,因为你违反了约束,则行为是不确定的)展开,这里是一个实际的C实现,其中sizeof(int) == sizeof(void*) ,但代码不等于printf( "%p", (void*)rand() );

摩托罗拉68000处理器具有用于通用计算16个寄存器,但它们是不等价的。 其中八(命名为a0通过a7 )用于存取存储器(地址寄存器),另一个八( d0d7 )用于算术(数据寄存器)。 此架构有效的调用约定将

  1. 首次通过两个整数参数d0d1 ; 在栈上传递的休息。
  2. 通过在头两个指针参数a0a1 ; 在栈上传递的休息。
  3. 通过在堆栈上所有其他类型,不分大小。
  4. 在栈上传递的参数都推从右到左类型无关。
  5. 基于堆栈的参数都在4字节边界对齐。

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

例如,调用函数void foo(int i, void *p)你会通过id0pa0

需要注意的是调用函数void bar(void *p, int i) ,你也将通过id0pa0

根据这些规则, 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和检索从堆栈中的参数。 但是,你没有通过堆栈上的任何参数! 这将读取未定义堆的垃圾,当它试图将其打印出来,好像它是一个字符串指针,将最有可能你的程序崩溃。



Answer 2:

C标准,7.21.6.1,在fprintf的功能,只是规定

p的参数应是一个指针void

通过附录J.2,这是一种约束 ,以及违反约束导致UB。

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

该段没有描述如何将void*从检索... ,但C标准本身提供用于此目的的唯一途径是7.16.1.1,将va_arg宏,它提醒我们,

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

如果你读了6.2.7,兼容型和复合型,那么就没有暗示void*int应该是兼容的,无论大小。 所以,我想说的是,因为va_arg是实现的唯一途径printf 标准C,该行为是不确定的。



Answer 3:

是的,这是不确定的。 从C ++ 11,3.7.4.2/4:

使用无效指针值(包括将它传递给一个释放函数)的效果是不明确的。

一个注脚:

在一些实现,它会导致系统运行时产生的故障。



Answer 4:

%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


文章来源: Is %p specifier only for valid pointers?