汇编-基本概念
在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app
来说,一个APP
安装在手机上面的可执行文件本质上是二进制文件。因为iPhone
手机本质上执行的指令是二进制。是由手机上的CPU
执行的,静态分析是建立在分析二进制上面。
汇编语言的发展
机器语言
由0
和1
组成的机器指令。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
代码在终端设备上的过程:
15193669666308.jpg
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言(不是一一对应关系,反推出是不准确的,只能大致。)
汇编语言的特点
- 可以直接访问、控制各种硬件设备,比如存储器、
CPU
等,能最大限度地发挥硬件的功能 - 能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 汇编指令是机器指令的助记符,同机器指令一一对应。每一种
CPU
都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性 - 开发者需要对
CPU
等硬件结构有所了解,不易于编写、调试、维护 - 不区分大小写,比如
mov
和MOV
是一样的
汇编的用途
- 编写驱动程序、操作系统(比如
Linux
内核的某些关键部分) - 对性能要求极高的程序或者代码片段,可与高级语言混合使用(
内联汇编
) - 软件安全
1.病毒分析与防治
2.逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客 - 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
汇编语言的种类
目前讨论比较多的汇编语言有:
-
8086
汇编(8086
处理器是16bit
的CPU
) -
Win32
汇编 -
Win64
汇编 -
ARM
汇编(嵌入式、Mac
、iOS
) - ......
iPhone
里面用到的是ARM
汇编,但是不同的设备也有差异(因CPU
的架构不同)。
位数 | 架构 | 设备 |
---|---|---|
32 | armv6 | iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch |
32 | armv7 | iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4 |
32 | armv7s | iPhone5, iPhone5C, iPad4(iPad with Retina Display) |
64 | arm64 | iPhone5s,iPhone6、7、8,iPhone6、7、8 Plus,iPhone X,iPad Air,iPad mini2(iPad mini with Retina Display) |
64 | arm64e | XS/XS Max/XR/ iPhone 11, iPhone 11 pro 及以后 |
64 | x86_64 | 模拟器64位处理器 (intel) |
32 | i386 | 模拟器32位处理器(intel) |
⚠️:苹果A7
处理器支持两个不同的指令集:32
位ARM
指令集(armv6|armv7|armv7s
)和64
位ARM
指令集(arm64
)
汇编相关的学习需要了解CPU等硬件结构,最为重要的是CPU/内存。在汇编中,大部分指令都是和CPU与内存相关的。
APP/程序的执行过程:
15193672391363.jpg
可执行文件(磁盘中)加载到内存中叫image
(镜像文件),早期都是直接拷贝,后面为了安全有了加壳加密签名验证。在cpu
眼中所有的数据都是0
和1
,那么怎么区分指令和数据呢?通过pc
寄存器区分。
总线
- 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
- 总线:一根根导线的集合
- 总线的分类: 地址总线(AB)、数据总线(CB)、控制总线(DB)
比如:
image.png
执行过程:
1.地址总线先去内存地址。
2.控制读取发送读/写命令。
3.数据总线写数据->内存/ 内存发送数据->数据总线
地址总线:
- 它的宽度决定了CPU的寻址能力(也就是寻址范围)
-
8086
的地址总线宽度是20
,所以寻址能力是1M
( 220)(这里的M
是大小,数量单位)
image.png
内存中的MB
是容量单位。如果内存很大, 地址总线宽度不够怎么处理?以前的cpu是通过2次寻址相加得到一个最终的值来访问内存,现在的cpu没有寻址能力的问题。
数量单位:M,K。1M = 1024K,1K= 1024。比如:10,100
容量单位:字节Byte。 1024B = 1KB,1024KB = 1MB。比如:10个,100只。(大部分计算机都是以1个字节为单位。银行系统的IBM电脑例外是2个字节为单位。)
对于100M 宽带,这里的100M是100Mbps(每秒钟传递多少二进制位,bit位。所以100M带宽理论下载速度12.5MB/s)。
数据总线:
- 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度(吞吐量)
-
8086
的数据总线宽度是16
,所以单次最大传递2
个字节的数据
我们现在常说的32位,64位cpu说的就是它的数据吞吐量。1次放电分别4字节,8字节数据。
控制总线:
- 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制
案例:
1.一个CPU 的寻址能力为8KB
,那么它的地址总线的宽度为____
答案:8KB对应 8192, 213 = 8192 所以为13。
-
8080,8088,80286,80386
的地址总线宽度分别为16根,20根,24根,32根
。那么他们的寻址能力分别为多少____KB, ____MB,____MB,____GB?
答案:1kb = 210 = 1024
1kb * 26 = 64kb
1kb * 1kb = 1mb
1mb * 24 = 16mb
1kb * 1kb * 1kb * 22 = 4gb -
8080,8088,8086,80286,80386 的数据总线宽度分别为8根,8根,16根,16根,32根.那么它们一次可以传输的数据为:____B,____B,____B,____B,____B
答案:1 、1、2、2、4
4.从内存中读取1024字节的数据,8086至少要读____次,80386至少要读取____次.
答案:8086 数据总线宽度为16。8086一次读2个字节,那么需要512次,80286数据总线宽度为32,一次4个字节,需要256次。
内存
image.png image.png image.png- 内存地址空间的大小受
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
进一
⚠️:进制的本质是符号。
案例:
- 1 + 1 在____情况下等于 3 ?
除了算错的情况下。在十进制由10
个符号组成,假如由:0 1 3 2 8 A B E S 7
组成逢十进一,那么在这种情况下1+1=3
传统定义的十进制和自定义的十进制不一样。那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的,可以用于加密!
⚠️:十进制由十个符号组成,逢十进一,符号是可以自定义的!!!
- 八进制运算:
- 2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
答案:5,6,11,24 -
277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .
答案:632, 20250, 163,47
image.png
八进制加法表
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
二进制的简写形式
二进制: 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 这就是十六进制了
数据的宽度
数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。
int test() {
int cTemp = 0x1FFFFFFFF;
return cTemp;
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%x",test());
}
输出:
ffffffff
image.png
数据溢出了。刚开始
cTemp
默认值1
,溢出后变为-1
(第一位符号位,1代表负数,0代表正数。往后逐位取反,末尾加1。)。
(lldb) p cTemp
(int) $0 = 1
(lldb) p cTemp
(int) $1 = -1
(lldb) p &cTemp
(int *) $2 = 0x000000016b3a9b1c
(lldb) x 0x000000016b3a9b1c
0x16b3a9b1c: ff ff ff ff 10 00 00 00 00 00 00 00 ef 98 3a 6b ..............:k
0x16b3a9b2c: 01 00 00 00 70 a9 f0 59 01 00 00 00 50 d4 a5 04 ....p..Y....P...
(lldb) p (uint)cTemp
(uint) $3 = 4294967295
在Debug -> Debug Workflow -> View Memory
中也可以查看(这里查看内容更新后需要翻页刷新然后切换回来才能显示新值):
再看下汇编代码(
Debug -> Debug Workflow -> Always Show Disassembly
):
TestDemo`test:
0x104a59ec8 <+0>: sub sp, sp, #0x10 ; =0x10
0x104a59ecc <+4>: mov w8, #-0x1
0x104a59ed0 <+8>: str w8, [sp, #0xc]
-> 0x104a59ed4 <+12>: ldr w0, [sp, #0xc]
0x104a59ed8 <+16>: add sp, sp, #0x10 ; =0x10
0x104a59edc <+20>: ret
可以看到直接将-1
给力w8
。指令在内存中占用4
字节。
计算机中常见的数据宽度
- 位(
Bit
): 1个位就是1个二进制位。0
或者1
。 - 字节(
Byte
): 1个字节由8个Bit
组成(8位
)。内存中的最小单元Byte
。 - 字(
Word
): 1个字由2个字节组成(16位
),这2
个字节分别称为高字节和低字节。 - 双字(
Doubleword
): 1个双字由两个字组成(32位
)。
计算机存储数据会分为有符号数和无符号数(对于数据本身内容没有变化,取决于你怎么看):
image.png
无符号数,直接换算!
有符号数:
正数: 0 1 2 3 4 5 6 7
负数: F E D B C A 9 8
-1 -2 -3 -4 -5 -6 -7 -8
自定义进制符号
案例:
- 现在有10进制数10个符号分别是:2,9,1,7,6,5,4, 8,3 , A 逢10进1 那么: 123 + 234 = ____
十进制:
0 1 2 3 4 5 6 7 8 9
自定义:
2 9 1 7 6 5 4 8 3 A
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
转换后加法表:
9+9 = 1
9+1 = 7 1+1 = 6
9+7 = 6 1+7 = 5 7+7 = 4
9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
9+5 = 4 1+5 = 8 7+5 = 3 6+5 = A 5+5 = 92
9+4 = 8 1+4 = 3 7+4 = a 6+4 = 92 5+4 = 99 4+4 = 91
9+8 = 3 1+8 = A 7+8 = 92 6+8 = 99 5+8 = 91 4+8 = 97 8+8 = 96
9+3 = A 1+3 = 92 7+3 = 99 6+3 = 91 5+3 = 97 4+3 = 96 8+3 = 95 3+3 = 94
9+A = 92 1+A = 99 7+A = 91 6+A = 97 5+A = 96 4+A = 95 8+A = 94 3+A = 98 A+A = 93
123 + 234 = 1A6
- 现在有9进制数 9个符号分别是:2,9,1,7,6,5,4, 8,3 逢9进1 那么: 123 + 234 = __
十进制:
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
转换后加法表:
9+9 = 1
9+1 = 7 1+1 = 6
9+7 = 6 1+7 = 5 7+7 = 4
9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
9+5 = 4 1+5 = 8 7+5 = 3 6+5 = 92 5+5 = 99
9+4 = 8 1+4 = 3 7+4 = 92 6+4 = 99 5+4 = 91 4+4 = 97
9+8 = 3 1+8 = 92 7+8 = 99 6+8 = 91 5+8 = 97 4+8 = 96 8+8 = 95
9+3 = 92 1+3 = 99 7+3 = 91 6+3 = 97 5+3 = 96 4+3 = 95 8+3 = 94 3+3 = 98
123 + 234 = 725
CPU&寄存器
内部部件之间由总线连接
image.png
CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。
CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器。
对于arm64
系的CPU
来说, 如果寄存器以x
开头则表明的是一个64位
的寄存器,如果以w
开头则表明是一个32
位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中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(零寄存器)
,这些通用寄存器有时也有特定用途。
1.64位
:X0-X30, XZR(零寄存器)
。w0
到w28
这些是32位
的。因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
2.32位
:W0-W30, WZR(零寄存器)
。w0
就是x0
的低32位
!
⚠️:了解过8086汇编的都知道,有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中。在ARM中并没有。
在"Xcode"中我们可以查看具体寄存器的内容:
image.png image.png image.png
分别看一下
x0
和w0
的值:
x0 unsigned long 0x0000000159f0a970
w0 unsigned int 0x59f0a970
验证了w0
是x0
的低32位。
通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间:
- CPU首先会将红色内存空间的值放到X0寄存器中:mov x0,红色内存空间
- 然后让X0寄存器与1相加:add x0,1
- 最后将值赋值给内存空间:mov 蓝色内存空间,x0
pc寄存器
单步执行汇编代码(pc始终指向下一条指令):
image.png
修改pc的指向(跳过了中间的指令):
image.png
- 为指令指针寄存器,它指示了CPU当前要读取指令的地址(指向下一条即将执行的指令)
- 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
- CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
比如 1110 0000 0000 0011 0000 1000 1010 1010,
可以当做数据 0xE003008AA。
也可以当做指令 mov x0, x8
- CPU根据什么将内存中的信息看做指令?
CPU将pc指向的内存单元的内容看做指令
如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过。
高速缓存
iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M。
CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成)。CPU直接从高速缓存依次读取指令来执行。
bl指令
bl
分位b
和l
:
b
:跳转。
l
:lr寄存器。
- 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
分析:
Xcode
中创建Empty
文件命名为asm.s
(.s
汇编代码会被Xcode
自动识别编译)。
//asm.s
.text // 告诉是代码
.global _A, _B //.global 是标号
_A:
mov x0,#0xa0 //a0 给 x0 x0 = 0xa0
mov x1,#0x00 //00 给x1 x1 = 0x00
add x1, x0, #0x14 //x0 + 0x14 给 x1 x1 = 0xb4
mov x0,x1 //x1 的值给 x0 x0 = 0xb4
bl _B //跳转B
mov x0,#0x0 //0x0 给 x0 x0 = 0x0
ret //return 上层调用的地方
_B:
add x0, x0, #0x10 //x0 + 0x10 给 x0 x0 = 0xc4
ret //return A
oc
调用汇编:
//ViewController.m
int A();
- (void)viewDidLoad {
[super viewDidLoad];
A();
}
swift
调用汇编:
//声明方法A。Swift中C和汇编都可以这么暴露。
@_silgen_name("A")
func A()
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
A();
}
}
答案:0x00
断点验证了
x0
最终值为0x00
。这里有个问题是发生死循环了。(bl
跳转指令导致的,lr
寄存器在跳转后需要保护现场还原。)
总结
-
汇编概述:
- 使用助记符代替集齐指令的一种编程语言。
- 汇编和及其指令是一一对应的关系,拿到二进制就可以反汇编。
- 由于汇编和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的值可以改变程序的执行流程!