任何錯誤,歡迎指正~~~
下面是系統和一些七七八八的東西,一定要用64位CPU的真機
C語言
arm64
Xcode9.3
iOS11.3
準備
- 因為我們需要從彙編的角度去研究這個問題,首先開啟Xcode的彙編除錯,依次選擇 Debug->DebugWorkflow->Always Show Disassembly
- 注意要在debug模式下除錯,release模式下編譯器優化後你可能只能看到兩行彙編了
三個case分支的switch
首先我們寫一個擁有三個case的switch;
void func(int a) {
switch (a) {
case 1:
printf("1");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
default:
printf("default");
break;
}
}
int main(int argc, char * argv[]) {
func(2);
return 0;
}
複製程式碼
在func函式switch語句處放一個斷點,執行後結果如下圖(截圖不全):
把程式碼複製下來方便註釋:
0x100d56838 <+0>: sub sp, sp, #0x40
0x100d5683c <+4>: stp x29, x30, [sp, #0x30]
0x100d56840 <+8>: add x29, sp, #0x30
0x100d56844 <+12>: stur w0, [x29, #-0x4]
; 引數=0x1
-> 0x100d56848 <+16>: ldur w0, [x29, #-0x4]
0x100d5684c <+20>: mov x8, x0
0x100d56850 <+24>: subs w0, w0, #0x1 ;w0-1
0x100d56854 <+28>: stur w8, [x29, #-0x8]
0x100d56858 <+32>: stur w0, [x29, #-0xc]
0x100d5685c <+36>: b.eq 0x100d5688c ; <+24>行的執行結果,引數是否等於1,等於則跳轉到 0x100d5688c
0x100d56860 <+40>: b 0x100d56864
; 引數=0x2
0x100d56864 <+44>: ldur w8, [x29, #-0x8]
0x100d56868 <+48>: subs w9, w8, #0x2
0x100d5686c <+52>: stur w9, [x29, #-0x10]
0x100d56870 <+56>: b.eq 0x100d568a0 ; <+48>行的執行結果,引數是否等於2,等於則跳轉到 0x100d568a0
0x100d56874 <+60>: b 0x100d56878
; 引數=0x3
0x100d56878 <+64>: ldur w8, [x29, #-0x8]
0x100d5687c <+68>: subs w9, w8, #0x3
0x100d56880 <+72>: stur w9, [x29, #-0x14]
0x100d56884 <+76>: b.eq 0x100d568b4 ;
<+68>行的執行結果,引數是否等於3,等於則跳轉到 0x100d568b4
0x100d56888 <+80>: b 0x100d568c8 ; 前三個分支均不匹配,跳轉到default
;列印1
0x100d5688c <+84>: adrp x0, 1
0x100d56890 <+88>: add x0, x0, #0xf24
0x100d56894 <+92>: bl 0x100d56bf4 ; symbol stub for: printf
0x100d56898 <+96>: str w0, [sp, #0x18]
0x100d5689c <+100>: b 0x100d568d8
;列印2
0x100d568a0 <+104>: adrp x0, 1
0x100d568a4 <+108>: add x0, x0, #0xf26
0x100d568a8 <+112>: bl 0x100d56bf4 ; symbol stub for: printf
0x100d568ac <+116>: str w0, [sp, #0x14]
0x100d568b0 <+120>: b 0x100d568d8
;列印3
0x100d568b4 <+124>: adrp x0, 1
0x100d568b8 <+128>: add x0, x0, #0xf28
0x100d568bc <+132>: bl 0x100d56bf4 ; symbol stub for: printf
0x100d568c0 <+136>: str w0, [sp, #0x10]
0x100d568c4 <+140>: b 0x100d568d8
;列印default
0x100d568c8 <+144>: adrp x0, 1
0x100d568cc <+148>: add x0, x0, #0xf2a
0x100d568d0 <+152>: bl 0x100d56bf4 ; symbol stub for: printf
0x100d568d4 <+156>: str w0, [sp, #0xc]
;函式結束
0x100d568d8 <+160>: ldp x29, x30, [sp, #0x30]
0x100d568dc <+164>: add sp, sp, #0x40
0x100d568e0 <+168>: ret
複製程式碼
彙編程式碼順序執行,關鍵程式碼新增了註釋,有幾個關鍵的暫存器和指令:
- x0(w0)為我們傳遞的引數,也就是1;
- subs指令會修改cpsr暫存器,這個暫存器會影響到B指令的執行(對這個有疑惑的可以參考這篇文章 ),偷一張圖~
註釋的很清楚啦,我們看下邏輯:
- 判斷是否滿足第一個case(等於1),滿足跳轉列印,不滿足繼續執行;
- 判斷是否滿足第二個case(等於2),滿足跳轉列印,不滿足繼續執行;
- 判斷是否滿足第三個case(等於3),滿足跳轉列印,不滿足執行default;
這不就是if判斷嘛!別急。。。繼續往下看
多於三個分支的switch
這是switch:
void func(int a) {
switch (a) {
case 1:
printf("1");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
case 4:
printf("4");
break;
default:
printf("default");
break;
}
}
int main(int argc, char * argv[]) {
func(2);
return 0;
}
複製程式碼
彙編程式碼如下,不截圖啦
0x104cae820 <+0>: sub sp, sp, #0x40
0x104cae824 <+4>: stp x29, x30, [sp, #0x30]
0x104cae828 <+8>: add x29, sp, #0x30
0x104cae82c <+12>: stur w0, [x29, #-0x4]
;引數減1,即第一個case的值
-> 0x104cae830 <+16>: ldur w0, [x29, #-0x4]
0x104cae834 <+20>: subs w0, w0, #0x1 ; =0x1
;引數減3,共減4,即最後一個case分支的值
0x104cae838 <+24>: mov x8, x0
0x104cae83c <+28>: subs w0, w0, #0x3 ; =0x3
;注意這裡,我們把減去第一個case的值(注意結果哦)入棧了[x29, #-0x10]
0x104cae840 <+32>: stur x8, [x29, #-0x10]
0x104cae844 <+36>: stur w0, [x29, #-0x14]
;引數總共減去了4,也就是看他是否滿足最大值4,不滿足則跳轉default
0x104cae848 <+40>: b.hi 0x104cae8b4
;這部分計算過後,會取到一個<地址>放到x8暫存器
0x104cae84c <+44>: adrp x8, 0
0x104cae850 <+48>: add x8, x8, #0x8d0
;x9暫存器取到剛才入棧的值,也就是引數減去第一個case的值(注意結果哦)
0x104cae854 <+52>: ldur x9, [x29, #-0x10]
;以下三條指令會獲取到一個地址並跳轉,跳轉的位置剛好是我們傳入的引數對應的case語句(緣分啊~)
0x104cae858 <+56>: ldrsw x10, [x8, x9, lsl #2]
;ldrsw:這個指令的意思是讀取記憶體地址中的2個字(word,即4個位元組,32位),並且把高位的符號位作為擴充套件填充到64位的暫存器中。
;這條指令會進行如下操作:
;1.將x9暫存器的值左移兩位(即乘以4)。
;2.將x8暫存器的值與這個值相加。
;3.將得到記憶體地址中的值存到x10暫存器。
0x104cae85c <+60>: add x8, x10, x8 ;將之前取到的地址(x8),與x10中的值相加,取得一個地址
0x104cae860 <+64>: br x8
;第1個case
0x104cae864 <+68>: adrp x0, 1
0x104cae868 <+72>: add x0, x0, #0xf20 ; =0xf20
0x104cae86c <+76>: bl 0x104caebf0 ; symbol stub for: printf
0x104cae870 <+80>: str w0, [sp, #0x18]
0x104cae874 <+84>: b 0x104cae8c4 ; <+164> at main.m:30
;第2個case
0x104cae878 <+88>: adrp x0, 1
0x104cae87c <+92>: add x0, x0, #0xf22 ; =0xf22
0x104cae880 <+96>: bl 0x104caebf0 ; symbol stub for: printf
0x104cae884 <+100>: str w0, [sp, #0x14]
0x104cae888 <+104>: b 0x104cae8c4 ; <+164> at main.m:30
;第3個case
0x104cae88c <+108>: adrp x0, 1
0x104cae890 <+112>: add x0, x0, #0xf24 ; =0xf24
0x104cae894 <+116>: bl 0x104caebf0 ; symbol stub for: printf
0x104cae898 <+120>: str w0, [sp, #0x10]
0x104cae89c <+124>: b 0x104cae8c4 ; <+164> at main.m:30
;第4個case
0x104cae8a0 <+128>: adrp x0, 1
0x104cae8a4 <+132>: add x0, x0, #0xf26 ; =0xf26
0x104cae8a8 <+136>: bl 0x104caebf0 ; symbol stub for: printf
0x104cae8ac <+140>: str w0, [sp, #0xc]
0x104cae8b0 <+144>: b 0x104cae8c4 ; <+164> at main.m:30
;default
0x104cae8b4 <+148>: adrp x0, 1
0x104cae8b8 <+152>: add x0, x0, #0xf28 ; =0xf28
0x104cae8bc <+156>: bl 0x104caebf0 ; symbol stub for: printf
;函式結束
0x104cae8c0 <+160>: str w0, [sp, #0x8]
0x104cae8c4 <+164>: ldp x29, x30, [sp, #0x30]
0x104cae8c8 <+168>: add sp, sp, #0x40 ; =0x40
0x104cae8cc <+172>: ret
複製程式碼
不一樣了對不對ಠ_ಠ,我們分析下這部分彙編:
- 斷點到<+60> 我們可以看到,x8暫存器的地址下,存了幾個很有意思的資料: 0xffffff94, 0xffffffa8, 0xffffffbc, 0xffffffd0 而後我們經過ldrsw指令取到了特定的值,存在x10暫存器
- 斷點到<+64> 將x10和x8中的值結合,我們得到了一個地址: 0x0000000104cae878
- 第<+64>行執行後,我們通過br指令跳轉到這個地址,這個地址剛好是<+88>行,也就是我們的case2。(奇蹟發生了,完美匹配了,我們之前傳入的引數就是2)
哎?什麼原理呢?
第<+44>行,我們取到了一個地址存到了X8暫存器,這個地址下存的是什麼呢?
實際上這是一個表,每四個位元組為一段資料,所以我們在ldrsw指令中會左移兩位,也就是乘以4。那麼這個資料是什麼呢?其實,0xffffff94與x8結合後得到的是case1中執行程式碼的記憶體地址,0xffffffa8與x8結合後得到的是case2中執行程式碼的記憶體地址,以此類推。至於為什麼這個表中的儲存的是這個值,我覺得這是編譯器做的事~。
同樣的在<+32>行把減去第一個case的值入棧,之後在<+52>行把這個值存到x9暫存器,此時這個在ldrsw指令中參與操作的x9暫存器的值,正是一個偏移值,這也是為什麼我們要減去case1再入棧的原因。(比如我們傳入1,減去case1的值之後就是0,也就是偏移0位,我們從表中取到的值就是第一個值,0xffffff94,而後通過這個值與x8暫存器的值計算,得到的就是case1中執行程式碼的記憶體地址。)
結論:
- switch通過這種巧妙的方式,在記憶體中建立了一個表,儲存一個用於計算獲取case執行指令地址的值。
- 將引數對應的case偏移位計算出來存到了x9暫存器中。
- 將一個用於計算的基址存在了X8暫存器中。
- 通過ldrsw指令,取到這個表中偏移x9的值,和x8進行計算,取到對應case指令的地址。
- 跳轉到對應的case執行。