從底層匯編實(shí)現(xiàn)來(lái)探索switch

??我們都知道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è)地址可以看到:

0x1007dff76

如果不相等,繼續(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)。

0x10061a230數(shù)據(jù)表

我們還發(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:

驗(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é))。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容