C 最好是未定义的行为,是边界断裂吗-糟糕的指针算法?或者干脆忽略别名?

C 最好是未定义的行为,是边界断裂吗-糟糕的指针算法?或者干脆忽略别名?,c,c99,undefined-behavior,pointer-arithmetic,strict-aliasing,C,C99,Undefined Behavior,Pointer Arithmetic,Strict Aliasing,我现在在c99上工作了几个星期,专注于未定义的行为。 我想在遵守规则的同时测试一些奇怪的代码。 结果是这个代码: (请原谅我的变量名,我吃了一个小丑) 因此,这段代码最好是未定义的行为。 我得到了不同的结果,无论是我没有访问任何我不拥有的内存区域,还是访问任何未初始化的内存。 (阿法克) 第一个关键规则是,我不允许添加或减去指针,这会使它们离开数组边界。但我可以把一个指针转换成整数,我可以用它来计算,就像我想的,不是吗 我的第二个假设是,我可以为指针分配一个有效的地址,将这个计算出的地址分配给指

我现在在c99上工作了几个星期,专注于未定义的行为。 我想在遵守规则的同时测试一些奇怪的代码。 结果是这个代码:

(请原谅我的变量名,我吃了一个小丑)

因此,这段代码最好是未定义的行为。 我得到了不同的结果,无论是我没有访问任何我不拥有的内存区域,还是访问任何未初始化的内存。 (阿法克)

第一个关键规则是,我不允许添加或减去指针,这会使它们离开数组边界。但我可以把一个指针转换成整数,我可以用它来计算,就像我想的,不是吗

我的第二个假设是,我可以为指针分配一个有效的地址,将这个计算出的地址分配给指针是一个有效的操作。 因为我使用的是char指针,所以也不会违反严格的别名规则,因为char*可以对任何内容进行别名

那么,哪个规则被打破了,这导致了UB

单变量是否也可以理解为“数组”,我打破了这个规则

-将指针加减到数组对象和 integer类型生成的结果不指向或刚好超出同一数组 对象(6.5.6)

如果是这样,我也可以这样做

int var;
int *ptr;
ptr = &var;
ptr = ptr + 1;
因为结果几乎可以肯定是未定义的行为。 使用MSVC2010编译时,它会输出预期的“U”, 但在使用clang和gcc的freeBSD上,根据优化级别,每次都会得到非常有趣和不同的结果。 (在我看来,巴哈维奥的定义不应该如此)


那么,你知道是什么导致了鼻龙吗?

你基本上是在将
int
转换为
char*
的作业中遇到了第6.3.2.3段指针ad 5

整数可以转换为任何指针类型。除非之前另有规定,否则 结果是定义了实现,可能未正确对齐,可能未指向 引用类型的实体,并且可能是陷阱表示形式

所有
abs
功能的使用使其非常依赖于实际实现情况。基本上,只有当
ITargetOfPointerAccess
的地址高于
StartVar
时,它才会工作。如果您丢失了所有出现的
abs
,我认为您将在大多数(如果不是所有的话)架构和大多数(如果不是所有的话)编译器上得到
'U'

具有讽刺意味的是,这不是未定义的行为,而是实现定义的行为。但是当您没有得到
'U'
时,
访问指针
没有指向引用类型的实体,很可能它根本没有指向实体

如果它不指向实体,那么(当然)在第6.5.3.2 ad 4段之后的
printf
中取消引用它时,您将遇到未定义的行为

一元*运算符表示间接。如果操作数指向函数,则结果为函数指示符;如果它指向一个对象,结果是一个指定该对象的左值。如果操作数的类型为“指向类型的指针”,则结果的类型为“类型”。如果为指针指定了无效值,则一元*运算符的行为未定义

让我们详细说明两个场景,其中堆栈上的所有地址都设置了位31,这在Linux下非常常见

场景A:假设
&StartVar<&itargeofpointeracces
,然后

  abs(LegalPointerCast1) - abs(LegalPointerCast2)
= LegalPointerCast2 - LegalPointerCast1 (by both < 0)
= (char*)(&StartVar) - (char*)(&iTargetOfPointeracces)
< 0 (by &StartVar < &iTargetOfPointeracces)
So uiDiffOfVars = (char*)(&StartVar) - (char*)(&iTargetOfPointeracces)
and signedIntToRespectTheRules = -uiDiffOfVars (by (int)uiDiffOfVars < 0)
thus  TheAccesingPointer
= (char *)(&StartVar + (char*)(&iTargetOfPointeracces) - (char*)(&StartVar))
= (char*)(&iTargetOfPointeracces)
  abs(LegalPointerCast1) - abs(LegalPointerCast2)
= LegalPointerCast2 - LegalPointerCast1 (by both < 0)
= (char*)(&StartVar) - (char*)(&iTargetOfPointeracces)
> 0 (by &StartVar > &iTargetOfPointeracces)
So uiDiffOfVars = (char*)(&StartVar) - (char*)(&iTargetOfPointeracces)
and signedIntToRespectTheRules = uiDiffOfVars (by (int)uiDiffOfVars > 0)
thus TheAccesingPointer
= (char *)(&StartVar + (char*)(&StartVar) - (char*)(&iTargetOfPointeracces))
= (char *)(2*(char*)&StartVar - (char*)(&iTargetOfPointeracces))
在这种情况下,访问指针不太可能指向某个实体,因此在取消引用该指针时会触发未定义的行为。因此,我的观点是,
访问指针的计算是由实现定义的,上面的计算非常常见。如果计算出的指针未指向
ItargetOfPointerAccess
,如场景B所示,则会触发未定义的行为

不同的优化级别可能导致堆栈上的
StartVar'和
iTargetOfPointeracces'的顺序不同,这可能解释了不同优化级别的不同结果


我认为单变量不能算作数组。

一个实现可能只定义
uintpr\t
intptr\t
,前提是它可以保证两件事:

  • 将有效或空指针转换为这些类型之一的行为将产生定义的行为

  • 如果该类型的某个值q在数值上等于此转换的结果,并且如果由转换指针标识的对象仍然存在,则将类型q的值转换回原始指针类型将生成一个与原始指针类型比较相等的指针

  • 如果uintptr\u t是64位无符号整数类型,则代码可以将任何有效指针转换为uintptr\u t,并像其他任何64位无符号整数一样对其进行操作,而不考虑原始对象的大小或任何其他因素。另一方面,从标准的角度来看,将这种转换的结果转换回指针类型只会在结果数字与从仍然有效的指针到uintptr_t的早期转换结果相匹配的情况下产生定义的行为

    请注意,顺便说一句,许多实现记录了指针和
    uintpttr\t
    值之间的关系,其程度远远超出了标准的要求,但这并不意味着代码利用这些知识将实际工作。例如,给定代码:

    static int x,y;
    int test(void)
    {
      int *p = outsideFunction(&x);
      y=1;
      *p=5;
      return y;
    }
    
    一些执行可能会记录程序员可以确定x和y的相对位移的方法。然而,即使是这样的实现,也可能生成这样的代码,即对*p的写入不可能影响y,因为它是一个从来没有地址的静态对象
    static int x,y;
    int test(void)
    {
      int *p = outsideFunction(&x);
      y=1;
      *p=5;
      return y;
    }