在逆向开发中,非常重要的一个环节就是静态分析,众所周知,手机上安装App其本质是一个二进制文件,而静态分析是建立在分析二进制上面的。所以在学习逆向之前,需要提前了解相关的汇编知识
汇编语言的发展
机器语言
是由0和1
组成的机器指令,表示特定的功能,如下所示
加:0100 0000 减:0100 1000 乘:1111 0111 1110 0000 除:1111 0111 1111 0000
汇编语言(assembly language)
由于使用机器语言表示时不方便记忆,于是开始使用助记符
来代替机器语言,,例如下面使用助记符表示的加减乘除
加:INC EAX 通过编译器 0100 0000 减:DEC EAX 通过编译器 0100 1000 乘:MUL EAX 通过编译器 1111 0111 1110 0000 除:DIV EAX 通过编译器 1111 0111 1111 0000
高级语言(high-level programming language)
在后期,为了更加高效的编程,在汇编语言的基础之上有了更高级的语言,例如C/C++/Java/OC/Swift
等,这些语言更加接近人类的自然语言。例如c语言表示的加减乘除
加:A+B 通过编译器 0100 0000 减:A-B 通过编译器 0100 1000 乘:A*B 通过编译器 1111 0111 1110 0000 除:A/B 通过编译器 1111 0111 1111 0000
最终的代码在终端设备上显示的过程是这样的
有以下几点说明:
- 1、
汇编语言
与机器语言
是一一对应的,每一条机器指令都有与之对应的汇编指令 - 2、
汇编语言
可以通过编译
得到机器语言
,机器语言
可以通过反汇编
得到汇编语言
- 3、
高级语言
可以通过编译
得到汇编语言/机器语言
,但汇编语言/机器语言几乎不可能还原成高级语言(因为不同的设备,对应不同的CPU架构,而CPU架构对应不同的指令集)
汇编语言的特点
汇编语言主要有以下几个特点:
- 1、可以
直接访问、控制各种硬件设备
,例如存储器、CPU等,能最大限度的发挥硬件的功能 - 2、能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 3、目标代码简洁,占用内存少,执行速度快
- 4、汇编指令是机器指令的助记符,同机器指令一一对。每一种CPU都有自己的机器指令集/汇编指令集,所以汇编语言不具备可移植性
- 5、知识点过多,要求过高,需要开发者对CPU等硬件结构有所了解,不易于编写、调试,以及维护
- 6、不区分大小写,例如mov和MOV是一样的
汇编语言的用途
按理说,汇编这么难,为什么还要学呢,以及学了能做什么?
汇编学习的目的
任何高级语言最终都会被编译成汇编,学习了解汇编的相关知识,可以更好的日常开发、学习探索中帮助我们更好的排查问题、理解底层运行的机制。大致分为有以下几点:
- 1、是理解整个计算机系统的最佳起点和最有效途径
- 2、为编写高效代码打下基础
- 3、理解代码的本质,例如:
- 函数的本质是什么?
- ++a底层是如何执行的?
- 编译器在底层到底帮我们做了哪些工作?
- DEBUG模式和RELEASE模式到底有哪些地方是不同的,以及被我们忽略的?
汇编使用的场景
- 1、可以
编写驱动程序、操作系统
,例如Linux内核的某些关键字 - 2、对性能要求极高的程序或者代码片段,可
与高级语言混合使用
(称为内联汇编) - 3、用于
软件安全方面
- 病毒的分析与防治
- 逆向、加壳、脱壳、破解、外挂、免杀、加解密、黑客等
所以综上所述,汇编是所有程序猿都需要了解的一门非常重要的语言,这也是为什么大学中计算机相关专业学生的必修课,就好比修房子,地基稳了,高楼才能平地起
!
汇编语言的种类
目前讨论较多的婚变语言有以下几种
- 1、8086汇编(8086处理器是16bit的CPU)
- 2、Win32汇编
- 3、Win64汇编
- 4、ARM汇编(嵌入式、Mac、iOS)
- ...
在iPhone中使用的是ARM汇编,但是不同的设备之间也会因为CPU架构的不同而有所差异,下面是iPhone中不同架构所对应的设备
必备常识
想要学好汇编,需要有以下几个认知
- 1、首先需要了解CPU等硬件的架构
- 2、App/程序的执行过程
- 可执行文件:程序/App在本地磁盘的文件
- image(镜像文件):可执行文件装载到内存的文件(在早期,其本质与可执行文件是一模一样的)
- 内存中除了指令,还有数据,但是都是0和1组合,CPU是如何区分的?是通过CPU上的部件PC寄存器来区分
- 3、硬件相关最为重要的是CPU/内存
- 4、在汇编中,大部分指令都是与CPU/内存相关的
总线
总线是CPU与内存之间的桥梁,如下图所示是iPhone X上的A11(CPU芯片)
从图中可以看出:每一个CPU芯片都有很多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
总线:是一根根导线的集合
总线的分类
总线主要分为三类,如下图所示:
- 1、地址总线:CPU是通过地址总线来指定存储单元的
- 2、数据总线:CPU与内存/其他部件之间的数据传送通道
- 3、控制总线:CPU通过控制总线对外部器件进行控制
举例:CPU从内存的3号单元读取数据
- 1、CPU想操作内存中的数据,首先需要找到内存地址:CPU通过地址总线,将3这个地址传递给内存,即寻址到内存的3号单元
- 2、需要操作3单元的数据,还需要确定是读还是写:CPU通过控制总线告诉内存需要进行的操作,假设是读
- 3、内存知道了CPU想要进行的操作:内存将3号单元的数据通过数据线传递给CPU
地址总线
- 它的宽度决定了
CPU的寻址能力
,即地址总线决定了CPU所能访问的最大内存空间的大小,例如10根地址线能访问的最大内存是 2^10 = 1024位二进制数据(即1B) - 地址总线是地址线数量之和
8086
地址总线宽度是20
,所以寻址能力是1M
(即2^20)- 内存地址的单元是 字节byte(简写为B),每个字节里面可以放8位(即bit),以下是内存条的图示
注意区分
数量单位
和容量单位
- 数量单位: 1M=1024k,1k=1024
- 容量单位: 字节Byte(B) 1024B=1KB,1024KB=1MB,IBM银行的独立系统是以2字节为一个单位,常用的电脑是以1字节为一个单位
- 网络带宽100M=100Mbpt(比特位,每秒传输12.5MB/s)
数据总线
- 它的宽度决定了
CPU的单次数据传送量
(即吞吐量),也就是数据传送速度
即CPU和外界的数据传送速度 - 每条数据线一次只能传输一位二进制数据,例如 8根数据线一次可传送一个8位二进制数据(即1个字节的数据)
- 数据总线是数据线数量之和
8086
的数据总线宽度是16
,所以单次最大传递2个字节的数据
我们常说的32位(4字节)、64位(8字节)CPU,这里的32、64指的就是数据吞吐量
控制总线
- 它的宽度决定了
CPU对其他器件的控制能力
,能有多少种控制,即CPU对外部器件的控制能力 - 控制总线是控制线数量之和
内存
- CPU是通过总线和硬件设备连接的
- 内存有RAM主存储器、RAM主存储器(内存条)
下图是按照物理地址划分的内存,有主存储器、显存地址、显卡地址、网卡地址
其中内存中的低地址
是给用户用
的,高地址
是给系统用
的,如下所示
- 内存地址空间的大小受CPU地址总线宽度的限制,8086的地址总线宽度为20,可以定位2^20个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
0x00000~0x9FFFF
:主存储器,可读可写
0xA0000~0xBFFFF
:向显存中写入数据,这些数据会被显卡输出到显示器,可读可写
0xC0000~0xFFFFF
:存储各种硬件/系统信息,只读
进制
每一种进制都是完美的,而我们通常总是以十进制为依托去考虑其他进制,总是先转换为十进制,在转换为其他进制,其实这种方式是错误的。所以想要学好进制,首先需要忘掉十进制,其次是忘掉进制间的转换
进制的定义
- 八进制由8个符号组成:0 1 2 3 4 5 6 7
逢八进一
- 十进制由10个符号组成:0 1 2 3 4 5 6 7 8 9
逢十进一
- N进制就是由N个符号组成:逢N进一
<!--练习--> - 1+1在___情况下等于3 <!--答案--> 十进制由10个符号组成: 0 1 3 2 8 A B E S 7 逢十进一 如果这样定义十进制:1+1等于3 <!--目的--> 传统定义的十进制和自定义的十进制不一样,如果不告诉别人符号表,别人是无法拿到具体的数据的。这样的应用场景主要是用于加密
进制的运算
八进制加法表
0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27 ... 1+1 = 2 1+2 = 3 2+2 = 4 1+3 = 4 2+3 = 5 3+3 = 6 1+4 = 5 2+4 = 6 3+4 = 7 4+4 = 10 1+5 = 6 2+5 = 7 3+5 = 10 4+5 = 11 5+5 = 12 1+6 = 7 2+6 = 10 3+6 = 11 4+6 = 12 5+6 = 13 6+6 =14 1+7 = 10 2+7 = 11 3+7 = 12 4+7 = 13 5+7 = 14 6+7 =15 7+7 = 16
八进制乘法表
0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27... 1*1 = 1 1*2 = 2 2*2 = 4 1*3 = 3 2*3 = 6 3*3 = 11 1*4 = 4 2*4 = 10 3*4 = 14 4*4 = 20 1*5 = 5 2*5 = 12 3*5 = 17 4*5 = 24 5*5 = 31 1*6 = 6 2*6 = 14 3*6 = 22 4*6 = 30 5*6 = 36 6*6 = 44 1*7 = 7 2*7 = 16 3*7 = 25 4*7 = 34 5*7 = 43 6*7 = 52 7*7 = 61
做个练习
- 2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
- 277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .
第一排的计算,我们可以很好的得出结论
但是第二排涉及数据较大时,如何计算呢,下面以277+333为例,计算过程如下
实战四则运算
277 236 276 234 + 333 - 54 * 54 / 4 -------- -------- -------- -------- 632 162 1370 47 + 1666
二进制的简写形式
二进制: 1 0 1 1 1 0 1 1 1 1 0 0 三个二进制一组: 101 110 111 100 八进制: 5 6 7 4 四个二进制一组: 1011 1011 1100 十六进制: b b c
使用二进制从0写到1111:0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
发现这样使用二进制太麻烦,所以将其改为更简单一点的符号
0 1 2 3 4 5 6 7 8 9 A B C D E F 这就是十六进制了
数据的宽度
数学上的数字是没有大小限制的,可以无限大
但是在计算机中,由于硬件的制约,数据都是有长度限制的(称为数据宽度
),超过最多宽度的数据会被丢弃
#import <UIKit/UIKit.h> #import "AppDelegate.h" int test(){ int cTemp = 0x1FFFFFFFF; return cTemp; } int main(int argc, char * argv[]) { printf("%x\n",test()); @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
断点调试结果如下,可以发现1溢出了
也可以通过获取的地址,然后在Debug-Debug Workflow-ViewMemory
中输入地址查看
计算机中常见的数据宽度
位(Bit)
:1个位就是1个二进制位,即0或1字节(Byte)
:1个字节由8个Bit组成,内存中的最小单元Byte字(Word)
:1个字由两个字节组成(16位),第2个字节分别称为高字节和低字节双字(DoubleWord)
:1个双字由两个字组成(32位)
那么计算机存储数据它会分为有符号数
和无符号数
,如下图所示
- 无符号数,直接换算
- 有符号数,符号放在第1位,第1位是0即正数,为1即负数:
- 正数:0 1 2 3 4 5 6 7
- 负数:F E D B C A 9 8
表示:-1 -2 -3 -4 -5 -6 -7 -8
自定义进制符号
练习1
现在有10进制数 10个符号分别是:2,9,1,7,6,5,4, 8,3 , A 逢10进1 那么: 123 + 234 = ____(AA6)
十进制: 0 1 2 3 4 5 6 7 8 9 自定义: 2 9 1 7 6 5 4 8 3 A (十进制10)92 99 91 97 96 95 94 98 93 9A 12 19 11 17 16 15 14 18 13 1A 72 79 71 77 76 75 74 78 73 7A 62 69 61 67 66 65 64 68 63 6A 52 59 51 57 56 55 54 58 53 5A 42 49 41 47 46 45 44 48 43 4A 82 89 81 87 86 85 84 88 83 8A 32 39 31 37 36 35 34 38 33 3A 922 (00)2 (01)9 (02)1 (03)7 (04)6 (05)5 (06)4 (07)8 (08)3 (09)A (10)92 (11)99 (12)91 (13)97 (14)96 (15)95 (16)94 (17)98 (18)93 (19)9A (20)12 (21)19 (22)11 (23)17 (24)16 (25)15 (26)14 (27)18 (28)13 (29)1A (30)72 (31)79 (32)71 (33)77 (34)76 (35)75 (36)74 (37)78 (38)73 (39)7A (40)62 (41)69 (42)61 (43)67 (44)66 (45)65 (46)64 (47)68 (48)63 (49)6A (50)52 59 51 57 56 55 54 58 53 5A (60)42 49 41 47 46 45 44 48 43 4A (70)82 89 81 87 86 85 84 88 83 8A (80)32 39 31 37 36 35 34 38 33 3A (90)922
刚才通过10进制运算可以转换,然后查表,但是如果是其他进制,就不能转换,要学会直接查表
练习2
现在有9进制数 9个符号分别是:2,9,1,7,6,5,4, 8,3 逢9进1 那么: 123 + 234 = ____(9926)
十进制: 0 1 2 3 4 5 6 7 8 自定义: 2 9 1 7 6 5 4 8 3 92 99 91 97 96 95 94 98 93 12 19 11 17 16 15 14 18 13 72 79 71 77 76 75 74 78 73 62 69 61 67 66 65 64 68 63 52 59 51 57 56 55 54 58 53 42 49 41 47 46 45 44 48 43 82 89 81 87 86 85 84 88 83 32 39 31 37 36 35 34 38 33 922 (00)2 (01)9 (02)1 (03)7 (04)6 (05)5 (06)4 (07)8 (08)3 (10)92 (11)99 (12)91 (13)97 (14)96 (15)95 (16)94 (17)98 (18)93 (20)12 19 11 17 16 15 14 18 13 (30)72 79 71 77 76 75 74 78 73 (40)62 69 61 67 66 65 64 68 63 (50)52 59 51 57 56 55 54 58 53 (60)42 49 41 47 46 45 44 48 43 (70)82 89 81 87 86 85 84 88 83 (80)32 39 31 37 36 35 34 38 33 (90)922
CPU & 寄存器
内部部件之间是由总线连接,如下图所示
- CPU除了有
控制器、运算器
,还有寄存器
,其中寄存器的作用就是进行数据的临时存储
- CPU的运算速度是非常快的,为了性能CPU在内部开辟了一小块临时存储区域,并在进行运算时先将数据从内存中复制到这一小块临时区域中。运算就在这一小块临时存储区进行,称这一小块临时存储区域为
寄存器
针对arm64的CPU来说,
- 如果寄存器以
x
开头,则表明是一个64位的寄存器
- 如果寄存器以
w
开头,则表明是一个32位的寄存器
在系统中没有提供16位和32位的寄存器供访问和使用,其中32位的寄存器是64位寄存器的低32位部分,并不是独立存在的
- 对于程序员来说,CPU中最主要的部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
- 不同CPU,寄存器的个数和结构是不相同的
浮点和向量寄存器
因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点寄存器
来处理浮点数
- 浮点寄存器
64位:D0 - D31
32位:S0 - S31
现在CPU支持向量运算
(向量运算在图形处理相关的领域用的非常多),为了支持向量计算,系统也提供了众多的向量寄存器
- 向量寄存器 128位:
V0 - V31
通用寄存器
- 通用寄存器也称为
数据地址寄存器
。通常用来做数据计算的临时存储、累加、计数、地址保存等功能
。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用 - arm64拥有
32个64位的通用寄存器X0-X30,以及XZR(零寄存器)
。这些通用寄存器有时也有特定用途
- 那么w0-w28这些32位的,因为64位CPU可以兼容32位,所以可以只使用64位寄存器的低32位
- 例如w0就是x0的低32位
注意:了解过8086汇编的一定知道,还有一种特殊的寄存器段寄存器:CS、DS、SS、ES。这四个寄存器主要用来保存这些段的基地址,是属于Intel架构的CPU的,在arm中并没有
- 通常,
CPU会先将内存中的数据存储到通用寄存器中,然后再对寄存器中的数据进行运算
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
- CPU首先会将红色内存空间的值放到X0寄存器中:
mov X0,红色内存空间
- 然后让X0寄存器与1相加:
add X0,1
- 最后将值赋值给内存空间:
mov 蓝色内存空间,X0
PC寄存器(program counter)
- 为
指令指针寄存器
,它指示了CPU当前要读取指令的地址
- 在内存/磁盘上,指令和数据没有任何区别,都是二进制信息
- CPU在工作时,将有的信息看作指令,有的看作数据,为同样的信息赋予了不同的意义
- 例如 1110 0000 0000 0011 0000 1000 1010 1010
- 可以当做数据 0xE003008AA
- 也可以当做指令 mov x0, x8
- CPU根据什么将内存中的信息看作指令呢?
- CPU将pc指向的内存单元的内容看作指令
- 如果内存中的某段内容曾经被CPU执行过,那么它所在的内存单元必然被pc指向过
寄存器案例分析
下面通过以下代码的例子来分析
#import <UIKit/UIKit.h> #import "AppDelegate.h" int test(){ int cTemp = 0x1FFFFFFFF; return cTemp; } int main(int argc, char * argv[]) { printf("%x\n",test()); @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
demo中寄存器种类如下所示
其中通用寄存器如下所示
以下是demo的汇编图示
pc寄存器调试
- 打印pc寄存器,现在是fdec4
按住control+Step into,继续打印
除了读还可以写
- register write pc 0x104c31ecc
- register read pc 此时是读不出来的,因为断点断住了,如果step into,此时断点断在哪里?最终通过验证发现,会断在cc的下一行
- 此时pc指向cc,会将cc中的指令拿出来执行,执行完毕后走到cc的下一条指令,而此时d0的指令是还没有执行的
高速缓存
iPhoneX上搭载的arm处理器A11,它的1级缓存的容量是64kb,2级缓存的容量是8M
CPU每执行一条指令前都需要从内存中将质量读取到CPU内存并执行,而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个
高速缓存区域
。当程序运行时,先将要执行的指令代码以及数据复制到高速缓存中(由操作系统完成),然后CPU直接从高速缓存依次读取指令来执行
bl指令
- CPU从何处执行指令,是由pc中的内容决定的,可以
通过改变pc的内容来控制CPU执行目标指令
- arm64提供了一个
mov指令(传送指令)
,可以用来改变大部分寄存器的值,例如 mov x0,#10、mov x1,#20 - 但是,
mov指令并不能用于设置pc的值
,arm64没有提供这样的功能 - arm64提供了另外的指令来
修改pc的值
,这些指令统一称为转移指令
,其中最简单的是bl指令
bl指令练习
现在有两段代码!假设程序先执行A,请写出指令执行顺序.最终寄存器x0的值是多少?
_A: mov x0,#0xa0 mov x1,#0x00 add x1, x0, #0x14 mov x0,x1 bl _B mov x0,#0x0 ret _B: add x0, x0, #0x10 ret <!--结果--> 流程: mov x0,#0xa0 -- x0:0xa0 mov x1,#0x00 -- x1:0x00 add x1, x0, #0x14 -- x1:0xa0+0x14=0xb4 mov x0,x1 -- x0:0xb4 add x0, x0, #0x10 -- x0:0xb4+0x10=0xc4 ret -- 回到bl跳转的下一行 mov x0,#0x0 -- x0:0x00 x0的值:0x00
汇编代码验证
下面来写一段汇编代码验证(com+n -- empty -- asm.s(汇编代码文件,会编译成源码))
如何执行呢?
- 在VC中定义函数的声明
在A()执行处加断点,并执行程序,开启汇编调试
按住control+step into,进入A的具体汇编
然后开始lldb调试,下面是一步步到0x00的过程
- 进入汇编代码A
汇编代码验证-07
进入汇编代码B
执行到A的ret
- 疑问:发现走到这里死循环了,这是为什么呢?预知后事如何,请看下一篇文章
总结
- 1、汇编概述
- 使用
助记符代替机器指令
的一种编程语言 - 汇编和机器指令是
一一对应
的关系,拿到二进制就可以反汇编 - 由于汇编和CPU的指令集是对应的,所以汇编不具备移植性
- 2、总线:是由一堆导线的集合
地址总线
:其宽度决定了寻址能力
数据总线
:其宽度决定了CPU数据的吞吐量
控制总线
:其宽度决定了CPU对其他器件的控制能力
- 3、进制
- 任意进制都是由对应个数的符号组成的,符号可以自定义
- 2/8/16是相对完美的进制,他们之间的关系
3
个二进制 使用一个8进制
标识4
个二进制 使用一个16进制
标识- 两个16进制可以标识一个字节,即8位
数量单位
- 1024 = 1k,1024k = 1M,1024M = 1G
容量单位
- 1024 = 1KB,1024KB = 1MB,1024MB = 1GB
- 数据的宽度
- 计算机中的数据是有宽度的,超过了就会溢出
- 4、
寄存器
:CPU为了性能,自内部开辟了一小块临时存储区域
浮点向量寄存器
:用于浮点数/向量的存储及运算异常状态寄存器
通用寄存器
:除了存放数据有时也有特殊的用途
ARM64
拥有32个64位
的通用寄存器X0-X30
以及XZR
(零寄存器)- 为了
兼容32位
,所以arm64位拥有W0-W28
以及WZR 30
个32位寄存器 32位寄存器并不是独立存在的
,例如 W0是X0的低32位
PC寄存器
:指令指针寄存器
- PC寄存器里面的
值
保存的就是CPU接下来需要执行的指令地址
- 改变PC的值可以改变程序的执行流程
mov
指令不能更改PC寄存器的值
,需要通过bl跳转
指令来改变PC寄存器的值