深入理解計算機系統(3.6)------匯編的流程控制


  前面我們所講的所有指令,代碼執行順序都是一條接着一條順序的執行。但是實際上在編碼過程中,會有某些結構,比如條件語句(if-else),循環語句(for,do-while)和分支語句(switch)等等,都要求有條件的執行,根據數據測試的結果來決定操作執行的順序。

  在機器代碼中,提供兩種基本的低級機制來實現有條件的行為:測試數據值,然后根據測試的結果來改變控制流或者數據流。

  那么本篇博客我們就來詳細介紹在匯編語言中的流程控制。

 

1、條件碼

  前面我們在 操作數指示符和數據傳送指令 中介紹了整數寄存器,在 32位 CPU 中包含一組 8 個存儲 32 位值的寄存器,即整數寄存器。它可以存儲一些地址或者整數的數據,有的用來記錄某些重要的程序狀態,有的則用來保存臨時數據。

  而這里我們要介紹的是條件碼(condition code)寄存器。它與整數寄存器不同,它是由單個位組成的寄存器,也就是它們當中的值只能為 0 或者 1。當有算術與邏輯操作發生時,這些條件碼寄存器當中的值會相應的發生變化。

  也就是說可以檢測這些寄存器來執行條件分支指令。常用的條件碼如下:

  ①、CF:進位標志寄存器。最近的操作是最高位產生了進位。它可以記錄無符號操作的溢出,當溢出時會被設為1。

  ②、ZF:零標志寄存器,最近的操作得出的結果為0。當計算結果為0時將會被設為1。

  ③、SF:符號標志寄存器,最近的操作得到的結果為負數。當計算結果為負數時會被設為1。

  ④、OF:溢出標志寄存器,最近的操作導致一個補碼溢出(正溢出或負溢出)。當計算結果導致了補碼溢出時,會被設為1。

  從上面可以看出,CF和OF可以判斷有符號和補碼的溢出,ZF判斷結果是否為0,SF判斷結果的符號。這是底層機器的設定,而我們所編程用的高級語言(比如C,Java)就是靠這四個寄存器,演化出各種各樣的流程控制。

 

2、設置條件碼

  通常情況下,條件碼寄存器的值無法主動被改變,它們大多時候是被動改變,這算是條件碼寄存器的特色。這其實理解起來並不困難,因為條件碼寄存器是1位的,而我們的數據格式最低為b,也就是8位,因此你無法使用任何數據傳送指令去傳送一個單個位的值。

  幾乎所有的算術與邏輯指令都會改變條件碼寄存器的值,不過改變的前提是觸發了條件碼寄存器的條件。比如對於subl %edx,%eax這個減法指令,假設%edx和%eax寄存器的值都為0x10,則兩者相減的結果為0,此時ZF寄存器將會被自動設為1。對於其它的指令運算,都是類似的,會根據結果的不同而設置不同的條件碼寄存器。

  這里我們需要說明的是,leal 指令作為地址計算的時候,是不改變任何條件碼的。

  前面我們所講的算術邏輯指令,在改變整數寄存器的值后,會根據結果設置不同的條件碼。而這里還有另外兩種指令,它們只設置條件碼,而不改變任何其他寄存器的值。如下圖:

  

  ①、CMP 指令,指令形式 CMP S2,S1。然后會根據 S1-S2 的差來設置條件碼。除了只設置條件碼而不更新目標寄存器外,CMP 指令和 SUB 指令的行為是一樣的。比如兩個操作數相等,那么之差為0,那么就會將零標志設置為 1;其他的標志也可以用來確定兩個數的大小關系。

  ②、TEST 指令,和 AND 指令一樣,除了TEST指令只設置條件碼而不改變目的寄存器的值。比如對於如下指令:

    MOV AL,40H

    TESTB AL,08H

    上面的指令就是用來測試 AL 寄存器的左起第四位是否為0,結果就是 0100 0000(40H)& 0000 1000(08H),測試結果左起第4位是0,所以各個標志位:CF=0,OF=0,SF=0,ZF=1

 

3、訪問條件碼

  對於普通寄存器來講,使用的時候一般是直接讀取它的值,而對於條件碼,通常不會直接讀取。常用的有如下三種方法:

  ①、可以根據條件碼寄存器的某個組合,將一個字節設置為0或1。

  ②、可以直接條件跳轉到程序的某個其它的部分。

  ③、可以有條件的傳送數據。

  對於第一種情況,下圖描述的指令便是根據條件碼的某個組合,將一個字節設置為0或1,這一整類指令稱為 SET 指令,它們的區別就在與它們考慮的條件碼的組合是什么,這些指令名字的不同后綴指明了它們所考慮的條件碼的組合。

  注意:這些指令的后綴表示不同的條件而不是操作數的大小。比如指令 setl 和 setb 表示 “小於時設置(set less)”和“低於時設置(set below)”,而不是“設置長字(set long word)”和“設置字節(set byte)”。

  

  上圖所說的同義名,比如說setg(表示“設置大於”)和 setnle(表示“不小於等於”)指的就是同一條機器指令,編譯器和反編譯器會隨意決定使用哪個名字。

  還有set指令中的目的操作數,只能是前面我們所講的8個單字節的寄存器或者是存儲一個字節的存儲器位置。

  下面我們分別對 set 指令出現的后綴做簡單介紹:

  ①、e->ZF(相等):equals的意思,這里代表的組合是ZF,因為ZF在結果為0時設為1。因此ZF代表的意義是相等。

  ②、ne->~ZF(不相等):not equals 的意思,這里代表的組合是~ZF,也就是ZF做“非運算”,則很明顯是不相等的意思。

  ③、s->SF(負數):這里代表的組合是SF,因為SF在計算結果為負數時設為1,此時可以認為b為0,即a<0。因此這里是負數的意思。

  ④、ns->~SF(非負數):與s相反,加上n則是not的意思,因此這里代表非負數。

  ⑤、l->SF^OF(有符號的小於):l代表的是less。這里的組合是SF^OF,即對SF和OF做“異或運算”。“異或運算”的意思則是代表,SF和OF不能相等。那么有兩種情況,當OF為0時,則代表沒有溢出,此時SF必須為1,SF為1則代表結果為負。即a-b<0,也就是a<b,也就是小於的意思。當OF為1時,則代表產生了溢出,而此時SF必須為0,也就是說結果最后為正數,那么此時則是負溢出,也可以得到a-b<0,即a<b。綜合前面兩種情況,SF^OF則代表小於的意思。

  ⑥、le->(SF^OF)|ZF(有符號的小於等於):le是less equals的意思。有了前面小於的基礎,這里就很容易理解了。SF^OF代表小於,ZF代表等於,因此兩者的“或運算”則代表小於等於。

  ⑦、g->~(SF^OF)&~ZF(有符號的大於):g是greater的意思。這里的組合是~(SF^OF)&~ZF,相對來說就比較復雜了。不過有了前面的鋪墊,這個也非常好理解。SF^OF代表小於,則~(SF^OF)代表大於等於,而~ZF代表不等於,將~(SF^OF)與~ZF取“與運算”,則代表大於等於且不等於,也就是大於。

  ⑧、ge->~(SF^OF)(有符號的大於等於):ge是greater equals的意思。

  ⑨、b->CF(無符號的小於):b是below的意思。CF是無符號溢出標志,這里的意思是指如果a-b結果溢出了,則代表a是小於b的,即a<b。其實這個結論很顯然,關鍵點就在於,無符號減法只有在減出負數的時候才可能溢出,也就是說只要結果溢出了,那么一定有a-b<0。因此這個結論就顯而易見了。

  ⑩、be->CF|ZF(無符號的小於等於):這里是below equals的意思。因此這里會與ZF計算“或運算”,字面上也很容易理解,即CF(小於)|(或)ZF(等於),也就是小於等於。

  ⑪、a->~CF&~ZF(無符號的大於):a代表的是above。這個組合也是非常好理解的,CF代表小於,則~CF代表大於等於,~ZF代表不等於,因此~CF&~ZF則代表大於等於且不等於,即大於。

  ⑫、ae->~CF(無符號的大於等於):ae是above equals的意思。

  比如對於setae %al指令來說,%al是%eax寄存器中的最后一個字節,這個指令的含義是,將~CF的值設置到%eax寄存器的最后一個字節。

 

4、跳轉指令 jump

  正常情況下,指令會按照他們出現的順序一條一條地執行。而跳轉指令(jump)會導致執行切換到程序中一個全新的位置,我們可以理解為方法或者函數的調用。在匯編代碼中,這些跳轉的目的地通常用一個標號(label)指明。比如如下代碼:

    movl $0,%eax
    jmpl  .L1
    movl (%eax),%edx
.L1:
    popl %edx

  指令 jmpl .L1 會導致程序跳過 movl 指令,從 popl 開始執行。在產生目標代碼文件時,匯編器會確定所有帶標號指令的地址,並將跳轉目標(目的指令的地址)編碼為跳轉指令的一部分。

  如下圖所示,jump 指令有三種跳轉方式:

  ①直接跳轉:跳轉目標是作為指令的一部分編碼的,比如上面的直接給一個標號作為跳轉目標

  ②間接跳轉:跳轉目標是從寄存器或者存儲器位置中讀出的,比如 jmp *%eax 表示用寄存器 %eax 中的值作為跳轉目標;再比如 jmp *(%eax) 以 %eax 中的值作為讀地址,從存儲器中讀取跳轉目標。

  ③其他條件跳轉:根據條件碼的某個組合,或者跳轉,或者繼續執行代碼序列中的下一條指令。

  

  比如對於如下代碼:文件名為 hello.c

int exchange(int x,int y)
{
	if(x < y){
		return y-x;
	}else{
		return x-y;
	}
} 

  我們執行如下命令,將C程序hello.c變為匯編程序 hello.s

gcc -O0 -S hello.c

  -O0是優化選項,還有O0 -->> O1 -->> O2 -->> O3,分別是從沒有優化到優化級別最高。

  

  相信看了前面幾篇博客的相關指令介紹,這個匯編代碼不難理解。x,y分別存放於棧頂地址偏移量為-4和-8的位置,然后比較x-y的值,也就是指令 cmpl -8(%rbp),%eax,如果x大於或等於y,那么跳轉到 .L2 的位置,然后計算 subl %eax,%ecx 的值,即x-y。

  我們還可以通過如下命令生成目標文件 hello.c

gcc -O0 -c hello.c

  然后通過如下命令查看反匯編代碼

objdump -d hello.o

  

 

5、循環 

  C 語言提供了多種循環結構,比如 do-while、while和for。匯編中沒有相應的指令存在,我們可以用條件測試和跳轉指令組合起來實現循環的效果。而大多數匯編器會根據一個循環的do-while 循環形式來產生循環代碼,即其他的循環一般也會先轉換成 do-while 形式,然后在編譯成機器代碼。

  比如如下 do-while 循環:

  

  上面的匯編代碼就不做過多的介紹了,應該很容易看明白。

 

6、條件傳送指令 cmov

  條件傳送指令。顧名思義,條件傳送指令的意思就是在滿足條件的時候進行傳送的指令,也就是cmov指令。它與set指令十分相似,同樣有12種,也就是加上12種條件碼寄存器的組合即可,如下所示:

  

  條件傳送指令相當於一個if/else的賦值判斷,一般情況下,條件傳送指令的性能高於if/else的賦值判斷。但是因為條件傳送指令將對兩個表達式都求值,因此如果兩個表達式計算量很大時,那么條件傳送指令的性能就可能不如if/else的分支判斷了。不過總的來說,這種情況還是很少的,因此條件傳送指令還是很有用的,只是並不是所有的處理器都支持條件傳送指令,這依賴於處理器以及編譯器的編譯方式。

  條件傳送指令最大的缺點便是可能引起意料之外的錯誤,比如對於下面這一段代碼。

int cread(int *xp){
    return (xp ? *xp : 0);
}

  咋一看,這一段代碼是沒問題的,不過如果使用條件傳送指令去實現這段代碼的話,將可能引起空指針引用的錯誤。因為條件傳送指令會先對兩個表達式進行計算,也就是說無論xp是否有值,都將計算*xp這個表達式,因此當xp為空指針0時,則會產生錯誤。由此可見,條件傳送指令也不是哪都能用的,通常情況下,編譯器會幫我們盡力處理這種錯誤。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM