美文网首页
汇编-循环、选择、判断

汇编-循环、选择、判断

作者: HotPotCat | 来源:发表于2021-04-01 10:07 被阅读0次

    内存分区

    逻辑上划分(编译器划分)

    • 代码区:存放代码,可读可执行
    • 栈区:参数、局部变量、临时数据。可短可写
    • 堆区:动态申请。可读可写
    • 全局变量:可读可写
    • 常量:只读

    全局变量和常量

    int g = 12;
    
    int func(int a, int b) {
        printf("test");
        int c = a + g + b;
        return c;
    }
    
    int main(int argc, char * argv[]) {
        func(1, 2);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
    

    对应的汇编:

    image.png
    可以看到printf函数的参数来源为:
    0x104492140 <+20>: adrp   x0, 1
    0x104492144 <+24>: add    x0, x0, #0xf81            ; =0xf81 
    

    X0存储的是一个地址,为字符串常量区。那么以上两条指令是怎么获得0x0000000104493f81这个地址的?

    • adrp:Address Page 内存地址以页寻址。
    • 0x104492140 <+20>: adrp x0, 1:定位到某一页数据的开始(文件起始位置)
      1.将1的值左移12位变成0x1000
      2.当前pc的值低12位清零。0x104492140 -> 0x104492000
      3.0x104492000 + 0x1000得到0x104493000。相当于pc后3位置为0,第4位加上x0后跟的值。
    • 0x104492144 <+24>: add x0, x0, #0xf81:偏移地址(当前代码偏移)
      0x104493000 + 0xf81得到0x104493f81

    这样就得到了常量区字符串的地址了。

    0x104493000尾数为000意味着000~fff -> 0~4095大小为4096也就是4k。也就是定位到某一页数据的开始。macpagesize 4k iOSpagesize 16k。这里是兼容的4k * 4 = 16k

    这里以pc寄存器为参照,也就是当前代码段地址为参照。

    继续执行:

    image.png
    通过上面的规则可以计算出x9最终的值为0x1044955d8也就是全局变量g的值。

    ⚠️全局变量和常量都是通过一个 基地址 + 偏移 获取。

    汇编还原高级代码

    先编译要还原的工程,进入.app找到macho文件并拖入Hopper

    image.png

    Hopper分析完成后搜索要分析的函数

    image.png

    通过汇编我们可以还原高级语言的行为。
    比如刚才func分析出来汇编代码为:

    
                         _func:
    000000010000612c         sub        sp, sp, #0x20                               ; CODE XREF=_main+32
    0000000100006130         stp        x29, x30, [sp, #0x10]
    0000000100006134         add        x29, sp, #0x10
    0000000100006138         stur       w0, [x29, var_4]
    000000010000613c         str        w1, [sp, #0x10 + var_8]
    0000000100006140         adrp       x0, #0x100007000                            ; 0x100007f81@PAGE, argument "format" for method imp___stubs__printf
    0000000100006144         add        x0, x0, #0xf81                              ; 0x100007f81@PAGEOFF, "test"
    0000000100006148         bl         imp___stubs__printf                         ; printf
    000000010000614c         ldur       w8, [x29, var_4]
    0000000100006150         adrp       x9, #0x100009000                            ; 0x1000095d8@PAGE
    0000000100006154         add        x9, x9, #0x5d8                              ; 0x1000095d8@PAGEOFF, _g
    0000000100006158         ldr        w10, [x9]                                   ; _g
    000000010000615c         add        w8, w8, w10
    0000000100006160         ldr        w10, [sp, #0x10 + var_8]
    0000000100006164         add        w8, w8, w10
    0000000100006168         str        w8, [sp, #0x10 + var_C]
    000000010000616c         ldr        w8, [sp, #0x10 + var_C]
    0000000100006170         mov        x0, x8
    0000000100006174         ldp        x29, x30, [sp, #0x10]
    0000000100006178         add        sp, sp, #0x20
    000000010000617c         ret
                            ; endp
    

    可以通过MachOView查找0000000100007f81

    image.png

    00000001000095d8:

    image.png
    分析完代码如下:
    int global_g = 12;
    
    int func2(int a, int b) {
        
    //_func:
    //000000010000612c         sub        sp, sp, #0x20                               ; CODE XREF=_main+32
    //0000000100006130         stp        x29, x30, [sp, #0x10]
    //0000000100006134         add        x29, sp, #0x10
        
        //4字节参数2个
    //0000000100006138         stur       w0, [x29, var_4]
    //000000010000613c         str        w1, [sp, #0x10 + var_8]
        
        //这里已经算好了adrp的结果 等价于 0000000100006140   adrp  x0, #0x1
    //0000000100006140         adrp       x0, #0x100007000                            ; 0x100007f81@PAGE, argument "format" for method imp___stubs__printf
    //0000000100006144         add        x0, x0, #0xf81                              ; 0x100007f81@PAGEOFF, "test"
        //这里可以算出地址为 0000000100007f81,在 MachOView 查找为 test
    //0000000100006148         bl         imp___stubs__printf                         ; printf
        printf("test");
    //000000010000614c         ldur       w8, [x29, var_4]
        int w8 = a;
    //0000000100006150         adrp       x9, #0x100009000                            ; 0x1000095d8@PAGE
    //0000000100006154         add        x9, x9, #0x5d8                              ; 0x1000095d8@PAGEOFF, _g
        //00000001000095d8 在 MachOView 中 为 0xC 也就是 12,定义全局变量 global_g = 12
    //0000000100006158         ldr        w10, [x9]                                   ; _g
        int w10 = global_g;
    //000000010000615c         add        w8, w8, w10
        w8 += w10;
    //0000000100006160         ldr        w10, [sp, #0x10 + var_8]
        w10 = b;
    //0000000100006164         add        w8, w8, w10
        w8 += w10;
    //0000000100006168         str        w8, [sp, #0x10 + var_C]
    //000000010000616c         ldr        w8, [sp, #0x10 + var_C]
    //0000000100006170         mov        x0, x8
        
    //0000000100006174         ldp        x29, x30, [sp, #0x10]
    //0000000100006178         add        sp, sp, #0x20
    //000000010000617c         ret
    //   ; endp
        return w8;
    }
    

    精简后:

    int global_g = 12;
    
    int func2(int a, int b) {
        printf("test");
        return a + global_g + b;
    }
    

    ⚠️:从上往下还原,代码执行流程不关心,只关心结果和原始的一样。

    if识别

    int  g = 12;
    
    void func(int a, int b) {
        if (a > b) {
            g = a;
        } else {
            g = b;
        }
    }
    
    Hopper解析出来的汇编
     0000000100006190         sub        sp, sp, #0x10                               ; CODE XREF=_main+32
     0000000100006194         str        w0, [sp, #0xc]
     0000000100006198         str        w1, [sp, #0x8]
     000000010000619c         ldr        w8, [sp, #0xc]
     00000001000061a0         ldr        w9, [sp, #0x8]
     
     a和b比较大小,cmp不影响 w8 和 w9 的值。做减法只影响标记寄存器的值。
     00000001000061a4         cmp        w8, w9
     <= 跳转 loc_1000061c0,大于直接往下执行不跳转
     00000001000061a8         b.le       loc_1000061c0
    
     //代码块1
     00000001000061ac         ldr        w8, [sp, #0xc]
     00000001000061b0         adrp       x9, #0x100009000
     00000001000061b4         add        x9, x9, #0x5d0                              ; _g
     00000001000061b8         str        w8, x9
     这里b跳转loc_1000061d0,跳过了else的代码
     00000001000061bc         b          loc_1000061d0
     //代码块2
                          loc_1000061c0:
     00000001000061c0         ldr        w8, [sp, #0x8]                              ; CODE XREF=_func+24
     00000001000061c4         adrp       x9, #0x100009000
     00000001000061c8         add        x9, x9, #0x5d0                              ; _g
     00000001000061cc         str        w8, x9
    
                          loc_1000061d0:
     00000001000061d0         add        sp, sp, #0x10                               ; CODE XREF=_func+44
     00000001000061d4         ret
    

    还原代码

    int global = 12;
    
    void func2(int a, int b) {
    //     0000000100006190         sub        sp, sp, #0x10                               ; CODE XREF=_main+32
        //两个参数
    //     0000000100006194         str        w0, [sp, #0xc]
    //     0000000100006198         str        w1, [sp, #0x8]
    //     000000010000619c         ldr        w8, [sp, #0xc]
    //     00000001000061a0         ldr        w9, [sp, #0x8]
        int w8 = a, w9 = b;
    //     a和b比较大小,cmp不影响 w8 和 w9 的值。做减法只影响标记寄存器的值。
    //     00000001000061a4         cmp        w8, w9
    //     <= 跳转 loc_1000061c0,大于直接往下执行不跳转
    //     00000001000061a8         b.le       loc_1000061c0
        if (w8 > w9) {
            //     //代码块1
            //     00000001000061ac         ldr        w8, [sp, #0xc]
            w8 = a;
            //     00000001000061b0         adrp       x9, #0x100009000
            //     00000001000061b4         add        x9, x9, #0x5d0                              ; _g
            //MachOView中查到 00000001000095d0 为  0xC 4字节
            //     00000001000061b8         str        w8, x9
            global = w8;
            //     这里b跳转loc_1000061d0,跳过了else的代码
            //     00000001000061bc         b          loc_1000061d0
    
        } else {
            //     //代码块2
            //                          loc_1000061c0:
            //     00000001000061c0         ldr        w8, [sp, #0x8]                              ; CODE XREF=_func+24
            w8 = b;
            //     00000001000061c4         adrp       x9, #0x100009000
            //     00000001000061c8         add        x9, x9, #0x5d0                              ; _g
            //     00000001000061cc         str        w8, x9
            global = w8;
        }
    //                          loc_1000061d0:
    //     00000001000061d0         add        sp, sp, #0x10                               ; CODE XREF=_func+44
         //ret前没有x8和x0的操作,返回void
    //     00000001000061d4         ret
    }
    

    精简后:

    int global = 12;
    
    void func2(int a, int b) {
        if (a > b) {
            global = a;
        } else {
            global = b;
        }
    }
    

    cmp(Compare)比较指令

    cmp 把一个寄存器的内容和另一个寄存器的内容或立即数进行比较。但不存储结果,只是正确的更改标志(cpsr)。
    一般cmp做完判断后会进行跳转,后面通常会跟上b指令!

    b 跳转指令

    b本身代表跳转,后面跟标号会有其他操作:

    • bl 标号:跳转到标号处执行,并且影响lr寄存器的值。用于函数返回。
    • br 寄存器:根据寄存器中的值跳转。
    • b.gt 标号:比较结果是 大于(greater than) 执行标号,否则不跳转。
    • b.ge 标号:比较结果是 大于等于(greater than or equal to) 执行标号,否则不跳转。
    • b.lt 标号 :比较结果是 小于(less than) 执行标号,否则不跳转。
    • b.le 标号:比较结果是 小于等于(less than or equal to) 执行标号,否则不跳转。
    • b.eq 标号 标号:比较结果是 等于(equal) 执行标号,否则不跳转。
    • b.ne 标号 标号:比较结果是 不等于(not equal) 执行标号,否则不跳转。
    • b.hi 标号 标号:比较结果是 无符号大于 执行标号,否则不跳转。
    • b.hs 标号:比较结果是 无符号大于等于 执行标号,否则不跳转。
    • b.lo 标号:比较结果是 无符号小于 执行标号,否则不跳转。
    • b.ls 标号:比较结果是 无符号小于等于 执行标号,否则不跳转。

    ⚠️:cmp后跟的标号条件是else

    循环

    do-while

    void func() {
        int nSum = 0;
        int i = 0;
        do {
            nSum = nSum + 1;
            i++;
        } while (i < 100);
    }
    
    Hopper解析do-while汇编代码

    while

    void func() {
        int nSum = 0;
        int i = 0;
        while (i < 100) {
            nSum = nSum + 1;
            i++;
        }
    }
    
    Hopper解析while汇编代码

    for

    void func() {
        int nSum = 0;
        for (int i = 0; i < 100; i++) {
            nSum = nSum + 1;
        }
    }
    
    Hopper解析for汇编代码
    ⚠️forwhile的汇编代码相同。

    Switch 选择指令

    void func(int a) {
        switch (a) {
            case 1:
                printf("case 1");
                break;
            case 2:
                printf("case 2");
                break;
            case 3:
                printf("case 3");
                break;
            default:
                printf("case default");
                break;
        }
    }
    
    Hopper解析switch(case <=3 )汇编代码

    case < 3的情况下底层汇编就是if-else

    修改代码让case多于3个:

    void func(int a) {
        switch (a) {
            case 1:
                printf("case 1");
                break;
            case 2:
                printf("case 2");
                break;
            case 3:
                printf("case 3");
                break;
            case 4:
                printf("case 4");
                break;
            default:
                printf("case default");
                break;
        }
    }
    
    int main(int argc, char * argv[]) {
        func(2);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
    

    分析汇编代码:

    TestDemo`func:
        0x102c220b4 <+0>:   sub    sp, sp, #0x20             ; =0x20 
        0x102c220b8 <+4>:   stp    x29, x30, [sp, #0x10]
        0x102c220bc <+8>:   add    x29, sp, #0x10            ; =0x10 
    
        //参数入栈 2
        0x102c220c0 <+12>:  stur   w0, [x29, #-0x4]
        //参数存入w8
    ->  0x102c220c4 <+16>:  ldur   w8, [x29, #-0x4]
        //参数和最小case相减 这里是减1 给到 w8(这里相减有可能溢出)
        0x102c220c8 <+20>:  subs   w8, w8, #0x1              ; =0x1 
        //x8的值存入x9
        0x102c220cc <+24>:  mov    x9, x8
        //从0~31取出来给x9,目的是把高32位清零。相当于拿到低32位数据。
        0x102c220d0 <+28>:  ubfx   x9, x9, #0, #32
        //比较 x9 和 0x3 的值。这里 0x3是 最大 case 和 最小 case 的差值。相当于 (参数 - 最小case - (最大case - 最小case))
        0x102c220d4 <+32>:  cmp    x9, #0x3                  ; =0x3 
        //x8低32位入栈
        0x102c220d8 <+36>:  str    x9, [sp]
        //比较结果无符号大于 跳转default。相当于不在差值区间也就是匹配不了case的情况下直接走default。
        0x102c220dc <+40>:  b.hi   0x102c22138               ; <+132> at main.m
    
        //在区间中的情况下
        //x8的值为 0x102c22150,对应的内存中值为  fffffffa8  -88
        0x102c220e0 <+44>:  adrp   x8, 0
        0x102c220e4 <+48>:  add    x8, x8, #0x150            ; =0x150 
        //这时候从栈中取数据给x11,栈中目前是x9。x9为(参数 - 最小case = 1)
        0x102c220e8 <+52>:  ldr    x11, [sp]
        //x8 + (x11 << 2)的值给到 x10。[x8内存地址 + 4] = 移到下一个位置(0x102c22154) 这里对应的值为-72。⚠️ret的地址为0x102c2214c + 4 = 0x102c22150,0x102c22150 + 4 = 0x102c22154
        //这里计算完x10 = -72
        0x102c220ec <+56>:  ldrsw  x10, [x8, x11, lsl #2]
        //x8 + 偏移找到的负数 给到 x9  0x102c22150 - 72(0x48) = 0x102C22108
        0x102c220f0 <+60>:  add    x9, x8, x10
        //跳转寄存器的地址 0x102C22108 对应 case 2 的地址
        0x102c220f4 <+64>:  br     x9
    
        //case中语句
        0x102c220f8 <+68>:  adrp   x0, 1
        0x102c220fc <+72>:  add    x0, x0, #0xf5d            ; =0xf5d 
        0x102c22100 <+76>:  bl     0x102c22588               ; symbol stub for: printf
        0x102c22104 <+80>:  b      0x102c22144               ; <+144> at main.m:48:1
        0x102c22108 <+84>:  adrp   x0, 1
        0x102c2210c <+88>:  add    x0, x0, #0xf64            ; =0xf64 
        0x102c22110 <+92>:  bl     0x102c22588               ; symbol stub for: printf
        0x102c22114 <+96>:  b      0x102c22144               ; <+144> at main.m:48:1
        0x102c22118 <+100>: adrp   x0, 1
        0x102c2211c <+104>: add    x0, x0, #0xf6b            ; =0xf6b 
        0x102c22120 <+108>: bl     0x102c22588               ; symbol stub for: printf
        0x102c22124 <+112>: b      0x102c22144               ; <+144> at main.m:48:1
        0x102c22128 <+116>: adrp   x0, 1
        0x102c2212c <+120>: add    x0, x0, #0xf72            ; =0xf72 
        0x102c22130 <+124>: bl     0x102c22588               ; symbol stub for: printf
        0x102c22134 <+128>: b      0x102c22144               ; <+144> at main.m:48:1
        0x102c22138 <+132>: adrp   x0, 1
        0x102c2213c <+136>: add    x0, x0, #0xf79            ; =0xf79 
        0x102c22140 <+140>: bl     0x102c22588               ; symbol stub for: printf
    
        0x102c22144 <+144>: ldp    x29, x30, [sp, #0x10]
        0x102c22148 <+148>: add    sp, sp, #0x20             ; =0x20 
        0x102c2214c <+152>: ret    
    
    0x102c220e0 <+44>:  adrp   x8, 0
    0x102c220e4 <+48>:  add    x8, x8, #0x150            ; =0x150 
    

    以上语句执行后x8的值:

    x8的值
    明显能够看出来是一个负数0xfffffffa8 ,在指向的地址中有一连串的负数。
    (lldb) p *(int *)0x102c22150
    (int) $5 = -88
    (lldb) p (int)0xffffffa8
    (int) $6 = -88
    (lldb) 
    
    Hopper解析switch(case > 3 )汇编代码

    核心逻辑:

    • subs w8, w8, #0x1:参数-最小case
    • ubfx x9, x9, #0, #32:从0~31取出来给x9,目的是把高32位清零。相当于拿到低32位数据。
    • cmp x9, #0x3:0x3最大case - 最小case
    • b.hi 0x102c22138: 大于为了判断(参数 - 最小case)是否在(最大case - 最小case)区间。无符号为了处理负数,否则负数永远小于,用无符号就永远大于直接排除掉了。
    • 0x102c220e0 <+44>: adrp x8, 0
      0x102c220e4 <+48>: add x8, x8, #0x150 ; =0x150:
      找到跳转表(jump table)地址,这张跳转表在编译的时候生成是连续的,永远在程序结束的后面。
    • ldrsw x10, [x8, x11, lsl #2]:[跳转表地址 +(参数-最小case)<< 2] 得到跳转表中的偏移值。<<2是因为表中数据是4个字节。这里为 差值 * 4
    • add x9, x8, x10:上面的到的偏移值加上跳转表地址就得到了case执行的地址。

    计算过程

    • 参数 - 最小case 得到表中index
    • index 与 (最大case - 最小case)无符号比较判断是否在区间内。
    • 不在区间内直接跳转defalult
    • 在区间内 表头地址 + index << 2获取偏移地址(为负数)。
    • 跳转表头地址 + 偏移地址执行对应case逻辑。

    ⚠️:表中为什么不直接存地址? 1.地址过长 2.有ASLR的存在

    1. switch语句的分支比较少的时候(< 3的时候没有意义)没有必要使用表结构,相当于if

    2. 各个分支常量的差值较大的时候,编译器会在效率还是内存进行取舍,这个时候编译器还是会编译成类似于if-else的结构。
      比如:100、200、300、400这种case还是和if-else相同,10、20、30、40会生成一张表。所以在写switch逻辑的时候最好使用连续的值。至于具体逻辑编译器会根据case差值进行优化选择。case越多,差值越小,数值越连贯 编译器会生成跳转表,否则还是if-else

    3. 在分支比较多的时候:在编译的时候会生成一个表(跳转表每个地址四个字节)。

    4. 跳转表中数量为最大case - 最小case + 1为一共有多少种可能性。

    5. switch分支的代码是连续的。由于case是连贯的用空间换时间。

    总结

    总结

    相关文章

      网友评论

          本文标题:汇编-循环、选择、判断

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