C语言中malloc/free的损坏常见问题概述

4
这个问题类似于c malloc questions (mem corruption),但我再次提出它是因为我想要比提供的更具体的信息。
所以我有一个程序,其中有一个malloc,然后是一些复杂的代码,最后是一个free。在复杂的代码中的某个地方,内存被双重释放或越界写入(分别在与原始malloc不同的内存区域中)而导致原始free失败。这种行为相当确定。
我的问题有几个:
  1. What are the minimal conditions for a memory corruption to affect a separate memory region like this.
  2. Are there any proactive steps that can be taken to prevent this cross-corruption.
  3. Is it defined behaviour wrt standards to use pointer arithmetic to jump back and forth between contiguously allocated memory.

    /* 3 example */
    void *m = malloc(sizeof(header_struct) + sizeof(body_struct));
    body_struct *b = (body_struct*) (((header_struct*)m)+1);
    header_struct *h = (header_struct*) (((header_struct*)b)-1);
    

1
你的示例没有考虑到可能存在的对齐要求。如果对齐不是问题,那么这段代码就可以了(尽管有些奇怪)。 - Oliver Charlesworth
指针算术仅在一个对象内部定义,例如通过调用malloc返回的块。无论两个不相关的对象是否相邻分配并不重要。如果两个对象是更大对象的子对象,那就没问题。 - Deduplicator
查看调试malloc,它通常可以用来帮助捕获问题。为了更加主动,当您进行测试时,也可以使用调试malloc来捕获可能不总是那么确定的问题。 - jdigital
2个回答

5

好问题。

Q1. 标准下的最小条件是任何会触发未定义行为的情况。不幸的是,这是一个相当长的列表,不可操作。实际上,该列表归结为4个常见场景:对象下溢、对象上溢、悬空引用或野指针。

对象下溢发生在您写入已分配块之前的字节时。这些字节通常包含关键块链接,损害通常很严重。

对象上溢发生在您写入已分配块之后的字节时。通常在末尾有一小段填充空间,所以一个或两个字节通常不会造成严重的损害。如果继续写入,最终会像下溢一样命中某些重要内容。

悬空引用意味着通过曾经有效的指针进行写入。它可能是指向已超出范围的局部变量的指针,或者指向已释放的块的指针。这些都很难处理。

野指针表示写入到已分配块之外的地址。这可能是一个小正地址(例如指针值为0x20),在调试环境中,这些区域通常可以受到保护,或者它可能是随机垃圾,因为指针本身已损坏。这些情况不太常见,但很难找到和修复。

Q2. 调试堆是您的第一层保护。它将检查链接并在未使用的空间中写入特殊模式,并通常帮助查找和解决问题。如果使用调试堆,则free()通常会触发某些诊断活动,但您通常可以找到其他调用来执行相同的操作。在Windows上,可以使用HeapValidate()。

您可以通过实现具有警戒/哨兵(请查询)和自己的堆检查函数的自己的堆来做更多事情。在C中,除此之外,您只需更好地编写代码。您可以添加断言,以便至少使代码快速失败。

然后,您可以使用外部工具。其中一个是valgrnd,但它并不总是可行的。在一个案例中,我们编写了一个完整的堆日志记录系统,以跟踪每个分配,以查找此类问题。

Q3. 您的第二个示例不能保证第2行中的body_struct正确对齐。根据C标准n1570 S7.22.3,由malloc()返回的内存适合用作任何对象的指针。编译器将按照这个假设布置结构体。

然而,此要求不适用于结构体数组的成员。对于类似这样的结构体数组的第二个成员是否对齐是实现定义的。

struct s {
  double d;
  char c;
} ss[2];

考虑到这一点,你的代码是有效的C代码,但由于对齐要求的不同,可能存在实现定义或未定义行为。因此,并不建议使用这种方式。


我认为你引用的标准只要求指针本身的对齐要求相同。 - Ross Ridge
@RossRidge:糟糕,结构体的开头没问题,但是结尾有误。请参见编辑。 - david.pfx
作为一个有序点,N1570是ISO/IEC 9899:201x的委员会草案,我建议在引用其中的段落并假设它们与最终(已发布的)ISO/IEC 9899:2011标准相同之前要谨慎。 - Andrew
@Andrew:我知道这个问题,但我认为没有选择。据我所知,已发布的标准不可用于链接,只能购买。我认为这对行业造成了严重的损害,如果最终结果有显著差异,那将会加剧所造成的伤害。幸运的是,通常情况下并不会出现这种情况。 - david.pfx
在所有方面都同意,David。这是ISO(和MISRA)商业模式的陷阱之一。但我经常看到人们引用ISO CD 26262(或DIS或FDIS),它们与已发布的文件非常不同:( - Andrew

0

(1)任何未定义的行为都可能导致这种内存损坏,包括但不限于写入任何非对象组成部分的内存位置。

(2)请仔细编写您的代码:-(

(3)第二个赋值操作不可移植,并且可能由于对齐问题而导致各种问题。为了使其正确并具有可移植性,通常使用灵活数组成员。如果您始终分配一个头和一个主体,则定义一个新的结构体。

typedef struct {
    header_struct header;
    body_struct body;
} body_plus_header_struct;

如果您分配了一个标题和可变数量的正文,请编写:
typedef struct {
    header_struct header;
    body_struct bodies [];
} body_plus_header_struct;

这里,body_plus_header_struct的大小保证会四舍五入,以便bodies数组的地址具有正确的对齐方式。要为n个bodies分配一个结构体,请分配

body_plus_header_struct* p = malloc (sizeof (*p) + n * sizeof (p->bodies [0]));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接