38

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

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

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

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

该文件指出:

尽管调用 free 后存储在指针中的值没有改变,但在某些目标上,它指向的内存可能不再存在,并且复制该指针的行为 可能会导致内存异常

除了结尾之外,我几乎对所有句子都满意。由于 p 和 q 都在堆栈上分配,指针的副本如何导致内存异常?

4

8 回答 8

44

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

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

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

转到那一章,我们看到:

6.2.4 对象的存储期限

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

什么是不确定的:

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

于 2014-11-02T21:08:29.633 回答
14

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

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

}
于 2014-11-02T21:39:52.793 回答
4

首先,一些历史...

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

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

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

MISRA C 试图捕捉这些行为将触发的陷阱。理论就这么多了……

--

现在具体到这个问题:

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

  1. 将指针重置为 NULL
  2. 保持指针不变
  3. 销毁指针

标准不能强制其中任何一个,因此正式将行为保留为undefined- 您的实现可能遵循一条路径,但不同的编译器可以做其他事情......您不能假设,并且依赖一种方法是危险的。

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

--

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

于 2014-11-06T11:36:32.893 回答
3

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

一旦成功分配的内存区域被释放,那么此时不知道谁可能正在使用该内存区域或该内存区域的配置。

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

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

MISRA 标准都是关于稳健性、可靠性和消除软件故障风险的。他们相当挑剔。

于 2014-11-02T21:21:45.037 回答
3

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

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

我记得有一个这样的目标,早在 1990 年代初期就是这样。当时不是嵌入式目标,而是广泛使用:Windows 2.x。它在 16 位保护模式下使用 Intel 架构,其中指针为 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

第二种选择有两个优点:

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

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

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

并非所有编译器都会使用该LES指令来仅复制指针值,因为它速度较慢,但​​有些编译器在被指示生成紧凑代码时会这样做,这是当时的常见选择,因为内存相当昂贵且稀缺。

C 标准允许这样做,并在代码中描述了一种未定义的行为形式,其中:

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

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

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

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

/* 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 */
于 2016-11-14T08:12:31.547 回答
0

即使指针从未被取消引用,在释放指针后检查指针的代码存在问题的原因有两个:

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

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

恕我直言,没有理由为什么释放指针上的相等、关系或指针差异运算符会对任何现代系统产生任何不利影响,而是因为编译器应用疯狂的“优化”是一种时尚,有用的构造应该可以用于普通平台已变得危险。

于 2016-11-14T00:08:15.523 回答
-1

示例代码中糟糕的措辞让您望而却步。

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

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

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

于 2014-11-03T09:28:26.733 回答
-3

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

此外,将 p 分配给 q 的行为绝对不是“未定义的”。存储在变量 p 中的位是无意义的,很容易且正确地复制到 q。这意味着现在任何被 p 访问的值现在也可以被 q 访问,并且由于 p 是未定义的废话,q 现在是未定义的废话。因此,使用其中任何一个来读取或写入都会产生“未定义”的结果。如果您有幸在可能导致内存故障的架构上运行,您将很容易检测到不正确的使用。否则,使用任何一个指针都意味着您的程序有缺陷。计划花费大量时间找到它。

于 2014-11-03T13:17:14.870 回答