# Chapter 10 條件跳轉
現在我們來了解條件跳轉。
```
#!cpp
#include <stdio.h>
void f_signed (int a, int b)
{
if (a>b)
printf ("a>b
");
if (a==b)
printf ("a==b
");
if (a<b)
printf ("a<b
");
};
void f_unsigned (unsigned int a, unsigned int b)
{
if (a>b)
printf ("a>b
");
if (a==b)
printf ("a==b
");
if (a<b)
printf ("a<b
");
};
int main()
{
f_signed(1, 2);
f_unsigned(1, 2);
return 0;
};
```
## 10.1 x86
### 10.1.1 x86 + MSVC
f_signed() 函數:
Listing 10.1: 非優化MSVC 2010
```
#!bash
_a$ = 8
_b$ = 12
_f_signed PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
cmp eax, DWORD PTR _b$[ebp]
jle SHORT $LN3@f_signed
push OFFSET $SG737 ; ’a>b’
call _printf
add esp, 4
$LN3@f_signed:
mov ecx, DWORD PTR _a$[ebp]
cmp ecx, DWORD PTR _b$[ebp]
jne SHORT $LN2@f_signed
push OFFSET $SG739 ; ’a==b’
call _printf
add esp, 4
$LN2@f_signed:
mov edx, DWORD PTR _a$[ebp]
cmp edx, DWORD PTR _b$[ebp]
jge SHORT $LN4@f_signed
push OFFSET $SG741 ; ’a<b’
call _printf
add esp, 4
$LN4@f_signed:
pop ebp
ret 0
_f_signed ENDP
```
第一個指令JLE意味如果小于等于則跳轉。換句話說,第二個操作數大于或者等于第一個操作數,控制流將傳遞到指定地址或者標簽。否則(第二個操作數小于第一個操作數)第一個printf()將被調用。第二個檢測JNE:如果不相等則跳轉。如果兩個操作數相等控制流則不變。第三個檢測JGE:大于等于跳轉,當第一個操作數大于或者等于第二個操作數時跳轉。如果三種情況都沒有發生則無printf()被調用,事實上,如果沒有特殊干預,這種情況幾乎不會發生。
f_unsigned()函數類似,只是JBE和JAE替代了JLE和JGE,我們來看f_unsigned()函數
Listing 10.2: GCC
```
#!bash
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_f_unsigned PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
cmp eax, DWORD PTR _b$[ebp]
jbe SHORT $LN3@f_unsigned
push OFFSET $SG2761 ; ’a>b’
call _printf
add esp, 4
$LN3@f_unsigned:
mov ecx, DWORD PTR _a$[ebp]
cmp ecx, DWORD PTR _b$[ebp]
jne SHORT $LN2@f_unsigned
push OFFSET $SG2763 ; ’a==b’
call _printf
add esp, 4
$LN2@f_unsigned:
mov edx, DWORD PTR _a$[ebp]
cmp edx, DWORD PTR _b$[ebp]
jae SHORT $LN4@f_unsigned
push OFFSET $SG2765 ; ’a<b’
call _printf
add esp, 4
$LN4@f_unsigned:
pop ebp
ret 0
_f_unsigned ENDP
```
幾乎是相同的,不同的是:JBE-小于等于跳轉和JAE-大于等于跳轉。這些指令(JA/JAE/JBE/JBE)不同于JG/JGE/JL/JLE,它們使用無符號值。
我們也可以看到有符號值的表示(35)。因此我們看JG/JL代替JA/JBE的用法或者相反,我們幾乎可以確定變量的有符號或者無符號類型。
main()函數沒有什么新的內容:
Listing 10.3: main()
```
#!bash
_main PROC
push ebp
mov ebp, esp
push 2
push 1
call _f_signed
add esp, 8
push 2
push 1
call _f_unsigned
add esp, 8
xor eax, eax
pop ebp
ret 0
_main ENDP
```
### 10.1.2 x86 + MSVC + OllyDbg
我們在OD里允許例子來查看標志寄存器。我們從f_unsigned()函數開始。CMP執行了三次,每次的參數都相同,所以標志位也相同。
第一次比較的結果:fig. 10.1.標志位:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.標志位名稱為OD對其的簡稱。
當CF=1 or ZF=1時JBE將被觸發,此時將跳轉。
接下來的條件跳轉:fig. 10.2.當ZF=0(zero flag)時JNZ則被觸發
第三個條件跳轉:fig. 10.3.我們可以發現[14](img/2014101711184580126.png)當CF=0 (carry flag)時,JNB將被觸發。在該例中條件不為真,所以第三個printf()將被執行。

Figure 10.1: OllyDbg: f_unsigned(): 第一個條件跳轉

Figure 10.2: OllyDbg: f_unsigned(): 第二個條件跳轉

Figure 10.3: OllyDbg: f_unsigned(): 第三個條件跳轉
現在我們在OD中看f_signed()函數使用有符號值。
可以看到標志寄存器:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.
第一種條件跳轉JLE將被觸發fig. 10.4.我們可以發現[14](img/2014101711184580126.png),當ZF=1 or SF≠OF。該例中SF≠OF,所以跳轉將被觸發。
下一個條件跳轉將被觸發:如果ZF=0 (zero flag): fig. 10.5.
第三個條件跳轉將不會被觸發,因為僅有SF=OF,該例中不為真: fig. 10.6.

Figure 10.4: OllyDbg: f_signed(): 第一個條件跳轉

Figure 10.5: OllyDbg: f_signed(): 第二個條件跳轉

Figure 10.6: OllyDbg: f_signed(): 第三個條件跳轉
### 10.1.3 x86 + MSVC + Hiew
我們可以修改這個可執行文件,使其無論輸入的什么值f_unsigned()函數都會打印“a==b”。
在Hiew中查看:fig. 10.7.
我們要完成以下3個任務:
```
1\. 使第一個跳轉一直被觸發;
2\. 使第二個跳轉從不被觸發;
3\. 使第三個跳轉一直被觸發。
```
我們需要使代碼流進入第二個printf(),這樣才一直打印“a==b”。
三個指令(或字節)應該被修改:
```
1\. 第一個跳轉修改為JMP,但跳轉偏移值不變。
2\. 第二個跳轉有時可能被觸發,我們修改跳轉偏移值為0后,無論何種情況,程序總是跳向下一條指令。跳轉地址等于跳轉偏移值加上下一條指令地址,當跳轉偏移值為0時,跳轉地址就為下一條指令地址,所以無論如何下一條指令總被執行。
3\. 第三個跳轉我們也修改為JMP,這樣跳轉總被觸發。
```
修改后:fig. 10.8.
如果忘了這些跳轉,printf()可能會被多次調用,這種行為可能是我們不需要的。

Figure 10.7: Hiew: f_unsigned() 函數

Figure 10.8: Hiew:我們修改 f_unsigned() 函數
### 10.1.4 Non-optimizing GCC
GCC 4.4.1非優化狀態產生的代碼幾乎一樣,只是用puts() (2.3.3) 替代 printf()。
### 10.1.5 Optimizing GCC
細心的讀者可能會問,為什么要多次執行CMP,如果標志寄存器每次都相同呢?可能MSVC不會做這樣的優化,但是GCC 4.8.1可以做這樣的深度優化:
Listing 10.4: GCC 4.8.1 f_signed()
```
#!bash
f_signed:
mov eax, DWORD PTR [esp+8]
cmp DWORD PTR [esp+4], eax
jg .L6
je .L7
jge .L1
mov DWORD PTR [esp+4], OFFSET FLAT:.LC2 ; "a<b"
jmp puts
.L6:
mov DWORD PTR [esp+4], OFFSET FLAT:.LC0 ; "a>b"
jmp puts
.L1:
rep ret
.L7:
mov DWORD PTR [esp+4], OFFSET FLAT:.LC1 ; "a==b"
jmp puts
```
我們可以看到JMP puts替代了CALL puts/RETN。稍后我們介紹這種情況11.1.1.。
不用說,這種類型的x86代碼是很少見的。MSVC2012似乎不會這樣做。其他情況下,匯編程序能意識到此類使用。如果你在其它地方看到此類代碼,更可能是手工構造的。
f_unsigned()函數代碼:
Listing 10.5: GCC 4.8.1 f_unsigned()
```
#!bash
f_unsigned:
push esi
push ebx
sub esp, 20
mov esi, DWORD PTR [esp+32]
mov ebx, DWORD PTR [esp+36]
cmp esi, ebx
ja .L13
cmp esi, ebx ; instruction may be removed
je .L14
.L10:
jb .L15
add esp, 20
pop ebx
pop esi
ret
.L15:
mov DWORD PTR [esp+32], OFFSET FLAT:.LC2 ; "a<b"
add esp, 20
pop ebx
pop esi
jmp puts
.L13:
mov DWORD PTR [esp], OFFSET FLAT:.LC0 ; "a>b"
call puts
cmp esi, ebx
jne .L10
.L14:
mov DWORD PTR [esp+32], OFFSET FLAT:.LC1 ; "a==b"
add esp, 20
pop ebx
pop esi
jmp puts
```
因此,GCC 4.8.1的優化算法并不總是完美的。
## 10.2 ARM
### 10.2.1 Keil + ARM mode優化后
Listing 10.6: Optimizing Keil + ARM mode
```
#!bash
.text:000000B8 EXPORT f_signed
.text:000000B8 f_signed ; CODE XREF: main+C
.text:000000B8 70 40 2D E9 STMFD SP!, {R4-R6,LR}
.text:000000BC 01 40 A0 E1 MOV R4, R1
.text:000000C0 04 00 50 E1 CMP R0, R4
.text:000000C4 00 50 A0 E1 MOV R5, R0
.text:000000C8 1A 0E 8F C2 ADRGT R0, aAB ; "a>b
"
.text:000000CC A1 18 00 CB BLGT __2printf
.text:000000D0 04 00 55 E1 CMP R5, R4
.text:000000D4 67 0F 8F 02 ADREQ R0, aAB_0 ; "a==b
"
.text:000000D8 9E 18 00 0B BLEQ __2printf
.text:000000DC 04 00 55 E1 CMP R5, R4
.text:000000E0 70 80 BD A8 LDMGEFD SP!, {R4-R6,PC}
.text:000000E4 70 40 BD E8 LDMFD SP!, {R4-R6,LR}
.text:000000E8 19 0E 8F E2 ADR R0, aAB_1 ; "a<b
"
.text:000000EC 99 18 00 EA B __2printf
.text:000000EC ; End of function f_signed
```
ARM下很多指令只有某些標志位被設置時才會被執行。比如做數值比較時。
舉個例子,ADD實施上是ADDAL,這里的AL是Always,即總被執行。判定謂詞是32位ARM指令的高4位(條件域)。無條件跳轉的B指令其實是有條件的,就行其它任何條件跳轉一樣,只是條件域為AL,這意味著總是被執行,忽略標志位。
ADRGT指令就像和ADR一樣,只是該指令前面為CMP指令,并且只有前面數值大于另一個數值時(Greater Than)時才被執行。
接下來的BLGT行為和BL一樣,只有比較結果符合條件才能出發(Greater Than)。ADRGT把字符串“a>b ”的地址寫入R0,然后BLGT調用printf()。因此,這些指令都帶有GT后綴,只有當R0(a值)大于R4(b值)時指令才會被執行。
然后我們看ADREQ和BLEQ,這些指令動作和ADR及BL一樣,只有當兩個操作數對比后相等時才會被執行。這些指令前面是CMP(因為printf()調用可能會修改狀態標識)。 然后我們看LDMGEFD,該指令行為和LDMFD指令一樣1,僅僅當第一個值大于等于另一個值時(Greater Than),指令才會被執行。
“LDMGEFD SP!, {R4-R6,PC}”恢復寄存器并返回,只是當a>=b時才被觸發,這樣之后函數才執行完成。但是如果a<b,觸發條件不成立是將執行下一條指令LDMFD SP!, {R4-R6,LR},該指令保存R4-R6寄存器,使用LR而不是PC,函數并不返回。最后兩條指令是執行printf()(5.3.2)。
f_unsigned與此一樣只是使用對應的指令為ADRHI, BLHI及LDMCSFD,判斷謂詞(HI = Unsigned higher, CS = Carry Set (greater than or equal))請類比之前的說明,另外就是函數內部使用無符號數值。
我們來看一下main()函數:
Listing 10.7: main()
```
#!bash
.text:00000128 EXPORT main
.text:00000128 main
.text:00000128 10 40 2D E9 STMFD SP!, {R4,LR}
.text:0000012C 02 10 A0 E3 MOV R1, #2
.text:00000130 01 00 A0 E3 MOV R0, #1
.text:00000134 DF FF FF EB BL f_signed
.text:00000138 02 10 A0 E3 MOV R1, #2
.text:0000013C 01 00 A0 E3 MOV R0, #1
.text:00000140 EA FF FF EB BL f_unsigned
.text:00000144 00 00 A0 E3 MOV R0, #0
.text:00000148 10 80 BD E8 LDMFD SP!, {R4,PC}
.text:00000148 ; End of function main
```
這就是ARM模式如何避免使用條件跳轉。
這樣做有什么好處呢?因為ARM使用精簡指令集(RISC)。簡言之,處理器流水線技術受到跳轉的影響,這也是分支預測重要的原因。程序使用的條件或者無條件跳轉越少越好,使用斷言指令可以減少條件跳轉的使用次數。
x86沒有這也的功能,通過使用CMP設置相應的標志位來觸發指令。
### 10.2.2 Optimizing Keil + thumb mode
Listing 10.8: Optimizing Keil + thumb mode
```
#!bash
.text:00000072 f_signed ; CODE XREF: main+6
.text:00000072 70 B5 PUSH {R4-R6,LR}
.text:00000074 0C 00 MOVS R4, R1
.text:00000076 05 00 MOVS R5, R0
.text:00000078 A0 42 CMP R0, R4
.text:0000007A 02 DD BLE loc_82
.text:0000007C A4 A0 ADR R0, aAB ; "a>b
"
.text:0000007E 06 F0 B7 F8 BL __2printf
.text:00000082
.text:00000082 loc_82 ; CODE XREF: f_signed+8
.text:00000082 A5 42 CMP R5, R4
.text:00000084 02 D1 BNE loc_8C
.text:00000086 A4 A0 ADR R0, aAB_0 ; "a==b
"
.text:00000088 06 F0 B2 F8 BL __2printf
.text:0000008C
.text:0000008C loc_8C ; CODE XREF: f_signed+12
.text:0000008C A5 42 CMP R5, R4
.text:0000008E 02 DA BGE locret_96
.text:00000090 A3 A0 ADR R0, aAB_1 ; "a<b
"
.text:00000092 06 F0 AD F8 BL __2printf
.text:00000096
.text:00000096 locret_96 ; CODE XREF: f_signed+1C
.text:00000096 70 BD POP {R4-R6,PC}
.text:00000096 ; End of function f_signed
```
僅僅Thumb模式下的B指令可能需要條件代碼輔助,所以thumb代碼看起來更普通一些。
BLE通常是條件跳轉小于或等于(Less than or Equal),BNE—不等于(Not Equal),BGE—大于或等于(Greater than or Equal)。
f_unsigned函數是同樣的,只是使用的指令用來處理無符號數值:BLS (Unsigned lower or same) 和BCS (Carry Set (Greater than or equal)).
- 第一章 CPU簡介
- 第二章 Hello,world!
- 第三章? 函數開始和結束
- 第四章 棧
- Chapter 5 printf() 與參數處理
- Chapter 6 scanf()
- CHAPER7 訪問傳遞參數
- Chapter 8 一個或者多個字的返回值
- Chapter 9 指針
- Chapter 10 條件跳轉
- 第11章 選擇結構switch()/case/default
- 第12章 循環結構
- 第13章 strlen()
- Chapter 14 Division by 9
- chapter 15 用FPU工作
- Chapter 16 數組
- Chapter 17 位域
- 第18章 結構體
- 19章 聯合體
- 第二十章 函數指針
- 第21章 在32位環境中的64位值
- 第二十二章 SIMD
- 23章 64位化
- 24章 使用x64下的SIMD來處理浮點數
- 25章 溫度轉換
- 26章 C99的限制
- 27章 內聯函數
- 第28章 得到不正確反匯編結果
- 第29章 花指令
- 第30章 16位Windows
- 第31章 類
- 三十二 ostream