美文网首页
iOS逆向实战--005:判断 & 循环 & 选择

iOS逆向实战--005:判断 & 循环 & 选择

作者: 帅驼驼 | 来源:发表于2021-04-09 10:08 被阅读0次
    判断

    cmp:(Compare)比较指令

    cmp把一个寄存器的内容和另一个寄存器的内容或立即数进行比较。但不存储结果,只是正确的更改标志

    一般cmp做完判断后会进行跳转,后面通常会跟上b指令

    有符号数

    • b.lt 标号:比价结果是小于(less than),执行标号,否则不跳转
    • b.le 标号:比较结果是小于等于(less than or qeual to),执行标号,否则不跳转
    • b.gt 标号:比较结果是大于(greater than),执行标号,否则不跳转
    • b.ge 标号:比较结果是大于等于(greater than or equal to),执行标号,否则不跳转
    • b.eq 标号:比较结果是等于(equal to),执行标号,否则不跳转
    • b.ne 标号:比较结果是不等于(not equal to),执行标号,否则不跳转

    无符号数

    • b.lo 标号:比较结果是小于,执行标号,否则不跳转
    • b.ls 标号:比较结果是小于等于,执行标号,否则不跳转
    • b.hi 标号:比较结果是大于,执行标号,否则不跳转
    • b.hs 标号:比较结果是大于等于,执行标号,否则不跳转

    案例:

    编译器对if判断的识别

    打开ViewController.m文件,写入以下代码:

    #import "ViewController.h"
    
    int g = 12;
    
    void func(int a, int b){
       if (a > b) {
           g = a;
       } else {
           g = b;
       }
    }
    
    @implementation ViewController
    
    - (void)viewDidLoad {
    //    [super viewDidLoad];
       func(1, 2);
    }
    
    @end
    

    使用真机编译项目,将Mach-O文件拖到Hopper中,找到func函数

    • func函数在Hopper中被分为四段

    func函数,第一段

                        _func:
    0000000100005f0c         sub        sp, sp, #0x10            ; CODE XREF=-[ViewController viewDidLoad]+28
    0000000100005f10         str        w0, [sp, #0xc]
    0000000100005f14         str        w1, [sp, #0x8]
    0000000100005f18         ldr        w8, [sp, #0xc]
    0000000100005f1c         ldr        w9, [sp, #0x8]
    0000000100005f20         cmp        w8, w9
    0000000100005f24         b.le       loc_100005f3c
    
    • 开辟栈空间,现场保护
    • cmp w8, w9:比较w8w9寄存器,相当于比较参数1参数2cmp指令相当于做减法,但只会影响状态寄存器,不会影响目标寄存器
    • b.le loc_100005f3c:如果参数1 - 参数2的结果<= 0,相当于触发else代码块,执行标号跳转到第三段。否则触发if代码块,向下执行第二段
    • 所以cmp指令之后的跳转,进入的是else代码分支

    func函数,第二段。如果参数1 - 参数2 > 0执行此段代码

    0000000100005f28         ldr        w8, [sp, #0xc]
    0000000100005f2c         adrp       x9, #0x10000d000
    0000000100005f30         add        x9, x9, #0x490           ; _g
    0000000100005f34         str        w8, x9
    0000000100005f38         b          loc_100005f4c
    
    • 此段代码,相当于if中的代码块
    • w8参数1x9为全局变量g,将w8写入x9
    • b指令,执行标号跳转到第四段

    func函数,第三段。如果参数1 - 参数2 <= 0执行此段代码

                        loc_100005f3c:
    0000000100005f3c         ldr        w8, [sp, #0x8]           ; CODE XREF=_func+24
    0000000100005f40         adrp       x9, #0x10000d000
    0000000100005f44         add        x9, x9, #0x490           ; _g
    0000000100005f48         str        w8, x9
    
    • 此段代码,相当于else中的代码块
    • w8参数2x9为全局变量g,将w8写入x9
    • 继续向下执行第四段

    func函数,第四段

                        loc_100005f4c:
    0000000100005f4c         add        sp, sp, #0x10            ; CODE XREF=_func+44
    0000000100005f50         ret
                           ; endp
    
    • 恢复栈平衡,返回
    循环

    不同的循环方式:

    • do...while
    • while
    • for

    案例1:

    编译器对do...while循环的识别

    打开ViewController.m文件,写入以下代码:

    #import "ViewController.h"
    
    void funcA(){
    
       int sum = 0;
       int i = 0;
       
       do {
           sum += 1;
           i++;
       } while (i < 100);
    }
    
    @implementation ViewController
    
    - (void)viewDidLoad {
    //    [super viewDidLoad];
       funcA();
    }
    
    @end
    

    使用真机编译项目,将Mach-O文件拖到Hopper中,找到funcA函数

    • funcA函数在Hopper中被分为三段

    funcA函数,第一段

                        _funcA:
    0000000100005f24         sub        sp, sp, #0x10            ; CODE XREF=-[ViewController viewDidLoad]+20
    0000000100005f28         str        wzr, [sp, #0xc]
    0000000100005f2c         str        wzr, [sp, #0x8]
    
    • 开辟栈空间,现场保护。将wzr寄存器的值,分别在sp + 0xcsp + 0x8地址入栈
    • 继续向下执行第二段

    funcA函数,第二段

                        loc_100005f30:
    0000000100005f30         ldr        w8, [sp, #0xc]             ; CODE XREF=_funcA+44
    0000000100005f34         add        w8, w8, #0x1
    0000000100005f38         str        w8, [sp, #0xc]
    0000000100005f3c         ldr        w8, [sp, #0x8]
    0000000100005f40         add        w8, w8, #0x1
    0000000100005f44         str        w8, [sp, #0x8]
    0000000100005f48         ldr        w8, [sp, #0x8]
    0000000100005f4c         cmp        w8, #0x64
    0000000100005f50         b.lt       loc_100005f30
    
    • 因为使用do...while循环,所以cmpb.lt指令在最下面。这意味着此代码块,至少也会执行一次
    • 读取sp + 0xc地址的值,写入w8,进行w8 += 1操作,将结果再次入栈
    • 读取sp + 0x8地址的值,写入w8,进行w8 += 1操作,将结果再次入栈
    • 再次将sp + 0x8地址的值,写入w8
    • cmp指令,将w8的值和#0x64,即10进制100进行比较
    • b.lt指令,如果w8 - 100 < 0,执行标号跳转到第二段,相当于递归执行。否则向下执行第三段,相当于跳出循环

    funcA函数,第三段

    0000000100005f54         add        sp, sp, #0x10
    0000000100005f58         ret
                           ; endp
    
    • 恢复栈平衡,返回

    案例2:

    编译器对while循环的识别

    打开ViewController.m文件,写入以下代码:

    #import "ViewController.h"
    
    void funcA(){
    
       int sum = 0;
       int i = 0;
     
       while (i < 100) {
           sum += 1;
           i++;
       }
    }
    
    @implementation ViewController
    
    - (void)viewDidLoad {
    //    [super viewDidLoad];
       funcA();
    }
    
    @end
    

    使用真机编译项目,将Mach-O文件拖到Hopper中,找到funcA函数

    • funcA函数在Hopper中被分为四段

    funcA函数,第一段

                        _funcA:
    0000000100005f20         sub        sp, sp, #0x10              ; CODE XREF=-[ViewController viewDidLoad]+20
    0000000100005f24         str        wzr, [sp, #0xc]
    0000000100005f28         str        wzr, [sp, #0x8]
    
    • 开辟栈空间,现场保护。将wzr寄存器的值,分别在sp + 0xcsp + 0x8地址入栈
    • 继续向下执行第二段

    funcA函数,第二段

                        loc_100005f2c:
    0000000100005f2c         ldr        w8, [sp, #0x8]              ; CODE XREF=_funcA+48
    0000000100005f30         cmp        w8, #0x64
    0000000100005f34         b.ge       loc_100005f54
    
    • 读取sp + 0x8地址的值,写入w8
    • cmp指令,将w8的值和#0x64,即10进制100进行比较
    • b.lt指令,如果w8 - 100 >= 0,执行标号跳转到第四段,相当于跳出循环。否则向下执行第三段,执行循环内部的代码块

    funcA函数,第三段

    0000000100005f38         ldr        w8, [sp, #0xc]
    0000000100005f3c         add        w8, w8, #0x1
    0000000100005f40         str        w8, [sp, #0xc]
    0000000100005f44         ldr        w8, [sp, #0x8]
    0000000100005f48         add        w8, w8, #0x1
    0000000100005f4c         str        w8, [sp, #0x8]
    0000000100005f50         b          loc_100005f2c
    
    • 读取sp + 0xc地址的值,写入w8,进行w8 += 1操作,将结果再次入栈
    • 读取sp + 0x8地址的值,写入w8,进行w8 += 1操作,将结果再次入栈
    • b指令,执行标号跳转到第二段,继续验证进入循环的条件限制,决定之后的代码流程

    funcA函数,第四段

                        loc_100005f54:
    0000000100005f54         add        sp, sp, #0x10               ; CODE XREF=_funcA+20
    0000000100005f58         ret
                           ; endp
    
    • 恢复栈平衡,返回

    案例3:

    编译器对for循环的识别

    打开ViewController.m文件,写入以下代码:

    #import "ViewController.h"
    
    void funcA(){
    
       int sum = 0;
       
       for (int i = 0; i < 100; i++) {
           sum += 1;
       }
    }
    
    @implementation ViewController
    
    - (void)viewDidLoad {
    //    [super viewDidLoad];
       funcA();
    }
    
    @end
    

    使用真机编译项目,将Mach-O文件拖到Hopper中,找到funcA函数

    • funcA函数在Hopper中被分为四段
    • 代码流程和while循环完全一致
    选择

    switch语句:当满足条件时,执行某些操作。可以使用if-else来实现,也可以使用switch来实现

    • 假设switch语句的分支比较少的时候,例如:少于4个的时候,没有必要使用此结构,此时switch的执行方式相当于if-else
    • 在分支比较多的时候,编译时会生成一张数据表。表中每个偏移值占四字节,通过查表找到指定代码的标号地址,无需遍历。以空间换取时间,提高效率
    • 各个分支常量的差值较大的时候,编译器会在效率和内存中进行取舍,这个时候编译器有可能会编译成类似于if-else的结构

    案例1:

    switch语句的分支比较少的时候,编译器会如何处理?

    打开ViewController.m文件,写入以下代码:

    #import "ViewController.h"
    
    void funcA(int a){
       switch (a) {
           case 1:
               printf("打坐");
               break;
           case 2:
               printf("加红");
               break;
           case 3:
               printf("加蓝");
               break;
           default:
               printf("待命");
               break;
       }
    }
    
    @implementation ViewController
    
    - (void)viewDidLoad {
    //    [super viewDidLoad];
       funcA(1);
    }
    
    @end
    

    使用真机编译项目,将Mach-O文件拖到Hopper中,找到funcA函数

    • funcA函数在Hopper中被分为十一段

    funcA函数,第一段

                        _funcA:
    0000000100005e9c         sub        sp, sp, #0x20               ; CODE XREF=-[ViewController viewDidLoad]+24
    0000000100005ea0         stp        x29, x30, [sp, #0x10]
    0000000100005ea4         add        x29, sp, #0x10
    0000000100005ea8         stur       w0, [x29, #-0x4]
    0000000100005eac         ldur       w8, [x29, #-0x4]
    0000000100005eb0         cmp        w8, #0x1
    0000000100005eb4         str        w8, [sp, #0x8]
    0000000100005eb8         b.eq       loc_100005ee0
    
    • 开辟栈空间,现场保护
    • w0寄存器的值,在x29 - 0x4地址入栈
    • 读取x29 - 0x4地址的值,写入w8
    • cmp指令,将w8的值和#0x1,即10进制1进行比较
    • w8寄存器的值,在sp + 0x8地址入栈
    • b.eq指令,如果w8 - 1 == 0,执行标号跳转到第七段,相当于进入case 1的代码块。否则向下执行第二段

    funcA函数,第二段

    0000000100005ebc         b          loc_100005ec0
    
    • b指令,执行标号跳转到第三段

    funcA函数,第三段

                        loc_100005ec0:
    0000000100005ec0         ldr        w8, [sp, #0x8]                ; CODE XREF=_funcA+32
    0000000100005ec4         cmp        w8, #0x2
    0000000100005ec8         b.eq       loc_100005ef0
    
    • 读取sp + 0x8地址的值,写入w8
    • cmp指令,将w8的值和#0x2,即10进制2进行比较
    • b.eq指令,如果w8 - 2 == 0,执行标号跳转到第八段,相当于进入case 2的代码块。否则向下执行第四段

    funcA函数,第四段

    0000000100005ecc         b          loc_100005ed0
    
    • b指令,执行标号跳转到第五段

    funcA函数,第五段

                        loc_100005ed0:
    0000000100005ed0         ldr        w8, [sp, #0x8]                ; CODE >XREF=_funcA+48
    0000000100005ed4         cmp        w8, #0x3
    0000000100005ed8         b.eq       loc_100005f00
    
    • 读取sp + 0x8地址的值,写入w8
    • cmp指令,将w8的值和#0x3,即10进制3进行比较
    • b.eq指令,如果w8 - 3 == 0,执行标号跳转到第九段,相当于进入case 3的代码块。否则向下执行第六段

    funcA函数,第六段

    0000000100005edc         b          loc_100005f10
    
    • b指令,执行标号跳转到第十段,相当于进入default的代码块

    funcA函数,第七段。即:case 1的代码块

                        loc_100005ee0:
    0000000100005ee0         adrp       x0, #0x100006000                ; argument #1 for method imp___stubs__printf, CODE XREF=_funcA+28
    0000000100005ee4         add        x0, x0, #0x634                  ; "\\xE6\\x89\\x93\\xE5\\x9D\\x90"
    0000000100005ee8         bl         imp___stubs__printf
    0000000100005eec         b          loc_100005f1c
    
    • 在常量区拿到字符串打坐,写入x0
    • 调用printf函数
    • b指令,执行标号跳转到第十一段。即:funcA函数的结尾处

    funcA函数,第八段。即:case 2的代码块

                        loc_100005ef0:
    0000000100005ef0         adrp       x0, #0x100006000                ; argument #1 for method imp___stubs__printf, CODE XREF=_funcA+44
    0000000100005ef4         add        x0, x0, #0x63b                  ; "\\xE5\\x8A\\xA0\\xE7\\xBA\\xA2"
    0000000100005ef8         bl         imp___stubs__printf
    0000000100005efc         b          loc_100005f1c
    
    • 在常量区拿到字符串加红,写入x0
    • 调用printf函数
    • b指令,执行标号跳转到第十一段。即:funcA函数的结尾处

    funcA函数,第九段。即:case 3的代码块

                        loc_100005f00:
    0000000100005f00         adrp       x0, #0x100006000                 ; argument #1 for method imp___stubs__printf, CODE XREF=_funcA+60
    0000000100005f04         add        x0, x0, #0x642                   ; "\\xE5\\x8A\\xA0\\xE8\\x93\\x9D"
    0000000100005f08         bl         imp___stubs__printf
    0000000100005f0c         b          loc_100005f1c
    
    • 在常量区拿到字符串加蓝,写入x0
    • 调用printf函数
    • b指令,执行标号跳转到第十一段。即:funcA函数的结尾处

    funcA函数,第十段。即:default的代码块

                        loc_100005f10:
    0000000100005f10         adrp       x0, #0x100006000                  ; argument #1 for method imp___stubs__printf, CODE XREF=_funcA+64
    0000000100005f14         add        x0, x0, #0x649                    ; "\\xE5\\xBE\\x85\\xE5\\x91\\xBD"
    0000000100005f18         bl         imp___stubs__printf
    
    • 在常量区拿到字符串待命,写入x0
    • 调用printf函数
    • 继续向下执行第十一段

    funcA函数,第十一段

                        loc_100005f1c:
    0000000100005f1c         ldp        x29, x30, [sp, #0x10]            ; CODE XREF=_funcA+80, _funcA+96, _funcA+112
    0000000100005f20         add        sp, sp, #0x20
    0000000100005f24         ret
                           ; endp
    
    • 还原x29x30的值
    • 恢复栈平衡
    • 返回

    上述案例中,每个case都会检查一次,当所有case都不满足时,跳转default。从代码流程上,感觉和使用if-else实现没有区别

    funcA函数,使用if-else实现:

    void funcA(int a){
       if(a == 1){
           printf("打坐");
       }
       else if (a == 2){
           printf("加红");
       }
       else if (a == 3){
           printf("加蓝");
       }
       else{
           printf("待命");
       }
    }
    

    使用真机编译项目,将Mach-O文件拖到Hopper中,找到funcA函数

    • 从流程上看,比switch语句少了一些b指令的中间跳转,但也是每个条件分支都会检查一次,当所有if都不满足时,跳转else

    结论:当switch语句的分支比较少的时候,例如:少于4个的时候,没有必要使用此结构,此时switch的执行方式相当于if-else

    案例2:

    在分支比较多的时候,如果case的值是连续的,或者间隔较小,编译时会生成一张数据表

    打开ViewController.m文件,写入以下代码:

    #import "ViewController.h"
    
    void funcA(int a){
       switch (a) {
           case 1:
               printf("打坐");
               break;
           case 2:
               printf("加红");
               break;
           case 3:
               printf("加蓝");
               break;
           case 4:
               printf("打怪");
               break;
           default:
               printf("待命");
               break;
       }
    }
    
    @implementation ViewController
    
    - (void)viewDidLoad {
    //    [super viewDidLoad];
       funcA(4);
    }
    
    @end
    

    真机运行项目,来到func方法

    • funcA函数的汇编代码分为八段

    funcA函数,第一段

       0x102269ec4 <+0>:   sub    sp, sp, #0x20             ; =0x20 
       0x102269ec8 <+4>:   stp    x29, x30, [sp, #0x10]
       0x102269ecc <+8>:   add    x29, sp, #0x10            ; =0x10 
       0x102269ed0 <+12>:  stur   w0, [x29, #-0x4]
       0x102269ed4 <+16>:  ldur   w8, [x29, #-0x4]
       0x102269ed8 <+20>:  subs   w8, w8, #0x1              ; =0x1 
       0x102269edc <+24>:  mov    x9, x8
       0x102269ee0 <+28>:  ubfx   x9, x9, #0, #32
       0x102269ee4 <+32>:  cmp    x9, #0x3                  ; =0x3 
       0x102269ee8 <+36>:  str    x9, [sp]
       0x102269eec <+40>:  b.hi   0x102269f48               ; <+132> at ViewController.m
    
    • 开辟栈空间,现场保护
    • w0x29 - 0x4地址入栈,然后再寻址写入w8
    • subs w8, w8, #0x1w8为当前变量的值,减去#0x1,写入w8寄存器
      w8viewDidLoad方法中,调用funcA函数,传入的参数为4
      #0x1:最小case的值,转为10进制1
      w8 -= 1,即:4 - 1 ,得到参数区间值3
      使用subs指令,影响目标寄存器,同时影响状态寄存器
    • x8写入x9
    • ubfx x9, x9, #0, #32:保留x9寄存器的低32位,剩余高位用0填充
      ubfx指令:从x9寄存器的第0位开始,提取32位x9寄存器,剩余高位用0填充
      例如:x90x0123456789abcdef,此指令执行后,x90x0000000089abcdef
    • cmp x9, #0x3:将x9的值和#0x3进行比较
      x9参数区间值
      #0x3:最大case值减去最小case值,得到case区间值3
    • x9sp所在位置入栈
    • b.hi 0x104105f48:如果参数区间值 - case区间值 > 0,执行标号跳转到第七段,相当于进入default的代码块。否则向下执行第二段

    funcA函数,第二段。通过查表,获取跳转到指定casedefault的代码标号

       0x102269ef0 <+44>:  adrp   x8, 0
       0x102269ef4 <+48>:  add    x8, x8, #0xf60            ; =0xf60 
       0x102269ef8 <+52>:  ldr    x11, [sp]
       0x102269efc <+56>:  ldrsw  x10, [x8, x11, lsl #2]
       0x102269f00 <+60>:  add    x9, x8, x10
       0x102269f04 <+64>:  br     x9
    
    • adrp x8, 0 ~ add x8, x8, #0xf60:将0x102269f60地址,写入x8
      0x102269f60:数据表的首地址,存储在funcA函数地址之后
      funcA函数最后指令地址:0x102269f5c,向后偏移4字节0x102269f60
    • 读取sp地址的值,写入x11
    • ldrsw x10, [x8, x11, lsl #2]
      ldrsw指令:通过x8 + x11左移2位计算地址,读取该地址的值,赋值给x10
      x8:值为0x102269f60。数据表首地址
      x11:值为3参数 - 最小case值,得到的参数区间值
      x11左移2位3 << 2 = 12,即:16进制0xc
      数据表首地址加上偏移地址:0x102269f60 + 0xc = 0x102269f6c
      读取0x102269f6c地址的值0xffffffffffffffd8,写入x10

    • add x9, x8, x10:将x8x10相加,写入x9
      x8:值为0x102269f60。数据表首地址
      x10:值为0xffffffffffffffd8,即:-4016进制0x-28。从数据表中获取距代码标号的偏移值
      0x102269f60 + 0x-28 = 0x102269f38,写入x9
    • br x9:执行x9寄存器所存储的标号
      x9:值为0x102269f38。执行标号跳转到第六段,相当于进入case 4的代码块

    funcA函数,第三段。即:case 1的代码块

       0x102269f08 <+68>:  adrp   x0, 1
       0x102269f0c <+72>:  add    x0, x0, #0x634            ; =0x634 
       0x102269f10 <+76>:  bl     0x10226a598               ; symbol stub for: printf
       0x102269f14 <+80>:  b      0x102269f54               ; <+144> at ViewController.m:28:1
    
    • 在常量区拿到字符串打坐,写入x0
    • 调用printf函数
    • b指令,执行标号跳转到第八段。即:funcA函数的结尾处

    funcA函数,第四段。即:case 2的代码块

       0x102269f18 <+84>:  adrp   x0, 1
       0x102269f1c <+88>:  add    x0, x0, #0x63b            ; =0x63b 
       0x102269f20 <+92>:  bl     0x10226a598               ; symbol stub for: printf
       0x102269f24 <+96>:  b      0x102269f54               ; <+144> at ViewController.m:28:1
    
    • 在常量区拿到字符串加红,写入x0
    • 调用printf函数
    • b指令,执行标号跳转到第八段。即:funcA函数的结尾处

    funcA函数,第五段。即:case 3的代码块

       0x102269f28 <+100>: adrp   x0, 1
       0x102269f2c <+104>: add    x0, x0, #0x642            ; =0x642 
       0x102269f30 <+108>: bl     0x10226a598               ; symbol stub for: printf
       0x102269f34 <+112>: b      0x102269f54               ; <+144> at ViewController.m:28:1
    
    • 在常量区拿到字符串加蓝,写入x0
    • 调用printf函数
    • b指令,执行标号跳转到第八段。即:funcA函数的结尾处

    funcA函数,第六段。即:case 4的代码块

       0x102269f38 <+116>: adrp   x0, 1
       0x102269f3c <+120>: add    x0, x0, #0x649            ; =0x649 
       0x102269f40 <+124>: bl     0x10226a598               ; symbol stub for: printf
       0x102269f44 <+128>: b      0x102269f54               ; <+144> at ViewController.m:28:1
    
    • 在常量区拿到字符串打怪,写入x0
    • 调用printf函数
    • b指令,执行标号跳转到第八段。即:funcA函数的结尾处

    funcA函数,第七段。即:default的代码块

       0x102269f48 <+132>: adrp   x0, 1
       0x102269f4c <+136>: add    x0, x0, #0x650            ; =0x650 
       0x102269f50 <+140>: bl     0x10226a598               ; symbol stub for: printf
    
    • 在常量区拿到字符串待命,写入x0
    • 调用printf函数
    • 继续向下执行第八段

    funcA函数,第八段

       0x102269f54 <+144>: ldp    x29, x30, [sp, #0x10]
       0x102269f58 <+148>: add    sp, sp, #0x20             ; =0x20 
       0x102269f5c <+152>: ret    
    
    • 还原x29x30的值
    • 恢复栈平衡
    • 返回

    上述案例中,switch语句生成的汇编代码逻辑

    【第一步】:判断参数值是否在case值范围之内

    • 通过参数值 - 最小case值,得到参数区间值
    • 通过最大case值 - 最小case值,得到case区间值
    • 参数区间值case区间值进行比较
    • 如果参数区间值 > case区间值,表示超出case范围,直接跳转到default代码标号。否则进入【第二步】

    【第二步】:通过查表,获取跳转到指定casedefault的代码标号

    • 获取数据表首地址
    • 计算数据表首地址加上参数区间值偏移x位后的地址
    • 通过计算后地址,从数据表中获取距代码标号的偏移值
    • 通过数据表首地址 + 16进制(偏移值),计算出指定casedefault的代码标号
    • 虽然参数在case值范围之内,也有可能进入default分支,因为case值不一定是连续的
    • 表中存储的数据量,是case区间值 + default的总数。所以case的值间隔越大,需要存储的数据量就会越多
    • 表中存储的数据,以表头地址为参照,计算出跳转到各分支代码的偏移值。它们在编译时已经被编译器计算好并存储在表中,因为switch的条件分支代码是连续的,因此可以这样计算
    • 数据表中每四字节存储一个偏移值,所以在ldrsw指令中有左移2位的计算
    • 由于ASLR技术(Address Spce Layout Randomization),翻译过来就是地址空间布局随机化。数据表中不直接存储地址,而是存储偏移值

    结论:在switch语句分支比较多的时候,如果case的值是连续的,或间隔较小,编译器会建立一张数据表。通过查表直接获取跳转到指定代码的标号地址,无需遍历每一个case分支,以空间换取时间

    案例3:

    case常量的差值较大时,编译器会如何处理?

    打开ViewController.m文件,写入以下代码:

    void funcA(int a){
       switch (a) {
           case 1:
               printf("打坐");
               break;
           case 20:
               printf("加红");
               break;
           case 40:
               printf("加蓝");
               break;
           case 80:
               printf("打怪");
               break;
           default:
               printf("待命");
               break;
       }
    }
    

    使用真机编译项目,将Mach-O文件拖到Hopper中,找到funcA函数

    • 如果case值的间隔较大,编译器会权衡是否还要以空间换取时间。上述案例中,如果建立数据表需要存储大量数据,耗费大量空间,此时编译器会选择使用if-else的执行方式

    结论:各个分支常量的差值较大的时候,编译器会在效率和内存中进行取舍,这个时候编译器有可能会编译成类似于if-else的结构

    总结

    判断(if)

    • cmp:比较指令,其实在做减法。不影响目标寄存器,只影响状态寄存器

    循环

    • do…while循环:判断条件在后面,满足循环条件往外跳
    • while循环:判断条件在前面,满足循环条件往里跳
    • for循环:和while循环逻辑一致

    选择

    • 分支少于4个switch底层代码和if-else一样
    • 在分支比较多的时候,编译时会生成一张数据表,通过查表进行代码调转,无需遍历。以空间换取时间,提高效率
    • 各个分支常量差值较大,底层代码可能使用数据表,也可能使用if-else。因为编译器会在效率和内存中进行取舍

    相关文章

      网友评论

          本文标题:iOS逆向实战--005:判断 & 循环 & 选择

          本文链接:https://www.haomeiwen.com/subject/vdttkltx.html