我是自学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
(附注:有没有出现在命令行中的任何-O
选项,但因为它默认为0级,明确加入-O0
不应该发生太大的变化。)
以上两个命令运行得非常好对我的Ubuntu VM(AMD64,GCC 7.3.0),但断言一个失败我的Android手机(AArch64或armv8-A,GCC 8.2.0)上。
2.30-test.c:13: main: assertion "tadd_ok(0x7FFFFFFF, 0x7FFFFFFF) == 0" failed
需要注意的是第一个断言通过,所以int
是保证在平台上的4个字节。
所以我解雇了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的输出看,结果很不一致,作为return
的2.30.c:10
声明达成一致,并返回值应该是0,但功能还是返回1,使得断言失败。
请提供一个想法是什么我越来越错在这里。
请尊重我所提出的。只是说这是不UB有关的平台,尤其是GDB输出,不会有任何帮助。
签名溢出是未定义行为在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_ok
到return 1
,或add
并检查溢出标志(V
在ARM,x86上OF
)。锵的优化ASM是>>31
一个有趣的组合,或与一个XOR操作,但-fwrapv
实际上改变的是ASM,所以它可能不是做了充分的溢出检查。)
可以说,gcc8“打破”你的代码,但实际上它已经尽可能为法律/便携式ISO C. gcc8破裂仅仅透露了这一事实。
为了更清楚地看到它,让隔离只是表达成一个功能。 gcc -O0
编译每个报表中单独无论如何,所以这只是程序运行时x<0
不影响-O0
代码生成在你tadd_ok
功能本声明的信息。
// 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;
}
On the Godbolt compiler explorer with AArch64 GCC8.2 -O0 -fverbose-asm
:
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
-ftree-dump-original
or -optimized
will even turn its GIMPLE back into C-like code with this optimization done (from the Godbolt link):;; 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
是多余的。 cset
is an alias of csinc
,使用零注册为两个来源。因此,这将产生0 / 1.其它寄存器,csinc
的一般情况是任何2个寄存器的一个条件选择和增量。
无论如何,cset
是AArch64的相当于86 setcc
的,用于在寄存器转弯标记条件为bool
。
如果你想为你写的代码工作,你需要compile with -fwrapv
使用C是-fwrapv
让GCC实现的变化方案中明确的行为。默认值是-fstrict-overflow
,像ISO C标准。
如果您想为您在现代的C符号溢出,你需要编写一个检测溢出没有实际造成它检查。这是困难,烦人,编译器作者和(某些)开发者之间争论的焦点。他们认为,围绕不确定的行为语言规则并不意味着作为借口,“无偿突破”的代码编译为目标机器它将使感ASM时。但是,现代的编译器大多只实现ISO C(及一些扩展和额外定义的行为),对于像x86和ARM目标架构,其中有符号整数没有填充(因而包装就好了),也不要陷在溢出编译时也是如此。
所以,你可以说,在那场战争“射击”,在gcc8.x的变化实际上是“破”不安全的代码是这样的。 :P
见Detecting signed overflow in C/C++和How to check for signed integer overflow in C without undefined behaviour?
由于符号和无符号除了是2的补数相同的二进制运算,也许你可以只投给unsigned
为补充,并投退已签署比较。这将使一个版本的功能,这是在“正常”实现安全:2的补,并unsigned
和int
之间铸造仅仅是一个同位的重新解释。
这不能UB,它只是不会给一个人的补充或符号/幅值C实现正确的答案。
return (int)((unsigned)x + (unsigned)y) > y;
这将编译(用gcc8.2 -O3为AArch64)至
add w0, w0, w1 // x+y
cmp w0, w1 // x+y cmp y
cset w0, gt
ret
如果你写了int sum = x+y
从return sum < y
一个单独的C语句,这UB将不可见禁用优化与gcc。但作为同一表达式的一部分,即使在默认gcc
-O0
可以看到它。
编译时可见UB是各种恶劣。在这种情况下,只有输入一定范围内会产生UB,所以编译器假定它不会发生。如果无条件UB被视为执行的路径上,优化编译器可以假设路径从未发生过。 (在没有分支的功能,它可以承担的功能不会被调用,它编译成一个单一的非法指令。)请参阅Does the C++ standard allow for an uninitialized bool to crash a program?更多有关编译时可见UB。
(-O0
并不意味着“无优化”,这意味着除了什么已经必须通过gcc的内部表示的方式转换到ASM存储任何目标平台没有多余的优化。@Basile Starynkevitch解释Disable all optimization options in GCC)
其他一些编译器可能“把他们的大脑关”更是与优化禁用,并做一些更贴近翻译c成ASM,但GCC是不是这样的。例如,GCC仍然通过在-O0
恒定使用整数除法乘法逆。 (Why does GCC use multiplication by a strange number in implementing integer division?)所有3个其他主要的x86编译器(铛/ ICC / MSVC)使用div
。
有符号整数溢出调用undefined behavior。您无法通过两数相加,如果他们以某种方式环绕检查检查溢出情况。虽然你可能摆脱这在x86 / x64系统上,也不能保证其他人的行为一样。
但是什么你能做的就是用INT_MAX
或INT_MIN
做检查以及一些算术。
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 INT_MIN - x < y;
if (x > 0)
return INT_MAX - x > y;
// x == 0
return 1;
}
表达INT_MAX - x > y
是算术相当于INT_MAX > x + y
但防止溢流的发生。类似地,INT_MIN - x < y
是算术相当于INT_MIN < x + y
但防止溢出。
编辑:
如果你想以武力来定义符号整数溢出,您可以使用-fwrapv
选项GCC。然而,你就要去完全避免溢出更好。
正如你已经告诉,你调用未定义的行为。溢出是不适合的C.符号整数,编译器理解定义,第二和第三if语句中有符号整数方面不确定,因此编译器决定,无论那支将采取在一个定义良好的程序也不会发生。因此,整个函数tadd_ok
折叠成单个return 1
。
如果禁用优化没关系:那这些if语句调用优化器开始工作之前,不确定的行为早已确定。
而且这还不要紧,你能够生成的调试信息,因为这不是改变它产生的(它只是增加了注释为解释二进制转储和进程状态的工具)的方式代码。
最后但并非最不重要的,当你GDB打印语句的结果(x+y)>y
它这样做C语言编译的范围之内,但在“上的金属跑”说明条款。后的C不是唯一的编译成二进制的语言。虽然符号整数的下溢是以C未定义,可以非常清楚在一些不同的语言定义;你可能希望能够使用GDB在这种方案也是如此。当比较与p (x+y)>y
的的C语句(x+y)>y
x
的输出和y
是signed int
,你跟苹果比较桔子;他们是非常不同的事情。
我知道你要的不是UB别的东西等,但恐怕这是什么原因导致你的情况的问题,即使您正在使用-O0
。让我们来看看生成的程序集。
我已经简化了你的函数这个隔离UB:
int tadd_ok(int x, int y) {
if (x > 0)
return (x + y) > y;
return 1;
}
对于AArch64(-O0 -x c -march=armv8-a
)生成的输出:
tadd_ok:
sub sp, sp, #16
str w0, [sp, 12]
str w1, [sp, 8]
ldr w0, [sp, 12]
cmp w0, 0
ble .L2 ; if (x <= 0) goto return stmt
ldr w0, [sp, 12] ; here we are runnig (x + y) > y branch
cmp w0, 0 ; x is compared to zero
cset w0, gt ; return value is set to (x > 0)
and w0, w0, 255
b .L3
.L2:
mov w0, 1
.L3:
add sp, sp, 16
ret
请记住,因为有符号整数不允许溢出,表达(x + y)
总是比y
除非x <= 0
更大。 GCC在优化踢之前知道这一点,所以它与(x + y) > y
取代x > 0
。
虽然它只是做相同的检查,似乎忘记了这一点 - 没有启用优化的副作用。
您可以替换上面这个C代码:
int tadd_ok(int x, int y) {
if (x > 0)
return x > 0;
return 1;
}
而输出不会改变:
tadd_ok:
sub sp, sp, #16
str w0, [sp, 12]
str w1, [sp, 8]
ldr w0, [sp, 12]
cmp w0, 0
ble .L2
ldr w0, [sp, 12]
cmp w0, 0
cset w0, gt
and w0, w0, 255
b .L3
.L2:
mov w0, 1
.L3:
add sp, sp, 16
ret
通过上面的代码,很显然会做什么变化优化:
tadd_ok:
mov w0, 1
ret
你用什么都不用改其他选项,平台并不重要,因为没有产生任何另外的指令。
至于GDB:它运行通过使用先前由编译器生成的相同的代码在被调试处理执行它们复杂表达式,所以输出将是没有什么不同。因此,评估tadd_ok(0x7FFFFFFF, 0x7FFFFFFF)
运行相同的代码。
我想补充一点,在海湾合作委员会应对签署此外,具有溢出并使其定义的简单方法。您可以使用在https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html执行签署操作记录的内置命令(ADD,SUB,MUL)被定义环绕,它会告诉你操作是否未溢出。
bool __builtin_add_overflow(type1 a, type2 b, type3 *res)
例如,你可以重写你这样的功能:
int tadd_ok(int x, int y) {
int result;
return !__builtin_add_overflow(x, y, &result);
// result now contains (int)((unsigned int)x + (unsigned int)y)
}
符号整数溢出是根据C标准未定义行为,不同于这是保证环绕无符号溢出。
尝试把你的代码上Godbolt与最新的GCC的x86-64和-O3。它得到优化:
mov eax, 1
ret
这是可以接受的。我想象等效指令序列射出的ARM64,但我不知道,建筑,不能仅仅通过看是肯定的。