为什么MISRA C声明指针的副本可能导致内存异常?

时间:2014-11-02 21:00:58

标签: c pointers memory-management misra

MISRA C 2012指令4.12是"不应使用动态内存分配"。

例如,该文档提供了此代码示例:

char *p = (char *) malloc(10);
char *q;

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

该文件指出:

  

虽然指针中存储的值在后面没有改变   打电话给自由,有可能,在一些目标上,记忆到   它指出的不再存在和复制指针的行为   可能会导致内存异常

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

8 个答案:

答案 0 :(得分:44)

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

阅读 J.2未定义的行为陈述:

  

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

转到那一章我们看到:

  

6.2.4对象的存储持续时间

     

对象的生命周期是存储期间程序执行的一部分   保证为它保留。存在一个对象,具有恒定的地址,33)并保留   它在其整个生命周期中的最后存储值.34)如果一个对象被引用到它的外面   一生,行为是不确定的。 当指针变为不确定时,指针的值变得不确定   它指向(或刚刚过去)的对象到达其生命周期的末尾。

什么是不确定的:

  

3.19.2 不确定值:   要么是未指定的值,要么是陷阱表示

答案 1 :(得分:14)

通过指针释放对象后,指向该内存的所有指针都变得不确定。 (偶数)reading不确定内存是未定义的行为(UB)。以下是UB:

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

}

答案 2 :(得分:4)

首先,一些历史......

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

许多编译器供应商以不同的方式解释了事情。

早期,他们决定不破坏任何现有功能......因此,在编译器实现不同的情况下,标准提供unspecifiedundefined行为。

MISRA C试图捕捉这些行为将触发的陷阱。这个理论太多了......

-

现在具体问题:

鉴于free()的目的是将动态内存释放回堆,有三种可能的实现,所有这些实现都是#34;在野外":

  1. 将指针重置为NULL
  2. 将指针保留为
  3. 销毁指针
  4. 标准不能强制执行其中任何一个,所以正式将行为保留为undefined - 您的实现可能遵循一条路径,但不同的编译器可以做其他事情......您不能假设,它是依靠一种方法是危险的。

    就个人而言,我更倾向于标准是特定的,并且需要free()将指针设置为NULL,但这只是我的意见。

    -

    TL; DR;答案是,遗憾的是:因为它是!

答案 3 :(得分:3)

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

一旦成功进行了malloced的内存区域被释放,那么在那一点上无法确定谁可能正在使用内存区域或内存区域的处置。

因此,一旦free()用于释放先前使用malloc()获得的内存区域,尝试使用内存区域是一种未定义的操作类型。你可能会很幸运,它会起作用。你可能不走运,但事实并非如此。一旦你free()一个内存区域,你就不再拥有它了,还有别的东西。

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

MISRA标准是关于稳健性,可靠性和消除软件故障风险的。他们非常挑剔。

答案 4 :(得分:2)

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

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

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

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

将指针值的选择器部分加载到段寄存器中会产生验证选择器值的副作用:如果选择器未指向有效的内存段,则会触发异常。

编译无辜的陈述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

第二种选择有两个优点:

  • 代码更紧凑,少了一条指令

  • 指针值被加载到可以直接用于取消引用内存的寄存器中,这可以减少为后续语句生成的指令。

释放内存可能会取消映射段并使选择器无效。该值成为陷阱值,并将其加载到ES:BX会触发异常,在某些体系结构上也称为陷阱

并非所有编译器都会使用LES指令来复制指针值,因为它较慢,但有些指令生成紧凑代码时,这是一个常见的选择,因为内存相当昂贵而且很少。

C标准允许这样做并描述代码的一种未定义行为:

  

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

因为这个值已经变得不确定了,如下所示:

  

3.19.2 indeterminate value:未指定的值或陷阱表示

但请注意,您仍然可以通过字符类型别名来操纵值:

/* 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 */

答案 5 :(得分:0)

有两个原因导致在释放指针后检查指针的代码有问题,即使指针永远不会被解除引用:

  1. C标准的作者不希望在指针包含有关周围内存块的信息的平台上干扰语言的实现,并且无论何时对它们进行任何操作都可以验证这些指针,无论它们是什么是否取消引用。如果存在此类平台,则使用违反标准的指针的代码可能无法使用它们。

  2. 有些编译器的假设是程序永远不会接收任何会调用UB的输入组合,因此任何产生UB的输入组合都应该被认为是不可能的。因此,如果编译器简单地忽略它们,即使是对目标平台没有不利影响的UB形式也可能最终产生任意和无限的副作用。

  3. 恕我直言,没有理由说平等,关系或指针差异 释放指针上的运算符应该对任何指针产生任何不利影响 现代系统,但因为编译器应用疯狂的时尚 &#34;优化&#34;,有用的结构,应该可以在普通的地方使用 平台变得危险。

答案 6 :(得分:-1)

示例代码中的不良措辞会让你失望。

它表示&#34; p的值是不确定的&#34;,但它不是p的值是不确定的,因为p仍然具有相同的值(已释放的内存块的地址)。

免费调用(p)不会改变p-p只有在你离开定义p的范围后才会改变。

相反,它是 p指向的值是不确定的,因为内存块已被释放,并且它也可能被操作系统取消映射。通过p或通过别名指针(q)访问它可能会导致访问冲突。

答案 7 :(得分:-3)

内化的一个重要概念是&#34;不确定&#34;或&#34;未定义&#34;行为。正是如此:未知和不可知。我们经常告诉学生&#34;将您的计算机融化成无形的斑点,或者将光盘飞到火星上是完全合法的。当我阅读原始文档时,我没有看到它说不使用malloc的任何地方。它只是指出一个错误的程序将失败。实际上,让程序记忆异常是一件好事,因为它会立即告诉你程序有缺陷。为什么该文件表明这可能是一件坏事逃避了我。什么是坏事是在大多数架构上,它不会占用内存异常。继续使用该指针将产生错误的值,可能导致堆不可用,并且,如果为不同的用途分配相同的存储块,则破坏该用途的有效数据,或将其值解释为您自己的值。底线:不要使用陈旧的&#39;三分球!或者,换句话说,编写有缺陷的代码意味着它不会工作。

此外,将p分配给q的行为最明显不是&#34;未定义&#34;。存储在变量p中的位是无意义的无意义,非常容易且正确地复制到q。所有这一切现在意味着p访问的任何值现在也可以被q访问,并且由于p是未定义的无意义,q现在是未定义的无意义。因此,使用其中任何一个进行读取或写入都会产生&#34; undefined&#34;结果。如果您足够幸运能够在可能导致内存故障的架构上运行,您将很容易检测到不正确的使用情况。否则,使用任一指针意味着您的程序有缺陷。计划花很多时间找到它。