在 ISO C90 标准中 C 语言负数比正数大?

演示环境

  • OS: Arch Linux x86_64
  • Kernel: Linux 6.10.10-arch1-1
  • GCC: 14.2.1

演示代码

int main(void)
{
	return -2147483648 < 2147483647;
}

编译和链接

gcc -std=c90 -m32 main.c # 添加 -masm=intel 选项可以生成 intel 语法的汇编

gcc 输入警告:

warning: this decimal constant is unsigned only in ISO C90

运行并查看结果

./a.out
echo $?

输出结果为:0

先看一下编译生成的汇编代码

  1. 执行命令
gcc -S -std=c90 -m32 main.c
  1. 生成的汇编代码如下(只截取了 main 标号部分):
main:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	movl	$0, %eax # 11 行
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:

看一下第 11 行汇编指令 movl $0, %eax,返回值 0 是 gcc 在编译阶段计算(优化)的结果(意料之中!特别是现代编译器,哪怕是在默认的优化级别下,也没理由不进行优化)。

关于 ISO C90 标准

在 ISO C90 标准中,-2147483648 是由一个负号和 2147483648 两部分组成。根据 C90 标准的规定,整数常量(不带后缀)会根据其大小自动决定是 int 类型、long int 类型还是 unsigned long int 类型。

2147483648 超出了 32 位 int 的最大范围(2147483647),因此编译器会把它当成 unsigned long int 类型。所以,-2147483648 其实是 - 和一个 unsigned long int 类型 2147483648 组成,因为这里的负号是尝试对一个无符号数取负,这将引发类型问题,这样的操作在 C 语言中是合法的,但会导致值的环绕(wrap around),最终得到一个很大的正数。

解决办法

  1. 以两个数相减的表达式来代替 -2147483648

既然问题出在 -2147483648 的绝对值太大了,如果将 -2147483648 改为其它的表达式形式,只要表达式的结果不变且表达式中的每个子表达式不超出范围不就变小了吗?这里以表达式 -2147483647 - 1 进行演示:

int main(void)
{
	return (-2147483647 - 1) < 2147483647;
}

重复之前的编译链接步骤,发现不但没有了警告,而且运行的结果也是对的。

  1. 分别使用宏代替 -21474836482147483647
#include <limits.h>

int main(void)
{
    return INT_MIN < INT_MAX;
}

我们知道宏是在预处理(预编译)阶段进行处理(替换)的,那么宏 INT_MININT_MAX 会分别替换成什么呢?

使用命令 gcc -E -std=c90 -m32 main.c 预处理的结果为(只截取了关键部分):

int main(void)
{
    return (-0x7fffffff - 1) < 0x7fffffff;
}

-0x7fffffff0x7fffffff 不就分别是 -21474836472147483647 的十六进制形式吗?所以,方法 1 和 2 本质是一样的。

  1. 使用变量(不推荐)
int main(void)
{
	int min = -2147483648;
	return min < 2147483647;
}

依然有相同的警告,但结果(1)居然是对的?还是先看一下编译生成的汇编代码(同样只截取关键部分):

main:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	movl	$-2147483648, -4(%ebp)
	cmpl	$2147483647, -4(%ebp)
	setne	%al
	movzbl	%al, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc

movl $-2147483648, -4(%ebp) 的意思是将 -2147483648 压栈,其实就是放到内存中,由于 -2147483648 是负数,在内存中存放的是补码形式(也就是十六进制的 0x80000000)。

cmpl $2147483647, -4(%ebp) 的意思是比较 2147483647(十六进制为 0x7FFFFFFF)和 0x80000000 的大小,比较的结果存放到标志寄存器的对应位中。

setne %al 的意思是如果比较的结果为不相等(明显 0x7FFFFFFF 和 0x80000000),将 al 寄存器置 1(二进制表示为:0000001

movzbl 指令负责拷贝一个字节,并用 0 填充其目的操作数中的其余各位。因此,movzbl %al, %eax 指令执行后,寄存器 eax(也就是返回值)为 0x00000001