美文网首页
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 - 汇编看函数

    栈区(SP & FP 寄存器) 栈是一种限定仅在表尾进行插入和删除操作的线性表。它的特点是后进先出(Last In...

  • 从汇编看函数

    一、简介 CPU中央处理器,内部主要包括寄存器、运算器、控制器。 寄存器:存储数据 运算器:处理数据 控制器:控制...

  • 反汇编看宏函数和函数

    通过 gdb 查看程序的汇编代码,比较宏和宏函数的工作效率。 程序 例子中的最大值实现,宏和函数逻辑基本相同。宏在...

  • 程序调用过程和堆栈的关系,为什么要传地址而不传值

    bug.c 使用gdb调试,main函数反汇编的代码 swap函数的反汇编代码 在执行到call swap函数之前...

  • Linux boot的第一步:启动汇编调用main函数

    为了讲清原理,我们首先介绍C函数调用机制,然后再介绍汇编调用C函数。 一、C函数调用机制 对于汇编中的函数,其使用...

  • 汇编学习(5),函数,栈帧

    本篇介绍 本篇介绍下汇编中的函数,栈帧内容。 函数 汇编也支持函数调用,如下是一个例子: 使用call + 标号就...

  • 7.汇编-汇编中的函数

    7.汇编-汇编中的函数 什么是函数 函数就是一系列指令的几个,为了完成某个会重复使用的特定功能 函数调用 用JMP...

  • 汇编(四)

    1. if语句的汇编代码 一个简单的if语句 main函数汇编 2. while语句的汇编代码 do while的...

  • 汇编学习(9), 命令行参数,C与汇编

    本篇介绍 本篇介绍下汇编如何支持命令行函数,以及C如何调用汇编。 命令行参数 看一个访问命令行参数的例子: 命令行...

  • iOS逆向之OC反汇编(下)

    本文主要理解OC对象反汇编,以及block常见类型的反汇编 OC反汇编 创建一个Person类,并在main函数中...

网友评论

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

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