深入理解計(jì)算機(jī)系統(tǒng)(3.6)------匯編的流程控制

前面我們所講的所有指令,代碼執(zhí)行順序都是一條接著一條順序的執(zhí)行。但是實(shí)際上在編碼過(guò)程中,會(huì)有某些結(jié)構(gòu),比如條件語(yǔ)句(if-else),循環(huán)語(yǔ)句(for,do-while)和分支語(yǔ)句(switch)等等,都要求有條件的執(zhí)行,根據(jù)數(shù)據(jù)測(cè)試的結(jié)果來(lái)決定操作執(zhí)行的順序。
在機(jī)器代碼中,提供兩種基本的低級(jí)機(jī)制來(lái)實(shí)現(xiàn)有條件的行為:測(cè)試數(shù)據(jù)值,然后根據(jù)測(cè)試的結(jié)果來(lái)改變控制流或者數(shù)據(jù)流。
那么本篇博客我們就來(lái)詳細(xì)介紹在匯編語(yǔ)言中的流程控制。
1、條件碼
前面我們?cè)?操作數(shù)指示符和數(shù)據(jù)傳送指令 中介紹了整數(shù)寄存器,在 32位 CPU 中包含一組 8 個(gè)存儲(chǔ) 32 位值的寄存器,即整數(shù)寄存器。它可以存儲(chǔ)一些地址或者整數(shù)的數(shù)據(jù),有的用來(lái)記錄某些重要的程序狀態(tài),有的則用來(lái)保存臨時(shí)數(shù)據(jù)。
而這里我們要介紹的是條件碼(condition code)寄存器。它與整數(shù)寄存器不同,它是由單個(gè)位組成的寄存器,也就是它們當(dāng)中的值只能為 0 或者 1。當(dāng)有算術(shù)與邏輯操作發(fā)生時(shí),這些條件碼寄存器當(dāng)中的值會(huì)相應(yīng)的發(fā)生變化。
也就是說(shuō)可以檢測(cè)這些寄存器來(lái)執(zhí)行條件分支指令。常用的條件碼如下:
①、CF:進(jìn)位標(biāo)志寄存器。最近的操作是最高位產(chǎn)生了進(jìn)位。它可以記錄無(wú)符號(hào)操作的溢出,當(dāng)溢出時(shí)會(huì)被設(shè)為1。
②、ZF:零標(biāo)志寄存器,最近的操作得出的結(jié)果為0。當(dāng)計(jì)算結(jié)果為0時(shí)將會(huì)被設(shè)為1。
③、SF:符號(hào)標(biāo)志寄存器,最近的操作得到的結(jié)果為負(fù)數(shù)。當(dāng)計(jì)算結(jié)果為負(fù)數(shù)時(shí)會(huì)被設(shè)為1。
④、OF:溢出標(biāo)志寄存器,最近的操作導(dǎo)致一個(gè)補(bǔ)碼溢出(正溢出或負(fù)溢出)。當(dāng)計(jì)算結(jié)果導(dǎo)致了補(bǔ)碼溢出時(shí),會(huì)被設(shè)為1。
從上面可以看出,CF和OF可以判斷有符號(hào)和補(bǔ)碼的溢出,ZF判斷結(jié)果是否為0,SF判斷結(jié)果的符號(hào)。這是底層機(jī)器的設(shè)定,而我們所編程用的高級(jí)語(yǔ)言(比如C,Java)就是靠這四個(gè)寄存器,演化出各種各樣的流程控制。
2、設(shè)置條件碼
通常情況下,條件碼寄存器的值無(wú)法主動(dòng)被改變,它們大多時(shí)候是被動(dòng)改變,這算是條件碼寄存器的特色。這其實(shí)理解起來(lái)并不困難,因?yàn)闂l件碼寄存器是1位的,而我們的數(shù)據(jù)格式最低為b,也就是8位,因此你無(wú)法使用任何數(shù)據(jù)傳送指令去傳送一個(gè)單個(gè)位的值。
幾乎所有的算術(shù)與邏輯指令都會(huì)改變條件碼寄存器的值,不過(guò)改變的前提是觸發(fā)了條件碼寄存器的條件。比如對(duì)于subl %edx,%eax這個(gè)減法指令,假設(shè)%edx和%eax寄存器的值都為0x10,則兩者相減的結(jié)果為0,此時(shí)ZF寄存器將會(huì)被自動(dòng)設(shè)為1。對(duì)于其它的指令運(yùn)算,都是類似的,會(huì)根據(jù)結(jié)果的不同而設(shè)置不同的條件碼寄存器。
這里我們需要說(shuō)明的是,leal 指令作為地址計(jì)算的時(shí)候,是不改變?nèi)魏螚l件碼的。
前面我們所講的算術(shù)邏輯指令,在改變整數(shù)寄存器的值后,會(huì)根據(jù)結(jié)果設(shè)置不同的條件碼。而這里還有另外兩種指令,它們只設(shè)置條件碼,而不改變?nèi)魏纹渌拇嫫鞯闹?。如下圖:
?、?、CMP 指令,指令形式 CMP S2,S1。然后會(huì)根據(jù) S1-S2 的差來(lái)設(shè)置條件碼。除了只設(shè)置條件碼而不更新目標(biāo)寄存器外,CMP 指令和 SUB 指令的行為是一樣的。比如兩個(gè)操作數(shù)相等,那么之差為0,那么就會(huì)將零標(biāo)志設(shè)置為 1;其他的標(biāo)志也可以用來(lái)確定兩個(gè)數(shù)的大小關(guān)系。
②、TEST 指令,和 AND 指令一樣,除了TEST指令只設(shè)置條件碼而不改變目的寄存器的值。比如對(duì)于如下指令:
MOV AL,40H
TESTB AL,08H
上面的指令就是用來(lái)測(cè)試 AL 寄存器的左起第四位是否為0,結(jié)果就是 0100 0000(40H)& 0000 1000(08H),測(cè)試結(jié)果左起第4位是0,所以各個(gè)標(biāo)志位:CF=0,OF=0,SF=0,ZF=1
3、訪問(wèn)條件碼
對(duì)于普通寄存器來(lái)講,使用的時(shí)候一般是直接讀取它的值,而對(duì)于條件碼,通常不會(huì)直接讀取。常用的有如下三種方法:
①、可以根據(jù)條件碼寄存器的某個(gè)組合,將一個(gè)字節(jié)設(shè)置為0或1。
②、可以直接條件跳轉(zhuǎn)到程序的某個(gè)其它的部分。
③、可以有條件的傳送數(shù)據(jù)。
對(duì)于第一種情況,下圖描述的指令便是根據(jù)條件碼的某個(gè)組合,將一個(gè)字節(jié)設(shè)置為0或1,這一整類指令稱為 SET 指令,它們的區(qū)別就在與它們考慮的條件碼的組合是什么,這些指令名字的不同后綴指明了它們所考慮的條件碼的組合。
注意:這些指令的后綴表示不同的條件而不是操作數(shù)的大小。比如指令 setl 和 setb 表示 “小于時(shí)設(shè)置(set less)”和“低于時(shí)設(shè)置(set below)”,而不是“設(shè)置長(zhǎng)字(set long word)”和“設(shè)置字節(jié)(set byte)”。
上圖所說(shuō)的同義名,比如說(shuō)setg(表示“設(shè)置大于”)和 setnle(表示“不小于等于”)指的就是同一條機(jī)器指令,編譯器和反編譯器會(huì)隨意決定使用哪個(gè)名字。
還有set指令中的目的操作數(shù),只能是前面我們所講的8個(gè)單字節(jié)的寄存器或者是存儲(chǔ)一個(gè)字節(jié)的存儲(chǔ)器位置。
下面我們分別對(duì) set 指令出現(xiàn)的后綴做簡(jiǎn)單介紹:
①、e->ZF(相等):equals的意思,這里代表的組合是ZF,因?yàn)閆F在結(jié)果為0時(shí)設(shè)為1。因此ZF代表的意義是相等。
②、ne->~ZF(不相等):not equals 的意思,這里代表的組合是~ZF,也就是ZF做“非運(yùn)算”,則很明顯是不相等的意思。
③、s->SF(負(fù)數(shù)):這里代表的組合是SF,因?yàn)镾F在計(jì)算結(jié)果為負(fù)數(shù)時(shí)設(shè)為1,此時(shí)可以認(rèn)為b為0,即a<0。因此這里是負(fù)數(shù)的意思。
④、ns->~SF(非負(fù)數(shù)):與s相反,加上n則是not的意思,因此這里代表非負(fù)數(shù)。
⑤、l->SF^OF(有符號(hào)的小于):l代表的是less。這里的組合是SF^OF,即對(duì)SF和OF做“異或運(yùn)算”?!爱惢蜻\(yùn)算”的意思則是代表,SF和OF不能相等。那么有兩種情況,當(dāng)OF為0時(shí),則代表沒(méi)有溢出,此時(shí)SF必須為1,SF為1則代表結(jié)果為負(fù)。即a-b<0,也就是a<b,也就是小于的意思。當(dāng)OF為1時(shí),則代表產(chǎn)生了溢出,而此時(shí)SF必須為0,也就是說(shuō)結(jié)果最后為正數(shù),那么此時(shí)則是負(fù)溢出,也可以得到a-b<0,即a<b。綜合前面兩種情況,SF^OF則代表小于的意思。
⑥、le->(SF^OF)|ZF(有符號(hào)的小于等于):le是less equals的意思。有了前面小于的基礎(chǔ),這里就很容易理解了。SF^OF代表小于,ZF代表等于,因此兩者的“或運(yùn)算”則代表小于等于。
⑦、g->~(SF^OF)&~ZF(有符號(hào)的大于):g是greater的意思。這里的組合是~(SF^OF)&~ZF,相對(duì)來(lái)說(shuō)就比較復(fù)雜了。不過(guò)有了前面的鋪墊,這個(gè)也非常好理解。SF^OF代表小于,則~(SF^OF)代表大于等于,而~ZF代表不等于,將~(SF^OF)與~ZF取“與運(yùn)算”,則代表大于等于且不等于,也就是大于。
⑧、ge->~(SF^OF)(有符號(hào)的大于等于):ge是greater equals的意思。
⑨、b->CF(無(wú)符號(hào)的小于):b是below的意思。CF是無(wú)符號(hào)溢出標(biāo)志,這里的意思是指如果a-b結(jié)果溢出了,則代表a是小于b的,即a<b。其實(shí)這個(gè)結(jié)論很顯然,關(guān)鍵點(diǎn)就在于,無(wú)符號(hào)減法只有在減出負(fù)數(shù)的時(shí)候才可能溢出,也就是說(shuō)只要結(jié)果溢出了,那么一定有a-b<0。因此這個(gè)結(jié)論就顯而易見(jiàn)了。
⑩、be->CF|ZF(無(wú)符號(hào)的小于等于):這里是below equals的意思。因此這里會(huì)與ZF計(jì)算“或運(yùn)算”,字面上也很容易理解,即CF(小于)|(或)ZF(等于),也就是小于等于。
?、a->~CF&~ZF(無(wú)符號(hào)的大于):a代表的是above。這個(gè)組合也是非常好理解的,CF代表小于,則~CF代表大于等于,~ZF代表不等于,因此~CF&~ZF則代表大于等于且不等于,即大于。
?、ae->~CF(無(wú)符號(hào)的大于等于):ae是above equals的意思。
比如對(duì)于setae %al指令來(lái)說(shuō),%al是%eax寄存器中的最后一個(gè)字節(jié),這個(gè)指令的含義是,將~CF的值設(shè)置到%eax寄存器的最后一個(gè)字節(jié)。
4、跳轉(zhuǎn)指令 jump
正常情況下,指令會(huì)按照他們出現(xiàn)的順序一條一條地執(zhí)行。而跳轉(zhuǎn)指令(jump)會(huì)導(dǎo)致執(zhí)行切換到程序中一個(gè)全新的位置,我們可以理解為方法或者函數(shù)的調(diào)用。在匯編代碼中,這些跳轉(zhuǎn)的目的地通常用一個(gè)標(biāo)號(hào)(label)指明。比如如下代碼:
movl $0,%eax
jmpl .L1
movl (%eax),%edx
.L1:
popl %edx
指令 jmpl .L1 會(huì)導(dǎo)致程序跳過(guò) movl 指令,從 popl 開(kāi)始執(zhí)行。在產(chǎn)生目標(biāo)代碼文件時(shí),匯編器會(huì)確定所有帶標(biāo)號(hào)指令的地址,并將跳轉(zhuǎn)目標(biāo)(目的指令的地址)編碼為跳轉(zhuǎn)指令的一部分。
如下圖所示,jump 指令有三種跳轉(zhuǎn)方式:
①直接跳轉(zhuǎn):跳轉(zhuǎn)目標(biāo)是作為指令的一部分編碼的,比如上面的直接給一個(gè)標(biāo)號(hào)作為跳轉(zhuǎn)目標(biāo)
②間接跳轉(zhuǎn):跳轉(zhuǎn)目標(biāo)是從寄存器或者存儲(chǔ)器位置中讀出的,比如 jmp *%eax 表示用寄存器 %eax 中的值作為跳轉(zhuǎn)目標(biāo);再比如 jmp *(%eax) 以 %eax 中的值作為讀地址,從存儲(chǔ)器中讀取跳轉(zhuǎn)目標(biāo)。
③其他條件跳轉(zhuǎn):根據(jù)條件碼的某個(gè)組合,或者跳轉(zhuǎn),或者繼續(xù)執(zhí)行代碼序列中的下一條指令。
比如對(duì)于如下代碼:文件名為 hello.c
int exchange(int x,int y)
{
if(x < y){
return y-x;
}else{
return x-y;
}
}
我們執(zhí)行如下命令,將C程序hello.c變?yōu)閰R編程序 hello.s
gcc -O0 -S hello.c
-O0是優(yōu)化選項(xiàng),還有O0 -->> O1 -->> O2 -->> O3,分別是從沒(méi)有優(yōu)化到優(yōu)化級(jí)別最高。

相信看了前面幾篇博客的相關(guān)指令介紹,這個(gè)匯編代碼不難理解。x,y分別存放于棧頂?shù)刂菲屏繛?4和-8的位置,然后比較x-y的值,也就是指令 cmpl -8(%rbp),%eax,如果x大于或等于y,那么跳轉(zhuǎn)到 .L2 的位置,然后計(jì)算 subl %eax,%ecx 的值,即x-y。
我們還可以通過(guò)如下命令生成目標(biāo)文件 hello.c
gcc -O0 -c hello.c
然后通過(guò)如下命令查看反匯編代碼
objdump -d hello.o

5、循環(huán)
C 語(yǔ)言提供了多種循環(huán)結(jié)構(gòu),比如 do-while、while和for。匯編中沒(méi)有相應(yīng)的指令存在,我們可以用條件測(cè)試和跳轉(zhuǎn)指令組合起來(lái)實(shí)現(xiàn)循環(huán)的效果。而大多數(shù)匯編器會(huì)根據(jù)一個(gè)循環(huán)的do-while 循環(huán)形式來(lái)產(chǎn)生循環(huán)代碼,即其他的循環(huán)一般也會(huì)先轉(zhuǎn)換成 do-while 形式,然后在編譯成機(jī)器代碼。
比如如下 do-while 循環(huán):
上面的匯編代碼就不做過(guò)多的介紹了,應(yīng)該很容易看明白。
6、條件傳送指令 cmov
條件傳送指令。顧名思義,條件傳送指令的意思就是在滿足條件的時(shí)候進(jìn)行傳送的指令,也就是cmov指令。它與set指令十分相似,同樣有12種,也就是加上12種條件碼寄存器的組合即可,如下所示:
條件傳送指令相當(dāng)于一個(gè)if/else的賦值判斷,一般情況下,條件傳送指令的性能高于if/else的賦值判斷。但是因?yàn)闂l件傳送指令將對(duì)兩個(gè)表達(dá)式都求值,因此如果兩個(gè)表達(dá)式計(jì)算量很大時(shí),那么條件傳送指令的性能就可能不如if/else的分支判斷了。不過(guò)總的來(lái)說(shuō),這種情況還是很少的,因此條件傳送指令還是很有用的,只是并不是所有的處理器都支持條件傳送指令,這依賴于處理器以及編譯器的編譯方式。
條件傳送指令最大的缺點(diǎn)便是可能引起意料之外的錯(cuò)誤,比如對(duì)于下面這一段代碼。
int cread(int *xp){
return (xp ? *xp : 0);
}
咋一看,這一段代碼是沒(méi)問(wèn)題的,不過(guò)如果使用條件傳送指令去實(shí)現(xiàn)這段代碼的話,將可能引起空指針引用的錯(cuò)誤。因?yàn)闂l件傳送指令會(huì)先對(duì)兩個(gè)表達(dá)式進(jìn)行計(jì)算,也就是說(shuō)無(wú)論xp是否有值,都將計(jì)算*xp這個(gè)表達(dá)式,因此當(dāng)xp為空指針0時(shí),則會(huì)產(chǎn)生錯(cuò)誤。由此可見(jiàn),條件傳送指令也不是哪都能用的,通常情況下,編譯器會(huì)幫我們盡力處理這種錯(cuò)誤。
