??我們都知道switch是一個(gè)選擇分支結(jié)構(gòu),其功能和if...else是一樣的,那么他們兩個(gè)有什么區(qū)別呢?我們什么時(shí)候用switch,什么時(shí)候該用if...else呢?帶著這些疑問(wèn),我們從匯編的角度剖析一下,ARM64架構(gòu)編譯器是如何做的??if...else的匯編我們就不過(guò)多講了,可以參考匯編if...else語(yǔ)句,我們主要講一下switch:
開(kāi)始之前我們先來(lái)了解一下基礎(chǔ)知識(shí):也可以點(diǎn)擊這里查看常用指令
- ASLR機(jī)制,(地址隨機(jī)化)是一種針對(duì)緩沖區(qū)溢出的安全保護(hù)技術(shù),通過(guò)對(duì)堆、棧、共享庫(kù)映射等線(xiàn)性區(qū)布局的隨機(jī)化,通過(guò)增加攻擊者預(yù)測(cè)目的地址的難度,防止攻擊者直接定位攻擊代碼位置;
- subs指令,運(yùn)算結(jié)果影響目標(biāo)寄存器,也影響狀態(tài)寄存器
- adrp x0, 1 ;將1向左移12位,補(bǔ)0??,即將PC寄存器的低12位清零
- ldrsw x10, [x8, x11, lsl #2] ;將x11寄存器向左移2位,加上x(chóng)8寄存器地址,作為尋址地址,讀出數(shù)值放到x10寄存器中。
首先如果一個(gè)switch有兩個(gè)分支:(一般很少用不講)
如果一個(gè)switch有兩個(gè)case分支:
void testFun(int a)
{
switch (b) {
case 5:
printf("O(∩_∩)O\n");
break;
case 9:
printf("haha\n");
break;
default:
printf("diaomao\n");
break;
}
}
調(diào)用
testFun(6);
我們看一下匯編代碼
Demo`testFun:
0x1007de1e4 <+0>: sub sp, sp, #0x30 ; =0x30
0x1007de1e8 <+4>: stp x29, x30, [sp, #0x20]
0x1007de1ec <+8>: add x29, sp, #0x20 ; =0x20
0x1007de1f0 <+12>: stur w0, [x29, #-0x4]
0x1007de1f4 <+16>: ldur w0, [x29, #-0x4]
0x1007de1f8 <+20>: mov x8, x0
0x1007de1fc <+24>: subs w0, w0, #0x5 ; =0x5
0x1007de200 <+28>: stur w8, [x29, #-0x8]
0x1007de204 <+32>: stur w0, [x29, #-0xc]
0x1007de208 <+36>: b.eq 0x1007de224 ; <+64> at main.m:34:13
0x1007de20c <+40>: b 0x1007de210 ; <+44> at main.m
0x1007de210 <+44>: ldur w8, [x29, #-0x8]
0x1007de214 <+48>: subs w9, w8, #0x9 ; =0x9
0x1007de218 <+52>: str w9, [sp, #0x10]
0x1007de21c <+56>: b.eq 0x1007de238 ; <+84> at main.m:37:13
0x1007de220 <+60>: b 0x1007de24c ; <+104> at main.m:47:13
0x1007de224 <+64>: adrp x0, 1
0x1007de228 <+68>: add x0, x0, #0xf76 ; =0xf76
0x1007de22c <+72>: bl 0x1007de5f4 ; symbol stub for: printf
0x1007de230 <+76>: str w0, [sp, #0xc]
0x1007de234 <+80>: b 0x1007de25c ; <+120> at main.m:63:1
0x1007de238 <+84>: adrp x0, 1
0x1007de23c <+88>: add x0, x0, #0xf83 ; =0xf83
0x1007de240 <+92>: bl 0x1007de5f4 ; symbol stub for: printf
0x1007de244 <+96>: str w0, [sp, #0x8]
0x1007de248 <+100>: b 0x1007de25c ; <+120> at main.m:63:1
-> 0x1007de24c <+104>: adrp x0, 1
0x1007de250 <+108>: add x0, x0, #0xf89 ; =0xf89
0x1007de254 <+112>: bl 0x1007de5f4 ; symbol stub for: printf
0x1007de258 <+116>: str w0, [sp, #0x4]
0x1007de25c <+120>: ldp x29, x30, [sp, #0x20]
0x1007de260 <+124>: add sp, sp, #0x30 ; =0x30
0x1007de264 <+128>: ret
??我們來(lái)分析一下,直到0x1007de1f8 <+20>: mov x8, x0這行指令w0寄存器是我們的形參值8,8比5如果eq,就跳轉(zhuǎn)到0x1007de224(也就是printf("O(∩_∩)O\n");)處執(zhí)行,那它是如何找到常量的呢?我們通過(guò)0x1007de224 <+64>: adrp x0, 1 0x1007de228 <+68>: add x0, x0, #0xf76能夠定位到一個(gè)內(nèi)存地址0x1007dff76,然后我們打開(kāi)view Memory,輸入這個(gè)地址可以看到:

如果不相等,繼續(xù)往下走,跟9比較,如果相等就跳到
0x1007de238處執(zhí)行,同理,也就是view memory的0x1007dff83地址,也就是上圖的haha,否則直接跳到default中,也就是對(duì)應(yīng)的view Memory的0x1007dff89地址----上圖的diaomao。
如果一個(gè)switch有三個(gè)case分支:
void testFun(int a)
{
switch (b) {
case 5:
printf("O(∩_∩)O\n");
break;
case 9:
printf("haha\n");
break;
case 2:
printf("kaiche\n");
break;
default:
printf("diaomao\n");
break;
}
}
調(diào)用
testFun(6);
注:可以自己測(cè)試一下,和上面兩個(gè)case分支是一樣的,也是一個(gè)一個(gè)比,然后執(zhí)行相應(yīng)的輸出
如果一個(gè)switch有四個(gè)case分支:
void testFun(int a)
{
switch (b) {
case 5:
printf("O(∩_∩)O\n");
break;
case 9:
printf("haha\n");
break;
case 2:
printf("kaiche\n");
break;
case 6:
printf("meng(*︶*)bi\n");
break;
default:
printf("diaomao\n");
break;
}
}
調(diào)用
testFun(6);
匯編代碼如下:
Demo`func:
0x10061a17c <+0>: sub sp, sp, #0x50 ; =0x50
0x10061a180 <+4>: stp x29, x30, [sp, #0x40]
0x10061a184 <+8>: add x29, sp, #0x40 ; =0x40
0x10061a188 <+12>: stur w0, [x29, #-0x4]
0x10061a18c <+16>: ldur w0, [x29, #-0x4]
0x10061a190 <+20>: subs w0, w0, #0x2 ; =0x2
0x10061a194 <+24>: mov x8, x0
0x10061a198 <+28>: subs w0, w0, #0x7 ; =0x7
0x10061a19c <+32>: stur x8, [x29, #-0x10]
0x10061a1a0 <+36>: stur w0, [x29, #-0x14]
0x10061a1a4 <+40>: b.hi 0x10061a214 ; <+152> at main.m:47:13
0x10061a1a8 <+44>: adrp x8, 0
0x10061a1ac <+48>: add x8, x8, #0x230 ; =0x230
-> 0x10061a1b0 <+52>: ldur x11, [x29, #-0x10]
0x10061a1b4 <+56>: ldrsw x10, [x8, x11, lsl #2]
0x10061a1b8 <+60>: add x9, x8, x10
0x10061a1bc <+64>: str x10, [sp, #0x20]
0x10061a1c0 <+68>: br x9
0x10061a1c4 <+72>: adrp x0, 1
0x10061a1c8 <+76>: add x0, x0, #0xf5e ; =0xf5e
0x10061a1cc <+80>: bl 0x10061a5dc ; symbol stub for: printf
0x10061a1d0 <+84>: str w0, [sp, #0x1c]
0x10061a1d4 <+88>: b 0x10061a224 ; <+168> at main.m:63:1
0x10061a1d8 <+92>: adrp x0, 1
0x10061a1dc <+96>: add x0, x0, #0xf6b ; =0xf6b
0x10061a1e0 <+100>: bl 0x10061a5dc ; symbol stub for: printf
0x10061a1e4 <+104>: str w0, [sp, #0x18]
0x10061a1e8 <+108>: b 0x10061a224 ; <+168> at main.m:63:1
0x10061a1ec <+112>: adrp x0, 1
0x10061a1f0 <+116>: add x0, x0, #0xf71 ; =0xf71
0x10061a1f4 <+120>: bl 0x10061a5dc ; symbol stub for: printf
0x10061a1f8 <+124>: str w0, [sp, #0x14]
0x10061a1fc <+128>: b 0x10061a224 ; <+168> at main.m:63:1
0x10061a200 <+132>: adrp x0, 1
0x10061a204 <+136>: add x0, x0, #0xf79 ; =0xf79
0x10061a208 <+140>: bl 0x10061a5dc ; symbol stub for: printf
0x10061a20c <+144>: str w0, [sp, #0x10]
0x10061a210 <+148>: b 0x10061a224 ; <+168> at main.m:63:1
0x10061a214 <+152>: adrp x0, 1
0x10061a218 <+156>: add x0, x0, #0xf88 ; =0xf88
0x10061a21c <+160>: bl 0x10061a5dc ; symbol stub for: printf
0x10061a220 <+164>: str w0, [sp, #0xc]
0x10061a224 <+168>: ldp x29, x30, [sp, #0x40]
0x10061a228 <+172>: add sp, sp, #0x50 ; =0x50
0x10061a22c <+176>: ret
??我們可以看到匯編中有5個(gè)adrp,很明顯是尋址常量O(∩_∩)O\n,haha\n,kaiche\n,meng(*︶*)bi\n,diaomao\n值,但是我們看到原來(lái)的b.eq,變成了b.hi,而且只有一個(gè)條件跳轉(zhuǎn),可是我們有4個(gè)case啊?納尼?
??這就是蘋(píng)果騷操作,編譯器會(huì)進(jìn)行優(yōu)化選擇。我們看看它是怎么優(yōu)化的,都做了什么。
首先是用形參值6減去2,subs w0, w0, #0x2,然后再減去7subs w0, w0, #0x7,納尼?減2可以理解,case有一個(gè)2,那這個(gè)7從哪里來(lái)?這里就不賣(mài)關(guān)子了(2是最小的case,7是最大case與最小case的差值),如果無(wú)符號(hào)大于,就直接default,也就是,判斷形參6是否在最小case與最大case區(qū)間[2, 9],不在[2, 9]區(qū)間就直接default(好聰明)。
實(shí)際上是用參數(shù)減去最小的case與case差值進(jìn)行無(wú)符號(hào)比較。我們?nèi)±诱f(shuō)明一下吧,其實(shí)是根據(jù)狀態(tài)寄存機(jī)的標(biāo)志位判斷:
-
例子1:如果參數(shù)是1,1-2 = -1,無(wú)符號(hào)情況下,這個(gè)-1是一個(gè)很大的數(shù)比7大,所以走default -
例子2:如果參數(shù)是10,10-2 = 8,本身就比7大,所以走default -
例子3:如果參數(shù)是6,6-2 = 4< 7,所以在[2,9]區(qū)間內(nèi)
??我們的6是在區(qū)間[2, 9]內(nèi),繼續(xù)往下執(zhí)行
0x10061a1a8 <+44>: adrp x8, 0
0x10061a1ac <+48>: add x8, x8, #0x230 ; =0x230
得到地址:0x10061a230
到這兩句指令我們可以得到x8的地址:0x10061a230,我們看看這個(gè)地址里面是一個(gè)連續(xù)的表,每4個(gè)字節(jié)存一個(gè)負(fù)數(shù),總共存了8個(gè)數(shù)據(jù)(最大case-最小case+1個(gè)default == 9-2+1 == 8)。

我們還發(fā)現(xiàn)
0x10061a230地址是緊跟在代碼最后一行地址0x10061a22c的。
0x10061a1b0 <+52>: ldur x11, [x29, #-0x10]
-> 0x10061a1b4 <+56>: ldrsw x10, [x8, x11, lsl #2]
從上圖(0x10061a230數(shù)據(jù)表)我們也可以看到x11是4,然后將4左移2位得到16,x8的地址0x10061a230偏移16字節(jié)作為尋址地址,取出值放到x10寄存器。根據(jù)上圖打印結(jié)果可以看到是:-48,我們也可以根據(jù)x11的值進(jìn)行打印:p (int)0xffffffffffffffd0得到 (int) $14 = -48
0x10061a1b8 <+60>: add x9, x8, x10
0x10061a1bc <+64>: str x10, [sp, #0x20]
0x10061a1c0 <+68>: br x9
然后根據(jù)x8(表的首地址),加上偏移地址-48,得到一個(gè)新的地址,作為尋址地址,再根據(jù)寄存器尋址br x9,找到要執(zhí)行的代碼,我們根據(jù)指令打印一下:
(lldb) p/x 0x10061a230-48
得到結(jié)果:(long) $17 = 0x000000010061a200
我們驗(yàn)證一下,地址0x10061a200是不是case6所對(duì)應(yīng)的代碼:
0x10061a200 <+132>: adrp x0, 1
0x10061a204 <+136>: add x0, x0, #0xf79 ; =0xf79
得到常量地址0x10061bf79
驗(yàn)證memory:

我們看到常量確實(shí)是
"meng(*︶*)bi",至此我們就知道了:當(dāng)case數(shù)量超過(guò)3個(gè)的時(shí)候,編譯器會(huì)在編譯時(shí)期將根據(jù)case的差值在代碼內(nèi)存后面建立一個(gè)表,存表地址與對(duì)應(yīng)的case的偏移值,通過(guò)一定的運(yùn)算找到唯一的映射直接執(zhí)行,這樣就比if...else優(yōu)雅很多,效率快很多,case數(shù)量越多,效率越明顯
拓展
如果差值過(guò)大,case數(shù)量過(guò)少,編譯器還是會(huì)選擇if...else
void testFun(int a)
{
switch (b) {
case 500:
printf("O(∩_∩)O\n");
break;
case 9:
printf("haha\n");
break;
case 2:
printf("kaiche\n");
break;
case 600:
printf("meng(*︶*)bi\n");
break;
default:
printf("diaomao\n");
break;
}
}
調(diào)用
testFun(6);
匯編代碼:
Demo`func:
0x1004d217c <+0>: sub sp, sp, #0x40 ; =0x40
0x1004d2180 <+4>: stp x29, x30, [sp, #0x30]
0x1004d2184 <+8>: add x29, sp, #0x30 ; =0x30
0x1004d2188 <+12>: stur w0, [x29, #-0x4]
-> 0x1004d218c <+16>: ldur w0, [x29, #-0x4]
0x1004d2190 <+20>: mov x8, x0
0x1004d2194 <+24>: subs w0, w0, #0x2 ; =0x2
0x1004d2198 <+28>: stur w8, [x29, #-0x8]
0x1004d219c <+32>: stur w0, [x29, #-0xc]
0x1004d21a0 <+36>: b.eq 0x1004d220c ; <+144> at main.m:40:13
0x1004d21a4 <+40>: b 0x1004d21a8 ; <+44> at main.m
0x1004d21a8 <+44>: ldur w8, [x29, #-0x8]
0x1004d21ac <+48>: subs w9, w8, #0x9 ; =0x9
0x1004d21b0 <+52>: stur w9, [x29, #-0x10]
0x1004d21b4 <+56>: b.eq 0x1004d21f8 ; <+124> at main.m:37:13
0x1004d21b8 <+60>: b 0x1004d21bc ; <+64> at main.m
0x1004d21bc <+64>: ldur w8, [x29, #-0x8]
0x1004d21c0 <+68>: subs w9, w8, #0x1f4 ; =0x1f4
0x1004d21c4 <+72>: stur w9, [x29, #-0x14]
0x1004d21c8 <+76>: b.eq 0x1004d21e4 ; <+104> at main.m:34:13
0x1004d21cc <+80>: b 0x1004d21d0 ; <+84> at main.m
0x1004d21d0 <+84>: ldur w8, [x29, #-0x8]
0x1004d21d4 <+88>: subs w9, w8, #0x258 ; =0x258
0x1004d21d8 <+92>: str w9, [sp, #0x18]
0x1004d21dc <+96>: b.eq 0x1004d2220 ; <+164> at main.m:43:13
0x1004d21e0 <+100>: b 0x1004d2234 ; <+184> at main.m:47:13
0x1004d21e4 <+104>: adrp x0, 1
0x1004d21e8 <+108>: add x0, x0, #0xf5e ; =0xf5e
0x1004d21ec <+112>: bl 0x1004d25dc ; symbol stub for: printf
0x1004d21f0 <+116>: str w0, [sp, #0x14]
0x1004d21f4 <+120>: b 0x1004d2244 ; <+200> at main.m:63:1
0x1004d21f8 <+124>: adrp x0, 1
0x1004d21fc <+128>: add x0, x0, #0xf6b ; =0xf6b
0x1004d2200 <+132>: bl 0x1004d25dc ; symbol stub for: printf
0x1004d2204 <+136>: str w0, [sp, #0x10]
0x1004d2208 <+140>: b 0x1004d2244 ; <+200> at main.m:63:1
0x1004d220c <+144>: adrp x0, 1
0x1004d2210 <+148>: add x0, x0, #0xf71 ; =0xf71
0x1004d2214 <+152>: bl 0x1004d25dc ; symbol stub for: printf
0x1004d2218 <+156>: str w0, [sp, #0xc]
0x1004d221c <+160>: b 0x1004d2244 ; <+200> at main.m:63:1
0x1004d2220 <+164>: adrp x0, 1
0x1004d2224 <+168>: add x0, x0, #0xf79 ; =0xf79
0x1004d2228 <+172>: bl 0x1004d25dc ; symbol stub for: printf
0x1004d222c <+176>: str w0, [sp, #0x8]
0x1004d2230 <+180>: b 0x1004d2244 ; <+200> at main.m:63:1
0x1004d2234 <+184>: adrp x0, 1
0x1004d2238 <+188>: add x0, x0, #0xf88 ; =0xf88
0x1004d223c <+192>: bl 0x1004d25dc ; symbol stub for: printf
0x1004d2240 <+196>: str w0, [sp, #0x4]
0x1004d2244 <+200>: ldp x29, x30, [sp, #0x30]
0x1004d2248 <+204>: add sp, sp, #0x40 ; =0x40
0x1004d224c <+208>: ret
那么這個(gè)臨界值是什么呢?猜想:case的數(shù)量與差值比較,差值大于case數(shù)量,編譯器會(huì)選擇if...else,否則會(huì)選擇建表方式。
然后我建了case分別是從2到49,加52,53,總共50個(gè)。差值是53-2=51,發(fā)現(xiàn)結(jié)果是建表詢(xún)問(wèn)的方式。
此時(shí)我再修改一下case分別是從2到51,總共50個(gè)。差值是51-2=49,發(fā)現(xiàn)結(jié)果還是建表詢(xún)問(wèn)的方式。
猜想不成立,感覺(jué)是根據(jù)時(shí)間復(fù)雜度進(jìn)行取舍選擇的。
總結(jié)
1、假設(shè)switch語(yǔ)句的分支比較少的時(shí)候(例如3,少于4的時(shí)候沒(méi)有意義)沒(méi)有必要使用此結(jié)構(gòu),相當(dāng)于if。
2、各個(gè)分支常量的差值較大的時(shí)候,編譯器會(huì)在效率還是內(nèi)存進(jìn)行取舍,這個(gè)時(shí)候編譯器還是會(huì)編譯成類(lèi)似于if,else的結(jié)構(gòu)。
3、在分支比較多的時(shí)候:在編譯的時(shí)候會(huì)生成一個(gè)表(跳轉(zhuǎn)表每個(gè)地址四個(gè)字節(jié))。