当前位置: 首页 > 知识库问答 >
问题:

gcc为什么/如何编译这个签名溢出测试中的未定义行为,以便它在x86上工作,而不是在ARM64上工作?

裴昊阳
2023-03-14

我在自学CSAPP,在运行断言测试时遇到了一个奇怪的问题,得到了一个奇怪的结果。

我不确定该从什么开始这个问题,所以让我先获得代码(文件名可见于注释中):

// File: 2.30.c
// Author: iBug

int tadd_ok(int x, int y) {
    if ((x ^ y) >> 31)
        return 1;  // A positive number and a negative integer always add without problem
    if (x < 0)
        return (x + y) < y;
    if (x > 0)
        return (x + y) > y;
    // x == 0
    return 1;
}
// File: 2.30-test.c
// Author: iBug

#include <assert.h>

int tadd_ok(int x, int y);

int main() {
    assert(sizeof(int) == 4);

    assert(tadd_ok(0x7FFFFFFF, 0x80000000) == 1);
    assert(tadd_ok(0x7FFFFFFF, 0x7FFFFFFF) == 0);
    assert(tadd_ok(0x80000000, 0x80000000) == 0);
    return 0;
}

和命令:

gcc -o test -O0 -g3 -Wall -std=c11 2.30.c 2.30-test.c
./test
2.30-test.c:13: main: assertion "tadd_ok(0x7FFFFFFF, 0x7FFFFFFF) == 0" failed

因此,我在手机上启动了gdb,试图获得一些见解:

(gdb) l 2.30.c:1
1       // File: 2.30.c
2       // Author: iBug
3
4       int tadd_ok(int x, int y) {
5           if ((x ^ y) >> 31)
6               return 1;  // A positive number and a negative integer always add without problem
7           if (x < 0)
8               return (x + y) < y;
9           if (x > 0)
10              return (x + y) > y;
(gdb) b 2.30.c:10
Breakpoint 1 at 0x728: file 2.30.c, line 10.
(gdb) r
Starting program: /data/data/com.termux/files/home/CSAPP-2019/ch2/test
warning: Unable to determine the number of hardware watchpoints available.
warning: Unable to determine the number of hardware breakpoints available.

Breakpoint 1, tadd_ok (x=2147483647, y=2147483647)
    at 2.30.c:10
10              return (x + y) > y;
(gdb) p x
$1 = 2147483647
(gdb) p y
$2 = 2147483647
(gdb) p (x + y) > y
$3 = 0
(gdb) c
Continuing.
2.30-test.c:13: main: assertion "tadd_ok(0x7FFFFFFF, 0x7FFFFFFF) == 0" failed

Program received signal SIGABRT, Aborted.
0x0000007fb7ca5928 in abort ()
   from /system/lib64/libc.so
(gdb) d 1
(gdb) p tadd_ok(0x7FFFFFFF, 0x7FFFFFFF)
$4 = 1
(gdb)

正如您在GDB输出中看到的,结果非常不一致,因为到达了2.30.C:10上的return语句,返回值应该是0,但是函数仍然返回1,使得断言失败。

请提供一个想法,我在这里做错了什么。

请尊重我所提出的。只说它是UB,而不联系平台,尤其是GDB输出,是没有任何帮助的。

共有1个答案

公风史
2023-03-14

签名溢出是ISO C中未定义的行为。您不能可靠地导致它,然后检查它是否发生。

在表达式(x+y)>y;中,编译器可以假设x+y没有溢出(因为这将是UB)。因此,它优化到检查x>0。(是的,真的,gcc甚至在-o0时也这样做)。

这种优化在GCC8中是新的。在x86和aarch64上是一样的;您一定在AArch64和x86上使用了不同的GCC版本。(即使在-O3,gcc7.x和更早版本(故意?)也没有进行这种优化。clang7.0也没有这样做。它们实际上执行32位的添加和比较。它们也没有优化TADD_OKreturn1,或者优化add并检查溢出标志(v,在ARM上v,在x86上of)。clang优化的asm是>31,以及一个XOR操作的有趣混合,但是

您可以说gcc8“破坏”了您的代码,但实际上,就合法/可移植的ISO C而言,它已经被破坏了。gcc8只是揭示了这一事实。

为了更清楚地看到它,让我们将这个表达式隔离到一个函数中。gcc-o0无论如何都会单独编译每个语句,因此只有在x<0时才运行的信息不会影响tadd_ok函数中此语句的-o0code-gen。

// compiles to add and checking the carry flag, or equivalent
int unsigned_overflow_test(unsigned x, unsigned y) {
    return (x+y) >= y;    // unsigned overflow is well-defined as wrapping.
}

// doesn't work because of UB.
int signed_overflow_expression(int x, int y) {
    return (x+y) > y;
}

在带有AArch64 gcc8.2-o0-fverbose-asm的Godbolt编译器资源管理器上:

signed_overflow_expression:
    sub     sp, sp, #16       //,,      // make a stack fram
    str     w0, [sp, 12]      // x, x   // spill the args
    str     w1, [sp, 8]       // y, y
   // end of prologue

   // instructions that implement return (x+y) > y; as return  x > 0
    ldr     w0, [sp, 12]      // tmp94, x
    cmp     w0, 0     // tmp94,
    cset    w0, gt  // tmp95,                  // w0 = (x>0) ? 1 : 0
    and     w0, w0, 255       // _1, tmp93     // redundant

  // epilogue
    add     sp, sp, 16        //,,
    ret     
;; Function signed_overflow_expression (null)
;; enabled by -tree-original

{
  return x > 0;
}

不幸的是,即使使用-wall-wextra-wpedantic,也没有关于比较的警告。这不是微不足道的事实;它仍然取决于x

优化后的asm是cmp w0,0/cset w0,gt/ret。与0xff是冗余的。CSETCSINC的别名,使用零寄存器作为两个源。所以它会产生0/1。对于其他寄存器,CSINC的一般情况是任意2个寄存器的条件选择和增量。

无论如何,cset是AARCH64与x86setcc的等价物,用于将标志条件转换为寄存器中的bool

因此,您可以说在这场战争中“开火了”,因为GCC8.x中的更改实际上是“破坏”不安全的代码,就像这样。:P

请参见在C/C++中检测有符号溢出以及如何在没有未定义行为的情况下检查C中有符号整数溢出?

由于有符号加法和无符号加法在2的补码中是相同的二进制运算,所以对于加法,您可以直接强制转换为unsigned,而对于有符号比较,则可以强制转换回来。这将使您的函数版本在“普通”实现中是安全的:2的补码,而在无符号int之间的转换只是对相同位的重新解释。

return  (int)((unsigned)x + (unsigned)y) > y;

这将编译(对于AArch64,gcc8.2-o3)到

    add     w0, w0, w1            // x+y
    cmp     w0, w1                // x+y  cmp  y
    cset    w0, gt
    ret

如果将int sum=x+y作为与return sum 分开的C语句编写,那么在禁用优化的情况下,gcc将看不到这个UB。但是,作为同一表达式的一部分,即使 gcc具有默认的 -o0也可以看到它。

编译时可见的UB是各种不好的。在这种情况下,只有特定范围的输入才会产生UB,所以编译器假定它不会发生。如果在执行路径上看到无条件UB,优化编译器可以假设路径从未发生。(在没有分支的函数中,它可以假设该函数从未被调用,并将其编译为一条非法指令。)参见C++标准允许未初始化的bool使程序崩溃吗?有关编译时可见UB的更多信息。

 类似资料:
  • 问题内容: 我正在尝试执行此命令 和 都不起作用(返回空白输出) 有人知道为什么吗? 问题答案: 因为top是一个交互式程序,旨在在终端上运行,而不是从脚本中执行。您可能需要运行带有参数的“ ps”命令,这些命令将按cpu利用率对输出进行排序。 http://www.devdaily.com/linux/unix-linux-process-memory-sort-ps-command- cpu

  • 在上调用时: gcc和clang都在std::数组的排序上返回一个错误——clang说 错误:使用未声明的标识符“sort”;你是说“性病::分类”吗? 更改为解决了这个问题。 MSVC按编写的方式编译上面的代码。 为什么和在治疗上存在差异;哪个编译器是正确的?

  • 我正在尝试制作两个程序。我希望一个打印我居住的城市的当前天气,我希望另一个从一个在线帐户获取数据并返回它。对于这些脚本,我导入了天气模块和请求模块。当我在shell中导入它们时,没有问题,但是当我运行脚本时,它说“ImportError:No module name you weather”。我做错了什么? 壳牌: 脚本: 这也适用于“天气”模块 谢谢你

  • 我正在尝试执行GET命令,以便我可以从服务器获取数据。下面的Curl适用于Postman。 在运行我的代码时,我能够获取会话ID。下一步是获取数据。但是当我执行GET时,我没有得到任何响应。相反,我得到一个错误,如下所示:“指定的值具有无效的HTTP标头字符。(参数'name')” 下面是我试图执行的C代码 问题:我没有收到来自服务器的响应,响应长度为零。 以下是答案:0 回答ErrorMessa

  • 我已经编写并成功安装了一个苹果tonto JAVACOS智能卡。 我可以将这个小程序安装到我的NXP J3H145和ACOSJ卡上,但是当我尝试与它交互时,我得到了错误代码6F00。 这是为什么? 我正在使用JCIDE开发小程序,并使用Global Platform Pro安装小程序。

  • 我正在为一个tomcat实例修补RH Linux7上的自签名证书,但有一段时间,我没有浏览器警告。我在这里遵循了这些人的说明(让Chrome接受自签名的本地主机证书),并尝试使用KeyTool将.crt导入到我的tomcat实例中。使用以下命令- 创建密钥库-keytool-keysize 2048-genkey-alias tomcat-keyalg RSA-keystore tomcat.ke