bl和ret指令
bl标号
- 将下一条指令的地址放入lr(x30)寄存器
- 转到标号处执行指令
注意:当我们遇到bl指令的时候,是跳转指令,跳转之后我们是怎么回来的?怎么回到bl下面的指令?遇到bl后,CPU会将下一个指令的内存地址存到lr寄存器当中。
验证:
-
随便写一个函数,在main中调用,run
-
lldb输入跳转指令进入函数内部,查看lr寄存器是否是上图bl下一条指令的内存地址
ret
- 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!
ARM64平台的特色指令,它面向硬件做了优化处理apple文档写的
练习题:
一
.text
.global _Test1,_Test2
_Test1:
mov x0,#0xaaaa
mov x0,#0xcccc
bl _Test2
mov x0,#0xdddd
ret
_Test2:
mov x0,#0xbbbb
mov x0,#0xffff
ret
二
.text
.global _Test1,_Test2
_Test1:
mov x0,#0xaaaa
str x30,[sp,#-0x10]!
bl _Test2
mov x0,#0xcccc
ldr x30,[sp],#0x10
ret
_Test2:
mov x0,#0xbbbb
ret
答案
- 一里面的习题会产生死循环,根据bl跳转之前会将下一条指令的地址付给lr寄存器,这样函数在调用完成 ret以后会跳到lr指向的指令。 出现了死循环
- 二里面 str 首先开辟一个栈空间,来将ret跳转的指令地址存到栈里面,当bl执行完成以后,在从栈空间读出来存到lr寄存器当中,来跳过死循环。
- x30就是lr寄存器
我们平时写的函数转换成汇编会变成什么?
int sum(int a, int b){
return a + b;
}
int main (){
sum(10,20);
return 0;
}
汇编中
-> 0x1049ba8ac <+0>: sub sp, sp, #0x10 ; =0x10
0x1049ba8b0 <+4>: str w0, [sp, #0xc]
0x1049ba8b4 <+8>: str w1, [sp, #0x8]
0x1049ba8b8 <+12>: ldr w0, [sp, #0xc]
0x1049ba8bc <+16>: ldr w1, [sp, #0x8]
0x1049ba8c0 <+20>: add w0, w0, w1
0x1049ba8c4 <+24>: add sp, sp, #0x10 ; =0x10
0x1049ba8c8 <+28>: ret
解读
- sub sp, sp, #0x10 首先开辟栈空间向下移动16个字节
- 后面的4句话将w0,w1的值存到栈空间,又从栈空间读到w0,w1,废话
- add w0, w0, w1 计算两个值的和,最后返回的值是w0
- add sp, sp, #0x10 因为函数使用完成,需要将该函数占用的栈空间清除,也就是栈平衡
练习题,自己写一个sum函数
int sumA(int a, int b);
int main(int argc, char * argv[]) {
int b = sumA(10, 20);
printf("%d",b);
return 0;
}
- 返回x0
.text
.global _sumA
_sumA:
add x0,x0,x1
ret
- 返回x1
.text
.global _sumA
_sumA:
add x1,x0,x1
ret
验证
返回x0寄存器是正确的值,返回x1寄存器的值是10
那么:
通常情况下函数的返回值通常都放在x0 里面!!ARM64下,函数的参数是存放在X0到X7(W0到W7)这8个寄存器里面的.如果超过8个参数,就会入栈.
代码验证:
int map(int a, int b,int c, int d, int e,int f,int g, int h, int i,int j){
return a+b+c+d+e+f+g+h+j+i+j;
}
int main(int argc, char * argv[]) {
map(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
return 0;
}
汇编代码
0x1005c289c <+0>: sub sp, sp, #0x30 ; =0x30
0x1005c28a0 <+4>: stp x29, x30, [sp, #0x20]
0x1005c28a4 <+8>: add x29, sp, #0x20 ; =0x20
0x1005c28a8 <+12>: orr w8, wzr, #0x1
0x1005c28ac <+16>: orr w9, wzr, #0x2
0x1005c28b0 <+20>: orr w2, wzr, #0x3
0x1005c28b4 <+24>: orr w3, wzr, #0x4
0x1005c28b8 <+28>: mov w4, #0x5
0x1005c28bc <+32>: orr w5, wzr, #0x6
0x1005c28c0 <+36>: orr w6, wzr, #0x7
0x1005c28c4 <+40>: orr w7, wzr, #0x8
0x1005c28c8 <+44>: mov w10, #0x9
0x1005c28cc <+48>: mov w11, #0xa
0x1005c28d0 <+52>: stur wzr, [x29, #-0x4]
0x1005c28d4 <+56>: stur w0, [x29, #-0x8]
0x1005c28d8 <+60>: str x1, [sp, #0x10]
0x1005c28dc <+64>: mov x0, x8
0x1005c28e0 <+68>: mov x1, x9
0x1005c28e4 <+72>: str w10, [sp]
0x1005c28e8 <+76>: str w11, [sp, #0x4]
0x1005c28ec <+80>: bl 0x1005c280c ; map at main.m:21 bl之前是当前的准备工作,开辟栈空间,设置局部变量
0x1005c28f0 <+84>: mov w8, #0x0
0x1005c28f4 <+88>: str w0, [sp, #0xc]
0x1005c28f8 <+92>: mov x0, x8
0x1005c28fc <+96>: ldp x29, x30, [sp, #0x20]
0x1005c2900 <+100>: add sp, sp, #0x30 ; =0x30
0x1005c2904 <+104>: ret
代码分析:
- sub sp, sp, #0x30 开辟空间,sp向下移动48个字节
- stp x29, x30, [sp, #0x20] 向sp向上偏移32个字节,开始存储x29,x30. sp + 0x20 = x29 sp + 0x20 + 1 = x30
- add x29, sp, #0x20 x29 = sp + 0x20
- orr w8, wzr, #0x1 w8 = wzr | #0x1
- orr w9, wzr, #0x2
- orr w2, wzr, #0x3
- orr w3, wzr, #0x4
- mov w4, #0x5 w4 = #0x5
- orr w5, wzr, #0x6
- orr w6, wzr, #0x7
- orr w7, wzr, #0x8
- mov w10, #0x9
- mov w11, #0xa 这以上不用解释可以类推 这几个赋值就是没有x0 x1,股么的x0,x1有用
- stur wzr, [x29, #-0x4] 0x00000000 写入到内存 x29 - 0x4
- stur w0, [x29, #-0x8] 正好8个字节放入到栈内存
- str x1, [sp, #0x10] 将x1放入到栈内存
- mov x0, x8 x0 = x8
- mov x1, x9 x1 = x9
- str w10, [sp] 将w10 放入到占内存中 sp
- str w11, [sp, #0x4] 将w11 放入栈内存中 sp,+#0x4
然后跳转到map函数里面
0x1005c280c <+0>: sub sp, sp, #0x30 ; =0x30
0x1005c2810 <+4>: ldr w8, [sp, #0x34]
0x1005c2814 <+8>: ldr w9, [sp, #0x30]
0x1005c2818 <+12>: str w0, [sp, #0x2c]
0x1005c281c <+16>: str w1, [sp, #0x28]
0x1005c2820 <+20>: str w2, [sp, #0x24]
0x1005c2824 <+24>: str w3, [sp, #0x20]
0x1005c2828 <+28>: str w4, [sp, #0x1c]
0x1005c282c <+32>: str w5, [sp, #0x18]
0x1005c2830 <+36>: str w6, [sp, #0x14]
0x1005c2834 <+40>: str w7, [sp, #0x10]
0x1005c2838 <+44>: str w9, [sp, #0xc]
0x1005c283c <+48>: str w8, [sp, #0x8]
0x1005c2840 <+52>: ldr w8, [sp, #0x2c]
0x1005c2844 <+56>: ldr w9, [sp, #0x28]
0x1005c2848 <+60>: add w8, w8, w9
0x1005c284c <+64>: ldr w9, [sp, #0x24]
0x1005c2850 <+68>: add w8, w8, w9
0x1005c2854 <+72>: ldr w9, [sp, #0x20]
0x1005c2858 <+76>: add w8, w8, w9
0x1005c285c <+80>: ldr w9, [sp, #0x1c]
0x1005c2860 <+84>: add w8, w8, w9
0x1005c2864 <+88>: ldr w9, [sp, #0x18]
0x1005c2868 <+92>: add w8, w8, w9
0x1005c286c <+96>: ldr w9, [sp, #0x14]
0x1005c2870 <+100>: add w8, w8, w9
0x1005c2874 <+104>: ldr w9, [sp, #0x10]
0x1005c2878 <+108>: add w8, w8, w9
0x1005c287c <+112>: ldr w9, [sp, #0x8]
0x1005c2880 <+116>: add w8, w8, w9
0x1005c2884 <+120>: ldr w9, [sp, #0xc]
0x1005c2888 <+124>: add w8, w8, w9
0x1005c288c <+128>: ldr w9, [sp, #0x8]
0x1005c2890 <+132>: add w0, w8, w9
0x1005c2894 <+136>: add sp, sp, #0x30 ; =0x30
0x1005c2898 <+140>: ret
这代码我就不分析,可以根据上面的分析进行类推,
- 稍微解释下,参数分析,一般函数参数,如果不超过8个直接可以用寄存器搞定,如果超过8个需要存到栈空间
- 到函数里面,汇编分析,如果如果函数参数超过8个,多余的会存到栈内存中,再将寄存器中的参数按照从低到高的顺序存储到内存当中跟上面多余的参数的内存是连续的,那么首先将栈内存当中按照从低到高的顺序去读取两个值,然后add相加给x8,最后得到的值在赋值给x0 返回
练习:
int sum(int a, int b){
int c = 6;
int d = 0;
return a + b + c;
}
int main(int argc, char * argv[]) {
sum(10,20);
return 0;
}
汇编分配的栈空间
sub sp,sp,#0x10
int sum(int a, int b){
int c = 6;
int d = 0;
int f = 1;
return a + b + c;
}
int main(int argc, char * argv[]) {
sum(10,20);
return 0;
}
汇编分配的栈空间
sub sp,sp,#0x20
结论
这个应该可以理解哦 ,因为ARM64是16位每次开辟空间都是16为基准,只能是16的倍数, int = 4字节,4* 4正好 4*5 = 20 需要再开辟一个16位的空间 也就是 sp + 0x20
练习
int sum(int a, int b){
int c = 6;
printf("%d",c);
return a + b + c;
}
int main(int argc, char * argv[]) {
sum(10,20);
return 0;
}
汇编分配的栈空间
sub sp,sp,#0x30
为什么会多呐? 因为嵌套函数必须保护寄存器x30,lr 回家的路
练习
int sum(int a, int b){
return a + b ;
}
int funcA(int a, int b){
int d = sum(a, b);
int e = sum(a, b);
return e;
}
0x102e62880 <+0>: sub sp, sp, #0x20 ; =0x20
0x102e62884 <+4>: stp x29, x30, [sp, #0x10]
0x102e62888 <+8>: add x29, sp, #0x10 ; =0x10
0x102e6288c <+12>: stur w0, [x29, #-0x4]
0x102e62890 <+16>: str w1, [sp, #0x8]
0x102e62894 <+20>: ldur w0, [x29, #-0x4]
0x102e62898 <+24>: ldr w1, [sp, #0x8]
0x102e6289c <+28>: bl 0x102e62860 ; sum at main.m:14
0x102e628a0 <+32>: str w0, [sp, #0x4]
0x102e628a4 <+36>: ldur w0, [x29, #-0x4]
0x102e628a8 <+40>: ldr w1, [sp, #0x8]
0x102e628ac <+44>: bl 0x102e62860 ; sum at main.m:14
0x102e628b0 <+48>: str w0, [sp]
0x102e628b4 <+52>: ldr w0, [sp]
0x102e628b8 <+56>: ldp x29, x30, [sp, #0x10]
0x102e628bc <+60>: add sp, sp, #0x20 ; =0x20
0x102e628c0 <+64>: ret
分析
- 开辟栈空间
- 保护回家的路
- 保存栈底
- 保存w0
- 保存w1
- 读取 w0
- 读取w1
- sum跳转
- 保存w0的值
- 读取a值到x0
- 读取b值到x1
- sum跳转
- 保存w0的的值搭配 栈
- 读取栈内存的值到w0
- 恢复x30的值 准备回家
- 平衡栈空间
- 回家
总结
bl指令: 跳转 将下一条执行的指令放入lr(x30)寄存器
ret指令:返回lr寄存器所保存的地址,执行代码
pc 寄存器指向马上要执行的代码地址
sp 指向了栈
函数调用会开辟一个栈空间,
- 函数的局部变量、参数、寄存器的保护
- 这个栈空间属于这个函数自己
函数中的参数放在哪里:
ARM64每一个寄存器都是64位的
- x0 ~ x7(1.个数有关系;2.数据类型也有关系)
- 放入栈
- 或者如果放不下放入浮点、向量寄存器
代码只有一份,内存不只一份
函数嵌套调用:
| - A(开辟)-->B(开辟)--->A(开辟)
| - A<-->A 死的溢出 递归
扩展 小知识
[self viewDidLoad];
OC方法的调用 本质上是不是调用msgSend()
msgSend(self,@selector(viewDidLoad));
x0 x1 寄存器!!