从底层汇编实现来探索switch

  我们都知道switch是一个选择分支结构,其功能和if...else是一样的,那么他们两个有什么区别呢?我们什么时候用switch,什么时候该用if...else呢?带着这些疑问,我们从汇编的角度剖析一下,ARM64架构编译器是如何做的??if...else的汇编我们就不过多讲了,可以参考汇编if...else语句,我们主要讲一下switch:

开始之前我们先来了解一下基础知识:也可以点击这里查看常用指令
  • ASLR机制,(地址随机化)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置;
  • subs指令,运算结果影响目标寄存器,也影响状态寄存器
  • adrp x0, 1 ;将1向左移12位,补0️⃣,即将PC寄存器的低12位清零
  • ldrsw x10, [x8, x11, lsl #2] ;将x11寄存器向左移2位,加上x8寄存器地址,作为寻址地址,读出数值放到x10寄存器中。

首先如果一个switch有两个分支:(一般很少用不讲)

如果一个switch有两个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;
    }
}
调用
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  

  我们来分析一下,直到0x1007de1f8 <+20>: mov x8, x0这行指令w0寄存器是我们的形参值8,8比5如果eq,就跳转到0x1007de224(也就是printf("O(∩_∩)O\n");)处执行,那它是如何找到常量的呢?我们通过0x1007de224 <+64>: adrp x0, 1 0x1007de228 <+68>: add x0, x0, #0xf76能够定位到一个内存地址0x1007dff76,然后我们打开view Memory,输入这个地址可以看到:

0x1007dff76

如果不相等,继续往下走,跟9比较,如果相等就跳到0x1007de238处执行,同理,也就是view memory的0x1007dff83地址,也就是上图的haha,否则直接跳到default中,也就是对应的view Memory的0x1007dff89地址----上图的diaomao

如果一个switch有三个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;
    }
}
调用
testFun(6);

注:可以自己测试一下,和上面两个case分支是一样的,也是一个一个比,然后执行相应的输出

如果一个switch有四个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;
    }
}
调用
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个adrp,很明显是寻址常量O(∩_∩)O\n,haha\n,kaiche\n,meng(*︶*)bi\n,diaomao\n值,但是我们看到原来的b.eq,变成了b.hi,而且只有一个条件跳转,可是我们有4个case啊?纳尼?
  这就是苹果骚操作,编译器会进行优化选择。我们看看它是怎么优化的,都做了什么。
首先是用形参值6减去2,subs w0, w0, #0x2,然后再减去7subs w0, w0, #0x7,纳尼?减2可以理解,case有一个2,那这个7从哪里来?这里就不卖关子了(2是最小的case,7是最大case与最小case的差值),如果无符号大于,就直接default,也就是,判断形参6是否在最小case与最大case区间[2, 9],不在[2, 9]区间就直接default(好聪明)。


实际上是用参数减去最小的case与case差值进行无符号比较。我们取例子说明一下吧,其实是根据状态寄存机的标志位判断:

  • 例子1:如果参数是1,1-2 = -1,无符号情况下,这个-1是一个很大的数比7大,所以走default
  • 例子2:如果参数是10,10-2 = 8,本身就比7大,所以走default
  • 例子3:如果参数是6,6-2 = 4 < 7,所以在[2,9]区间内

  我们的6是在区间[2, 9]内,继续往下执行

    0x10061a1a8 <+44>:  adrp   x8, 0
    0x10061a1ac <+48>:  add    x8, x8, #0x230            ; =0x230 
得到地址:0x10061a230

到这两句指令我们可以得到x8的地址:0x10061a230,我们看看这个地址里面是一个连续的表,每4个字节存一个负数,总共存了8个数据(最大case-最小case+1个default == 9-2+1 == 8)

0x10061a230数据表

我们还发现0x10061a230地址是紧跟在代码最后一行地址0x10061a22c的。

0x10061a1b0 <+52>:  ldur   x11, [x29, #-0x10]
->  0x10061a1b4 <+56>:  ldrsw  x10, [x8, x11, lsl #2]

从上图(0x10061a230数据表)我们也可以看到x11是4,然后将4左移2位得到16,x8的地址0x10061a230偏移16字节作为寻址地址,取出值放到x10寄存器。根据上图打印结果可以看到是:-48,我们也可以根据x11的值进行打印:p (int)0xffffffffffffffd0得到 (int) $14 = -48

    0x10061a1b8 <+60>:  add    x9, x8, x10
    0x10061a1bc <+64>:  str    x10, [sp, #0x20]
    0x10061a1c0 <+68>:  br     x9

然后根据x8(表的首地址),加上偏移地址-48,得到一个新的地址,作为寻址地址,再根据寄存器寻址br x9,找到要执行的代码,我们根据指令打印一下:
(lldb) p/x 0x10061a230-48
得到结果:(long) $17 = 0x000000010061a200

我们验证一下,地址0x10061a200是不是case6所对应的代码:

    0x10061a200 <+132>: adrp   x0, 1
    0x10061a204 <+136>: add    x0, x0, #0xf79            ; =0xf79 
得到常量地址0x10061bf79

验证memory:

验证Memory

我们看到常量确实是"meng(*︶*)bi",至此我们就知道了:
当case数量超过3个的时候,编译器会在编译时期将根据case的差值在代码内存后面建立一个表,存表地址与对应的case的偏移值,通过一定的运算找到唯一的映射直接执行,这样就比if...else优雅很多,效率快很多,case数量越多,效率越明显

拓展

如果差值过大,case数量过少,编译器还是会选择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;
    }
}
调用
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    

那么这个临界值是什么呢?猜想:case的数量与差值比较,差值大于case数量,编译器会选择if...else,否则会选择建表方式。
然后我建了case分别是从2到49,加52,53,总共50个。差值是53-2=51,发现结果是建表询问的方式。
此时我再修改一下case分别是从2到51,总共50个。差值是51-2=49,发现结果还是建表询问的方式。

猜想不成立,感觉是根据时间复杂度进行取舍选择的。

总结

1、假设switch语句的分支比较少的时候(例如3,少于4的时候没有意义)没有必要使用此结构,相当于if。
2、各个分支常量的差值较大的时候,编译器会在效率还是内存进行取舍,这个时候编译器还是会编译成类似于if,else的结构。
3、在分支比较多的时候:在编译的时候会生成一个表(跳转表每个地址四个字节)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容