如何更快地判斷算術運算有沒有溢出?

在編寫代碼的過程中,算術溢出是一件讓人十分頭疼的事。因此在代碼中檢測是否溢出是一件很有必要的事,那麼該如何檢測算術運算中的溢出呢?

以C語言中的算術乘法爲例,可以使用以下的代碼檢測是否有溢出:

//判斷a與b的乘積是否溢出,是返回-1,否返回0
int overflow(int a,int b)
{
    int temp=a*b;
    temp/=b;
    return temp==a?0:-1;
}

在函數中,首先計算a與b的乘積temp,然後再計算temp除以b。如果得到的結果是a的話,那麼就沒有溢出,否則運算過程中發生了溢出。誠然,該函數可以有效地檢測算術乘法是否溢出,但是對於CPU來說,除法的開銷相對於加法、移位等操作來說是非常大的,因此我們應該嘗試避免。

在這裏,我們嘗試使用彙編語言來判斷算術運算中的溢出。

首先,讓我們來了解一些背景知識。

  • CPU中維護着一組條件碼,描述了最近的算術運算或邏輯運算的屬性。常見的有:
    • CF(carry flag) 進位標誌。最近的操作使最高位產生了進位。
    • ZF(zero flag)零標誌。最近的操作得到的結果爲0。
    • SF(sign flag)符號標誌。最近操作得到的結果爲負數。
    • OF(overflow flag)溢出標誌。最近操作得到的結果導致一個補碼溢出。
  • 彙編語言中SET指令根據條件碼的組合,將一個字節設置爲0或1。
指令 同義指令 效果 設置條件
sete D setz D ← ZF Equal / zero
setne D setnz D ← ~ZF Not equal / not zero
sets D D ← SF Negative
setns D D ← ~SF Nonnegative
setg D setnle D ← ~(SF ^ OF) & ~ZF Greater (signed >)
setge D setnl D ← ~(SF ^ OF) Greater or equal (signed >=)
setl D setnge D ← SF ^ OF Less (signed <)
setle D setng D ← (SF ^ OF) | ZF Less or equal (signed <=)
seta D setnbe D ← ~CF & ~ZF Above (unsigned >)
setae D setnb D ← ~CF Above or equal (unsigned >=)
setb D setnae D ← CF Below (unsigned <)
setbe D setna D ← CF | ZF Below or equal (unsigned <=)

在這裏,我們的目標是讀取到OF這個標誌位。但是在上表中,並不能單獨地把OF標誌位存到寄存器中。仔細觀察後可以發現,上表中可以設置SF^OF(setl)和SF(sets)這兩個標誌位,而(SF^OF)^SF=OF!因此,我們只需要將使用setl和sets指令設置兩個寄存器的值,然後再將這兩個寄存器中的值異或就可以得到OF標誌位的值。

話不多說,讓我們開始吧!
首先編寫SignedOverflow.c文件:

int SignedOverflow(int a,int b)
{
	return a*b;
}

使用gcc -Og -S SignedOverflow.c命令產生的彙編文件如下:

	.file	"SignedOverflow.c"
	.text
	.globl	SignedOverflow
	.def	SignedOverflow;	.scl	2;	.type	32;	.endef
	.seh_proc	SignedOverflow
SignedOverflow:
	.seh_endprologue
	movl	%ecx, %eax
	imull	%edx, %eax
	ret
	.seh_endproc
	.ident	"GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 7.3.0"

然後我們在此基礎上實現檢測溢出的函數:

	.file	"SignedOverflow.c"
	.text
	.globl	SignedOverflow
	.def	SignedOverflow;	.scl	2;	.type	32;	.endef
	.seh_proc	SignedOverflow
SignedOverflow:
	.seh_endprologue
	movl	%ecx, %eax
	imull	%edx, %eax          ;計算%edx與%ecx的乘積
	sets	%r10b               ;設置%r10b寄存器的值爲SF
	setl	%r11b               ;設置%r11b寄存器的值爲SF^OF
	xorb	%r10b, %r11b        ;異或%r10b與%r11b,得到OF,結果存在%r11b中
	movl	$0,   %eax          ;將返回值置爲0
	movl	$-1,   %r10d         ;將%r10d的值置爲-1
	testb 	%r11b,  %r11b       ;測試%r11b(OF)
	cmovne	%r10d, %eax         ;如果%r11b不爲0,將返回值置爲-1
	ret                         ;函數返回
	.seh_endproc
	.ident	"GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 7.3.0"

編寫完成後,使用gcc -Og -c SignedOverflow.s生成SignedOverflow.o目標文件。

再編寫測試函數main.c:

#include <stdio.h>

//判斷a與b的乘積是否溢出,是返回-1,否返回0
int SignedOverflow(int a,int b);

int main()
{
	int a=0x7FFFFFFF,b=2,ans;
	ans=SignedOverflow(a,b);            //預期結果爲溢出
	printf("%d\n",ans);
	a=0x3FFFFFFF,b=2;
	ans=SignedOverflow(a,b);            //預期結果爲不溢出
	printf("%d\n",ans);
	return 0;
}

使用gcc -Og main.c SignedOverflow.o得到可執行文件a.exe。執行a.exe得到結果:

-1
0

大功告成!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章