逆向学习笔记4——汇编的循环&选择

常用指令

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

  • BL 标号:跳转到标号处执行

  • B.LE 标号:比较结果是小于等于(less than or equal),执行标号,否则不跳转

  • B.GT 标号:比较结果是大于(greater than),执行标号,否则不跳转

  • B.GE 标号:比较结果是大于等于(greater than or equal to),执行标号,否则不跳转

  • B.EQ 标号:比较结果是等于,执行标号,否则不跳转

  • B.HI 标号:比较结果是无符号大于,执行标号,否则不跳转

  • ldrsw:[X8,X9,LSL#2]` 表示以 X8 为基地址,X9的二进制左移2位,再相加得到一个新的地址值。
    例如:A表示(X8 为基地址),B 表示(X9的二进制左移2位)
    那么 X10 = [A B];

if

void func(int a, int b){
    if(a > b){
        printf("a大于b");
    }else{
        printf("a不大于b");
    }
}

上面代码的汇编如下:

demo1`func:
    0x100f926a0 <+0>:  sub    sp, sp, #0x20             ; =0x20 //拉升栈空间
    0x100f926a4 <+4>:  stp    x29, x30, [sp, #0x10]
    0x100f926a8 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x100f926ac <+12>: stur   w0, [x29, #-0x4]
    0x100f926b0 <+16>: str    w1, [sp, #0x8]
    0x100f926b4 <+20>: ldur   w0, [x29, #-0x4]
    0x100f926b8 <+24>: ldr    w1, [sp, #0x8]
   //上面这些代码不用管,全是函数对局部变量的操作

    //比较w0和w1的值
    0x100f926bc <+28>: cmp    w0, w1
    //如果w0<=w1 跳转到地址为0x100f926d8的指令
    0x100f926c0 <+32>: b.le   0x100f926d8               ; <+56> at main.m:17 

    //下面两句代码得到的x0对应的常量值为0x100f9365c
    0x100f926c4 <+36>: adrp   x0, 1
    0x100f926c8 <+40>: add    x0, x0, #0x65c            ; =0x65c 

    //打印
    0x100f926cc <+44>: bl     0x100f92a68               ; symbol stub for: printf
    0x100f926d0 <+48>: str    w0, [sp, #0x4]

    //跳转地址为0x100f926e8指令-->相当于高级语音中的goto
    0x100f926d4 <+52>: b      0x100f926e8               ; <+72> at main.m:19

    //下面两句代码得到的x0对应的常量值为0x100f93665
    0x100f926d8 <+56>: adrp   x0, 1
    0x100f926dc <+60>: add    x0, x0, #0x665            ; =0x665 
    //打印
    0x100f926e0 <+64>: bl     0x100f92a68               ; symbol stub for: printf
    0x100f926e4 <+68>: str    w0, [sp]

    //栈平衡
    0x100f926e8 <+72>: ldp    x29, x30, [sp, #0x10]
    0x100f926ec <+76>: add    sp, sp, #0x20             ; =0x20 

    //返回
    0x100f926f0 <+80>: ret

while

先补充两个指令
Zero Register: 在大多数情况下,作为源寄存器使用时, r31读出来的值 是0; 作为目标寄存器使用时, 丢弃结果。 WZR(word zero rigiser)或者XZR(64位)

Stack Register: 当 用作load/store 的base register时, 或者 一些算术指令中, r31提供当前的stack pointer WSP或者 SP

void func(int a, int b){
    while(a < b){
        printf("a大于b");
    }
}

上面代码的汇编如下:

    0x1009ca700 <+0>:  sub    sp, sp, #0x10             ; =0x10 
    //将0寄存器的值写进内存
    0x1009ca704 <+4>:  str    wzr, [sp, #0xc]
    //将刚刚存的值取出来并放在w8寄存器中
    0x1009ca708 <+8>:  ldr    w8, [sp, #0xc]
    //比较w8 和 10的大小
    0x1009ca70c <+12>: cmp    w8, #0xa                  ; =0xa 
    如果w8 >= 10,跳转0x1009ca724
    0x1009ca710 <+16>: b.ge   0x1009ca724               ; <+36> at main.m:18
    //从内存拿出值赋值w8
    0x1009ca714 <+20>: ldr    w8, [sp, #0xc]
    //w8 += 1
    0x1009ca718 <+24>: add    w8, w8, #0x1              ; =0x1 
    //将w8值写进内存
    0x1009ca71c <+28>: str    w8, [sp, #0xc]
    //跳转到0x1009ca708
    0x1009ca720 <+32>: b      0x1009ca708               ; <+8> at main.m:15
    //栈平衡
    0x1009ca724 <+36>: add    sp, sp, #0x10             ; =0x10 
    0x1009ca728 <+40>: ret     

for / do-while基本和while类似,此处就不再赘述

Switch

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

CBZ

比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)

void func(int a){
    switch (a) {
        case 0:
        {
            printf("0");
        }
            break;
        case 1:
        {
            printf("1");
        }
            break;
    
            
        default:
            break;
    }
}

上面代码的汇编代码如下

demo1`func:
    0x1005ae6a0 <+0>:   sub    sp, sp, #0x30             ; =0x30 
    0x1005ae6a4 <+4>:   stp    x29, x30, [sp, #0x20]
    0x1005ae6a8 <+8>:   add    x29, sp, #0x20            ; =0x20
    0x1005ae6ac <+12>:  stur   w0, [x29, #-0x4]
    0x1005ae6b0 <+16>:  ldur   w0, [x29, #-0x4]
    0x1005ae6b4 <+20>:  mov    x8, x0
    0x1005ae6b8 <+24>:  stur   w8, [x29, #-0x8]
   //上面全是栈操作和其他无关紧要操作

   //如果传进来的w0的值为0,就跳转地址0x1005ae6d8的指令,否则继续往下执行
    0x1005ae6bc <+28>:  cbz    w0, 0x1005ae6d8           ; <+56> at main.m:17
    
     //跳转地址为0x1005ae6c4的指令
    0x1005ae6c0 <+32>:  b      0x1005ae6c4               ; <+36> at main.m
    //拿到w8=x0的值
    0x1005ae6c4 <+36>:  ldur   w8, [x29, #-0x8]
    //w9=w8 - 1
    0x1005ae6c8 <+40>:  subs   w9, w8, #0x1              ; =0x1 
    //将w9的值存入内存
    0x1005ae6cc <+44>:  stur   w9, [x29, #-0xc]
    //w9=0,就跳转地址为0x1005ae6ec的指令,否则不跳转
    0x1005ae6d0 <+48>:  b.eq   0x1005ae6ec               ; <+76> at main.m:22
     //跳转地址为0x1005ae700指令
    0x1005ae6d4 <+52>:  b      0x1005ae700               ; <+96> at main.m:28

    //下面三句代码是一个打印,printf("0");
    0x1005ae6d8 <+56>:  adrp   x0, 1
    0x1005ae6dc <+60>:  add    x0, x0, #0x670            ; =0x670 
    0x1005ae6e0 <+64>:  bl     0x1005aea7c               ; symbol stub for: printf
    0x1005ae6e4 <+68>:  str    w0, [sp, #0x10]

  //跳转汇编为0x1005ae704的指令
    0x1005ae6e8 <+72>:  b      0x1005ae704               ; <+100> at main.m:31
   //下面三句是打印
    0x1005ae6ec <+76>:  adrp   x0, 1
    0x1005ae6f0 <+80>:  add    x0, x0, #0x672            ; =0x672 
    0x1005ae6f4 <+84>:  bl     0x1005aea7c               ; symbol stub for: printf
   
    0x1005ae6f8 <+88>:  str    w0, [sp, #0xc]

      //跳转地址为0x1005ae704指令
    0x1005ae6fc <+92>:  b      0x1005ae704               ; <+100> at main.m:31
    0x1005ae700 <+96>:  b      0x1005ae704               ; <+100> at main.m:31
    0x1005ae704 <+100>: ldp    x29, x30, [sp, #0x20]
    0x1005ae708 <+104>: add    sp, sp, #0x30             ; =0x30 
    0x1005ae70c <+108>: ret    

void func(int a){
    switch (a) {
        case 0:
        {
            printf("0");
        }
            break;
        case 1:
        {
            printf("1");
        }
            break;
        case 2:
        {
            printf("2");
        }
            break;
        case 3:
        {
            printf("3");
        }
            break;
            
        default:
            break;
    }
    
}

上面代码的汇编如下

    0x1002e2658 <+0>:   sub    sp, sp, #0x40             ; =0x40 
    0x1002e265c <+4>:   stp    x29, x30, [sp, #0x30]
    0x1002e2660 <+8>:   add    x29, sp, #0x30            ; =0x30 
    0x1002e2664 <+12>:  stur   w0, [x29, #-0x4]
    0x1002e2668 <+16>:  ldur   w0, [x29, #-0x4]
    0x1002e266c <+20>:  mov    x8, x0
    0x1002e2670 <+24>:  mov    x0, x8
   //以上代码全是栈操作这里就不做分析了

   //w0 = w0 - 3;
    0x1002e2674 <+28>:  subs   w0, w0, #0x3              ; =0x3 
    //将x8的值写入内存
    0x1002e2678 <+32>:  stur   x8, [x29, #-0x10]
    //将w0写入内存
    0x1002e267c <+36>:  stur   w0, [x29, #-0x14]
    //如果w0>0就跳转到地址为0x1002e26ec的指令,否则不跳转
    0x1002e2680 <+40>:  b.hi   0x1002e26ec               ; <+148> at main.m:37
    //将常量0的地址赋值给x8  x8 = 0
    0x1002e2684 <+44>:  adrp   x8, 0
    0x1002e2688 <+48>:  add    x8, x8, #0x6fc            ; =0x6fc 

   //将上面0x1002e2678存入内存的值取出来
    0x1002e268c <+52>:  ldur   x9, [x29, #-0x10]

    //这个是x10 = x8 + x9<<2,得出系统给我们存的switch的那个表值得地址
    0x1002e2690 <+56>:  ldrsw  x10, [x8, x9, lsl #2]
    //x8 = x10 + x8
    0x1002e2694 <+60>:  add    x8, x10, x8
   //计算得到x8的值后,跳转到x8指向的地址值
    0x1002e2698 <+64>:  br     x8

   //下面3句打印
    0x1002e269c <+68>:  adrp   x0, 1
    0x1002e26a0 <+72>:  add    x0, x0, #0x66c            ; =0x66c 
    0x1002e26a4 <+76>:  bl     0x1002e2a78               ; symbol stub for: printf
    //保护w0
    0x1002e26a8 <+80>:  str    w0, [sp, #0x18]
   //跳转地址为0x1002e26f0的指令
    0x1002e26ac <+84>:  b      0x1002e26f0               ; <+152> at main.m:40
   
   //下面3句打印
    0x1002e26b0 <+88>:  adrp   x0, 1
    0x1002e26b4 <+92>:  add    x0, x0, #0x66e            ; =0x66e 
    0x1002e26b8 <+96>:  bl     0x1002e2a78               ; symbol stub for: printf
    0x1002e26bc <+100>: str    w0, [sp, #0x14]
    0x1002e26c0 <+104>: b      0x1002e26f0               ; <+152> at main.m:40

    //下面3句打印
    0x1002e26c4 <+108>: adrp   x0, 1
    0x1002e26c8 <+112>: add    x0, x0, #0x670            ; =0x670 
    0x1002e26cc <+116>: bl     0x1002e2a78               ; symbol stub for: printf
    0x1002e26d0 <+120>: str    w0, [sp, #0x10]
    0x1002e26d4 <+124>: b      0x1002e26f0               ; <+152> at main.m:40

    //下面3句打印
    0x1002e26d8 <+128>: adrp   x0, 1
    0x1002e26dc <+132>: add    x0, x0, #0x672            ; =0x672 
    0x1002e26e0 <+136>: bl     0x1002e2a78               ; symbol stub for: printf
    0x1002e26e4 <+140>: str    w0, [sp, #0xc]
    0x1002e26e8 <+144>: b      0x1002e26f0               ; <+152> at main.m:40
    0x1002e26ec <+148>: b      0x1002e26f0               ; <+152> at main.m:40
    //栈平衡
    0x1002e26f0 <+152>: ldp    x29, x30, [sp, #0x30]
    0x1002e26f4 <+156>: add    sp, sp, #0x40             ; =0x40 
    0x1002e26f8 <+160>: ret 

执行过程

    1. switch 内分支比较多的时候(超过4个),在编译的时候会生成一个表(跳转表每个地址四个字节);
    1. 先判断是否是 default 分支;
    1. 根据相应的操作计算出要跳转的地址;
      这个是x10 = x8 + x9<<2,得出系统给我们存的switch的那个表值得地址
      0x1002e2690 <+56>: ldrsw x10, [x8, x9, lsl #2]
      通过计算得到跳转的指令的地址
      0x1002e2694 <+60>: add x8, x10, x8
    1. 跳转到X8的地址;
      0x1002e2698 <+64>: br x8

注意

虽然说正常分支超过4个,编译器就会生成一张表;但是也有不生成表的,比如你一定要写出case1,case 1000,case 200,编译器存储是连续的地址,会根据1,2,3...1000,如果还按照表的形式会浪费大量的空间,编译器会在效率和存储空间做出最合理的选择,将他们做成if,else的形式表现出来。

总结

switch语句 和 if...else语句执行效率问题,通过上面汇编代码得出结论:
当 switch 分支和 if...else的条件判断小于4的时候,执行效率是一样的;
当 switch 分支和 if...else的条件判断大于等于4的时候,switch 执行效率更高。(你的case是连续常量)

你可能感兴趣的:(逆向学习笔记4——汇编的循环&选择)