docs: 汇编研究

This commit is contained in:
LiuBinPeng
2022-06-23 14:59:47 +08:00
parent 55d66cc4c5
commit f0e20eaf2e
30 changed files with 1042 additions and 347 deletions

640
Chapter1 - iOS/1.109.md Normal file
View File

@@ -0,0 +1,640 @@
# 汇编学习
## 基础知识回顾
地址总线:它的宽度决定了 CPU 的寻址能力。比如8086地址总线宽度为20则寻址能力为1M2的20次方
数据总线:它的宽度决定了 CPU 单次数据传送量也就是数据传输速度。比如8086的数据总线宽度为16所以单次最大传递2个字节的数据/
控制总线:它的宽度决定了 CPU 对其他其间的控制能力,能有多少种控制。
内存的分段管理:起始地址+偏移地址=物理地址,计算出物理地址再去访问内存。
偏移地址为16位16位地址的寻址能力位64kb所以一个段的长度最大为64kb。
## CPU 的典型构成
- 寄存器:信息存储
- 运算器:信息处理
- 控制器:控制其他器件进行工作
对开发同学来说CPU 中最主要的部件就是寄存器,“可以通过改变寄存器的内容来实现对 CPU 的控制”。
不同的 CPU寄存器个数、结构是不同的比如8086是16为结构的 CPU8086有14个寄存器
## 通用寄存器
AX、BX、CX、DX 这4个寄存器通常用来存放一般性的数据成为通用寄存器有时候也有特定用途
通常 CPU 会把内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
例如在内存中有一个内存空间a值为3需要将它的值加1然后将结果存储到内存空间b上。
- `mov ax, a`。CPU 首先会将内存空间 a 的值放到寄存器 ax 中
- `add ax, 1`。调用 add 指令,将 ax + 1
- `mov b, ax`。调用 mov 将 ax 中的值赋值给内存空间 b
### CS和IP
CS 为代码段IP 为指令指针寄存器,它们代表 CPU 当前要读取指令的地址
任意时刻8086 CPU 都会将 `CS:IP` 指向的指令作为下一条需要取出执行的指令。
IP 只为 CS 提供服务。
8086 CPU 工作过程如下:
-`CS:IP` 指向的内存单元读取指令,读取的指令进入指令缓冲器
- IP = IP + 当前所读取指令的长度。从而指向 IP 指向下一条指令
- 执行指令转到步骤1重复执行
在8086CPU 加电启动或者复位后(即 CPU 刚开始工作时CS 被设置位 CS=FFFFHIP 被设置为 IP=0000H即在8086PC 机刚启动时CPU 从内存 FFFF0H 单元中读取指令执行FFFF0H 单元中的指令是 8086PC 机开机后执行的第一条指令。
注意在内存或者磁盘上指令和数据其实没有差别都是二进制信息。CPU 在工作时把有的信息看成指令,有些信息看数据,为同样的信息赋予了不同意义。
那么 CPU 根据什么来判断这块内存上的信息是指令还是数据?
- CPU 将 `CS:IP` 所指向的内存单元的内容看作指令
- 如果内存中的某段内容曾被 CPU 执行过,那么它所在的内存单元肯定被 `CS:IP` 指向过
### jmp 指令
mov 指令不能用于设置 CS、IP 的值8086没有提供该功能。可以通过 jmp 指令来实现修改 CS、IP 的值,这些指令被成为转移指令。
`jmp 段地址:偏移地址` 可以实现同时修改 CS、IP 的值,表示用指令中给出的段地址修改 CS偏移地址 IP。
`jmp 2AE3:3` 执行后表示CS=2AE3HIP=0003HCPU 将从2AE33H处读取指令。
QA下面3条指令执行完毕后CPU 修改了几次 IP 寄存器?
```shell
mov ax, bx
sub ax, ax
jmp ax
```
修改了4次。每执行一条指令IP 都会被修改1次IP=IP+该条指令的长度最后一条指令执行后IP 寄存器的值也会被修改1次共3+1=4次。
### ds 寄存器
CPU 要读写一个内存单元时必须要给出这个内存单元的地址在8086中内存地址由段地址和偏移地址组成。
8086中有一个 DS 段寄存器,通常用来存放要访问数据的段地址
```shell
mov bx, 1000H
mov ds, bx
mov al, [0]
```
上面3条指令的意思是将 10000H 1000:0中的内存数据赋值到 al 寄存器中。
`mov al, [address]` 的意思是将 DSaddress 该地址中的内存数据赋值到 al 寄存器中。
由于 al 是8位寄存器所以上述命令是将一个字节的数据赋值给 al 寄存器。
tips8086 不支持将数据直接送入段寄存器,所以 `mov ds, 1000H` 是错误的。
QA写指令来实现将 al 中的数据写入到内存单元 10000H 中
```shell
mov ax, 1000
mov ds, ax
mov [0], al
```
QA内存中有如下数据写出下面指令执行后寄存器 ax 的值?
| 10000H | 23 |
| ------ | --- |
| 10001H | 11 |
| 10002H | 22 |
| 10003H | 66 |
```shell
mov ax, 1000H
mov ds, ax
mov ax, [2]
```
代码分析:
- 第一条指令ax 寄存器存放了 1000H 这个地址
- 第二条指令,将访问 ds 数据段,在 1000H 这个地址出访问
- 第三条指令,将数据段中 1000H 这个地址处,偏移 2也就是内存中 10002H 这个的值22写入到 ax 中,由于 ax 寄存器是16位所以会取2个单位的数据22和66。所以 ax 的值为2266。
8086 CPU 下AX、BX、CX、DX 等通用寄存器均被分为高位和低位AX = AH + AL其中高位寄存器和低位寄存器。高位和低位都是16位。所以会从10002H 开始取2个16位的数据赋值给 ax。
思考:如果代码改变下呢,如下
```shell
mov ax, 1000H
mov ds, ax
mov al, [2]
```
此时 al 的值为多少al 和 ax 的区别在于 ax = ah + al所以 al 的情况下直接从 10002H 开始取1个16位的数据所以 al 为 0022。
### 大小端序
小端序,指的是数据的高字节保存在内存的高地址中,数据的低字节保存在内存的低地址中
大端序,指的是数据的高字节保存在内存的低地址中,数据的低字节保存在内存的高地址中。
注意:这里的大小端序还存在网络大小端序 NBO 和主机大小端序 HBO详细可以查看我[这篇文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md)
Big EndianPowerPC、IBM、Sun
Little Endianx86、DEC
ARM大小端序模式下均可工作。
16bit 宽的数0x1234 分别在大小端序模式的 CPU 内存存放形式为
| 内存地址 | 小端序 | 大端序 |
| ------ | ---- | ---- |
| 0x4000 | 0x34 | 0x12 |
| 0x4001 | 0x12 | 0x34 |
32bit宽的 0x12345678 分别在大小端序模式的 CPU 内存存放形式为
| 内存地址 | 小端序 | 大端序 |
| ------ | ---- | ---- |
| 0x4000 | 0x78 | 0x12 |
| 0x4001 | 0x56 | 0x34 |
| 0x4002 | 0x34 | 0x56 |
| 0x4003 | 0x12 | 0x78 |
QA将 0x1122 存放在 0x40002 中,如何存储?
分析 0x1122需要2个字节0x40002 是1个字节所以肯定需要在 0x40002和向后的一个字节中存储。然后考虑主机序的大小端情况。假设小端模式下
0x40000
0x40001
0x40002 0x22
0x40003 0x11
### 指令操作明确 CPU 操作的内存
```shell
mov ax, 1000H
mov ds, ax
mov word ptr [0], 66h
```
上述代码先把 1000H 写入 ax 寄存器,然后访问数据段的 1000H 内存然后将66h写入到数据段的0位置但是 word 告诉了 CPU 需要操作2个字节也就是 00 66
指令执行前1000: 0000 11 22 00 00 00 00 00 00
指令执行后1000: 0000 00 66 00 00 00 00 00 00
如果将第三行代码改为 `mov byte ptr [0], 66h`意味着明确告诉计算机需要操作1个字节也就是66 。
指令执行前1000: 0000 11 22 00 00 00 00 00 00
指令执行后1000: 0000 66 22 00 00 00 00 00 00
## 栈
栈是一种后进先出特点的数据存储空间LIFO
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Stack.png)
- 8086 会将 CS 作为代码段的段地址,将 `CS:IP` 指向的指令作为下一条需要取出执行的指令
- 8060会将 DS 作为数据段的段地址,`mov ax, [address]` 就是取出 `DS:address` 内存区域上的数据放到 ax 寄存器中
- 8086会将 SS 作为栈段的段地址,`SS:SP` 指向栈顶元素
- 8086提供了 PUSH 指令用来入栈POP 出栈。PUSH ax 是将 ax 的数据入栈pop ax 是将栈顶的数据送入 ax
SS: 栈的段地址
SP堆栈寄存器存放栈的偏移地址
#### push
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StackPush.png)
`push ax` 指令执行,会拆解为:
- `SP = SP-2` `SS:SP` 指向当前栈顶前面的单元,更新栈顶指针
- 将 ax 中的数据送入到 `SS:SP` 所指向的内存单元处
ax = ah + al所以 ax 中的数据入栈需要占据2个单位sp = sp - 2
#### pop
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/stackPop.png)
`pop ax` 指令执行,会拆解为:
-`SS:SP` 栈顶指向的内存单元中的数据2个单位写入到 ax 寄存器中
- `SP = SP + 2`,更新 `SS:SP` 栈顶的地址
注意:
当一个栈空间是空的时候,`SS:SP` 指向栈空间最高地址单元的下一个单元。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/emptyStack.png)
当一个栈空或者满的时候,执行 PUSH、POP 指令需要注意,因为 `SP = SP + 2``SP = SP - 2` 都会导致将错误的数据入栈或者错误的数据出栈,导致发生不可预期的事情。
QA将10000H~1000FH 这段空间当作栈初始栈为空AX = 001AHBX=001BH利用栈交换 AX、BX 中的数据
```shell
push ax
push bx
pop ax
pop bx
```
### 段总结
数据段:存放数据的段
代码段:存放代码的段
栈段:将一个段当作栈
对于数据段,将它的段地址放在 DS 中,用 mov、add、sub 等访问内存单元的指令时CPU 则认为数据段中的内容是数据来访问
对于代码段,将它的段地址存放在 CS 中,将段中的第一条指令的偏移地址放在 IP 中,这样 CPU 就将执行我们定义的代码段的指令(每执行一条指令之前,就会将 IP 的值更新,规则为 IP = IP + 当前指令的长度,以保证该条指令执行完可以根据 段地址 + 偏移地址获取到下条指令的地址)
对于栈段,将它的段地址存放在 SS 中,将栈顶单元的偏移地址存放在 SP 中,这样 CPU 在进行栈操作LIFO的时候比如 push、pop 指令,就可以操作 SP将我们定义的栈段当作栈空间来使用
### 中断
中断是由于软件或者硬件的信号,使得 CPU 暂停当前的任务,转而去执行另一段子程序。
在程序运行过程中,系统出现了一个必须由 CPU 立即处理的情况此时CPU 暂时终止当前程序的执行转而处理这个新情况的过程就叫中断。
中断分为:
- 硬中断(外中断):由外部设备(网卡、硬盘)随机引发的,比如当网卡收到数据包的时候,就会发出一个中断
- 软中断(内中断):由执行中断指令产生,可以通过程序控制触发
汇编中主要指的是软中断,可以通过指令 `int n` 产生中断,其中 `n` 表示中断码,内存中有一张中断向量表,用来存放中断码对应中断处理程序的入口地址。
Demo写一个打印 Hello World 的汇编代码
```powershell
; 提醒开发者每个段的定义增加可读性
assume cs:code, ds:data
;------ 数据段 begin --------
data segment
age db 20h
no dw 30h
db 10 dup(6) ; 生成连续10个6
string db 'Hello world!$'
data ends
;------ 数据段 end --------
;------ 代码段 begin --------
code sgement
start:
; 设置 ds 的值
mov ax, data
mov ds, ax
mov ax, no
mov bl, age
; 打印字符串
mov dx, offset string ; offset string 代表 string 的偏移地址将地址赋值给 dx
mov ah, 9h
int 21h ; 打印字符串其实也是一次中断
; 退出程序
mov ax, 4c00h
int 21h
code ends
end start
;------ 代码段 end --------
```
- start 代表汇编程序的入口
- `mov ax, 4c00h``int 21h` 代表程序正常中断
QA“全局变量的地址在编译那一刻就确定好了”怎么理解
全局变量存放在数据段,我们开发者写的代码存放在代码段,位置不一样,编译期就可以确定全局变量的地址。
### call 和 ret 指令
实现打印3次 "Hello"
方法1
```powershell
assume ds:data, ss: stack, cs: code
; 栈段
stack segment
ends stack
; 数据段
data segment
ends data
; 代码段
code segment
start:
; 设置 dsss
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 业务逻辑
; 打印
; ds:dx 告诉字符串地址
mov dx, offset string
mov ah, 9h
int 21h
mov dx, offset string
mov ah, 9h
int 21h
mov dx, offset string
mov ah, 9h
int 21h
; 程序正常退出
mov ax, 4c00h
int 21h
code ends
end start
```
有没有问题重复出现2次以及以上需要封装为函数汇编也遵循这个原则
方法2
```shell
assume ds:data, ss: stack, cs: code
; 栈段
stack segment
ends stack
; 数据段
data segment
ends data
; 代码段
code segment
start:
; 设置 ds、ss
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 业务逻辑
call print
call print
call print
; 程序正常退出
mov ax, 4c00h
int 21h
print:
; 打印
; ds:dx 告诉字符串地址
mov dx, offset string
mov ah, 9h
int 21h
; 函数正常退出
ret
code ends
end start
```
说明:
call 会将下一条指令的偏移地址入栈会转到标号print: 处执行指令
ret 会将栈顶的值出栈,赋值给 `CS:IP` ret 即 return
## 函数调用的本质
函数的3要素参数、返回值、局部变量
#### 返回值
函数运算的结果,一般是放在 ax 通用寄存器中。可以拿 Xcode 将下面的代码执行下,断点开启在 test 方法内的 return 处Debug - Debug WorkFlow - Always show Disassembly
```objectivec
#import <Foundation/Foundation.h>
int test (void) {
return 9;
}
int main(int argc, const char * argv[]) {
int res = test();
printf("%d", res);
return 0;
}
```
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssembleReturn.png)
可以看到 return 的值是保存在 eax 寄存器中。为什么是 ee是32位的意思环境老款 MBP 电脑运行)。
#### 参数
需要用的时候 push最后不用则 pop所以用栈来传参。
注意利用栈传递参数在函数内部计算之后return出来需要保证**栈平衡**,也就是函数调用前后的栈顶指针要一致。
栈不平衡会导致栈空间迟早溢出,发生不可预期的错误。
Demo
```shell
push 1122h
push 2233h
call sum
add sp, 4
sum:
; 访问栈中的参数
mov bp, sp
mov ax, ss:[bp + 2]
add ax, ss:[bp + 4]
ret
```
- 上面代码调用2次 push 将方法参数入栈,调用 sum 方法call sumcall 本质会将下一行指令的地址压入栈所以目前栈有3个元素。
- 函数执行完毕,调用 ret 指令会将栈顶的指令 pop 出来,更改 `CS:IP` 然后马上执行
- 访问栈中的参数的时候,由于 call 指令会将下一条指令地址也入栈,所以访问需要 +2
- 访问栈中参数 +2 的时候不能直接 `sp + 2 `,需要用 bp
但目前函数调用结束了栈里面还存在2个局部变量导致空间浪费了会存在“栈平衡”问题。所以在函数调用完毕需要告诉内存将栈顶指针恢复原位 `sp = sp + 4`(类比程序员告诉计算机,这块内存我不用了,后续其他人的代码可以用这块内存存某个值)
QAstack overflow
清楚函数调用原理 call、ret、stack 就知道函数调用函数,常见的递归或者循环,其实函数都在 stack 上进行操作比如函数参数、函数下一条指令也会入栈在递归或者函数内不断调用函数的过程中stack 不及时”栈平衡“,很容易出现栈溢出的情况,也就是 stack overflow。
### 内平栈/外平栈
外平栈
```shell
push 1122h
push 2233h
call sum
add sp, 4
sum:
; 访问栈中的参数
mov bp, sp
mov ax, ss:[bp + 2]
add ax, ss:[bp + 4]
ret
```
内平栈
```shell
push 1122h
push 2233h
call sum
sum:
; 访问栈中的参数
mov bp, sp
mov ax, ss:[bp + 2]
add ax, ss:[bp + 4]
ret 4
```
内平栈的好处是函数调用者不用去处理“栈平衡”
### 函数调用的约定
`__cdecl` 外平栈,参数从右到左入栈
`_stdcall` 内平栈,参数从右到左入栈
`_fastcall` 内平栈ecx、edx 分别传递前面2个参数其他参数从右到左入栈
寄存器传递参数效率更高速度更快iOS 平台函数采用6到8个 寄存器传参,剩余的从右到左入栈。
### c 代码可与汇编混合开发
验证函数的返回值是存放在 eax 寄存器中eax 和 ax 区别在于位数)
```c
#import <Foundation/Foundation.h>
int test (int a, int b) {
return a + b;
}
int main(int argc, const char * argv[]) {
test(2, 8);
int c = 0;
__asm {
mov c, eax
}
printf("%d", c);
return 0;
}
// 10
```
## 函数局部变量
大多数情况下函数内部会存在局部变量,但是不知道局部变量到底有多少,如何保证局部变量不会被污染呢?
CPU 会在栈内部将局部变量的地方临时分配10字节大小空间用来存储局部变量。这个怎么实现呢`SP = SP - 10` 这条指令用来将栈顶指针改变留出10字节大小空间。但是留出的空间是空的万一 `CS:IP` 指向这块区域会把里面的数据当作指令去执行则可能发生一些不可预知的错误。Windows 平台,针对预留的局部变量空间,会走动填充 cc也就是 `int 3 ` 断点中断,只要 `CS:IP` 去执行就会断点中断,更安全。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssembleFunctionStack.png)
关键代码如下:
```powershell
; 返回值放在 ax 寄存器中
; 传递2个参数放入栈中
sum:
; 保护 bp
push bp
; 保存 sp 之前的值指向 bp 以前的值
mov bp, sp
; 预留10个字节的空间用来存放局部变量(内部是高地址向)
sub sp, 10
; 保护可能用到的寄存器
push si
push di
push bx
; 给局部变量空间填充 int 3cccc调试中断以增加 `CS:IP` 安全性
; stosw 的作用 ax 的值拷贝到 es:di
mov ax, 0cccch
; es 等于 ss
mov es, ss
mov es, bx
; di = bp - 10局部变量地址的最小处
mov di, bp
sub di, 10
; cx 的值决定了 rep 的执行次数
mov cx, 5
; rep 重复执行某条指令次数由 cx 的值决定
rep stosw
; 业务逻辑
; 定义2个局部变量
mov word ptr ss:[bp-2], 3
mov word ptr ss:[bp-4], 4
mov ax, ss:[bp-2]
add ax, ss:[bp-4]
mov ss:[bp-6], ax
; 访问栈中的参数
mov ax, ss:[bp+4]
add ax, ss:[bp+6]
add ax, ss:[bp-6]
; 恢复寄存器中的值
pop bx
pop di
pop si
; 恢复 sp
mov sp, bp
; 恢复 bp
pop
```
## 栈帧
Stack Frame Layout代表一个函数的执行环境。包括参数、返回地址、局部变量和包括在本函数内部执行的所有内存操作等
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StackFrame.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSStackFrame.png)