前言
我们使用Hopper Disassembler
等反编译工具查看Mach-O可执行文件
时,看到的都是汇编代码,所以只有我们学会了汇编后,才能更好的去调试,去分析App的逻辑
学会了汇编之后,甚至可以直接修改汇编代码,导出可执行文件,再经过签名安装使用了,都不用通过之前的tweak方式hook了,当然,这需要对汇编掌握的及其深入,并且经过调试分析清楚了App整体逻辑之后了。
一、汇编
- CPU的内部有寄存器、运算器和控制器,它们之间由总线连接。其中运算器负责信息处理,由CPU硬编码指令完成;控制器负责协调控制计算机的其他器件进行工作;寄存器进行数据的临时存储,程序员只需关心寄存器的数据存取即可改变运行CPU运行结果。
- 在iOS领域,根据CPU架构不同,汇编主要分为两种:
模拟器上的x86汇编
、真机上的arm64汇编
,这里我们重点介绍真机上的arm64汇编
- 在iOS领域,根据CPU架构不同,汇编主要分为两种:
- 想要学好
arm64汇编
,主要学三个方面:寄存器、指令、堆栈
- 想要学好
二、寄存器
寄存器就是CPU内部临时存储数据的地方,分为有很多种,如下图所示,我们下面依次讲解:
image-
我们把
x0~x28
称为通用寄存器,通用寄存器通常用来存放一般性的数据,通用寄存器分为64位和32位-
64位(8字节)
的通用寄存器有:x0 ~ x28
,29个寄存器,8个字节代表这个寄存器最多能放8个字节的数据 -
32位(4字节)
的通用寄存器有:w0 ~ w28
,这29个4字节的寄存器其实是8字节寄存器x0 ~ x28的低32位 -
通用寄存器的x0与w0之间的关系,如下图所示,其他通用寄存器也是如此
image -
通过LLDB命令,我们也可以验证出他们的关系,如下图所示:
image image -
x0~x7寄存器,一般会存储函数的参数,大于8个的会通过堆栈传参
-
x0:一般会存储函数的返回值
-
-
- pc寄存器,也叫做程序计数器,记录着当前CPU执行的是哪一条指令,存储着当前CPU正在执行指令的地址,类似于8086汇编的ip寄存器
-
堆栈寄存器,有两个,分别是:
栈顶指针寄存器sp
和栈低指针寄存器fp
,栈低指针寄存器fp
也叫做x29寄存器
,堆栈寄存器是用来控制函数分配栈空间的
-
堆栈寄存器,有两个,分别是:
-
链接寄存器lr,也叫做
x30寄存器
,存储着函数的返回地址
-
链接寄存器lr,也叫做
-
-
程序状态寄存器,有两种,分别是
cpsr(current Program Status Register)
和spsr(Saved Program Status Register)
,cpsr是当前程序的运行状态,spsr是在异常状态下使用的,CPSR寄存器和其他寄存器不一样(其他寄存器是用来存放数据的,都是整个寄存器具有一个含义),是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息,如下图所示:
- CPSR的低8位(包括I、F、T和M[4~0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位! - N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行,意义重大!
-
程序状态寄存器,有两种,分别是
-
xzr零寄存器,表示
zero register
,里面存储的值都是0,xzr
代表8字节,wzr
代表4字节
-
xzr零寄存器,表示
-
总结:
-
x0 ~ x28
是通用寄存器
,有8个字节的空间,其中较低的4个字节的空间,是w0~w28寄存器
,x
代表8个字节,w
代表4个字节 -
x29
就是fp寄存器
,也就是栈底寄存器
,代表着函数的栈底 -
x30
就是lr寄存器
,也就是链接寄存器
,存储着函数的返回地址,使用bl指令跳转时,会把bl的下面的一条指令的地址,存放到lr寄存器
中,如果bl跳转之后,遇到了ret指令
,ret指令
会把lr寄存器
的值给pc寄存器
,这样CPU就会执行lr寄存器
存放的指令了,就相当于函数返回了
-
-
总结:
三、指令
-
mov指令,传送指令,格式是
mov {条件}{S}目标寄存器 ,源操作数
,mov指令可以完成从另一个寄存器或者是将一个立即数,加载到目的寄存器中,其中S选项决定执行的操作是否影响CPSR中条件标志位的值,当没有S时指令不更新CPSR中的条件标志位的值。-
mov指令实例如下:
-
mov x1,x0
,意思是将寄存器x0
的值传送到寄存器x1
中 -
mov pc,x14
,意思是将寄存器x14
的值传送到寄存器pc
中,pc寄存器通常用来存放CPU正在执行的指令的地址,所以常用于子程序返回 -
mov x1,x0,#0x3
,意思是将寄存器x0
的值加上0x3
,然后传送到寄存器x1
中
-
-
-
ret指令,返回指令,作用是函数返回,本质上是将
lr(x30)寄存器
的值赋值给pc寄存器
,pc寄存器
存储CPU当前执行的指令的地址,lr寄存器
存储着函数的返回地址,将lr寄存器的值,给了pc寄存器,相当于实现了函数返回
-
ret指令,返回指令,作用是函数返回,本质上是将
-
add指令,加法指令,格式是
add {条件}{S}目标寄存器 ,操作数1,操作数2
,add指令用于将 两个操作数相加,并将结果存放到目的寄存器中,操作数1必须是一个寄存器,操作数2可以是寄存器,也可以是立即数-
add指令实例如下:
-
add x0,x1,x3
,意思是将寄存器x1
的值加上寄存器x3
的值,赋值给寄存器x0
-
add x0,x1,#0x77
,意思是将寄存器x1
的值加上0x77
,然后赋值给寄存器x0
-
add x0,x1,x3,LSL0x1
,意思是将寄存器x1
的值加上寄存器x3
左移0x1
,然后赋值给寄存器x0
,也就是:x0 = x1 + (x3 << 1)
-
-
-
sub指令,减法指令,格式是
sub {条件}{S}目标寄存器 ,操作数1,操作数2
,sub指令用于把操作数1减去操作数2,并将结果存放到目的寄存器中,操作数1必须是一个寄存器,操作数2可以是寄存器,也可以是立即数-
sub指令实例如下:
-
sub x0,x1,x3
,意思是将寄存器x1
的值减去寄存器x3
的值,赋值给寄存器x0
-
sub x0,x1,#0x88
,意思是将寄存器x1
的值减去0x88
,然后赋值给寄存器x0
-
sub x0,x1,x3,LSL0x1
,意思是将寄存器x1
的值减去寄存器x3
左移0x1
,然后赋值给寄存器x0
,也就是:x0 = x1 - (x3 << 1)
-
-
-
cmp指令,比较指令,格式是
cmp {条件}{S}目标寄存器 ,操作数1,操作数2
,cmp指令用于把一个寄存器的内容和另一个寄存器的内容或者立即数,进行比较,同时更新CPSR中的条件标志位,cmp指令进行的是一次减法运算,不会存储结果,只会更改条件标志位。-
cmp指令实例如下:
-
cmp x0,x1
,意思是将寄存器x0
的值与寄存器x1
的值相减,并根据结果设置CPSR的标志位 -
cmp x0,#0x88
,意思是将寄存器x0
的值与立即数0x88
相减,并 根据结果设置CPSR的标志位
-
-
-
b指令,不带返回的跳转指令,常与
cmp指令
配合使用,格式是b {条件} 目的地址
,一旦遇到b指令,ARM处理器将立即跳转到目的地址,从那里继续执行,注意存储在跳转指令中的是相对于当前pc值的一个偏移量,而不是一个绝对地址,偏移量是由汇编器来计算的。
-
b指令,不带返回的跳转指令,常与
-
bl 指令,带返回的跳转指令,常与
cmp指令
配合使用,格式是bl {条件} 目的地址
,bl 指令的格式为:bl{条件} 目标地址,bl是另一个跳转指令,在跳转之前,会把下一条指令的地址,存储到lr寄存器
中,等到子函数执行完毕,执行ret指令
时,ret指令
会把lr寄存器
的值给了pc寄存器
,pc寄存器
存储的是CPU当前执行的指令的地址,这样就实现了子函数返回。例如:cmp与b配合使用,如下所示
-
0x100432624 <+88>: cmp x1, #0x1 ; =0x1
0x100432628 <+92>: b.le 0x100432630 ;
<1>. cmp: 将寄存器 x1 的值与立即数 0x1 相减,并根据结果设置 CPSR 的标志位
<2>. b.le 0x100432630:表示如果x1 <= 0x1那么就执行0x100432630
-
ldr 指令,从内存加载数据到寄存器,格式是:
ldr{条件} 目的寄存器,<存储器地址>
,ldr指令用于从存储器中将读取相应大小的字节数,传送到目的寄存器中-
ldr指令实例如下:
-
ldr x0,[x1]
,意思是从x1寄存器
存储的地址取出8个字节,然后存到x0寄存器
中 -
ldr w0,[x1]
,意思是从x1寄存器
存储的地址取出4个字节,然后存到w0寄存器
中 -
ldr x0,[x1,#0x888]
,意思是从x1寄存器
存储的值加上0x888
得到地址处,取出8个字节,然后存到x0寄存器
中 -
ldr x0,[x1,#0x888]!
,意思是从x1寄存器
存储的值加上0x888
得到地址处,取出8个字节,然后存到x0寄存器
中,并且将新地址x1+0x888
,存入到x1寄存器
中
-
-
-
str 指令,将寄存器的数据存储到内存中,格式是:
str{条件} 源寄存器,<存储器地址>
-
str指令实例如下:
-
str x0,[x1]
,意思是从x0寄存器
存储的地址取出8个字节,然后存到x1寄存器
值的内存中 -
ldr w0,[x1,#0x888]
,意思是从w0寄存器
存储的地址取出4个字节,然后存到x1寄存器
的值加上0x888
的地址中
-
-
三、叶子函数的堆栈
- 函数分为:叶子函数和非叶子函数,这两种函数的堆栈分配情况略有不同,叶子函数就是像叶子一样没有分支了,函数内部不会调用其他函数了;非叶子函数,就是函数内部会调用其他函数。
- 我们先来看一个叶子函数,C代码和汇编代码如下:(w代表4个字节,x代表8个字节,;代表注释)
C代码:
void test(){
int a = 2;
int b = 3;
}
test的函数汇编代码如下:
sub sp, sp, #16 ; =16 ;sp = sp - 16,开辟16个字节的栈空间
orr w8, wzr, #0x2 ;设置w8寄存器为2
str w8, [sp, #12] ;在sp地址偏移12个字节的地方开始存储4个字节的数据,也就是w8寄存器的值,也就是存储了2
orr w8, wzr, #0x3 ;设置w8寄存器为3
str w8, [sp, #8] ;在sp指针+8的地址开始,存储4个字节的数据,也就是将w8的值存储到sp偏移8字节的位置
add sp, sp, #16 ; =16 ;sp = sp + 16,回收16个字节的空间,保持堆栈平衡
ret
-
叶子函数的堆栈开辟,可以用下面一幅图表示:
image
-
四、非叶子函数的堆栈
- 我们再来看一个非叶子函数,C代码和汇编代码如下:(w代表4个字节,x代表8个字节,;代表注释)
C代码如下,我们在good函数里,调用了test函数
void test(){
int a = 2;
int b = 3;
}
void good(){
int a = 7;
int b = 8;
test();
}
good函数的汇编代码如下:
sub sp, sp, #32 ; =32 ;sp = sp-32,开辟函数栈空间,也就是将sp指针往低字节移动32字节
stp x29, x30, [sp, #16] ; 16-byte Folded Spill ; 在sp+16的地方,往高字节的方向,依次存放x29、x30的数据,x29的数据占8个字节,x30的数据也占8个字节
add x29, sp, #16 ; =16 ;x29 = sp + 16,x29也就是fp栈底指针
orr w8, wzr, #0x7 ;将w8置为7
stur w8, [x29, #-4] ;将w8的值存放到x29-4的位置
orr w8, wzr, #0x8 ;将w8置为8
str w8, [sp, #8] ;将w8的值存放到sp+8的位置
bl _test ;调用test函数,执行test函数的汇编代码
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload ;调用完test,会返回到这里,继续执行,从sp+16的地址,加载8个字节到x29,然后再加载8个字节的数据到x30
add sp, sp, #32 ; =32 ;sp = sp + 32,回收函数的栈空间,将sp指针复原
ret ;good函数调用完毕,将x30的值给了pc寄存器,CPU会执行pc寄存器中存放的地址所对应的指令的
-
我们可以用一副图,清晰的表示good函数的调用过程,的如下所示:
image
-
网友评论