初识汇编
在逆向开发中,非常重要的一个环节就是静态分析,这里以 iOS
系统为例,首先我们是逆向 iOS
系统上面的 APP
,那么我们知道,一个 APP
安装在手机上面的可执行文件本质上是二进制文件,因为 iPhone
手机本质上执行的指令是二进制,是由手机上的 CPU
执行的,所以静态分析是建立在分析二进制文件上面,所以我们要从基础的汇编开始了解。
汇编语言的发展
机器语言
由 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
等硬件结构有所了解,不易于编写、调试、维护 -
不区分大小写,比如
mov
和MOV
是一样的
汇编的用途
- 编写驱动程序、操作系统(比如
Linux
内核的某些关键部分) - 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
- 病毒分析与防治
- 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
- 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
- 函数的本质究竟是什么?
-
++a + ++a + ++a
底层如何执行的? - 编译器到底帮我们干了什么?
-
DEBUG
模式和RELEASE
模式有什么关键的地方被我们忽略
越底层越单纯!真正的程序员都需要了解的一门非常重要的语言,汇编!
汇编语言的种类
-
目前讨论比较多的汇编语言有
-
8086
汇编(8086
处理器是16bit
的CPU
) -
Win32
汇编 -
Win64
汇编 -
ARM
汇编(嵌入式、Mac
、iOS
)等
-
-
我们
iPhone
里面用到的是ARM
汇编,但是不同的设备也有差异,因CPU
的架构而不同。
架构 | 设备 |
---|---|
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以后 |
几个必要的常识
- 要想学好汇编,首先需要了解
CPU
等硬件结构 - APP/程序的执行过程
- 硬件相关最为重要是
CPU
/内存 - 在汇编中,大部分指令都是和
CPU
与内存相关的
总线
- 每一个
CPU
芯片都有许多管脚,这些管脚和总线相连,CPU
通过总线跟外部器件进行交互 - 总线:一根根导线的集合
- 总线的分类
- 地址总线
- 数据总线
- 控制总线
举个例子:
- 地址总线
- 它的宽度决定了
CPU
的寻址能力 - 8086 的地址总线宽度是 20,所以寻址能力是 1M( 2^20)
- 它的宽度决定了
-
数据总线
- 它的宽度决定了
CPU
的单次数据传送量,也就是数据传送速度 - 8086 的数据总线宽度是 16,所以单次最大传递 2 个字节的数据
- 它的宽度决定了
-
控制总线
- 它的宽度决定了
CPU
对其他器件的控制能力、能有多少种控制
- 它的宽度决定了
内存
-
内存地址空间的大小受
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 进一
进制的运算
八进制加法表
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 这就是十六进制了
数据的宽度
数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。
#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]));
}
}
计算机中常见的数据宽度
- 位(
Bit
):1 个位就是 1 个二进制位,0 或者 1 - 字节(
Byte
):1 个字节由 8 个Bit
组成(8位),内存中的最小单元Byte
。 - 字(
Word
):1个字由 2 个字节组成(16位),这 2 个字节分别称为高字节和低字节。 - 双字(
Doubleword
):1个双字由两个字组成(32位)
那么计算机存储数据它会分为有符号数和无符号数,那么关于这个看图就理解了!
无符号数,直接换算!
有符号数:
正数: 0 1 2 3 4 5 6 7
负数: F E D B C A 9 8
-1 -2 -3 -4 -5 -6 -7 -8
CPU&寄存器
内部部件之间由总线连接:
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
(零寄存器),这些通用寄存器有时也有特定用途。- 那么
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
寄存器指向过
-
高速缓存
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
指令