在C語言中,if和switch是條件分支的重要組成部分。if的功能是計算判斷條件的值,根據返回的值的不同來決定跳轉到哪個部分。值為真則跳轉到if語句塊中,否則跳過if語句塊。下面來分析一個簡單的if實例:
if(argc > 0) { printf("argc > 0\n"); } if (argc <= 0) { printf("argc <= 0\n"); } printf("argc = %d\n", argc);
它對應的匯編代碼如下:
9: if(argc > 0) 00401028 cmp dword ptr [ebp+8],0 0040102C jle main+2Bh (0040103b) ;argc <= 0就跳轉到下一個if處 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (0042003c) 00401033 call printf (00401090) 00401038 add esp,4 12: } 13: if (argc <= 0) ;argc > 0跳轉到后面的printf語句輸出argc的值 0040103B cmp dword ptr [ebp+8],0 0040103F jg main+3Eh (0040104e) 14: { 15: printf("argc <= 0\n"); 00401041 push offset string "argc <= 0\n" (0042002c) 00401046 call printf (00401090) 0040104B add esp,4 16: } 17: printf("argc = %d\n", argc); 0040104E mov eax,dword ptr [ebp+8] 00401051 push eax 00401052 push offset string "argc = %d\n" (0042001c) 00401057 call printf (00401090) 0040105C add esp,8
根據匯編代碼我們看到,首先執行第一個if中的比較,jle表示當cmp得到的結果≤0時會進行跳轉,第二個if在匯編中的跳轉條件是>0,從這個上面可以看出在代碼執行過程當中if轉換的條件判斷語句與if的判斷結果時相反的,也就是說cmp比較后不成立則跳轉,成立則向下執行。同時每一次跳轉都是到當前if語句的下一條語句。
下面來看看if...else...語句的跳轉。
if(argc > 0) { printf("argc > 0\n"); }else { printf("argc <= 0\n"); } printf("argc = %d\n", argc);
它所對應的匯編代碼如下:
00401028 cmp dword ptr [ebp+8],0 0040102C jle main+2Dh (0040103d) ;條件不滿足則跳轉到else語句塊中 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (0042003c) 00401033 call printf (00401090) 00401038 add esp,4 12: }else 0040103B jmp main+3Ah (0040104a);如果執行if語句塊就會執行這條語句跳出else語句塊 13: { 14: printf("argc <= 0\n"); 0040103D push offset string "argc <= 0\n" (0042002c) 00401042 call printf (00401090) 00401047 add esp,4 15: } 16: printf("argc = %d\n", argc); 0040104A mov eax,dword ptr [ebp+8]
上述的匯編代碼指出,對於if...else..語句,首先進行條件判斷,if表達式為真,則繼續執行if快中的語句,然后利用jmp跳轉到else語句塊外,否則會利用jmp跳轉到else語句塊中,然后依次執行其后的每一句代碼。
最后再來展示if...else if...else這種分支結構:
if(argc > 0) { printf("argc > 0\n"); }else if(argc < 0) { printf("argc < 0\n"); }else { printf("argc == 0\n"); } printf("argc = %d\n", argc);
匯編代碼如下:
9: if(argc > 0) 00401028 cmp dword ptr [ebp+8],0 0040102C jle main+2Dh (0040103d);條件不滿足則會跳轉到下一句else if中 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (00420f9c) 00401033 call printf (00401090) 00401038 add esp,4 12: }else if(argc < 0) 0040103B jmp main+4Fh (0040105f) ;當上述條件符合則執行這條語句跳出分支外,跳轉的地址正是else語句外的printf語句 0040103D cmp dword ptr [ebp+8],0 00401041 jge main+42h (00401052) 13: { 14: printf("argc < 0\n"); 00401043 push offset string "argc < 0\n" (0042003c) 00401048 call printf (00401090) 0040104D add esp,4 15: }else 00401050 jmp main+4Fh (0040105f) 16: { 17: printf("argc == 0\n"); 00401052 push offset string "argc <= 0\n" (0042002c) 00401057 call printf (00401090) 0040105C add esp,4 18: } 19: printf("argc = %d\n", argc); 0040105F mov eax,dword ptr [ebp+8]
通過匯編代碼可以看到對於這種結構,會依次判斷每個if語句中的條件,當有一個滿足,執行完對應語句塊中的代碼后,會直接調轉到分支結構外部,當前面的條件都不滿足則會執行else語句塊中的內容。這個邏輯結構在某些情況下可以利用if return if return 這種結構來替代。當某一條件滿足時執行完對應的語句后直接返回而不執行其后的代碼。一條提升效率的做法是將最有可能滿足的條件放在前面進行比較,這樣可以減少比較次數,提升效率。
switch是另一種比較常用的多分支結構,在使用上比較簡單,效率上也比if...else if...else高,下面將分析switch結構的實現
switch(argc)
{
case 1:
printf("argc = 1\n");
break;
case 2:
printf("argc = 2\n");
break;
case 3:
printf("argc = 3\n");
break;
case 4:
printf("argc = 4\n");
break;
case 5:
printf("argc = 5\n");
break;
case 6:
printf("argc = 6\n");
break;
default:
printf("else\n");
break;
}
對應的匯編代碼如下:
0040B798 mov eax,dword ptr [ebp+8] ;eax = argc 0040B79B mov dword ptr [ebp-4],eax 0040B79E mov ecx,dword ptr [ebp-4] ;ecx = eax 0040B7A1 sub ecx,1 0040B7A4 mov dword ptr [ebp-4],ecx 0040B7A7 cmp dword ptr [ebp-4],5 0040B7AB ja $L544+0Fh (0040b811) ;argc 》 5則跳轉到default處,至於為什么是5而不是6,看后面的說明 0040B7AD mov edx,dword ptr [ebp-4] ;edx = argc 0040B7B0 jmp dword ptr [edx*4+40B831h] 11: case 1: 12: printf("argc = 1\n"); 0040B7B7 push offset string "argc = 1\n" (00420fc0) 0040B7BC call printf (00401090) 0040B7C1 add esp,4 13: break; 0040B7C4 jmp $L544+1Ch (0040b81e) 14: case 2: 15: printf("argc = 2\n"); 0040B7C6 push offset string "argc = 2\n" (00420fb4) 0040B7CB call printf (00401090) 0040B7D0 add esp,4 16: break; 0040B7D3 jmp $L544+1Ch (0040b81e) 17: case 3: 18: printf("argc = 3\n"); 0040B7D5 push offset string "argc = 3\n" (00420fa8) 0040B7DA call printf (00401090) 0040B7DF add esp,4 19: break; 0040B7E2 jmp $L544+1Ch (0040b81e) 20: case 4: 21: printf("argc = 4\n"); 0040B7E4 push offset string "argc = 4\n" (00420f9c) 0040B7E9 call printf (00401090) 0040B7EE add esp,4 22: break; 0040B7F1 jmp $L544+1Ch (0040b81e) 23: case 5: 24: printf("argc = 5\n"); 0040B7F3 push offset string "argc < 0\n" (0042003c) 0040B7F8 call printf (00401090) 0040B7FD add esp,4 25: break; 0040B800 jmp $L544+1Ch (0040b81e) 26: case 6: 27: printf("argc = 6\n"); 0040B802 push offset string "argc <= 0\n" (0042002c) 0040B807 call printf (00401090) 0040B80C add esp,4 28: break; 0040B80F jmp $L544+1Ch (0040b81e) 29: default: 30: printf("else\n"); 0040B811 push offset string "argc = %d\n" (0042001c) 0040B816 call printf (00401090) 0040B81B add esp,4 31: break; 32: } 33: 34: return 0; 0040B81E xor eax,eax
上面的代碼中並沒有看到像if那樣,對每一個條件都進行比較,其中有一句話 “jmp dword ptr [edx*4+40B831h]” 這句話從表面上看應該是取數組中的元素,再根據元素的值來進行跳轉,而這個元素在數組中的位置與eax也就是與argc的值有關,下面我們跟蹤到數組中查看數組的元素值:
0040B831 B7 B7 40 00
0040B835 C6 B7 40 00
0040B839 D5 B7 40 00
0040B83D E4 B7 40 00
0040B841 F3 B7 40 00
0040B845 02 B8 40 00
通過對比可以發現0x0040b7b7是case 1處的地址,后面的分別是case 2、case 3、case 4、case 5、case 6處的地址,每個case中的break語句都翻譯為了同一句話“jmp $L544+1Ch (0040b81e)”,所以從這可以看出,在switch中,編譯器多增加了一個數組用於存儲每個case對應的地址,根據switch中傳入的整數在數組中查到到對應的地址,直接通過這個地址跳轉到對應的位置,減少了比較操作,提升了效率。編譯器在處理switch時會首先校驗不滿足所有case的情況,當這種情況發生時代碼調轉到default或者switch語句塊之外。然后將傳入的整數值減一(數組元素是從0開始計數)。最后根據參數值找到應該跳轉的位置。
上述的代碼case是從0~6依次遞增,這樣做確實可行,但是當我們在case中的值並不是依次遞增的話會怎樣?此時根據不同的情況編譯器會做不同的處理。
1)一般任然會建立這樣的一個表,將case中出現的值填寫對應的跳轉地址,沒有出現的則將這個地址值填入default對應的地址或者switch語句結束的地址,比如當我們上述的代碼去掉case 5, 這個時候填入的地址值如下圖所示:
2)如果每兩個case之間的差距大於6,或者case語句數小於4則不會采取這種做法,如果再采用這種方式,那么會造成較大的資源消耗。這個時候編譯器會采用索引表的方式來進行地址的跳轉。
下面有這樣一個例子:
switch(argc) { case 1: printf("argc = 1\n"); break; case 2: printf("argc = 2\n"); break; case 5: printf("argc = 5\n"); break; case 6: printf("argc = 6\n"); break; case 255: printf("argc = 255\n"); default: printf("else\n"); break; }
它對應的匯編代碼如下:
0040B798 mov eax,dword ptr [ebp+8] 0040B79B mov dword ptr [ebp-4],eax 0040B79E mov ecx,dword ptr [ebp-4] ;到此eax = ecx = argc 0040B7A1 sub ecx,1 0040B7A4 mov dword ptr [ebp-4],ecx 0040B7A7 cmp dword ptr [ebp-4],0FEh 0040B7AE ja $L542+0Dh (0040b80b) ;當argc > 255則跳轉到default處 0040B7B0 mov eax,dword ptr [ebp-4] 0040B7B3 xor edx,edx 0040B7B5 mov dl,byte ptr (0040b843)[eax] 0040B7BB jmp dword ptr [edx*4+40B82Bh] 11: case 1: 12: printf("argc = 1\n"); 0040B7C2 push offset string "argc = 1\n" (00420fb4) 0040B7C7 call printf (00401090) 0040B7CC add esp,4 13: break; 0040B7CF jmp $L542+1Ah (0040b818) 14: case 2: 15: printf("argc = 2\n"); 0040B7D1 push offset string "argc = 3\n" (00420fa8) 0040B7D6 call printf (00401090) 0040B7DB add esp,4 16: break; 0040B7DE jmp $L542+1Ah (0040b818) 17: case 5: 18: printf("argc = 5\n"); 0040B7E0 push offset string "argc = 5\n" (00420f9c) 0040B7E5 call printf (00401090) 0040B7EA add esp,4 19: break; 0040B7ED jmp $L542+1Ah (0040b818) 20: case 6: 21: printf("argc = 6\n"); 0040B7EF push offset string "argc < 0\n" (0042003c) 0040B7F4 call printf (00401090) 0040B7F9 add esp,4 22: break; 0040B7FC jmp $L542+1Ah (0040b818) 23: case 255: 24: printf("argc = 255\n"); 0040B7FE push offset string "argc <= 0\n" (0042002c) 0040B803 call printf (00401090) 0040B808 add esp,4 25: default: 26: printf("else\n"); 0040B80B push offset string "argc = %d\n" (0042001c) 0040B810 call printf (00401090) 0040B815 add esp,4 27: break; 28: } 29: 30: return 0; 0040B818 xor eax,eax
這段代碼與上述的線性表相比較區別並不大,只是多了一句 “mov dl,byte ptr (0040b843)[eax]” 這似乎又是一個數組,通過查看內存可以知道這個數組的值分別為:00 01 05 05 02 03 05 05 ... 04,下一句根據這些值在另外一個數組中查找數據,我們列出另外一個數組的值:
C2 B7 40 00 D1 B7 40 00 E0 B7 40 00 EF B7 40 00 FE B7 40 00 0B B8 40 00
通過對比我們發現,這些值分別是每個case與default入口處的地址,編譯器先查找到每個值在數組中對應的元素位置,然后根據這個位置值再在地址表中從、找到地址進行跳轉,這個過程可以用下面的圖來表示:
這樣通過一個每個元素占一個字節的表,來表示對應的case在地址表中所對應的位置,從而跳轉到對應的地址,這樣通過對每個case增加一個字節的內存消耗來達到,減少地址表對應的內存消耗。
在上述的匯編代碼中,是利用dl寄存器來存儲對應case在地址表中項,這樣就會產生一個問題,當case 值大於 255,也就是超出了一個字節的,超出了dl寄存器的表示范圍時,又該如何來進行跳轉這個時候編譯器會采用判定樹的方式來進行判定,在根節點保存的是所有case值的中位數, 左子樹都是大於這個大於這個值的數,右字數是小於這個值的數,通過每次的比較來得到正確的地址。比如下面的這個判定樹:
首先與10進行比較,根據與10 的大小關系進入左子樹或者右子樹,再看看左右子樹的分支是否不大於3,若不大於3則直接轉化為對應的if...else if... else結構,大於3則檢測分支是否滿足上述的優化條件,滿足則進行對應的地址表或者索引表的優化,否則會再次對子樹進行優化,以便減少比較次數。