初识汇编
静态分析 看汇编 二进制代码 分析出代码的实现逻辑 在逆向iOS系统上面的App。我们知道,一个App安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是有手机CPU执行的。所以静态分析是建立在分析二进制上面。
动态分析 调试应用 例如可以通过xcode调试App的UI
为什么是用二进制? CPU只能读懂0和1。0代表没电,1代表有电。 0和1是最稳定的 如果是用10进制表示,0安到9安表示数据,1.5安怎么表示?最稳定的就两个状态,有电1和没电0。
##汇编语言的发展
###机器语言 有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
PS: 助记符就是汇编语言的前身. PS: 任何语言的产生,都会需要这门语言的编译器。编译器会读懂高级语言,将高级语言转化为0和1的二进制组合, 让CPU可以读懂。后期为了更加高效的编程,就有了高级语言。
高级语言(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
PS: 高级语言也会有弊端。比如 高级语言为了保证高级语言的特性,比较抽象的概念,编译的过程中文件的体积就会更大,不会那么直观。高级语言写出来的文件会比二进制文件更大。
我们的代码在终端设备上是这样的过程:
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过汇编得到汇编语言/机器语言, 但是汇编语言/机器语言几乎不可能还原为高级语言
PS: 高级语言和汇编语言不是一一对应的,所以不能还原。不同的语言,不同的代码,可以生成同样的汇编指令。同一门语言,不同的代码,可以生成同样的汇编指令。所以只能大致推算出逻辑和算法,不能完全还原。 PS: CPU有不同的型号。不同的型号会有不同的CPU架构。不同的CPU架构对应不同的指令集。
汇编语言的特点
- 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度的发挥硬件的功能。
- 能不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 汇编指令是机器指令的助记符,同机器指令一一对应,每一种CPU都有自己的机器指令集\汇编指令集,汇编语言不具备可移植性
- 知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试和维护
- 不区分大小写,比如mov和MOV是一样的
汇编的种类
目前讨论比较多的汇编语言有:
- 8086汇编(8086处理器是16bit的CPU)
- Win32汇编
- Win64汇编
- ARM汇编(嵌入式, Mac, iOS)
- ...
我们iPhone里面用到的ARM汇编,但是不同的设备也有差异。因CPU的架构不同
PS: armv6和armv7是32bit的 PS: 32bit和64bit在数据吞吐量上不一样,和总线是有关系的。
几个必要的常识
- 要想学好汇编,首先需要了解CPU等硬件结构
- App/程序的执行流程
- 硬件相关最为重要的是CPU/内存
- 在汇编中,大部分指令都是和CPU与内存相关的
PS:可执行文件->加载到内存之后->image(镜像文件) PS: CPU读取内存的时候,如何区分指令和数据。 PC寄存器
总线
- 每个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟 外部的器件进行交互
- 总线:一个个导线的集合
- 总线的分类
- 地址总线 确定地址到内存寻找数据
- 数据总线 数据的查找和读写,传递数据
- 控制总线
举个例子:CPU从内存的3单元读取数据
- CPU 通过地址总线通知内存,要操作3位置的数据
- CPU 通过控制总线通知内存,要读取数据
- 通过数据总线,将内存中3位置的数据返回给CPU
PS:《汇编语言》 王爽
地址总线
- 它的宽度决定了CPU的寻址能力
- 8086的总线宽度为20,所以寻址能力为1M(2^^20)
PS: 有多少根地址总线就有多宽。比如有10个,每一个表达的能力是0和1,10根线,就有10个0和1的组合。和表示的数据的大小有关。地址总线越大,读到的数值就越大,那么寻址能力愈强。
PS:1M 是数量单位 1MB 是内存的容量 内存的地址单元是字节Byte,内存最大是1MB
PS: 数量单位 1M = 1024K 1K = 1024 PS: 容量单位 1024B = 1KB 1024KB = 1MB 内存的单位是1个字节 PS: IBM 银行的单独系统不是字节为单位 是两个字节为一个单位 PS: 宽度 100M 是100Mbps 每秒钟传输多少个二进制位(bit位)是传输速率。理论的下载速度 100/8 = 12.5MB/s
数据总线
- 它的宽度决定了CPU的单次数据传输量,也就输数据的传输速度
- 8086的数据总线的宽度是16,所以单次最大传递2个字节的数据
PS: 数据的吞吐量 平时说的32位 64位就是数据的吞吐量 32位 32 / 8 = 4字节 64位 64 / 8 = 8字节。64位,一个指针是8个字节。 PS: CPU上数据单元的大小
控制总线
- 它的宽度决定了CPU对其他器件的控制能力,能有多少种控制
内存
- 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位2^^20 个不同的内存单元【内存地址范围0x00000~0xFFFFF】,所以8086的内存空间的大小为1MB
- 0x00000~0x9ffff: 主存储器。可读可写
- 0xA0000~0xBFFFF: 向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
- 0xC0000~0xfffff: 存储各种硬件/系统信息。只读
进制
学习进制的障碍 很多人学不好进制,原因是总以十进制为依托去考虑其他进制,需要原酸的时候也总是先转换成是进制,这种学习方法是错误的。
PS 进制本身是符号
进制的定义
- 8进制有8个字符组成 0 1 2 3 4 5 6 7 逢八进一
- 10进制有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个符号如果我们不告诉别人这个符号表,别人就没有办法拿到我们具体的数据。可以用户加密。 PS: 十进制有十个符号组成,逢十进一,符号就是自定义的
进制的运算
二进制的简写形式
PS: 半斤八两 古称的1斤就是16两
| 二进制 | 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 就是十六进制了
计算机中常用的数据宽度
- 位(bit): 1位就是一个二进制位。0或者1
- 字节(Byte): 1个字节是有8个bit组成(8位), 内存中的最小单元Byte
- 字(Word): 1个字有两个字节组成(16位),这两个字节分别称为高字节和低字节
- 双字(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
PS: 符号放在第一位
自定义进制符号
CPU和寄存器
内部部件之间由总线链接
CPU除了由控制器、运算器,还有寄存器。其中寄存器的作用就是进行数据的临时存储。
PS: CPU的运算速度是非常快的,为了性能,CPU在那边开辟了一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块存储区域中,运算时就在这一小块临时存储区域进行。我们称这一小块临时存储区域位寄存器。
对于arm64系的CPU来说,如果寄存器以x开头,则表明是一个64位的寄存器,如果以w开头,则说明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器访问和使用。其中32位寄存器是64位寄存器的低32位部分,不是独立存在的。
- 对于程序员来说,CPU中最重要的部件是寄存器,可以通过改变寄存器的内容事件对CPU的控制
- 不同的CPU,寄存器的个数、结构不相同
浮点和向量寄存器
通用寄存器
- 通用寄存器也称数据地址寄存器,通常用来做数据计算的临时存储、累加、计数、地址保存等功能。定义这个寄存器的作用,主要是在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
PS: 一条汇编指令占用4的字节
通用寄存器
pc寄存器(program counter)
- 为指令指针寄存器,它只是了CPU当前要读取指令的地址
- 在内存或者磁盘上,指令和数据是没有区别的,都是二进制信息
- CPU在工作的时候把有的信息看作指令,有的信息看作数据,为同样的信息赋予了不同的意义
- 比如1110 0000 0000 0011 0000 1000 1010 1010
- 可以当作数据0xE003008AA
- 也可以当作指令mov x0, x8
- CPU是根据什么将内存中的信息看作指令?
- CPU将pc指向的内存单元的内容看作指针
- 如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过
PS: ps是CPU即将执行的指令
控制pc寄存器的指向的例子
高速缓冲
iPhoneX上搭载的ARM处理器A11它的1级缓存的容量为64KB,二级缓存的容量为8MB
PS: CPU每执行一条指令前都需要从内存中将指令读取到CPU内执行。而寄存器的运行速度相比内存读取要快得多,为了性能,CPU还集成了一个高速缓存存储区域。当程序运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成)。CPU直接从高速缓存依次读取指令来执行。
PS: 操作系统会建立高速缓存和虚拟内存的映射关系 和 pagefault的策略很像
bl指令
- CPU从何处执行指令是由pc中的内容决定,我们可以通过改变pc的内容来控制CPU执行目标指令
- ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如 * mov x0,#10 mov x1, #20
- 但是,mov指令不能设置pc的值,ARM64没有提供这样的功能
- ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的事bl指令
创建一个汇编文件asm.s,代码如下
.text
.global _A, _B
_A:
mov x0,#0xa0
mov x1,#0x00
add x1,x0,#0x14
mov x0,x1
bl _B
mov x0,#0x00
ret
_B:
add x0,x0,#0x10
ret
在原文件中声明A函数,并调用
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
int A();
void test() {
int a = 10;
// printf("%d", a);
}
- (void)viewDidLoad {
[super viewDidLoad];
// test();
A();
}
@end
总结
- 汇编的概述
- 使用助记符代替机器指令的一种编程语言
- 汇编和机器指令是一一对应的关系,拿到二进制就可以反汇编
- 由于汇编和CPU的指令集是对应的,所以汇编不具备移植性
- 总线 是有一堆导线的集合
- 地址总线
- 地址总线的宽度决定了寻址能力
- 数据总线
- 数据总线的宽度决定了CPU数据的吞吐量
- 地址总线
- 进制 其实是符号
- 任意进制,都是有对应个数的符号组成,符号可以自定义
- 2\8\16是相对完美的进制,他们之间的关系
- 3个二进制位 使用一个8进制标识
- 4个二进制位 使用一个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\WZR30个32位寄存器
- 32位寄存器并不是独立存在的,比如w0时x0的低32位
- PC寄存器:指令指针寄存器
- pc寄存器里面保存的就是CPU接下来需要执行的指令的地址
- 改变pc的值可以改变程序的执行流程