OC逆向02:函数本质(上)
前言
本文的主要目的是理解 函数栈
以及涉及的相关 指令
在讲函数的本质之前,首先需要讲下以下几个概念 栈、SP、FP
常识
栈
- 栈:是一种
具有特殊的访问方式的存储空间
(即后进先出
Last In First Out,LIFO
)
高地址往低地址存数据(
存:高-->低
)栈空间开辟:往低地址开辟(
开辟:高-->低
)
SP和FP寄存器
SP寄存器
:在任意时刻会保存我们栈顶的地址
FP寄存器
(也称为x29
寄存器):属于通用寄存器,但是在某些时刻(例如函数嵌套调用时)可以利用它保存栈底的地址
1 | 注意: |
以下是arm64之前和arm64之后的一个对比
- 在arm64之前,栈顶指针是压栈时一个数据移动一个单元
- 在arm64开始,首先是
从高地址往低地址开辟一段空间(由编译器决定)
,然后再放入数据,所以不存在push、pop操作。这种情况可以通过内存读写指令(ldr/ldp、str/stp
)对其进行操作
函数调用栈
以下是常见的函数调用 开辟(sub)
以及 恢复栈空间(add)
的汇编代码
1 | // 开辟栈空间 |
内存读写指令
注意:
读/写数据
都是往 高地址读/写
写数据:先拉伸栈空间,在拿sp进行写数据,即
先申请空间再写数据
str(store register)指令:
- 将数据从寄存器中读出来,存到
内存
中(即一个寄存器是8字节=64位
)
ldr(load register)指令:
将数据从内存中读出来,存到
寄存器
中此时
ldr
和str
的变种ldp和stp
还可以操作2个
寄存器(即16字节=128位
)
堆栈练习
使用32个字节空间作为这段程序的栈空间,然后利用栈将x0和x1的值进行交换
1 | sub sp, sp, #0x20 ;拉伸栈空间32个字节 |
栈的操作如下图所示:
调式查看栈
- 在 asm文件中写入如下代码:
1 | .text |
- 在 VC 中调用
1 | @interface ViewController () |
- 断点到
sub sp, sp, #0x20
: 进行调试,如下图
由上图可知,sp的初始地址是 0x000000016f491a80
,正常由高地址往低地址 拉伸(开辟)
#0x20个地址后,sp的最新地址为 0x000000016f491a80 - 0x20 = 0x000000016f491a60
- 按住
control Step into
,往下走一步查看结果
- 此时
x0,x1
的默认地址如下图
stp x0, x1, [sp, #0x10]
:将sp
的偏移#0x10
写入x0,x1
,结果如下图:
ldp x1, x0, [sp, #0x10]
:将sp
的内存地址加上#0x10
,读取x0,x1
的数据并交换结果如下;
add sp, sp, #0x20
:走到栈平衡,最后将sp
指针地址加上#0x20
,即sp恢复了
,此时的a和b仍然在内存中,等待着下一轮栈拉伸后数据的写入覆盖。如果此时读取,读取到的是垃圾数据
- 如果上面默认的地址不方便看,你可以重写
register write x0 0xa
和register write x1 0xb
的来断点调试,如下图:
疑问:栈空间不断开辟,死循环,会不会崩溃?
在这里我们将会处理上篇 OC逆向01:初始汇编 文章中文末遗留的问题
下面我们通过一个汇编代码来演示
1 | <!--asm.s--> |
运行结果发现:死循环会崩溃,会导致 堆栈溢出
bl、ret指令
bl标号
- b:转到标号处执行指令(即b)
- 将
下一条指令的地址
放入通用寄存器lr(x30)
中
等到B函数ret时,通过lr获取回家的路(注:lr就是保存回家的路
)
ret
默认使用
lr(x30)寄存器
的值,通过底层指令提示CPU此处作为下条指令地址arm64平台的特色指令,它面向硬件做了优化处理的
练习
下面通过汇编代码来演示 bl、ret指令
- asm文件下代码如下
1 | .text |
- vc中执行如下代码
1 | // 函数的声明 |
- 断点
mov x0,#0xaaaa
运行:发现lr
保存了下一条指令的地址是1f78
- 往下执行
bl _B
:此时x0
的地址为0xaaaa
- 继续执行
bl _B
,跳转到B
,此时的lr
会变成A
中bl
的下一条指令的地址1f20
- 执行完B中的
mov x0, #0xbbbb
,x0 变成bbbb
- 执行
B
中的ret
,lr
会回到A
中1f20
- 继续执行
A
中的ret
,会再次回到1f20
走到这里,发现
死循环了
,主要是因为lr
一直是1f20
,ret
只会看lr
。- 其中
pc
是指接下来要执行的内存地址
,ret
是指让CPU将lr
作为接下来执行的地址(相当于将lr赋值给pc)
- 其中
疑问1:此时B回到A没问题,那么A回到viewDidLoad怎么回事呢?
需要在A的bl之前
保护lr寄存器
疑问2:是否可以保存到其他寄存器上?
答案是
不可以
,原因是不安全,因为你不确定这个寄存器会在什么时候被别人使用正确做法:
lr保存到栈区
系统中函数嵌套是如何返回的?
下面我们来看一下系统是如何操作的,例如:d -> c -> viewDidLoad
1 | void d(){ |
- 查看汇编,断点在
c()
函数
stp x29,x30,[sp,#-0x10]
:边开辟栈,边写入,其中x29就是fp(栈底)
,x30是lr
,!
的含义是,将[]中计算的结果给sp,即sp-=0x10
ldp x29, x30, [sp], #0x10
:读取sp执行地址的数据,放入x29、x30;[sp], #0x10
表示sp=sp+0x10
,以此保证栈平衡
【结论】:当有函数嵌套调用时,将上一个函数的地址通过 lr(即x30)
放在栈中保存,保证可以找到回家的路,如下图所示
自定义汇编代码完善:_A中保存回家的路
所以根据系统的函数嵌套操作,最终在_A中增加了如下汇编代码,用于保存回家的路
1 | <!--导致死循环的汇编代码--> |
修改_A、_B:改成简写形式
- 其中lr是x30的一个别名
1 | _A: |
【断点调试】:
str x30, [sp, #-0x10]
:查看此时sp寄存器
的地址sp = 0x000000016b411a80
,lr
的地址lr = 0x00000001003d9f90
- 执行到
mov x0, #0xaaaa
:sp变了,lr未变
- 执行
mov x0, #0xaaaa
,x0 为aaaa
- 执行A中的
bl _B
:跳转到B,此时lr = 0x0000000100929f40
,x0
变成bbbb
,sp = 0x000000016f4d9a70
- 执行B的ret:从B回到A,此时lr还是
lr = 0x0000000100929f40
- 执行A中的
ldr x30, [sp], #0x10
,发现此时 lr变了为lr = 0x0000000100929f90
,sp也变了为sp = 0x000000016f4d9a80
。从这里可以看出,A找到了回家的路
x30寄存器
x30
寄存器存放
的是函数的返回地址
,当ret指令执行时刻,会寻找x30寄存器保存的地址值注意:
在函数嵌套调用时,需要将x30入栈
lr
是x30
的别名sp
栈里面的操作必须是16字节对齐
,崩溃是在栈的操作时挂的
总结
栈
:是一种具有特殊的访问方式的存储空间(先进后出
,Last In First Out,FIFO
)- ARM64里面对
栈的操作
是16字节对齐
的
- ARM64里面对
SP和FP寄存器
- SP寄存器在任意时刻会
保存栈顶的地址
- FP寄存器也称为
x29寄存器
,属于通用寄存器
,但是在某些时刻利用它保存栈底的地址
- SP寄存器在任意时刻会
栈的读写指令
读:
ldr
(load register)指令LDR、LDP
写:
str
(store register)指令STR、STP
汇编练习
指令
sub sp,sp,$0x10
:拉伸栈空间16
字节stp x0,x1,[sp]
:sp所在位置存放x0、x1
bl
指令跳转指令:
bl
标号,表示程序执行到标号处。将下一条指令的地址保存到lr寄存器B
代表着跳转
L
表示lr
(x30)寄存器
ret
指令- 类似函数的
return
- 让CPU执行lr寄存器所指向的指令
- 类似函数的
避免嵌套函数无法回去:需要保护bl(即
lr寄存器,存放回家的路
),保存在当前函数自己的栈空间
- Post title:OC逆向02:函数本质(上)
- Post author:张建
- Create time:2022-04-20 15:51:12
- Post link:https://redefine.ohevan.com/2022/04/20/OC逆向/OC逆向02:函数本质(上)/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.