在 ISO C90 標準中 C 語言負數比正數大?

escwqa發表於2024-10-02

演示環境

  • OS: Arch Linux x86_64
  • Kernel: linux-6.11.1
  • GCC: 14.2.1

演示程式碼(main.c)

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

編譯和連結

gcc -std=c90 -m32 main.c #

gcc 輸入警告:

main.c:3:9: warning: this decimal constant is unsigned only in ISO C90

執行並檢視結果

./a.out
echo $?

輸出結果為:0

先看一下編譯生成的彙編程式碼

  1. 執行命令
gcc -S -std=c90 -m32 main.c # 新增 -masm=intel 選項可以生成 intel 語法的彙編
  1. 生成的彙編程式碼如下(只擷取了關鍵部分):
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. 使用宏(推薦)
#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;
}

依然有相同的警告,但結果居然是對的?還是先看一下編譯生成的彙編程式碼(同樣只擷取關鍵部分):

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(0x01)

movzbl 指令負責複製一個位元組,並用 0 填充其目的運算元中的其餘各位。因此,movzbl %al, %eax 指令執行後,暫存器 eax(也就是返回值)為 0x00000001

相關文章