美文网首页
02 - 汇编看函数

02 - 汇编看函数

作者: 卡布奇诺_95d2 | 来源:发表于2021-03-25 18:22 被阅读0次

    栈区(SP & FP 寄存器)

    • 栈是一种限定仅在表尾进行插入和删除操作的线性表。它的特点是后进先出(Last In Out Firt,LIFO)。

    • 栈区是由高地址向低地址开辟空间


    • SP寄存器(X30):用来保存栈顶地址

    • FP寄存器(X29):在某些时候用来保存栈底地址

    函数调用栈

    常见的函数调用开辟和恢复栈空间的汇编如下:

    sub    sp, sp, #0x40             ; 拉伸0x40(64字节)空间,栈区是由高往低开辟空间
    stp    x29, x30, [sp, #0x30]     ; x29、x30 寄存器入栈保护
    add    x29, sp, #0x30            ; x29指向栈帧的底部
    ... 
    ldp    x29, x30, [sp, #0x30]     ; 恢复x29、x30 寄存器的值
    add    sp, sp, #0x40             ; 栈平衡
    ret
    

    上述代码可以进行部分简写:

    //将sub、stp进行合并
    sub    sp, sp, #0x40             ; 拉伸0x40(64字节)空间,栈区是由高往低开辟空间
    stp    x29, x30, [sp, #0x40]     ; x29、x30 寄存器入栈保护
    //合并之后
    stp    x29, x30, [sp, #-0x40]!     
    
    //将add、ldp进行合并
    ldp    x29, x30, [sp, #0x40]     ; 恢复x29、x30 寄存器的值
    add    sp, sp, #0x40             ; 栈平衡
    //合并之后
    ldp    x29, x30, [sp], #0x40
    

    内存读写指令

    在详细描述SP寄存器和FP寄存器之前,先来了解一下内存的读写指令。

    • str(store register)指令:将数据从寄存器中读出来,存到内存中,它的偏移为正数
    • stur指令:它也是str的变种,它的偏移为负数
    • stp指令:它是str的变种,同时可以操作两个寄存器
    • ldr(load register)指令:将数据从内存中读出来,存到寄存器中,它的偏移为正数
    • ldur指令:它也是ldr指令的变种,它的偏移为负数
    • ldp指令:它是ldr指令的变种,同时操作两个寄存器

    栈区操作详解

    假设有两个数,a=10,b=20,现在希望通过使用汇编使其完成两个数的交换,即结果:a=20,b=10。那要如何实现呢?
    【第一步】分别将10和20存入x0、x1寄存器

    mov x0, #0x0a
    mov x1, #0x14
    

    此时通过lldb断点调试读取寄存器x0、x1的内容。

    (lldb) register read x0
          x0 = 0x000000000000000a
    (lldb) register read x1
          x1 = 0x0000000000000014
    

    【第二步】拉取32字节的栈空间

    sub sp, sp, #0x20
    

    在执行sub执行前后,分别查看sp的内容

    //执行前
    (lldb) register read sp
          sp = 0x000000016b6c5b90
          
    //执行 sub sp, sp, #0x20 后
    (lldb) register read sp
          sp = 0x000000016b6c5b70
    

    【第三步】将x0、x1寄存器的内容存储至sp所指向的内存中

    stp x0, x1, [sp]
    

    在执行stp执行前后,分别查看内存中的内容

    //执行前,此时内存中的数据为垃圾数据
    (lldb) x/4g 0x000000016b6c5b70
    0x16b6c5b70: 0x0000000104c08080 0x00000001e6cd5f08
    0x16b6c5b80: 0x000000016b6c5bb0 0x000000010473df5c
    
    //执行 stp x0, x1, [sp] 后
    (lldb) x/4g 0x000000016b6c5b70
    0x16b6c5b70: 0x000000000000000a 0x0000000000000014
    0x16b6c5b80: 0x000000016b6c5bb0 0x000000010473df5c
    

    【第四步】为了实现交换两个寄存器内容的功能,则根据存入内存的顺序反向读出来即可

    ldp x1, x0, [sp]
    

    在执行ldp执行前后,分别查看x0、x1寄存器的内容

    (lldb) register read x0
          x0 = 0x0000000000000014
    (lldb) register read x1
          x1 = 0x000000000000000a
    

    【第五步】栈操作使用完成之后,需要恢复栈平衡

    add sp, sp, #0x20
    

    在执行add后,再查看sp的内容

    (lldb) register read sp
          sp = 0x000000016b6c5b90
    

    此时sp已经恢复至操作之前的位置

    bl指令和ret指令

    bl指令格式:bl 标号

    • 将下一条指令的地址存入LR寄存器(也表示为:X30)
    • 跳转到标号处执行指令

    ret指令格式:ret
    默认使用LR(X30)寄存器的值,通过底层指令提示CPU,LR(X30)寄存器的中值为下条指令地址。

    LR(X30)寄存器

    X30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找X30寄存器保存的地址值。
    接下来通过分析一段代码来看一下当函数进行嵌套调用时,LR寄存器的内容变化,以及函数执行的情况?

    //此处是调用汇编中定义的A函数
    - (void)viewDidLoad {
        [super viewDidLoad];
        A();
    }
    //假设汇编代码如下:
    _A:
        mov x0,#0xa0
        bl _B
        mov x0,#0xb0
    
    _B:
        mov x0,#0xc0
        ret
    

    【第一步】当进入汇编A函数时,lldb断点调试

    Demo`A:
    ->  0x102611ec4 <+0>:  mov    x0, #0xa0
        0x102611ec8 <+4>:  bl     0x102611ed4               ; B
        0x102611ecc <+8>:  mov    x0, #0xb0
        0x102611ed0 <+12>: ret    
        
    //此时读取LR寄存器的内容,LR寄存器中存储的是 viewDidLoad 中A后面一条指令的地址
    (lldb) register read lr
          lr = 0x0000000102611f60  Demo`-[ViewController viewDidLoad] + 72 at ViewController.m:32:1
    

    【第二步】当从汇编A函数中,跳转至B中时

    Demo`B:
    ->  0x102611ed4 <+0>: mov    x0, #0xc0
        0x102611ed8 <+4>: ret      
        
    //此时读取LR寄存器的内容,LR寄存器中存储的是 A 函数中bl _B后面一条指令的地址(0x102611ecc <+8>:  mov    x0, #0xb0)
    (lldb) register read lr
          lr = 0x0000000102611ecc  Demo`A + 8
    

    【第三步】从汇编的B函数返回时

    Demo`A:
        0x102611ec4 <+0>:  mov    x0, #0xa0
        0x102611ec8 <+4>:  bl     0x102611ed4               ; B
    ->  0x102611ecc <+8>:  mov    x0, #0xb0
        0x102611ed0 <+12>: ret    
    
    //从_B返回时,LR寄存器保存了下一条需要执行指令的地址,因此回到_A中的 0x102611ecc 地址处,但此处读取LR寄存器,发现其中仍然保存了 0x102611ecc 这个地址
    (lldb) register read lr
          lr = 0x0000000102611ecc  Demo`A + 8
    

    【第四步】当从汇编A函数返回时,此时程序运行情况和LR寄存器内容如下

    Demo`A:
        0x102611ec4 <+0>:  mov    x0, #0xa0
        0x102611ec8 <+4>:  bl     0x102611ed4               ; B
    ->  0x102611ecc <+8>:  mov    x0, #0xb0
        0x102611ed0 <+12>: ret    
    
    (lldb) register read lr
          lr = 0x0000000102611ecc  Demo`A + 8
    //此时出现了奇怪的现象,本来当A执行完成时,需要从A返回至viewDidLoad,但单步调试发现,当A函数的ret执行完之后,回到了 0x102611ecc 地址处执行。并陷入(0x102611ecc->ret->0x102611ecc)的循环。
    
    • 这是因为当执行ret时,会将LR(X30)寄存器的中值为下条指令地址。
    • 当第一次函数调用时(从viewDidLoad 调用A()),而LR(X30)寄存器的值保存了 viewDidLoad 中A后面一条指令的地址。
    • 当第二次函数调用时(从A()调用B()),而而LR(X30)寄存器的值被修改了,此时保存了 A 函数中调用B函数后面一条指令的地址。
    • 当从函数B返回时,可以根据LR(X30)寄存器的值回到A继续执行。
    • LR(X30)寄存器的值因为没有保护起来,所以再也无法回到 viewDidLoad 中。

    注意:如果存在函数嵌套调用,则需要对X30进行保护(如何保护将在后续章节中详解),否则会在ret时,会因为无法找到正确的函数返回地址而进入死循环。

    LR(X30)寄存器的保护

    上面讲到,若存在函数嵌套调用时,需要将LR(X30)寄存器保护起来,否则会因为LR(X30)寄存器的值被修改而陷入死循环。那如何对LR(X30)寄存器进行保护呢?

    答:在函数跳转至其它函数之前,将LR(X30)寄存器保存到当前函数的栈区。从其它函数返回的后,再从当前函数的栈区取出保存的LR(X30)寄存器的值。

    _A:
        sub sp, sp, #0x10   ;在函数调用栈中拉取16字节空间
        str x30, [sp]       ;将LR寄存器(x30)的值存储至SP寄存器,进行入栈保护
        mov x0,#0xa0        
        bl _B
        ldr x30,[sp]        ;恢复LR寄存器(x30)的值
        mov x0,#0xb0        
        add sp, sp, #0x10   ;函数栈平衡
        ret
    

    函数的参数和返回值

    • ARM64下,函数的参数是存放在X0到X7(W0到W7)8个寄存器里面的。如果超过8个参数,就会入栈
    • 函数的返回值是放在X0寄存器里面的。
      【情况1】当函数存在8个以下参数时,寄存器的存储情况如何?
      void testSum(int a, int b, int c, int d){
          int f = a+b+c+d;
      }
      
      int main(int argc, char * argv[]) {
          testSum(1, 2, 3, 4);
      }
      

    此时的汇编如下:

        Demo`main:
            //拉取32字节内存空间
            0x100746250 <+0>:  sub    sp, sp, #0x20             ; =0x20 
            //对x29、x30进行保护,将其存入函数栈中
            0x100746254 <+4>:  stp    x29, x30, [sp, #0x10]
            //将x29指向sp+0x10的位置
            0x100746258 <+8>:  add    x29, sp, #0x10            ; =0x10 
            //将x29偏移-0x4,然后存入w0,此时的w0是main函数的第一个参数argc
            0x10074625c <+12>: stur   w0, [x29, #-0x4]
            //将x1存入sp所指向的位置,此时x1是main函数的第二个参数argv
            0x100746260 <+16>: str    x1, [sp]
            //将w0赋值为0x1
            0x100746264 <+20>: mov    w0, #0x1
            //将w1赋值为0x2
            0x100746268 <+24>: mov    w1, #0x2
            //将w2赋值为0x3
            0x10074626c <+28>: mov    w2, #0x3
            //将w3赋值为0x4
            0x100746270 <+32>: mov    w3, #0x4
            //先将 0x100746278 地址保存至LR寄存器,再跳转至 0x1044ce1ec 地址,即testSum
        ->  0x100746274 <+36>: bl     0x1044ce1ec               ; testSum at main.m:11
            //将w8赋值为0x0
            0x100746278 <+40>: mov    w8, #0x0
            //将x0赋值为0x8,此处x0为main函数的返回值,即返回0
            0x10074627c <+44>: mov    x0, x8
            //恢复x29, x30寄存器的值
            0x100746280 <+48>: ldp    x29, x30, [sp, #0x10]
            //恢复栈平衡
            0x100746284 <+52>: add    sp, sp, #0x20             ; =0x20 
            //函数返回
            0x100746288 <+56>: ret    
        
        Demo`testSum:
            //拉取32字节内存空间
        ->  0x1044ce1ec <+0>:  sub    sp, sp, #0x20             ; =0x20 
            //将sp偏移0x1c,然后存入w0
            0x1044ce1f0 <+4>:  str    w0, [sp, #0x1c]
            //将sp偏移0x18,然后存入w1
            0x1044ce1f4 <+8>:  str    w1, [sp, #0x18]
            //将sp偏移0x14,然后存入w2
            0x1044ce1f8 <+12>: str    w2, [sp, #0x14]
            //将sp偏移0x10,然后存入w3
            0x1044ce1fc <+16>: str    w3, [sp, #0x10]
            //将sp偏移0x1c,然后将内存中的值读取到w8,此时w8=1
            0x1044ce200 <+20>: ldr    w8, [sp, #0x1c]
            //将sp偏移0x18,然后将内存中的值读取到w9,此时w9=2
            0x1044ce204 <+24>: ldr    w9, [sp, #0x18]
            //将w8与w9相加,并将结果存入w8,此时w8=1+2=3
            0x1044ce208 <+28>: add    w8, w8, w9
            //将sp偏移0x14,然后将内存中的值读取到w9,此时w9=3
            0x1044ce20c <+32>: ldr    w9, [sp, #0x14]
            //将w8与w9相加,并将结果存入w8,此时w8=3+3=6
            0x1044ce210 <+36>: add    w8, w8, w9
            //将sp偏移0x10,然后将内存中的值读取到w9,此时w9=4
            0x1044ce214 <+40>: ldr    w9, [sp, #0x10]
            //将w8与w9相加,并将结果存入w8,此时w8=6+4=10
            0x1044ce218 <+44>: add    w8, w8, w9
            //将sp偏移0xc,然后存入w8,由于代码中没有返回值,因此此处也只是将结果存在函数调用栈而已
            0x1044ce21c <+48>: str    w8, [sp, #0xc]
            //函数执行完成,恢复栈平衡
            0x1044ce220 <+52>: add    sp, sp, #0x20             ; =0x20 
            //返回
            0x1044ce224 <+56>: ret   
    

    【情况2】当存在8个以上参数时,寄存器的存储情况

        int testSum(int a0, int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8){
            return a0+a1+a2+a3+a4+a5+a6+a7+a8;
        }
        
        int main(int argc, char * argv[]) {
            int a = testSum(1, 2, 3, 4, 5, 6, 7, 8, 9);
            return 0;
        }
    

    汇编如下(下面代码中与前面类似代码不做详解):

        Demo`main:
            0x104e4a230 <+0>:  sub    sp, sp, #0x30             ; =0x30 
            0x104e4a234 <+4>:  stp    x29, x30, [sp, #0x20]
            0x104e4a238 <+8>:  add    x29, sp, #0x20            ; =0x20 
            0x104e4a23c <+12>: stur   w0, [x29, #-0x4]
            0x104e4a240 <+16>: str    x1, [sp, #0x10]
            //在此之前,是拉取栈空间,将main函数的参数以及x29, x30存入栈中
            0x104e4a244 <+20>: mov    w0, #0x1
            0x104e4a248 <+24>: mov    w1, #0x2
            0x104e4a24c <+28>: mov    w2, #0x3
            0x104e4a250 <+32>: mov    w3, #0x4
            0x104e4a254 <+36>: mov    w4, #0x5
            0x104e4a258 <+40>: mov    w5, #0x6
            0x104e4a25c <+44>: mov    w6, #0x7
            0x104e4a260 <+48>: mov    w7, #0x8
            //在此之前,对w0-w7进行赋值
            //将sp赋值给x8
        ->  0x104e4a264 <+52>: mov    x8, sp
            //对w9进行赋值
            0x104e4a268 <+56>: mov    w9, #0x9
            //将w9存入x8指向的位置,此时,将w9存入了函数栈中
            0x104e4a26c <+60>: str    w9, [x8]
            //将 0x104e4a274 地址保存至LR寄存器,并跳转至 0x104e4a1b8 这个位置执行
            0x104e4a270 <+64>: bl     0x104e4a1b8               ; testSum at main.m:11
            0x104e4a274 <+68>: str    w0, [sp, #0xc]
            0x104e4a278 <+72>: mov    w9, #0x0
            0x104e4a27c <+76>: mov    x0, x9
            0x104e4a280 <+80>: ldp    x29, x30, [sp, #0x20]
            0x104e4a284 <+84>: add    sp, sp, #0x30             ; =0x30 
            0x104e4a288 <+88>: ret    
            
        Demo`testSum:
            //拉取48字节内存空间
            0x104e4a1b8 <+0>:   sub    sp, sp, #0x30             ; =0x30 
            //注意:此时是将sp+0x30处的值读取到w8。sp+0x30是前一个函数的栈空间,对于本案例,sp+0x30是main函数的栈空间
        ->  0x104e4a1bc <+4>:   ldr    w8, [sp, #0x30]
            0x104e4a1c0 <+8>:   str    w0, [sp, #0x2c]
            0x104e4a1c4 <+12>:  str    w1, [sp, #0x28]
            0x104e4a1c8 <+16>:  str    w2, [sp, #0x24]
            0x104e4a1cc <+20>:  str    w3, [sp, #0x20]
            0x104e4a1d0 <+24>:  str    w4, [sp, #0x1c]
            0x104e4a1d4 <+28>:  str    w5, [sp, #0x18]
            0x104e4a1d8 <+32>:  str    w6, [sp, #0x14]
            0x104e4a1dc <+36>:  str    w7, [sp, #0x10]
            0x104e4a1e0 <+40>:  str    w8, [sp, #0xc]
            0x104e4a1e4 <+44>:  ldr    w8, [sp, #0x2c]
            0x104e4a1e8 <+48>:  ldr    w9, [sp, #0x28]
            0x104e4a1ec <+52>:  add    w8, w8, w9
            0x104e4a1f0 <+56>:  ldr    w9, [sp, #0x24]
            0x104e4a1f4 <+60>:  add    w8, w8, w9
            0x104e4a1f8 <+64>:  ldr    w9, [sp, #0x20]
            0x104e4a1fc <+68>:  add    w8, w8, w9
            0x104e4a200 <+72>:  ldr    w9, [sp, #0x1c]
            0x104e4a204 <+76>:  add    w8, w8, w9
            0x104e4a208 <+80>:  ldr    w9, [sp, #0x18]
            0x104e4a20c <+84>:  add    w8, w8, w9
            0x104e4a210 <+88>:  ldr    w9, [sp, #0x14]
            0x104e4a214 <+92>:  add    w8, w8, w9
            0x104e4a218 <+96>:  ldr    w9, [sp, #0x10]
            0x104e4a21c <+100>: add    w8, w8, w9
            0x104e4a220 <+104>: ldr    w9, [sp, #0xc]
            0x104e4a224 <+108>: add    w0, w8, w9
            0x104e4a228 <+112>: add    sp, sp, #0x30             ; =0x30 
            0x104e4a22c <+116>: ret  
    

    【情况3】当函数的返回值为多个字节时

        struct str {
            int a;
            int b;
            int c;
            int d;
            int f;
            int g;
        };
        struct str getStr(int a,int b,int c,int d,int f,int g){
            struct str str1;
            str1.a = a;
            str1.b = b;
            str1.c = d;
            str1.d = d;
            str1.f = f;
            str1.g = g;
            return str1;
        }
        int main(int argc, char * argv[]) {
            struct str str2 =  getStr(1, 2, 3, 4, 5, 6);
        }
    

    汇编如下:

        Demo`main:
            0x100dfa23c <+0>:  sub    sp, sp, #0x40             ; =0x40 
            0x100dfa240 <+4>:  stp    x29, x30, [sp, #0x30]
            0x100dfa244 <+8>:  add    x29, sp, #0x30            ; =0x30 
            0x100dfa248 <+12>: stur   w0, [x29, #-0x4]
            0x100dfa24c <+16>: stur   x1, [x29, #-0x10]
            //将sp+0x8的地址存入x8
            0x100dfa250 <+20>: add    x8, sp, #0x8              ; =0x8 
            0x100dfa254 <+24>: mov    w0, #0x1
            0x100dfa258 <+28>: mov    w1, #0x2
            0x100dfa25c <+32>: mov    w2, #0x3
            0x100dfa260 <+36>: mov    w3, #0x4
            0x100dfa264 <+40>: mov    w4, #0x5
            0x100dfa268 <+44>: mov    w5, #0x6
        ->  0x100dfa26c <+48>: bl     0x100dfa158               ; getStr at main.m:20
            0x100dfa270 <+52>: mov    w9, #0x0
            0x100dfa274 <+56>: mov    x0, x9
            0x100dfa278 <+60>: ldp    x29, x30, [sp, #0x30]
            0x100dfa27c <+64>: add    sp, sp, #0x40             ; =0x40 
            0x100dfa280 <+68>: ret  
        
        Demo`getStr:
        ->  0x100dfa158 <+0>:  sub    sp, sp, #0x20             ; =0x20 
            0x100dfa15c <+4>:  str    w0, [sp, #0x1c]
            0x100dfa160 <+8>:  str    w1, [sp, #0x18]
            0x100dfa164 <+12>: str    w2, [sp, #0x14]
            0x100dfa168 <+16>: str    w3, [sp, #0x10]
            0x100dfa16c <+20>: str    w4, [sp, #0xc]
            0x100dfa170 <+24>: str    w5, [sp, #0x8]
            0x100dfa174 <+28>: ldr    w9, [sp, #0x1c]
            //重点:将w9存入x8的地址中,此时x8是main函数的栈空间
            0x100dfa178 <+32>: str    w9, [x8]
            0x100dfa17c <+36>: ldr    w9, [sp, #0x18]
            0x100dfa180 <+40>: str    w9, [x8, #0x4]
            0x100dfa184 <+44>: ldr    w9, [sp, #0x10]
            0x100dfa188 <+48>: str    w9, [x8, #0x8]
            0x100dfa18c <+52>: ldr    w9, [sp, #0x10]
            0x100dfa190 <+56>: str    w9, [x8, #0xc]
            0x100dfa194 <+60>: ldr    w9, [sp, #0xc]
            0x100dfa198 <+64>: str    w9, [x8, #0x10]
            0x100dfa19c <+68>: ldr    w9, [sp, #0x8]
            0x100dfa1a0 <+72>: str    w9, [x8, #0x14]
            0x100dfa1a4 <+76>: add    sp, sp, #0x20             ; =0x20 
            0x100dfa1a8 <+80>: ret   
    

    总结

    • 当函数有嵌套时,FP(x29)寄存器的值和LR(x30)寄存器的值会存入函数栈中,一般位于栈底。
    • 当A函数调用B函数,且B函数的参数小于8个时。B函数中传入的参数是存在x0-x7/w0-w7寄存器中。
    • 当A函数调用B函数,且B函数的参数大于8个时。B函数中传入的参数分为两部分:一部分存在x0-x7/w0-w7寄存器中;另一部分存在A函数的函数栈中,一般从栈顶开始存。
    • 当A函数调用B函数,且B函数的返回值为1字节时,B函数的返回值存在x0寄存器中。
    • 当A函数调用B函数,且B函数的返回值为多字节时,B函数的返回值存在A函数的函数栈中,一般也是栈顶位置开始。

    相关文章

      网友评论

          本文标题:02 - 汇编看函数

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