为啥这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针?

Posted

技术标签:

【中文标题】为啥这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针?【英文标题】:Why does this example of what compilers aren't allowed to do cause null pointer dereferencing using cmov?为什么这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针? 【发布时间】:2015-11-27 19:18:47 【问题描述】:

C 代码:

int cread(int *xp) 
    return (xp ? *xp : 0);

汇编代码:(来自一个教科书示例,说明编译器不允许做什么)使用条件移动指令

movl    $0, %eax
testl   %edx, %edx
cmovne  (%edx), %eax

这是 Computer Systems: A Programmer's Perspective(第 2 版)中使用的示例,用于说明如果条件的任一分支导致错误,则无法使用条件数据传输编译代码。在这种情况下,错误将是 xp 的空指针解引用。

我知道 xp 被取消引用,但我不明白 xp 是如何变成空指针的。这不取决于将指针作为参数传递给函数吗?

【问题讨论】:

是的,只有NULL作为参数传入。但代码的重点是检查NULL,在这种情况下返回零。 cmov 版本无法做到这一点,因为它总是会尝试取消引用指针,所以它会出错而不是返回零。 这个问题有些误导。我不认为显示的汇编代码实际上是由 gcc 生成的。如果是,那就是编译器错误。 @WumpusQ.Wumbley:你是对的。书中的汇编代码实际上将Invalid implementation of function cread xp in register %edx 作为无效示例代码的前言(OP 向我们展示的部分)。它不是由 gcc 生成的,正如这个 OP 所暗示的那样。 在我看来它匹配。因为movl 0,eax 而不是 xoring eax ? cmov[cc] (mem), %reg 不是安腾式的推测性负载。 无条件地执行加载,仅预测目标寄存器的值。 【参考方案1】:

汇编代码在技术上是有效的,但如果输入为 NULL 并且与 C 代码的行为不匹配,则会出错。鉴于事情的重点是在这种情况下返回零而不是错误,这是错误的。 C 等效项是:

int cread(int *xp) 
    int val = *xp;
    return (xp ? val : 0);

如您所见,它首先取消引用xp,然后才检查xp 是否为NULL,因此这显然不适用于NULL 输入。

【讨论】:

我认为代码是有效的。 CSAPP 3e 更改了这部分代码。 有趣但不足为奇的是,GCC 和 ICC 都将您展示的 C 代码编译成一个简单的mov eax, [rdi] 相关:Hard to debug SEGV due to skipped cmov from out-of-bounds memory 详细解释了 cmov 是一个始终需要两个操作数的 ALU 选择操作,这与 ARM 谓词加载或 Itanium 推测加载不同。【参考方案2】:

如果您拨打电话

cread(0);

cmovene 指令会出现段错误,因为它会评估 *xp,即使该值永远不会被使用。

在汇编语言中,这由(%edx) 表示。 IE。无论edx 的值如何,都会加载%edx 地址处的内存内容。

cmov 的值已被普遍质疑。例如Linus Torvalds 不是粉丝。

【讨论】:

【参考方案3】:

我知道 xp 被取消引用,但我不明白 xp 是如何变成空指针的。这不取决于将指针作为参数传递给函数吗?

您在技术上是正确的(而教科书在技术上是错误的——理论上,在某些情况下,编译器可以合法地生成该代码)。

但是;可以生成该代码的情况是:

a) 编译器(和/或链接器)可以证明没有调用者将 NULL 传递给函数。在这种情况下,编译器还证明cmov 是没有意义的,可以用普通存储替换(mov 之前没有任何测试)。

b) 编译器(和/或链接器)知道在程序集中引用 NULL(这不是 C 语言并且不需要遵循 C 语言的规则)是可以的。通常 C 中的 NULL 是汇编中的地址 0x00000000,并且通常故意使地址 0x000000 处的区域无法访问以帮助捕获错误;但是操作系统或程序没有理由无法使 0x0000000 处的区域可访问(例如,通常只使用链接器脚本就可以做到这一点)。

【讨论】:

以上是关于为啥这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个方法调用不明确?

为啥使用“新”会导致内存泄漏?

为啥递归调用会导致不同堆栈深度的 ***?

C++ 唯一指针;为啥这个示例代码会出现编译错误?错误代码太长了,我无法指定

当 Row 接受可变参数时,为啥 Scala 编译器会失败并显示“此处不允许 ': _*' 注释”?

为啥将 lambda 传递给受约束的类型模板参数会导致“不完整类型”编译器错误?