来自不相关块的指针的比较(>,> =,<,< =)

时间:2017-12-16 10:49:26

标签: c arrays pointers memory-management language-lawyer

我正在查看obstack的GNU实现,我注意到obstack_free子例程正在使用指针比较链接列表的前一个链接的开头和结尾,以找到指针指向哪个块被释放属于。

https://code.woboq.org/userspace/glibc/malloc/obstack.c.html

while (lp != 0 && ((void *) lp >= obj || (void *) (lp)->limit < obj))
{
     plp = lp->prev;
     CALL_FREEFUN (h, lp);
     lp = plp;
     h->maybe_empty_object = 1;
} //...

根据http://port70.net/~nsz/c/c11/n1570.html#6.5.8p5

,这种比较似乎未定义
  

当比较两个指针时,结果取决于相对值   指向的对象的地址空间中的位置。如果两个   指向对象类型的指针既指向同一个对象,也指向两个指向   一个超过同一个数组对象的最后一个元素,他们进行比较   等于。如果指向的对象是同一聚合的成员   对象,稍后声明的结构成员的指针比较大   而不是指向结构中先前声明的成员的指针,和   指向具有较大下标值的数组元素的指针   大于指向同一数组元素的指针   下标值。指向同一个union对象的成员的所有指针   比较平等。如果表达式P指向数组的元素   对象和表达式Q指向同一个的最后一个元素   数组对象,指针表达式Q + 1比较大于P.   在所有其他情况下,行为未定义。

是否有完全符合标准的方式来实施obstacks。如果没有,这种比较几乎可以在哪些平台上突破?

1 个答案:

答案 0 :(得分:1)

我不是语言律师,所以我不知道如何回答OP的问题,除了简单阅读标准并没有描述整个画面。

虽然标准说比较不相关的指针会产生不确定的结果,但符合标准的C编译器的行为受到更多限制。

关于指针比较的部分中的第一句是

  

当比较两个指针时,结果取决于指向的对象的地址空间中的相对位置。

并且有很好的理由。

如果我们检查指针比较代码如何使用的可能性,我们发现除非编译器能够在编译时确定比较指针属于哪些对象,否则同一地址空间中的所有指针必须在算术上进行比较。他们所指的地址。

(如果我们证明标准要求符合标准的C编译器提供特定结果,当C标准本身的简单读数表明结果未定义时,是否符合这些符号标准?我不知道知道。我只知道这样的代码在实践中有用。)

对标准的字面解释可能导致人们相信绝对没有办法确定指针是否指向数组元素。特别是,观察

int is_within(const char *arr, const size_t len, const char *ptr)
{
    return (ptr >= arr) && (ptr < (arr + len));
}

符合标准的C编译器可以决定,因为不相关的指针之间的比较是未定义的,所以将上述函数优化为

是合理的。
int is_within(const char *arr, const size_t len, const char *ptr)
{
    if (size)
        return ptr != (arr + len);
    else
        return 0;
}

对于数组const char arr[len]中的指针返回1,对于刚好超过数组末尾的元素返回0,就像标准要求一样;所有未定义的案例都是1。

当一个调用者在一个单独的编译单元中做出这样的思考时,会产生这种思路的问题。

char  buffer[1024];
char *p = buffer + 768;
if (is_within(buffer, (sizeof buffer) / 2, p)) {
    /* bug */
} else {
    /* correct */
}

显然,如果is_within()函数被声明为static(或static inline),编译器可以检查最终在is_within()中的所有调用链,并生成正确的代码

但是,当is_within()与其调用者相比处于单独的编译单元中时,编译器不能再做出这样的假设:它不会事先知道对象边界,也无法知道。相反,它可以由符合标准的C编译器实现的唯一方法是依赖于指针所指的地址,盲目地;

之类的东西
int is_within(const char *arr, const size_t len, const char *ptr)
{
    const uintptr_t  start = POINTER_TO_UINTPTR(arr);
    const uintptr_t  limit = POINTER_TO_UINTPTR(arr + len);
    const uintptr_t  thing = POINTER_TO_UINTPTR(ptr);

    return (thing >= start) && (thing < limit);
}

其中POINTER_TO_UINTPTR()将是编译器内部宏或函数,它将指针无损地转换为无符号整数值(意图是会有相应的UINTPTR_TO_POINTER()可以恢复精确来自无符号整数值的相同指针),不考虑C标准允许的任何优化或规则。

因此,如果我们假设代码是在一个单独的编译单元中编译给用户的,那么编译器就会被迫生成代码,这些代码提供了比C标准的简单读数更多的保证。

特别是,如果arrptr 在同一地址空间,则C编译器必须生成比较指针指向的地址的代码,即使C标准表示,无关指针的比较会产生不确定的结果;仅仅因为理论上至少可以使一个对象数组占据地址空间的任何子区域。编译器无法做出以后破坏符合C代码的假设。

在GNU obstack实现中,obstack都存在于同一地址空间中(因为它们是从OS /内核获取的)。代码假定提供给它的指针引用这些对象。虽然代码确实在检测到指针无效时返回错误,但它并不能保证它总是检测到无效指针;因此,我们可以忽略无效指针的情况,并简单地假设因为所有的障碍都来自同一个地址空间,所以用户提供的所有指针都是如此。

有许多架构具有多个地址空间。带有分段内存模型的x86就是其中之一。许多微控制器具有Harvard architecture,具有用于代码和数据的单独地址空间。一些微控制器具有单独的地址空间(不同的机器指令),用于访问RAM和闪存(但能够从两者执行),等等。

甚至可能存在一种体系结构,其中每个指针不仅具有其存储器地址,而且还具有与其相关联的某种唯一对象ID。这没什么特别的;它只是意味着在这样的架构上,每个对象都有自己的地址空间。