02 - 汇编看函数
栈区(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函数的函数栈
中,一般也是栈顶位置开始。