iOS汇编语音有不少钟。常见的有8086汇编、arm汇编、x86汇编等等。xcode
iOS的架构从最初的armv6发展到后来的armv7和armv7s,最后发展到如今的arm64,不论是armv6仍是后来的armv7,以及arm64都是arm处理器的指令集。armv7和armv7s是真机32位处理器使用的架构,而arm64是真机64位处理器使用的架构。bash
iPhone 5C是最后一款arm32位版本的iPhone,在iPhone5s以后,全部的iPhone设备都采用arm64架构。arm64汇编在真机上使用,以下:markdown
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汇编是模拟器使用的汇编语言,它的指令和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 复制代码
在日常开发中,在调试程序的时候,若是程序crash,一般会定位到具体的崩溃代码。可是有时候也会遇到一些比较诡异的crash,好比说崩溃在了系统库中,这个时候定位到具体的crash缘由会很是困难。若是利用汇编调试技巧来进行调试,可能会让咱们事半功倍。架构
在逆向别人App过程当中,咱们能够经过LLDB对内存地址进行断点操做,可是当执行到断点时,LLDB展示给咱们的是汇编代码,而不是OC代码,因此想要逆向而且动态调试别人的App,就须要学习汇编的知识。iphone
想要学习arm64汇编,须要从如下三个方面入手,寄存器、指令和堆栈。函数
arm64中有34个寄存器,以下学习
也会有人将x0 ~ x30叫作通用寄存器,可是在实际使用中x29和x30并无对应的低32位的寄存器w2九、w30,并且x29和x30寄存器有着特殊的用途,因此在此我只讲x0 ~ x28记为通用寄存器测试
pc (Program Counter)寄存器,它记录着当前CPU正在执行的指令的地址,经过register read pc查看寄存器中存储的值spa
(lldb) register read pc pc = 0x000000010286e588 TestFont`-[ViewController test] + 20 at ViewController.m:28 (lldb) 复制代码
lr (Link Register)寄存器,也就是以前所说的x30寄存器,它存储着函数的返回地址
arm体系中包含一个当前程序状态寄存器cpsr (Current Program Status Register)和五个备份的程序状态寄存器spsr (Saved Program Status Registe),备份的程序状态寄存器用来进行异常处理。
ARM指令以下:
助记符 | ARM指令及功能描述 |
---|---|
ADC | 带进位加法指令 |
ADD | 加法指令 |
AND | 逻辑与指令 |
B | 跳转指令 |
BIC | 位清除指令 |
BL | 带返回的跳转指令 |
BLX | 带返回和状态切换的跳转指令 |
BX | 带状态切换的跳转指令 |
CDP | 协处理器数据操做指令 |
CMN | 比较反值指令 |
CMP | 比较指令 |
EOR | 异或指令 |
LDC | 存储器带协处理器的数据传输指令 |
LDM | 加载多个寄存器指令 |
LDR | 存储器到寄存器的数据传输指令 |
MCR | 从ARM寄存器到协处理器寄存器的数据传输指令 |
MLA | 乘加运算指令 |
MOV | 数据传送指令 |
MRC | 从协处理器寄存器到ARM寄存器的数据传输指令 |
MRS | 传送CPSR或SPSR的内容到通用寄存器指令 |
MSR | 传送通用寄存器到CPSR或SPSR指令 |
MUL | 32位乘法指令 |
MLA | 32位乘加指令 |
MVN | 数据反传送指令 |
ORR | 逻辑或指令 |
RSB | 逆向减法指令 |
RSC | 带借位的逆向减法指令 |
SBC | 带借位减法指令 |
STC | 协处理器寄存器写入存储器指令 |
STM | 批量内存字写入指令 |
STR | 寄存器到寄存器的数据传输指令 |
SUB | 减法指令 |
SWI | 软件中断指令 |
SWP | 交换指令 |
TEQ | 相等测试指令 |
TST | 位测试指令 |
mov指令能够将另外一个寄存器、被移位的寄存器或者将一个当即数加载到目的寄存器
; 此处.text表示此代码放在text段中 .text ; .global表示将后面跟随的方法给暴露出去,否则外部没法调用,方法名以_开头 .global _test ; 此处为_test方法 _test: ; mov指令,将当即数4加载到x0寄存器中 mov x0, #0x4 mov x1, x0 ; 汇编指令中,ret表示函数的终止 ret 复制代码
#ifndef test_h #define test_h void test(void); #endif /* test_h */ 复制代码
(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指令表示函数的返回,并且它还有一个很是重要的做用,就是将lr(x30)寄存器的值赋值给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()函数起始地址
(lldb) register read lr lr = 0x00000001021965a8 TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24 (lldb) register read pc pc = 0x0000000102196abc TestFont`test 复制代码
(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指令是将两个操做数相加,并将结果存放到目标寄存器中。具体说明以下
在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指令是将操做数1减去操做数2,再减去cpsr中的C条件标志位的反码,并将结果存放到目标寄存器中
cmp指令是把一个寄存器的内容和另外一个寄存器的内容或者当即数作比较,同时会更新CPSR寄存器中条件标志位的值
.text .global _test _test: mov x0, #0x4 mov x1, #0x3 cmp x0, x1 ret 复制代码
(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,符合非零非负的条件。
_test: mov x0, #0x4 mov x1, #0x3 cmp x1, x0 ret 复制代码
(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以后所指定的目标地址处执行。
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 复制代码
当处理器工做在arm状态时,几乎全部的指令均根据CPSR寄存器中条件码的状态和指令的条件域有条件的执行,当指令的执行条件知足时,指令被执行,不然指令被忽略。
每一条ARM指令包含4位的条件码,位于指令的最高四位[31:28]。条件码共有16种,每种条件码可用两个字符表示,这两个字符可用添加在指令助记符的后面和指令同时使用。例如:跳转指令B后可用加上后缀EQ变为BEQ,表示相等则跳转,即当CPSR寄存器中的Z标志置位时发生跳转。
- (void)test{ int a = 1; int b = 2; if (a == b) { NSLog(@"a==b"); }else{ printf("a!=b"); } } 复制代码
(lldb) register read cpsr cpsr = 0x80000000 复制代码
获得对应16进制的值为
在16种条件标志码中,只有15种可使用,以下图,第16种(1111)为系统保留,暂时不能使用
内存操做指令分为内存读取和内存写入指令
LDR(条件) 目的寄存器, <存储器地址>
复制代码
LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令一般用于从存储器中读取32位字数据到通用寄存器中,而后对数据进行处理。当程序计数器PC做为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而实现程序流程的跳转。该指令在程序设计中比较经常使用,切寻址方式灵活多样。示例以下:
LDR x0, [x1] ;将存储器地址为x1的字数据读入寄存器x0 LDR x0, [x1, x2] ;将存储器地址为x1+x2的字数据读入寄存器x0 LDR x0, [x1, #8] ;将存储器地址为x1+8的字数据读入寄存器x0 LDR x0, [x1, x2]! ;将存储器地址为x1+x2的字数据读入寄存器x0,并将新地址x1+x2写入x1 LDR x0, [x1, #8]! ;将存储器地址为x1+8的字数据读入寄存器x0,并将新地址x1+8写入x1 LDR x0, [x1], x2 ;将存储器地址为x1的字数据读入寄存器x0,并将新地址x1+x2写入x1 LDR x0, [x1, x2, LSL#2]! ;将存储地址为x1+x2*4的字数据写入寄存器x0,并将新地址x1+x2*4写入x1 LDR x0. [x1], x2, LSL#2 ;将存储地址为x1的字数据写入寄存器x0,并将新地址x1+x2*4写入x1 复制代码
经过一个简单的例子来了解LDR的做用:
.text
.global _test
_test:
; ldr指令,找到x1寄存器中存储的地址,从该地址开始读取8个字节的数据,存放到x0寄存器中
ldr x0, [x1]
ret
复制代码
为何此处是读取8个字节的数据呢?由于目标寄存器x0能够存放8个字节的数据,若是将x0换成w0,则读取4个字节的数据存放到w0中
- (void)viewDidLoad{ [super viewDidLoad]; int a = 5; test(); } 复制代码
能够发现,此时的x1寄存器存放着a变量的地址。
(lldb) x 0x000000016f2c52ec 0x16f2c52ec: 05 00 00 00 50 9a e0 31 01 00 00 00 58 0f b4 00 ....P..1....X... 0x16f2c52fc: 01 00 00 00 c7 3b 0f e6 01 00 00 00 50 9a e0 31 .....;......P..1 复制代码
前4个字节存放的是5,也就是变量a的值
LDUR指令用法和LDR指令相同,区别在于LDUR后的当即数为负数,以下
LDR x0, [x1, #8] LDUR x0, [x1, #-8] 复制代码
LDP中的P是pair的简称,能够看出LDP能够同时操做两个寄存器
; 如下命令表示,从sp寄存器的地址加上0x30后的地址开始,读取前8个字节的数据存放到寄存器x29中,读取后8个字节的数据放入x30寄存器中 ldp x29, x30, [sp, #0x30] 复制代码
STP指令的格式为:
STR{条件} 源寄存器, <存储器地址>
复制代码
STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。示例以下
STR x0, [x1], #8 ;将x0中的字数据写入以x1为地址的存储器中,并将新地址x1+8写入x1 STR x0, [x1, #8] ;将x0中的字数据写入以x1+8为地址的存储器中 复制代码
STUR指令和STR指令用法相同,区别在于STUR后的当即数为负数
STR x0, [x1, #8] STUR x0, [x1, #-8] 复制代码
STP指令能够同时操做两个寄存器
; 如下指令表示,将x29+x30的字数据写入以sp+0x8为地址的存储器中, stp x29, x30, [sp, #0x8] 复制代码
零寄存器中存放的值为0,主要做用是进行寄存器的置0操做
#OC代码 int a = 0; ; 汇编代码 str wzr, [sp, #0xc] 复制代码
具体效果是将wzr寄存器中的字数据,也就是0,写入sp+0xc为地址的存储器中
所谓寻址方式就是处理器根据指令中给出的地址信息来寻找物理地址的方法,目前ARM支持如下几种常见的寻址方式
当即寻址也叫作当即数寻址,是一种特殊的寻址方式,操做数自己就在指令中给出来,只要取出指令也就取到了操做数,这个操做数被称为当即数,对应的寻址方式也叫作当即寻址,例如如下指令:
ADD x0, x1, #1 ; x0 ← x1+1 ADD x0, x1, #0x3f ; x0 ← x1+0x3f 复制代码
在以上两条指令中,第二个操做数即为当即数,要求以“#”号为前缀,对于以16进制表示的当即数,还要求在“#”后加上“0x”或“&”。
寄存器寻址就是利用寄存器中的数值做为操做数,这种寻址方式是各种微处理器常常采用的一种方式,也是一种执行效率较高的寻址方式,指令以下
ADD x0, x1, x2 ; x0 ← x1+x2
复制代码
该指令的执行效果是将寄存器x1和x2的内容相加,其结果存放在寄存器x0中
寄存器间接寻址就是以寄存器中的值做为操做数的地址,而操做数自己存放在存储器中,例如以下指令
ADD x0, x1, [x2] ; x0 ← x1+[x2]
LDR x0, [x1] ; x0 ← [x1]
STR x0, [x1] ; [x1] ← x0
复制代码
基址变址寻址就是将寄存器(该寄存器通常称做基址寄存器)的内容与指令中给出的地址偏移量相加,从而获得一个操做数的有效地址。变址寻址方式经常使用于访问某基地址附近的地址单元。采用变址寻址方式的指令有如下常见的几种形式:
LDR x0, [x1, #4] ; x0 ← [x1+4] LDR x0, [x1, #4]! ; x0 ← [x1+4]、x1 ← x1+4 LDR x0, [x1], #4 ; x0 ← [x1]、x1 ← x1+4 LDR x0, [x1, x2] ; x0 ← [x1+x2] 复制代码
采用多寄存器寻址方式,一条指令能够完成多个寄存器值的传送,这种寻址方式能够用一条指令完成传送最多16个通用寄存器的值,指令格式以下:
LDMIA x0, [x1, x2, x3, x4] ; x1 ← [x0]
; x2 ← [x0+4]
; x3 ← [x0+8]
; x4 ← [x0+12]
复制代码
该指令的后缀IA表示在每次执行完加载/存储操做后,x0按字长度增长,所以,指令能够将连续存储单元的值传送到x1~x4
与基址变址寻址方式相相似,相对寻址以程序计数器PC的当前值为基地址,指令中的地址标号为偏移量,将二者相加之和获得操做数的有效地址。如下程序段完成子程序的调用和返回,跳转指令BL就是采用了相对寻址方式:
BL NEXT ; 跳转到子程序NEXT处执行
......
NEXT
......
MOV PC, LR ; 从子程序返回
复制代码
堆栈是哟中数据结构,按先进后出(FILO)的方式工做,使用一个称做堆栈指针的专用寄存器指示当前的操做位置,堆栈指针老是指向栈顶位置。
当栈顶指针指向最后压入堆栈的数据时,称为满堆栈(Full Stack),而当堆栈指针指向下一个将要放入数据的空位置时,称为空堆栈(Empty Stack)
同时、根据堆栈的生成方式,又能够分为递增堆栈(Ascending Stack)和递减堆栈(Decending Stack),当堆栈由低地址向高地址生成时,称为递增堆栈,当堆栈由高地址向低地址生成时,称为递减堆栈。这样就有四种类型的堆栈工做方式,ARM微处理器支持这四种类型的堆栈工做方式。即:
在了解堆栈操做以前,首先得了解函数的类型,函数类型主要分为两种:叶子函数、非叶子函数
了解了什么是叶子函数和非叶子函数,那么咱们就要从汇编代码的层面来深刻理解叶子函数和非叶子函数的区别,以及堆栈指针在其中起到的做用。
上文介绍过叶子函数的具体定义,下面经过具体的汇编代码来深刻了解叶子函数
void leafFuncion(){ int a = 1; int b = 2; } 复制代码
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
复制代码
sub sp, sp, #16 ; sp = sp - 16 orr w8, wzr, #0x2 orr w9, wzr, #0x1 str w9, [sp, #12] str w8, [sp, #8] add sp, sp, #16 ; sp = sp + 16 ret 复制代码
堆栈指针<font color=red>sp</font>开始指向<font color=red>0x10010</font>,偏移以后指向<font color=red>0x10000</font>,至关于开辟了从<font color=red>0x10000</font>到<font color=red>0x10010</font>这一段内存供函数使用。
复制代码
为何要维持堆栈平衡?由于在函数调用以前,堆栈指针sp会偏移一段内存地址,为当前须要调用的函数分配一段内存空间,在函数调用完成以后将sp指针重置到开始位置,这样,刚刚分配的那段内存空间就是垃圾内存,下一次再有函数调用的时候,这段内存空间可重复利用。这就作到了堆栈平衡。若是函数调用完成以后不重置sp指针,那么,若是有足够多的函数一直调用,最后确定会出现栈溢出的问题。
非叶子函数和叶子函数的区别在因而否有调用其它函数,下面一样经过具体的汇编代码来深刻了解非叶子函数
void leafFuncion(){ int a = 1; int b = 2; } void nonLeafFunction(){ int a = 3; int b = 4; leafFuncion(); } 复制代码
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
复制代码
sub sp, sp, #32 ; sp=sp-32 stp x29, x30, [sp, #16] ; 8-byte Folded Spill add x29, sp, #16 ; x29=sp+16 orr w8, wzr, #0x4 orr w9, wzr, #0x3 stur w9, [x29, #-4] str w8, [sp, #8] bl _leafFuncion ldp x29, x30, [sp, #16] ; 8-byte Folded Reload add sp, sp, #32 ; sp=sp+32 ret 复制代码
开始分析汇编代码
sub sp, sp, #32指令是执行内存分配的操做,将sp指针向前偏移32位,获得一片连续的内存空间
stp x29, x30, [sp, #16]指令是将x29(fp)和x30(lr)寄存器中存放的值写入以sp+16的地址为起始地址的一段内存空间中去,每一个寄存器占8个字节的空间。
add x29, sp, #16指令是将sp + 16的地址存放在x29(fp)寄存器中,由此,能够获得从sp到x29(fp),这两个地址之间的一段内存空间就是当前函数可使用的内存空间。
如上图所示,橙色的那段内存就是咱们可使用的内存空间。
orr w8, wzr, #0x4和orr w9, wzr, #0x3其实就是将4赋值给寄存器w8,将3赋值给寄存器w9
stur w9, [x29, #-4]指令是将w9中存储的值,也就是3,写入到以x29(fp)- 4的地址为开始地址的4个字节的内存中去。str w8, [sp, #8]指令则是将w8中存储的值4,写入到以sp+8为起始地址的4个字节的内存中去,以下
bl _leafFuncion指令则表示跳转到_leadFunction函数的操做,前面提到过,执行bl指令以前,会将bl指令的下一条汇编指令ldp x29, x30, [sp, #16]的地址存放到lr寄存器中,以便执行完_leadFunction函数以后,能跳转回ldp x29, x30, [sp, #16]指令继续执行。这就能够明白为何以前须要先存储lr寄存器中的值,由于一旦执行完bl _leafFuncion指令以后,若是不将lr指令重置为初始值的话,一旦执行到后面的ret函数,会从新跳到ldp x29, x30, [sp, #16]指令的地址处从新执行,如此反复。
执行完_leadFunction函数以后,会回到lr中存储的地址,也就是ldp x29, x30, [sp, #16]指令继续执行。ldp x29, x30, [sp, #16]指令的做用是以sp+16的地址为开始地址,依次读取16个字节的数据,前8个字节的数据存放到x29(fp)寄存器中去,后8个字节的数据存放到x30(lr)寄存器中去。其实就是将x29(fp)和x30(lr)寄存器的值恢复到调用函数以前所存放的值。
最后add sp, sp, #32指令是将sp+32的地址值赋值给sp,其实就是还原sp指针的值,至此整个函数就调用完毕,给当前函数分配的内存空间就成了垃圾内存空间,能够给以后的函数重复使用。至此,咱们就能够明白叶子函数和非叶子函数的区别,以及堆栈指针在当前函数调用过程当中起到的做用。
在非叶子函数调用过程当中,sp指针一直指向被分配栈空间的栈顶,因此又叫作栈顶指针,而fp指针指向可用栈空间的栈底,因此又叫作栈底指针。两个指针所指地址的中间一段内存就是函数可使用的内存空间。
函数执行开始和结束的汇编指令就是用来分配内存以及维持堆栈平衡的操做。