一、汇编语言
① 汇编语言的发展
- 机器语言:是由 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
- 最终的代码在终端设备上显示,过程如下所示:
- 说明:
-
- 汇编语言与机器语言是一一对应的,每一条机器指令都有与之对应的汇编指令;
-
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言;
-
- 高级语言可以通过编译得到汇编语言/机器语言,但汇编语言/机器语言几乎不可能还原成高级语言(因为不同的设备,对应不同的 CPU 架构,而 CPU 架构对应不同的指令集)。
② 汇编语言的特点
- 可以直接访问、控制各种硬件设备,例如存储器、CPU 等,能最大限度的发挥硬件的功能;
- 能够不受编译器的限制,对生成的二进制代码进行完全的控制;
- 目标代码简洁,占用内存少,执行速度快;
- 汇编指令是机器指令的助记符,同机器指令一一对,每一种 CPU 都有自己的机器指令集/汇编指令集,所以汇编语言不具备可移植性;
- 知识点过多,要求过高,需要开发者对 CPU 等硬件结构有所了解,不易于编写、调试,以及维护;
- 不区分大小写,如 mov 和 MOV 是一样的。
③ 汇编语言的种类
- 目前讨论较多的汇编语言有:
-
- 8086 汇编(8086 处理器是 16bit 的 CPU);
-
- Win32 汇编;
-
- Win64 汇编;
-
- ARM 汇编(嵌入式、Mac、iOS);
-
- …
- 在 iPhone 中使用的是 ARM 汇编,但是不同的设备之间也会因为 CPU 架构的不同而有所差异。如下所示,iPhone 中不同架构所对应的设备:
架构 | 设备 |
---|---|
armv6 | iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch |
armv7 | iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4 |
armv7s | iPhone5, iPhone5C, iPad4(iPad with Retina Display) |
arm64 | iPhone5S 以后 iPhoneX , iPad Air, iPad mini2以后 |
④ 汇编语言的用途
- 任何高级语言最终都会被编译成汇编,学习了解汇编的相关知识,可以更好的日常开发、学习探索中帮助我们更好的排查问题、理解底层运行的机制。
- 汇编语言可以编写驱动程序、操作系统,例如 Linux 内核的某些关键字。
- 汇编语言可以对性能要求极高的程序或者代码片段,可与高级语言混合使用(称为内联汇编)。
- 汇编语言可以用于软件安全方面:病毒的分析与防治,逆向、加壳、脱壳、破解、外挂、免杀、加解密、黑客等。
- 汇编语言可以帮助我们理解整个计算机系统的最佳起点和最有效途径,可以为编写高效代码打下基础。
- 汇编语言可以帮助我们理解代码的本质,例如:
-
- 函数的本质是什么?
-
- ++a 底层是如何执行的?
-
- 编译器在底层到底帮我们做了哪些工作?
-
- DEBUG 模式和 RELEASE 模式到底有哪些地方是不同的?有什么关键的地方被我们忽略了?
-
- …
二、总线
- App/程序的执行过程:
- 总线是 CPU 与内存之间的桥梁,如下图所示,iPhone X 上的 A11 芯片:
- 每一个 CPU 芯片都有很多管脚,这些管脚和总线相连,CPU 通过总线跟外部器件进行交互,总线就是一根根导线的集合。
- 总线主要分为三类,如下图所示:
-
- 地址总线:CPU 是通过地址总线来指定存储单元的;
-
- 数据总线:CPU 与内存/其他部件之间的数据传送通道;
-
- 控制总线:CPU 通过控制总线对外部器件进行控制。
- 如下所示,CPU 从内存的 3 号单元读取数据:
-
- CPU 想操作内存中的数据,首先需要找到内存地址:CPU 通过地址总线,将 3 这个地址传递给内存,即寻址到内存的 3 号单元;
-
- 需要操作 3 单元的数据,还需要确定是读还是写:CPU 通过控制总线告诉内存需要进行的操作,假设是读;
-
- 内存知道 CPU 想要进行的操作:内存将 3 号单元的数据通过数据线传递给 CPU。
① 地址总线
- 地址总线的宽度决定 CPU 的寻址能力,即地址总线决定 CPU 所能访问的最大内存空间的大小。例如:10 根地址线能访问的最大内存是 210 = 1024 位二进制数据(即 1B);
- 地址总线是地址线数量之和,8086 地址总线宽度是 20,所以寻址能力是 1M(即 220);
- 内存地址的单元是字节 byte(简写为 B),每个字节里面可以放 8 位(即 bit)。如下所示,地址 0x0001 表示的是 byte,后面的 01001001 表示的是 bit:
- byte 和 bit 是什么?
-
- byte 是字节的意思,字节是计算机存储容量的基本单位,一个字节由 8 位二进制数组成。在计算机内部,一个字节可以表示一个数据,也可以表示一个英文字母,两个字节可以表示一个汉字。
-
- bit 是位的意思,位是计算机中存储数据的最小单位,指二进制数中的一个位数,其值为“0”或“1”。
- byte 和 bit 区别:
-
- 容量大小不同:一个 byte 由 8 bits 组成,是数据存储的基础单位,1byte 又称为一个字节,用一个字节 Byte 储存,可区别 256 个数字;bit (比特) 是表示信息的最小单位,是二进制数的一位包含的信息或 2 个选项中特别指定 1 个的需要信息量。
-
- 存储数据类型不同:一个 byte 由 8 bits 所组成,可代表一个字元(A ~ Z)、数字(0 ~ 9)、或符号(,.?!%&±*/),是记忆体储存资料的基本单位,每个中文字则须要两 bytes;bit 是电脑记忆体中最小的单位,在二进位电脑系统中,每一 bit 可以代表0 或 1 的数位讯号。
② 数据总线
- 数据总线的宽度决定了 CPU 的单次数据传送量(即吞吐量),也就是数据传送速度即 CPU 和外界的数据传送速度;
- 数据总线的每条数据线一次只能传输一位二进制数据,例如,8 根数据线一次可传送一个 8 位二进制数据(即 1 个字节的数据);
- 数据总线是数据线数量之和,8086 的数据总线宽度是 16,所以单次最大传递 2 个字节的数据;
- 常说的 32 位(4 字节)、64 位(8 字节)CPU,这里的 32、64 指的就是数据吞吐量。
③ 控制总线
- 控制总线的宽度决定 CPU 对其他器件的控制能力,能有多少种控制,即 CPU 对外部器件的控制能力;
- 控制总线是控制线数量之和。
④ 内存
- CPU 是通过总线和硬件设备连接的,内存有 RAM 主存储器、RAM 主存储器(内存条);
- 按照物理地址划分的内存,有主存储器、显存地址、显卡地址、网卡地址:
- 内存中的低地址是供用户用的,高地址是供系统用的,如下所示:
- 内存地址空间的大小受 CPU 地址总线宽度的限制,8086 的地址总线宽度为 20,可以定位 220 个不同的内存单元(内存地址范围 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 进一;
- 示例:
-
- 如果十进制由这样 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
- 以 277+333 为例,计算过程如下:
- 实战四则运算:
277 236 276 234
+ 333 - 54 * 54 / 4
-------- -------- -------- --------
632 162 1370 47
③ 二进制的简写形式
二进制: 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
④ 数据的宽度
- 数学上的数字是没有大小限制的,可以无限大。但是在计算机中,由于硬件的制约,数据都是有长度限制的,称为数据宽度,超过最多宽度的数据会被丢弃。
- 可以来做一下测试:
#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 即负数。
⑥ 自定义进制符号
- 现有 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
- 现有 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 位。
- 通常,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 指向过。
④ 寄存器案例分析
- 继续使用上文中数据的宽度的例子继续分析,寄存器种类如下所示:
-
- Exception State Registers:异常寄存器
-
- Floating Point Registers:浮点寄存器
-
- general Purpose Registers:通用寄存器
- 汇编演示:
-
- 打印 pc 寄存器,现在是 fdec4:
-
- 按住 control+Step into,继续打印:
-
- 寄存器除了读还可以写:register write pc 0x104c31ecc,register read pc,此时是读不出来的,因为断点断住,如果 step into,此时断点断在哪里?最终通过验证发现,会断在 cc 的下一行:
-
- 此时 pc 指向 cc,会将 cc 中的指令拿出来执行,执行完毕后走到 cc 的下一条指令,而此时 d0 的指令是还没有执行的。
④ 高速缓存
- iPhoneX 上搭载的 arm 处理器 A11,它的一级缓存的容量是 64kb,二级缓存的容量是 8M。
- CPU 每执行一条指令前都需要从内存中将质量读取到 CPU 内存并执行,而寄存器的运行速度相比内存读写要快很多。
- 为了性能,CPU 还集成了一个高速缓存区域,当程序运行时,先将要执行的指令代码以及数据复制到高速缓存中(由操作系统完成),然后 CPU 直接从高速缓存依次读取指令来执行。
⑤ bl 指令
- CPU 从何处执行指令,是由 pc 中的内容决定的,可以通过改变pc的内容来控制 CPU 执行目标指令;
- arm64 提供了一个 mov 指令(传送指令),可以用来改变大部分寄存器的值,例如 mov x0,#10、mov x1,#20;
- mov 指令并不能用于设置 pc 的值,arm64 没有提供这样的功能;
- arm64 提供了另外的指令来修改 pc 的值,这些指令统一称为转移指令,其中最简单的是 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
- 现在来验证这段汇编代码的执行,新建一个空文件,命名为 asm.s,这可以看成是一个汇编代码文件,会编译成源码:
- 在 VC 中定义函数的声明:
// 函数声明
int A();
int test() {
int cTemp = 0x1FFFFFFFF;
return cTemp;
}
- (void)viewDidLoad {
[super viewDidLoad];
A();
}
- 在 A() 执行处加断点,并执行程序,开启汇编调试;
- 按住 control+step into,进入 A 的详情汇编:
- 然后开始 lldb 调试,进入汇编代码 A:
- 进入汇编代码 B:
- 执行到 A 的 ret:
- 执行到这里出现了死循环,这是为什么呢?这是因为返回地址寄存器的关系,关系到了函数的调用栈。
五、总结
- 汇编概述:
-
- 使用助记符代替机器指令的一种编程语言;
-
- 汇编和机器指令是一一对应的关系,拿到二进制就可以进行反汇编;
-
- 由于汇编和 CPU 的指令集是对应的,所以汇编不具备移植性。
- 总线:是一堆导线的集合;
-
- 地址总线:地址总线的宽度决定了寻址能力;
-
- 数据总线:数据总线的宽度决定了 CPU 数据的吞吐量。
- 进制:
-
- 任意进制,都是对应个数的符号组成,符号可以自己定义;
-
- 2/8/16 进制是相对完美的进制,他们之间的关系:
-
-
- 3 个 2 进制位使用一个 8 进制位标识;
-
-
-
- 4 个 2 进制位使用一个 16 进制位标识;
-
-
-
- 两个 16 进制位可以标识一个字节;
-
-
- 数量单位:1024 = 1K,1024K = 1M,1024M = 1G;
-
- 容量单位:
-
-
- 1024B = 1KB,1024KB = 1MB,1024MB = 1GB;
-
-
-
- B:byte(字节)1B = 8bit;
-
-
-
- bit(比特):一个二进制位;
-
-
-
- 数据宽度:计算机中的数据是有宽度的,超过就会溢出。
-
- 寄存器: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 寄存器的值。
-