为什么MISRA C指出指针副本可能导致内存exception?

MISRA C 2012指令4.12是“不应该使用dynamic内存分配”。

作为一个例子,文档提供了这个代码示例:

char *p = (char *) malloc(10); char *q; free(p); q = p; /* Undefined behaviour - value of p is indeterminate */ 

该文件指出:

虽然存储在指针中的值在释放之后保持不变,但在某些目标上,可能指向的存储器不再存在, 并且复制该指针的操作 可能导致内存exception

几乎所有的句子,我都可以,但最后。 由于p和q都被分配在堆栈上,指针副本如何导致内存exception?

根据标准,复制指针q = p; ,是未定义的行为。

阅读J.2未定义的行为状态:

使用(6.2.4)一个指向终生已经结束的对象的指针的值。

去那一章我们看到:

6.2.4对象的存储时间

对象的生命周期是程序执行的一部分,在此期间保证存储器的存储空间。 一个对象存在,具有一个常量地址,并且在其整个生命周期中保留其最后存储的值.34)如果一个对象被引用到其生命周期之外,那么这个行为是不确定的。 指针的值在它指向的对象(或刚刚过去)达到其生命周期结束时变得不确定。

什么是不确定的:

3.19.2 不确定值 :一个未指定的值或一个陷阱表示

一旦你通过指针释放一个对象,所有指向这个内存的指针变得不确定。 (偶) 读取不确定的内存是未定义的行为(UB)。 以下是UB:

 char *p = malloc(5); free(p); if(p == NULL) // UB: even just reading value of p as here, is UB { } 

首先,一些历史…

当ISO / IEC JTC1 / SC22 / WG14首次开始正式使用C语言(产生现在的ISO / IEC 9899:2011)时,他们遇到了问题。

许多编译器厂商用不同的方式来解释事物。

早期,他们决定不打破任何现有的function…因此,在编译器实现发散的地方,标准提供unspecifiedundefined行为。

MISRA C试图阻止这些行为将触发的陷阱。 这么多理论…

现在到这个问题的具体问题:

考虑到free()的意义在于将dynamic内存释放回堆中,有三种可能的实现,所有这些实现都是“疯狂的”:

  1. 将指针重置为NULL
  2. 离开指针
  3. 销毁指针

标准不能强制任何一个,所以正式的保留了undefined的行为 – 你的实现可能遵循一条path,但是一个不同的编译器可以做别的事情……你不能假设,依靠一个方法是危险的。

就个人而言,我宁愿标准是特定的,并要求free()将指针设置为NULL,但这只是我的意见。

所以TL; DR; 答案是不幸的,因为这是!

虽然pq都是堆栈中的指针variables,但malloc()返回的内存地址不在堆栈上。

一旦成功配对的存储区被释放,那么在那个时候就不知道谁可能正在使用存储区或存储区的configuration。

所以一旦free()被用来释放先前使用malloc()获得的内存区域,尝试使用内存区域是一个未定义types的操作。 你可能会很幸运,它会工作。 你可能不走运,也不会。 一旦你free()一个内存区域,你不再拥有它,还有其他的东西。

这里的问题似乎是将一个值从一个内存位置复制到另一个内存所涉及的机器代码。 请记住,MISRA的目标是embedded式软件的开发,所以问题总是在那里做什么样的时髦的处理器,做一些特殊的副本。

MISRA标准都是关于健壮性,可靠性以及消除软件故障的风险。 他们很挑剔。

示例代码中可怜的措辞将您抛弃。

它说“p的值是不确定的”,但不是p的值是不确定的,因为p仍然具有相同的值(已经释放的内存块的地址)。

调用free(p)不会改变p – p只会在您离开p定义的范围后才会更改。

相反,由于内存块已经被释放,所以p值是不确定的 ,因此操作系统也可以不映射它。 通过p或通过别名指针(q)访问它可能会导致访问冲突。

有两个原因,解放指针后检查指针的代码是有问题的,即使指针永不解除引用:

  1. C标准的作者不希望干扰在指针包含周围的内存块的信息的平台上的语言的实现,并且无论什么时候对它们进行任何操作(无论它们是否被解引用),都可以validation这样的指针。 如果存在这样的平台,则使用违反标准的指针的代码可能不适用于它们。

  2. 一些编译器的操作假定程序永远不会收到任何调用UB的input组合,因此任何可能产生UB的input组合都应该被认为是不可能的。 因此,即使编译器忽略它们,甚至对目标平台没有不利影响的UBforms也可能具有任意和无限的副作用。

恕我直言,没有理由为什么平等,关系型或指针差异的操作符在释放指针时应该对任何现代系统产生任何不利影响,但是由于编译器应用疯狂的“优化”是时髦的,所以应该使用有用的构造平凡的平台变得危险。

在指向的内存被释放之后, p的值不能被使用。 更一般地说,未初始化指针的值具有相同的状态:甚至只是为了复制而读取它以调用未定义的行为。

这种令人惊讶的限制的原因是陷阱表示的可能性。 释放p指向的内存可以使其值成为陷阱表示。

我记得20世纪90年代早期的一个这样的目标,就是这样的。 然后不embedded目标,而是广泛使用:Windows 2.x. 它采用16位保护模式的英特尔架构,其指针为32位宽,16位select器和16位偏移量。 为了访问存储器,指针被加载到一对寄存器(一个段寄存器和一个地址寄存器)中,并带有特定的指令:

  LES BX,[BP+4] ; load pointer into ES:BX 

将指针值的select器部分加载到段寄存器中会对validationselect器值产生副作用:如果select器没有指向有效的内存段,则会触发exception。

编译无辜的前瞻性陈述q = p; 可以用许多不同的方式进行编译:

  MOV AX,[BP+4] ; loading via DX:AX registers: no side effects MOV DX,[BP+6] MOV [BP-6],AX MOV [BP-4],DX 

要么

  LES BX,[BP+4] ; loading via ES:BX registers: side effects MOV [BP-6],BX MOV [BP-4],ES 

第二个选项有两个好处:

  • 代码更紧凑,less1条指令

  • 指针值被加载到寄存器中,这些寄存器可以直接用来取消引用内存,这可能会导致为后续语句生成更less的指令。

释放内存可能会使片段无法映射,导致select器无效。 该值成为陷阱值并将其加载到ES:BX触发一个exception,在一些体系结构上也称为陷阱

并不是所有的编译器都会使用LES指令来复制指针值,因为它的速度较慢,但​​是有些编译器在指示生成紧凑的代码时是一种常见的select,然后是内存相当昂贵和稀缺。

C标准允许这一点,并描述了一种未定义行为的forms代码在哪里:

使用指向已经结束生命期的对象的指针(6.2.4)。

因为这个价值已经成为不确定的这样定义:

3.19.2不确定值:一个未指定的值或一个陷阱表示

但请注意,您仍然可以通过字符types的别名操作值:

 /* dumping the value of the free'd pointer */ unsigned char *pc = (unsigned char*)&p; size_t i; for (i = 0; i < sizeof(p); i++) printf("%02X", pc[i]); /* no problem here */ /* copying the value of the free'd pointer */ memcpy(&q, &p, sizeof(p)); /* no problem either */ 

内化的一个重要概念是“不确定”或“未定义”行为的含义。 这正是:未知和不可知的。 我们经常告诉学生:“把你的电脑融化成一个没有形状的团块,或者让磁盘飞向火星是完全合理的”。 当我阅读包含的原始文档时,我没有看到它说不使用malloc的地方。 它只是指出一个错误的程序将会失败。 实际上,让程序记忆exception是一件好事,因为它会立即告诉你程序有缺陷。 为什么文件表明这可能是一个坏事情逃避我。 什么是坏事在大多数架构上,它不会带来内存exception。 继续使用该指针会产生错误的值,可能导致堆不可用,并且,如果同一个存储块被分配给不同的用途,则会破坏该用法的有效数据,或将其值解释为您自己的值。 底线:不要使用“陈旧”的指针! 或者换句话说,编写有缺陷的代码意味着它将不起作用。

此外,将p赋给q的行为最明显不是“未定义”。 存储在variablesp中的位是无意义的,很容易,正确地复制到q。 所有这一切意味着现在任何由p访问的值现在也可以被q访问,并且因为p是未定义的无意义的,所以q现在是未定义的无意义的。 因此,使用其中一个读取或写入将产生“未定义”的结果。 如果您足够幸运,可以在可能导致内存故障的架构上运行,那么您将很容易检测到不正确的使用情况。 否则,使用任何一个指针都意味着程序有问题。 计划花费大量的时间find它。