注册

汇编-函数本质(上)


栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)

9eaf30082846581966d22ab1f6fc966a.png

SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址。
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!(没有出现函数嵌套调用的时候不需要fp,相当于分界点)
    ⚠️:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stpARM64里面 对栈的操作是16字节对齐的!!

ARM64是先开辟一段栈空间,fp移动到栈顶再往栈中存放内容(编译期就已经确定大小)。不存在push操作。在iOS中栈是往低地址开辟空间


09ea02e43e034abee2b2a23a3caa65ae.png

函数调用栈

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

//开辟栈空间
sub sp, sp, #0x40 ; 拉伸0x4064字节)空间
stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
add x29, sp, #0x30 ; x29指向栈帧的底部
...
//恢复栈空间
ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
add sp, sp, #0x40 ;栈平衡
ret

恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。

内存读写指令

⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x020x03

str(store register)指令
将数据从寄存器中读出来,存到内存中。

ldr(load register)指令
将数据从内存中读出来,存到寄存器中。

ldr 和 str 的变种 ldp 和 stp 还可以操作2个寄存器。


堆栈操作案例

使用32个字节空间作为这段程序的栈空间,然后利用栈将x0x1的值进行交换。

.text
.global _C

_C:
sub sp, sp, #0x20 ;拉伸栈空间32个字节
stp x0, x1, [sp, #0x10] ;sp 偏移 16字节存放 x0和x1 []的意思是寻址。这sp并没有改变
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0。这里内存相当于temp 交换了 x0 和 x1。寄存器中的值交换了,内存中的值不变。
add sp, sp, #0x20 ;恢复栈空间
ret
这段代码相当于 x0,x1遍历,sp和内存没有变。
栈空间分配:

73bb772e9b339ab5a3ef7ba85cd8b74f.png



断点调试

0x102e6e518断点处对x0x1分别赋值0xa0xb。然后单步执行:


49738679cfef8230cba972d6c096f473.png


拉伸后sp也变了。

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb)

看下0x000000016cf95b10的空间:


f46be7913e36b109b3eaff026476de0c.png


目前还没有写入内存,是脏数据。接着单步执行:

1da4f3248e440fd29590886445e70d21.png83ee6d19ea0ce2a69f62de55ad881c24.png


这个时候x0x1的数据完成了交换。内存的数据并没有变化。
继续单步执行:

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb)

sp还原了,栈空间释放,这时候0xa0xb还依然存在内存中,等待下次拉伸栈空间写数据覆盖:


edd322ac1cf4a5a12e78ba17883650c6.png


bl和ret指令

bl标号

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处执行指令

b就是跳转,l将下一条指令的地址放入lr(x30)寄存器。

f16718f92a3a3db055fd7ffb39da94f9.png

lr相当于保存的”回家的路“。


ret

  • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

ret只会看lr


ARM64平台的特色指令,它面向硬件做了优化处理。



x30寄存器

x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!
一个嵌套调用的案例,汇编代码如下:

.text
.global _C, _D

_C:
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
ret

_D:
mov x0,#0xbbbb
ret
ViewController.m中调用:

int C();
int D();
- (void)viewDidLoad {
[super viewDidLoad];
printf("C");
C();
printf("D");
}
C();打断点执行,进入C中:

ffbf8e98a7963d3b620421ad4f475db8.png

695dd510a01b42f0aeb0bdb4620fff28.png3732196f484aa730c2ed7f25b5e03753.png


继续执行发现一直在0x104c8e4f80x104c8e4fc中跳转返不回去viewDidLoad中了,发生了死循环。

->  0x104c8e4f8 <+8>:  mov    x0, #0xaaaa
0x104c8e4fc <+12>: ret
那么如果要返回,就必须将viewDidLoad中下一条指令告诉lr,这个时候就必须在bl之前保护lr寄存器(遇到bllr就会改变。需要保护“回家的路”)。那么这个时候能不能把lr保存到其它寄存器?这里我们没法保证其它寄存器不会被使用。这个时候唯一属于当前函数的也就是自己的栈区了。保存到栈区应该就能解决了。
可以看下系统是怎么实现的,写一个c函数断点调试看下:

void c() {
d();
return;;
}

void d() {

}

- (void)viewDidLoad {
[super viewDidLoad];
c();
}

系统的实现如下:

f00a0fd961bea7856d4c048cb0d80e0f.png

TestDemo`c:
//边开辟空间边写入 x29(fp) x30(lr) 的值。[sp, #-0x10]! !代表赋值给sp,相当于 sp -= 0x10
-> 0x102a21e84 <+0>: stp x29, x30, [sp, #-0x10]!
0x102a21e88 <+4>: mov x29, sp
0x102a21e8c <+8>: bl 0x102a21e98 ; d at ViewController.m:34:1
//将sp所指向的地址读取给x29,x30。[sp], #0x10 等价于 sp += 0x10
0x102a21e90 <+12>: ldp x29, x30, [sp], #0x10
0x102a21e94 <+16>: ret

可以看到系统先开辟栈空间,然后将x29x30寄存器的值存入栈区。在ret之前恢复x29x30的值。

  • stp x29, x30, [sp, #-0x10]!:开辟空间并将x29x30存入栈区。!代表赋值给sp,相当于 sp -= 0x10
  • ldp x29, x30, [sp], #0x10:将栈区的值给x29x30并回收空间。[sp], #0x10 等价于 sp += 0x10

那么对于CD的案例自己实现下保存和恢复lr寄存器。


.text
.global _C, _D

_C:
//sub sp,sp,#0x10
//str x30,[sp] ;等价
str x30, [sp,#-0x10]! ;16字节对齐,必须最小0x10
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
//ldr x30,[sp]
//add sp,#0x10 ;等价
ldr x30,[sp],#0x10
ret

_D:
mov x0,#0xbbbb
ret

4f52ade7403dc8443eda7744dff1bf91.png22e1872b7f6aabb3debf4b083586611b.pngd98836cb56f144f73f39000644617237.png

这个时候进入Dlr值已经发生变化。

19a83580399c0283178e3642f74560cd.png987453ca8a761057dddcd8c97784f7a5.png

继续执行正常返回viewDidload了,这个时候死循环就已经解决了。

⚠️:在函数嵌套调用的时候,需要将x30入栈!开辟空间需要16字节对齐。如果开辟8字节再读的时候会坏地址访问。写的时候没问题。


179b6c930d64324269890059c06e6bee.png

函数的参数和返回值

先看下系统的实现:

int sum(int a, int b) {
return a + b;
}

- (void)viewDidLoad {
[super viewDidLoad];
sum(10,20);
}
2f7d69eb97ed96b500b7576f3990443a.png



可以看到变量1020分别存入了w0w1
sum调用如下(release模式下编译器会优化):

TestDemo`sum:
//开辟空间
-> 0x100121e68 <+0>: sub sp, sp, #0x10 ; =0x10
//w0 w1 存入栈中
0x100121e6c <+4>: str w0, [sp, #0xc]
0x100121e70 <+8>: str w1, [sp, #0x8]
//从栈中读取参数
0x100121e74 <+12>: ldr w8, [sp, #0xc]
0x100121e78 <+16>: ldr w9, [sp, #0x8]
//参数相加存入w0
0x100121e7c <+20>: add w0, w8, w9
//恢复栈空间
0x100121e80 <+24>: add sp, sp, #0x10 ; =0x10
//返回
0x100121e84 <+28>: ret
从上面可以看出返回值在w0中。那么自己实现sum函数的汇编代码:

.text
.global _suma

_suma:
add x0,x0,x1
ret
调用:

int suma(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
printf("%d",suma(10,20));
}

⚠️ARM64下,函数的参数是存放在X0X7(W0W7)这8个寄存器里面的。如果超过8个参数就会入栈。那么oc的方法最好不要超过6个(selfcmd)。
函数的返回值是放在X0寄存器里面的。


参数超过8个


int test(int a, int b, int c ,int d, int e, int f, int g, int h, int i) {
return a + b + c + d + e + f + g + h + i;
}

- (void)viewDidLoad {
[super viewDidLoad];
test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}


e14bde777a2ad77c3260fd5e2e222436.png

可以看到前8个参数分别保存在w0~w7寄存器中,第9个参数先保存在w10中,然后写入x8中(这个时候x8指向sp,相当于第9个参数写入了当前函数栈中)。


TestDemo`-[ViewController viewDidLoad]:
//拉伸栈空间,保存fp lr
0x100f09e5c <+0>: sub sp, sp, #0x40 ; =0x40
0x100f09e60 <+4>: stp x29, x30, [sp, #0x30]

//fp指向 sp+0x30
0x100f09e64 <+8>: add x29, sp, #0x30 ; =0x30
//fp-0x8 存放x0
0x100f09e68 <+12>: stur x0, [x29, #-0x8]
//fp-0x10 存放x1
0x100f09e6c <+16>: stur x1, [x29, #-0x10]
//fp-0x8 给到 x8
0x100f09e70 <+20>: ldur x8, [x29, #-0x8]
//sp+0x10 指针给到 x9
0x100f09e74 <+24>: add x9, sp, #0x10 ; =0x10
//x8写入 sp+0x10
0x100f09e78 <+28>: str x8, [sp, #0x10]

//adrp = address page 内存中取数据
0x100f09e7c <+32>: adrp x8, 4
0x100f09e80 <+36>: add x8, x8, #0x418 ; =0x418
//x8所指向的内容去出来
0x100f09e84 <+40>: ldr x8, [x8]
//x8写入栈中,这个时候x9指向地址,这个时候是一个新的x8
0x100f09e88 <+44>: str x8, [x9, #0x8]
0x100f09e8c <+48>: adrp x8, 4
0x100f09e90 <+52>: add x8, x8, #0x3e8 ; =0x3e8
0x100f09e94 <+56>: ldr x1, [x8]
0x100f09e98 <+60>: mov x0, x9
0x100f09e9c <+64>: bl 0x100f0a568 ; symbol stub for: objc_msgSendSuper2

//sp 一直没有改变过,w0~w7 分别存放前8个参数
0x100f09ea0 <+68>: mov w0, #0x1
0x100f09ea4 <+72>: mov w1, #0x2
0x100f09ea8 <+76>: mov w2, #0x3
0x100f09eac <+80>: mov w3, #0x4
0x100f09eb0 <+84>: mov w4, #0x5
0x100f09eb4 <+88>: mov w5, #0x6
0x100f09eb8 <+92>: mov w6, #0x7
0x100f09ebc <+96>: mov w7, #0x8
//x8 指向 sp
-> 0x100f09ec0 <+100>: mov x8, sp
//参数 9 存入 w10
0x100f09ec4 <+104>: mov w10, #0x9
//w10 存入 x8地址中,也就是sp栈底中
0x100f09ec8 <+108>: str w10, [x8]

0x100f09ecc <+112>: bl 0x100f09de4 ; test at ViewController.m:41
0x100f09ed0 <+116>: ldp x29, x30, [sp, #0x30]
0x100f09ed4 <+120>: add sp, sp, #0x40 ; =0x40
0x100f09ed8 <+124>: ret

1ec98237f635a9268224ff5b8718e9fa.png

接着往下直接跳转到test函数中:

TestDemo`test:
//开辟空间48字节
0x100f09de4 <+0>: sub sp, sp, #0x30 ; =0x30

//从viewDidLoad栈中取数据 第9个参数(读写往高地址)
0x100f09de8 <+4>: ldr w8, [sp, #0x30]

//参数入栈,分别占4个字节
0x100f09dec <+8>: str w0, [sp, #0x2c]
0x100f09df0 <+12>: str w1, [sp, #0x28]
0x100f09df4 <+16>: str w2, [sp, #0x24]
0x100f09df8 <+20>: str w3, [sp, #0x20]
0x100f09dfc <+24>: str w4, [sp, #0x1c]
0x100f09e00 <+28>: str w5, [sp, #0x18]
0x100f09e04 <+32>: str w6, [sp, #0x14]
0x100f09e08 <+36>: str w7, [sp, #0x10]
0x100f09e0c <+40>: str w8, [sp, #0xc]

-> 0x100f09e10 <+44>: ldr w8, [sp, #0x2c]
0x100f09e14 <+48>: ldr w9, [sp, #0x28]
0x100f09e18 <+52>: add w8, w8, w9
0x100f09e1c <+56>: ldr w9, [sp, #0x24]
0x100f09e20 <+60>: add w8, w8, w9
0x100f09e24 <+64>: ldr w9, [sp, #0x20]
0x100f09e28 <+68>: add w8, w8, w9
0x100f09e2c <+72>: ldr w9, [sp, #0x1c]
0x100f09e30 <+76>: add w8, w8, w9
0x100f09e34 <+80>: ldr w9, [sp, #0x18]
0x100f09e38 <+84>: add w8, w8, w9
0x100f09e3c <+88>: ldr w9, [sp, #0x14]
0x100f09e40 <+92>: add w8, w8, w9
0x100f09e44 <+96>: ldr w9, [sp, #0x10]
0x100f09e48 <+100>: add w8, w8, w9
0x100f09e4c <+104>: ldr w9, [sp, #0xc]
//最终相加结果给 w0
0x100f09e50 <+108>: add w0, w8, w9
//栈平衡
0x100f09e54 <+112>: add sp, sp, #0x30 ; =0x30
0x100f09e58 <+116>: ret

e802aa437d8009f3dfbd133e4a963c33.png

最终函数返回值放入w0中,如果在release模式下test不会被调用(被优化掉,因为没有意义,有没有对app没有影响。)

自己实现一个简单有参数并且嵌套调用的汇编:


.text
.global _func,_sum

_func:
//sub sp,sp,#0x10
//stp x29,x30,[sp]
stp x29,x30,[sp, #-0x10]!
bl _sum
//ldp x29,x30,[sp]
//add sp,sp,#0x10
ldp x29,x30,[sp],#0x10
ret
_sum:
add x0,x0,x1
ret


篇幅限制 分为2篇

作者:HotPotCat
链接:https://www.jianshu.com/p/69b9c49b0e71




0 个评论

要回复文章请先登录注册