【问题标题】:Safely checking for overlapping memory regions安全检查重叠的内存区域
【发布时间】:2020-11-25 02:34:25
【问题描述】:

我正在努力加深我在 C 方面的知识和经验,所以我正在编写一些小实用程序。

我正在复制内存,根据memcpy(3) 的手册页:

注意事项

未能遵守内存区域不重叠的要求 真正的错误的来源。 (POSIX 和 C 标准明确表示采用 具有重叠区域的 memcpy() 会产生未定义的行为。)最值得注意的是,在 glibc 2.13 memcpy() 在某些平台上的性能优化(包括 x86-64) 包括更改字节从 src 复制到 dest 的顺序。

显然,传递给memcpy(3) 的重叠内存区域可以cause a lot of problems

我正在尝试编写一个安全的包装器作为学习 C 的一部分,以确保这些内存区域不会重叠:

int safe_memcpy(void *dest, void *src, size_t length);

我试图实现的逻辑是:

  • 检查NULL 的源指针和目标指针。
  • 使用长度参数为源和目标建立指针“范围”。
  • 确定源范围是否与目标范围相交,反之亦然。

到目前为止我的实现:

#define SAFE_MEMCPY_ERR_NULL 1
#define SAFE_MEMCPY_ERR_SRC_OVERLAP 2
#define SAFE_MEMCPY_ERR_DEST_OVERLAP 3

int safe_memcpy(void *dest, void *src, size_t length) {
    if (src == NULL || dest == NULL) {
        return SAFE_MEMCPY_ERR_NULL;
    }

    void *dest_end = &dest[length - 1];
    void *src_end = &src[length - 1];

    if ((&src >= &dest && &src <= &dest_end) ||
            (&src_end >= &dest && &src_end <= &dest_end)) {
        // the start of src falls within dest..dest_end OR
        // the end of src falls within dest..dest_end
        return SAFE_MEMCPY_ERR_SRC_OVERLAP;
    }

    if ((&dest >= &src && &dest <= &src_end) ||
            (&dest_end >= &src && &dest_end <= &src_end)) {
        // the start of dest falls within src..src_end
        // the end of dest falls within src..src_end
        return SAFE_MEMCPY_ERR_DEST_OVERLAP;
    }

    // do the thing
    memcpy(dest, src, length);

    return 0;
}

可能有更好的方法来处理错误,但这就是我现在所拥有的。

我很确定我在这段代码中触发了一些未定义的行为,因为我在不重叠的内存区域上点击了SAFE_MEMCPY_ERR_DEST_OVERLAP。当我使用调试器检查状态时,我看到(例如)以下值:

  • src: 0x7ffc0b75c5fb
  • src_end: 0x7ffc0b75c617
  • dest: 0x1d05420
  • dest_end: 0x1d0543c

显然,这些地址甚至没有远程重叠,因此我认为我正在触发 UB,并且编译器警告表明如下:

piper.c:68:27: warning: dereferencing ‘void *’ pointer
     void *dest_end = &dest[length - 1];

It seems 我需要将指针转换为不同的类型,但我不确定要使用哪种类型:内存是无类型的,所以我应该使用char * 将内存“查看”为字节?如果是这样,我应该将所有内容都转换为char *吗?我应该改用intptr_t 还是uintptr_t

给定两个指针和每个指针的长度,我如何安全地检查这些区域是否相互重叠?

【问题讨论】:

    标签: c


    【解决方案1】:

    首先,符合标准的程序不能对void * 类型的指针执行指针运算,也不能(相关地)对其应用索引运算符,即使对索引0 也不行。 void 是一个不完整的类型,它的独特之处在于它无法完成。最相关的含义是该类型不传达任何关于它所指向的事物大小的信息,并且指针算术是根据所指向的对象来定义的。

    所以是的,像 &amp;dest[length - 1] 这样的表达式相对于 C 标准具有未定义的行为。一些实现提供了影响它的扩展,而另一些则在编译时拒绝了此类代码。原则上,实现可以接受代码并用它做一些奇怪的事情,但这相对不太可能。

    其次,你建议

    编写一个安全包装器作为学习 C 的一部分,以确保这些内存区域不重叠

    ,但是对于通用指针没有一致的方法可以做到这一点。指针比较和指针差异仅针对指向同一数组(或指向数组末尾之后的一个元素)的指针定义,其中指向标量的指针在这方面被视为指向 1 维数组的第一个元素的指针.

    转换为不同的指针类型,可能是char *,将解决指针算术问题,但在一般情况下不会解决指针可比性问题。它可能会从某些实现中准确地得到您想要的行为,甚至可靠,但它不是解决问题的一致方法,并且随后的未定义行为可能会在其他实现中产生真正的错误。

    相对经常,您可以静态地知道指针不指向重叠区域。特别是,如果有问题的指针是指向范围内局部变量的指针或指向由当前函数分配的内存块的指针,那么您通常可以确定是否存在重叠。对于您不知道,或者您知道肯定有重叠的情况,正确的方法是使用memmove() 而不是memcpy()

    【讨论】:

    • 我之前读过this article 关于指针的内容,这似乎是对基本问题的回答:“指针既不是简单的,也不是(只是)整数。”但是,由于我仍然只是在学习 C 的基本原理,所以它并没有立即点击,但删除 void * 是有意义的。
    【解决方案2】:

    这个“安全”memcpy 并不安全,因为它不会在程序需要它时复制任何内容。使用memmove 是安全的

    您不应使用&amp;src&amp;dest,因为它不是数据或缓冲区的开头,而是参数srcdest 本身的地址。

    srcenddestend 也是如此

    【讨论】:

    • 回复:“不复制:”是的,它是一个不同的 API,而不是一个替代的替代品,一个不同的、容易出错的返回签名。至于使用&amp;src/&amp;dest,我正在尝试比较地址本身以检查重叠,我应该不使用地址运算符吗?
    • 正如我所写的 &amp;src 并没有给你 sourcerc 缓冲区的开头,只是src 参数(变量)的地址。您应该只使用 src 和 dest。或者,如果您有理解问题,请使用 &amp;src[0] ....
    【解决方案3】:

    给定两个指针和每个指针的长度,我如何安全地检查这些区域是否相互重叠?

    &lt;&lt;=&gt;=&gt; 在 2 个指针与同一个对象不相关时未定义。

    一种乏味的方法检查一个端点与另一个元素的所有端点,并利用源和目标的长度相同。

    int safe_memcpy(void *dest, const void *src, size_t length) {
      if (length > 0) {
        unsigned char *d = dest;
        const unsigned char *s = src;
        const unsigned char *s_last = s + length - 1;
    
        for (size_t i = 0; i < length; i++) {
          if (s == &d[i]) return 1; // not safe
          if (s_last == &d[i]) return 1; // not safe
        }
    
        memcpy(dest, src, length);
      }
      return 0;
    }
    

    如果缓冲区长度不同,请检查较短的端点与较长元素的地址。


    我应该将所有内容都转换为char *

    使用unsigned char *
    mem...()str...() 的行为就像每个数组元素都是unsigned char

    对于本子条款中的所有函数,每个字符都应被解释为具有unsigned char 类型(因此每个可能的对象表示都是有效的并且具有不同的值)。 C17dr § 7.24.1 3

    对于罕见的非 2 补码,unsigned char 对于避免 已签名 char 陷阱和保持 -0、+0 独特性很重要。字符串仅在 +0 处停止。

    对于使用整数数学的int strcmp/memcmp()unsigned char 等函数,在比较[0...CHAR_MAX] 范围之外的元素以返回正确签名的结果时,这一点很重要。


    即使void * 允许索引,void *dest_end = &amp;dest[length - 1];length == 0 就像&amp;dest[SIZE_MAX]; 时非常糟糕


    &amp;src &gt;= &amp;dest s/b src &gt;= dest 甚至有机会工作。

    src, dest 的地址与副本无关,重要的是它们的值。

    我怀疑这个错误代码会导致 OP 的其他代码中出现 UB。


    我应该改用intptr_t 还是uintptr_t

    请注意,(u)intptr_t 是可选类型 - 它们可能不存在于符合标准的编译器中。

    即使类型存在,指针上的数学也没有定义为与整数值上的数学相关。


    显然,这些地址甚至没有远程重叠,因此我认为我正在触发 UB,

    “显然”如果一个 假设一个线性映射地址到整数,在 C 中没有指定的东西。

    【讨论】:

      【解决方案4】:

      内存是无类型的,所以我应该使用 char * 将内存“查看”为字节吗?如果是这样,我应该将所有内容都转换为 char * 吗?

      如果您需要取消引用数据,请使用unsigned char*,或者如果您想按字节数递增/递减指针值,请使用char*

      It's common 做:

      void a_function_that_takes_void(void *x, void *y) {
          char *a = x;
          char *b = y;
          /* uses a and b throughout here */
      }
      

      如果是这样,我应该将所有内容都转换为 char * 吗?

      是的。这也很常见:

       void_pointer = (char*)void_pointer + 1;
      

      我应该改用 intptr_t 还是 uintptr_t?

      您可以,但这与使用 char* 相同,除了 char*intptr_t 的转换。

      如何安全地检查这些区域是否相互重叠?

      做一些研究很好。 how to implement overlap-checking memcpy in C

      【讨论】:

      • 这很有趣,因为几乎所有严格的可移植性狂热者都使用gcc,它不符合 C 的编译器,实际上实现了自己的 C 方言。gcc 扩展允许 void 指针算术。
      • 我应该改用 intptr_t 还是 uintptr_t? 。也就是说,允许将指针转换为整数,并且您对结果有更广泛的算术选项,但不能保证它们之间的关系以及与指定长度的关系可以确定您的区域是否重叠。
      • @P__J__ (标准说,符合标准的实现可能有扩展,void* 上的算术是 undefined 行为,这意味着标准对行为没有要求,这意味着符合标准的编译器可能对 void* 指针有一个自定义的算术行为,需要从其他地方,例如来自 gcc 文档。这是一个扩展,而不是不符合的标志)
      • @KamilCuk 它不是 gcc 中的 UB,因为它不符合编译器。 gcc.gnu.org/onlinedocs/gcc/Pointer-Arith.html 正如我所写,最常用的编译器不符合标准。
      • void * 类型的指针算术定义行为不会使 GCC 不符合标准。但是,未能发出有关此类操作的诊断确实,因为此类操作违反了语言约束。 GCC 的 -pedantic 选项可以解决大部分(如果不是全部)围绕所需诊断的一致性问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-04
      • 1970-01-01
      • 1970-01-01
      • 2017-01-06
      相关资源
      最近更新 更多