iOS汇编
iOS汇编语音有很多钟。常见的有8086汇编、arm汇编、x86汇编等等。
arm汇编
iOS的架构从最初的armv6发展到后来的armv7和armv7s,最后发展到现在的arm64,不管是armv6还是后来的armv7,以及arm64都是arm处理器的指令集。armv7和armv7s是真机32位处理器使用的架构,而arm64是真机64位处理器使用的架构。 iPhone 5C是最后一款arm32位版本的iPhone,在iPhone5s之后,所有的iPhone设备都采用arm64架构。arm64汇编在真机上使用,如下:
TestFont`-[ViewController test]:
0x10286e574 <+0>: sub sp, sp, #0x20 ; =0x20
0x10286e578 <+4>: mov w8, #0x14
0x10286e57c <+8>: mov w9, #0xa
0x10286e580 <+12>: str x0, [sp, #0x18]
0x10286e584 <+16>: str x1, [sp, #0x10]
-> 0x10286e588 <+20>: str w9, [sp, #0xc]
0x10286e58c <+24>: str w8, [sp, #0x8]
0x10286e590 <+28>: add sp, sp, #0x20 ; =0x20
0x10286e594 <+32>: ret
x86汇编
x86汇编是模拟器使用的汇编语言,它的指令和arm64汇编的语法不同,如下
TestFont`-[ViewController test]:
0x10b089520 <+0>: pushq %rbp
0x10b089521 <+1>: movq %rsp, %rbp
0x10b089524 <+4>: movq %rdi, -0x8(%rbp)
0x10b089528 <+8>: movq %rsi, -0x10(%rbp)
-> 0x10b08952c <+12>: movl $0xa, -0x14(%rbp)
0x10b089533 <+19>: movl $0x14, -0x18(%rbp)
0x10b08953a <+26>: popq %rbp
0x10b08953b <+27>: retq
为什么要学习arm64汇编?
代码调试
在平常开发中,在调试程序的时候,如果程序crash,通常会定位到具体的崩溃代码。但是有时候也会遇到一些比较诡异的crash,比如说崩溃在了系统库中,这个时候定位到具体的crash原因会非常困难。如果利用汇编调试技巧来进行调试,可能会让我们事半功倍。
逆向调试
在逆向别人App过程中,我们可以通过LLDB对内存地址进行断点操作,但是当执行到断点时,LLDB展现给我们的是汇编代码,而不是OC代码,所以想要逆向并且动态调试别人的App,就需要学习汇编的知识。
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这有个iOS交流群:642363427,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术,iOS开发者一起交流学习成长!
arm64汇编入门
想要学习arm64汇编,需要从以下三个方面入手,寄存器、指令和堆栈。
寄存器
arm64中有34个寄存器,如下
通用寄存器
- 64 bit的通用寄存器有29个,分别是x0 ~ x28
- 32 bit的也有29个,分别是w0 ~ w28(属于x0 ~ x28的低32位)
- 其中x0 ~ x7通常拿来存放函数的参数,如果参数更多,则采用堆栈来进行传递
- x0中通常存放函数的返回值
也会有人将x0 ~ x30叫做通用寄存器,但是在实际使用中x29和x30并没有对应的低32位的寄存器w29、w30,而且x29和x30寄存器有着特殊的用途,所以在此我只讲x0 ~ x28记为通用寄存器
程序计数器
pc (Program Counter)寄存器,它记录着当前CPU正在执行的指令的地址,通过register read pc查看寄存器中存储的值
(lldb) register read pc
pc = 0x000000010286e588 TestFont`-[ViewController test] + 20 at ViewController.m:28
(lldb)
堆栈指针
- sp (Stack Pointer)
- fp (Frame Pointer),也就是之前所说的x29
链接寄存器
lr (Link Register)寄存器,也就是之前所说的x30寄存器,它存储着函数的返回地址
程序状态寄存器
arm体系中包含一个当前程序状态寄存器cpsr (Current Program Status Register)和五个备份的程序状态寄存器spsr (Saved Program Status Registe),备份的程序状态寄存器用来进行异常处理。
- 程序状态寄存器的每一位都有特定的用途,此处只介绍几种常用的标志位
- 其中N、Z、C、V均为条件码标志位,他们的内容可被算数或者逻辑运算的结果所改变,并且可以决定某条指令是否被执行。条件码标志各位的具体含义如下
指令
mov指令
mov指令可以将另一个寄存器、被移位的寄存器或者将一个立即数加载到目的寄存器
mov指令在arm64汇编中的实际使用
- 在xcode中新建test.s文件,在test.s文件中添加以下代码
; 此处.text表示此代码放在text段中
.text
; .global表示将后面跟随的方法给暴露出去,不然外部无法调用,方法名以_开头
.global _test
; 此处为_test方法
_test:
; mov指令,将立即数4加载到x0寄存器中
mov x0, #0x4
mov x1, x0
; 汇编指令中,ret表示函数的终止
ret
- 在xcode中新建test.h头文件,将test.s中的_test方法暴露出来
#ifndef test_h
#define test_h
void test(void);
#endif /* test_h */
- 在viewDidLoad中调用test()函数,然后在LLDB中使用register read x0 读取寄存器中存放的值
(lldb) register read x0
x0 = 0x000000010320c980
(lldb) si
(lldb) register read x0
x0 = 0x0000000000000004
(lldb) register read x1
x1 = 0x00000001e60f3bc7 "viewDidLoad"
(lldb) si
(lldb) register read x1
x1 = 0x0000000000000004
通过对汇编指令增加断点,一步一步调试可以看出,在执行完mov指令后,x0和x1寄存器的值都被修改了
ret
ret指令表示函数的返回,而且它还有一个非常重要的作用,就是将lr(x30)寄存器的值赋值给pc寄存器
- 在viewDidLoad中调用test()函数,在test()函数上打上断点,执行程序如下
- 使用register read 查看lr和pc寄存器的值
(lldb) register read lr
lr = 0x00000001021965a4 TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23
(lldb) register read pc
pc = 0x00000001021965a4 TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23
(lldb)
此时,lr寄存器和pc寄存器的值都是test()函数起始地址
- 使用si指令跳转到test()函数中
- 再次查看lr和pc寄存器的值,发现lr的值变成了test()函数的下一条指令的地址,也就是test()函数执行完成之后,主程序需要执行的下一条指令。pc寄存器保存了当前即将执行的指令的地址,如下
(lldb) register read lr
lr = 0x00000001021965a8 TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) register read pc
pc = 0x0000000102196abc TestFont`test
- 执行完test()函数,发现程序跳转到了lr寄存器所保存的指令地址,也就是0x00000001021965a8,此时再次查看lr和pc寄存器的值,发现pc寄存器存放的地址已经变成了lr寄存器存放的地址
(lldb) register read lr
lr = 0x00000001021965a8 TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) register read pc
pc = 0x00000001021965a8 TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb)
add指令
add指令是将两个操作数相加,并将结果存放到目标寄存器中。具体说明如下
在arm64汇编中,相应的就是操作x0~x28,执行如下汇编代码
.text
.global _test
_test:
mov x0, #0x4
mov x1, #0x3
add x0, x1, x0
ret
执行完test()函数,通过register read查询x0的值,最后可以看到x0存放的值为7,如下
(lldb) register read x0
x0 = 0x0000000000000004
(lldb) si
(lldb) register read x1
x1 = 0x0000000000000003
(lldb) si
(lldb) register read x0
x0 = 0x0000000000000007
sub指令
sub指令是将操作数1减去操作数2,再减去cpsr中的C条件标志位的反码,并将结果存放到目标寄存器中
cmp指令
cmp指令是把一个寄存器的内容和另一个寄存器的内容或者立即数做比较,同时会更新CPSR寄存器中条件标志位的值
- 执行如下汇编代码
.text
.global _test
_test:
mov x0, #0x4
mov x1, #0x3
cmp x0, x1
ret
- 在执行cmp代码之前和之后各打印一次CPSR寄存器的值如下
(lldb) register read cpsr
cpsr = 0x60000000
(lldb) si
(lldb) si
(lldb) si
(lldb) register read cpsr
cpsr = 0x20000000
(lldb)
可以发现,在执行cmp操作之后,cpsr寄存器的值变成了0x20000000,转换成16进制后,得到32位标志位如下
可以发现第31位,也就是N位的值为0,同时第30位,也就是Z位的值也为0,这就表示,x0和x1寄存器相比较之后的值为非零非负,而使用x0 - x1得到的结果是1,符合非零非负的条件。
- 修改汇编代码,调换x0和x1寄存器的位置,如下
_test:
mov x0, #0x4
mov x1, #0x3
cmp x1, x0
ret
- 再次在cmp代码执行前后读取CPSR寄存器的值
(lldb) register read cpsr
cpsr = 0x60000000
(lldb) s
(lldb) register read cpsr
cpsr = 0x80000000
(lldb)
这个时候,cpsr寄存器的值变成了0x80000000,转换成16进制后,如下
可以看出,第31位N位的值变成了1,第30位Z位的值为0,这表示,x0和x1寄存器相比较之后的值为非零负数,使用x1-x0得到的结果是-1,符合非零负数的条件
B指令
B指令是最简单的跳转指令,一旦遇到B指令,程序会无条件跳转到B之后所指定的目标地址处执行。
BL指令
BL指令是另一个跳转指令,但是在跳转之前,它会先将当前标记位的下一条指令存储在寄存器lr(x30)中,然后跳转到标记处开始执行代码,当遇到ret时,会将lr(x30)中存储的地址重新加载到PC寄存器中,使得程序能返回标记位的下一条指令继续执行。
- 首先执行以下汇编代码
.text
.global _test
label:
mov x0, #0x1
mov x1, #0x8
ret
_test:
mov x0, #0x4
bl label
mov x1, #0x3
cmp x1, x0
ret
- 断点到bl label指令时,读取lr寄存器和PC寄存器的值
- 执行bl label指令,跳转到label标记处,再次读取lr(x30)寄存器和PC寄存器的值,这个时候会发现lr(x30)寄存器存放的地址已经变成mov x1, #0x3这条指令的内存地址
- 执行完label标记中的所有代码,发现程序再次回到lr寄存器所存储的地址,也就是mov x1, #0x3这句指令继续执行,并且此时pc寄存器所存储的地址也变成了mov x1, #0x3这句指令的地址。
想了解更多逆向相关知识不妨动动小手,添加一下咱们的交流群642363427来为你的技术多添一份光彩。