前言
我们在学习逆向开发之前,我们要了解一个基本的逆向原理.首先我们是逆向 iOS 系统上面的 APP .那么我们知道,一个 APP 安装在手机上面的可执行文件本质上是二进制文件.因为 iPhone 手机本质上执行的指令是二进制.是由手机上的 CPU 执行的.所以逆向开发是建立在分析二进制上面.所以今天我们接下来的课程从非常基础的东西开始讲解.
1.1 汇编语言的发展
1.1.1 机器语言
由 0 和 1 组成的机器指令.
- 加:0100 0000
- 减:0100 1000
- 乘:1111 0111 1110 0000
- 除:1111 0111 1111 0000
1.1.2 汇编语言(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.1.3 汇编语言的特点
可以直接访问、控制各种硬件设备,比如存储器、CPU 等,能最大限度地发挥硬件的功能
能够不受编译器的限制,对生成的二进制代码进行完全的控制
目标代码简短,占用内存少,执行速度快
汇编指令是机器指令的助记符,同机器指令一一对应。每一种 CPU 都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
知识点过多,开发者需要对 CPU 等硬件结构有所了解,不易于编写、调试、维护
不区分大小写,比如 mov 和 MOV 是一样的
1.1.4 汇编的用途
编写驱动程序、操作系统(比如 Linux 内核的某些关键部分)
对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
-
软件安全
- 病毒分析与防治
- 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
理解整个计算机系统的最佳起点和最有效途径
为编写高效代码打下基础
-
弄清代码的本质
- 函数的本质究竟是什么?
- ++a + ++a + ++a 底层如何执行的?
- 编译器到底帮我们干了什么?
- DEBUG 模式和 RELEASE 模式有什么关键的地方被我们忽略
- ......
1.1.5 汇编语言的种类
-
目前讨论比较多的汇编语言有
- 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,iPhoneXR, iPhoneXS , iPad Air, iPad mini2以后 |
注意:
真机 32 位处理器是 armv7/armv7s 架构
真机 64 位处理器是 arm64 架构
几个必要的常识
- 要想学好汇编,首先需要了解 CPU 等硬件结构
- 程序的执行过程
- 硬件相关最为重要是 CPU /内存
- 在汇编中,大部分指令都是和CPU与内存相关的
1.2 总线
- 每一个 CPU 芯片都有许多管脚,这些管脚和总线相连,CPU 通过总线跟外部器件进行交互
- 总线:一根根导线的集合
- 总线的分类
- 地址总线
- 数据总线
- 控制总线
举个例子
1.2.1 地址总线
它的宽度决定了 CPU 的 寻址能力
8086的地址总线宽度是 20,所以寻址能力是 1M( 2^20 )
1.2.2 数据总线
它的宽度决定了 CPU 的单次数据传送量(吞吐量),也就是数据的 传送速度
8086的数据总线宽度是 16 ,所以单次最大传递 2个字节 的数据
1.2.3 控制总线
- 它的宽度决定了 CPU 对其他器件的 控制能力 , 也就是能有多少种控制
1.3 内存
内存地址空间的大小受 CPU 地址总线宽度的限制。8086 的地址总线宽度为 20,可以定位 2^20 个不同的内存单元(内存地址范围 0x00000~0xFFFFF),所以 8086 的内存空间大小为1MB
0x00000~0x9FFFF:主存储器。可读可写
0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
0xC0000~0xFFFFF:存储各种硬件/系统信息。只读
1.3 进制
1.3.1 进制的定义
- 八进制由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个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的!用于加密!
十进制由十个符号组成,逢十进一,符号是可以自定义的!!
1.3.2 进制的运算
八进制加法表
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.3.3 二进制的简写形式
二进制: 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 这就是十六进制了
1.3.4 数据的宽度
数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。
#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.3.5 计算机中常见的数据宽度
- 位(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
1.4 寄存器
内部部件之间由总线连接
- 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
- 不同的CPU,寄存器的个数、结构是不相同的
ARM64 有34个寄存器,包括31个通用寄存器、SP、PC、CPSR。
寄存器 | 位数 | 描述 |
---|---|---|
x0到x30 | 64位 | 通用寄存器,如果有需要的时候可以当做 32 位使用(w0到w30) |
SP | 64位 | 在任何时刻都是保存栈地址(栈顶地址) |
PC | 64位 | 总是指向CPU下一条将要执行的指令的地址,在ARM64 中软件不能改写PC中的值 |
CPSR | 64位 | 状态寄存器 |
1.4.1 通用寄存器
-
ARM64 拥有有 31 个 64 位的通用寄存器 x0 到 x30 ,这些寄存器通常用来存放一般性的数据,称为通用寄存器(有时也有特定用途)
- 那么 w0 到 w28 表示 32 位的. 因为 64 位 CPU 可以兼容 32位. 所以可以只使用 64 位寄存器的低 32 位.
- 比如 w0 就是 x0 的低 32 位!
15193699098685.jpg
- 通常,CPU 会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
- 假设内存中有块红色内存空间的值是 3,现在想把它的值加 1,并将结果存储到蓝色内存空间
- CPU首先会将红色内存空间的值放到X0寄存器中:mov X0, 红色内存空间
- 然后让X0寄存器与1相加:add X0, 1
- 最后将值赋值给内存空间:mov 蓝色内存空间, X0
1.4.2 PC 寄存器(program counter)
也称为指令指针寄存器,它指示了CPU当前要读取指令的地址
在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
-
CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- 比如 1110 0000 0000 0011 0000 1000 1010 1010
- 可以当做数据 0xE003008AA
- 也可以当做指令 mov x0, x8
-
CPU 根据什么将内存中的信息看做指令?
- CPU 将 pc 指向的内存单元的内容看做指令
- 如果内存中的某段内容曾被 CPU 执行过,那么它所在的内存单元必然被 pc 指向过