docs: clang 插件开发

This commit is contained in:
杭城小刘
2024-04-27 13:01:58 +08:00
parent 6e47061735
commit 851797d133
257 changed files with 9060 additions and 239 deletions

View File

@@ -10,6 +10,8 @@
偏移地址为16位16位地址的寻址能力位64kb所以一个段的长度最大为64kb。
## CPU 的典型构成
- 寄存器:信息存储
@@ -22,6 +24,81 @@
不同的 CPU寄存器个数、结构是不同的比如8086是16为结构的 CPU8086有14个寄存器
## 说明
- 汇编中,小括号内存放的一定是内存地址。
- 指令后面的字母代表操作数长度。比如 b = byte(8-bit)s = short(16-bit integer or 32-bit floating point)、w = word(16-bit)、l=long(32-bit integer or 64-bit floating point)、q=quad(64 bit)、t=tem bytes(80-bit floating point)。比如 ` movq $0xa, 0x86c1(%rip)``let a:Int = 10` 的汇编实现。
- rip 存储的说指令的地址。CPU 要执行的下一条指令地址就存储在 rip 中
- rax、rdx 寄存器一般作为函数返回值使用
```swift
func getValue() -> Int {
return 10
}
var v = getValue()
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssemblyRaxFunctionReturnValueDemo1.png" style="zoom:25%">
在第5行代码加断点第4行汇编遇到 call 函数调用LLDB 输入 `si` 进去,可以看到将十六进制 `0xa` 也就是10保存到寄存器 `%eax` 也就是`%rax` 中。
```assembly
SwiftDemo`getValue():
-> 0x100003b20 <+0>: pushq %rbp
0x100003b21 <+1>: movq %rsp, %rbp
0x100003b24 <+4>: movl $0xa, %eax
0x100003b29 <+9>: popq %rbp
0x100003b2a <+10>: retq
```
LLDB 输入 `finsh` 结束函数调用这段汇编可以看到在汇编的第5行将 `%rax` 保存的 10 赋值到 `%rip + 0x86d0 ` 地址。可以看 `%rip + 0x86d0` 是个全局变量,大概就是 v 的地址(可以继续用汇编验证,绝对是 v
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssemblyRaxFunctionReturnValueDemo2.png" style="zoom:25%">
- rdi、rsi、rdx、rcx、r8、r9 寄存器一般用来存储函数参数。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssemblyFunctionParamsRegisterDemo.png" style="zoom:25%">
可以看到第四行汇编的 `%edi` ... `%r9d` 和上面描述的寄存器顺序一致。
- rsp、rbp 寄存器用于栈操作。栈顶指针,指向栈的顶部
- leaq 和 movq 是有区别的。`leaq 0xd(%rip), %rax` 是从 `%rip + 0xd` 算出来的地址值赋值给 `%rax` `movq 0xd(%rip), %rax` 是从 `%rip + 0xd ` 算出来的地址值取8个字节给 `%rax`。
- `xorl` 抑或运算。
## 寄存器的高低位兼容设计
汇编中高位对于低位寄存器的兼容性设计:
%r 开头的寄存器都是64位8 Byte
%e 开头的寄存器都是32位的4 Byte
那如果所有的寄存器再去分 %r、%e 那就会存在很多寄存器了,使用和记忆很难了。
同时早期的寄存器之下写的汇编代码,升级的时候要改写,成本太大了。如何设计才可以兼容升级呢?
设计很巧妙。假设一个 %rax 的64位寄存器063位
- 64位则 all in 全部使用
- 32位为了兼容低的32位寄存器则拿出低的4字节031位当作 %eax 32位寄存器来使用
- 16位为了兼容16位的寄存器则拿出低的2个字节015位当作 %ax 16位寄存器来使用
- 8位为了兼容8位的寄存器则拿出低的2个字节015位分为2段高8位、低8位来使用分别是 %ah、%al 寄存器。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RegisterHighAndLow1.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RegisterHighAndLow2.png)
寄存器:
- r 开头64 bit8 Byte
- e 开头32 bit4 Byte
- ax、bx、cx、dx16 bit2 Byte
- ah、al、bh、bl...8 bit1 Byte
## 通用寄存器
AX、BX、CX、DX 这4个寄存器通常用来存放一般性的数据成为通用寄存器有时候也有特定用途
@@ -36,6 +113,8 @@ AX、BX、CX、DX 这4个寄存器通常用来存放一般性的数据成为
- `mov b, ax`。调用 mov 将 ax 中的值赋值给内存空间 b
### CS和IP
CS 为代码段IP 为指令指针寄存器,它们代表 CPU 当前要读取指令的地址
@@ -62,6 +141,8 @@ IP 只为 CS 提供服务。
- 如果内存中的某段内容曾被 CPU 执行过,那么它所在的内存单元肯定被 `CS:IP` 指向过
### jmp 指令
mov 指令不能用于设置 CS、IP 的值8086没有提供该功能。可以通过 jmp 指令来实现修改 CS、IP 的值,这些指令被成为转移指令。
@@ -80,6 +161,10 @@ jmp ax
修改了4次。每执行一条指令IP 都会被修改1次IP=IP+该条指令的长度最后一条指令执行后IP 寄存器的值也会被修改1次共3+1=4次。
`jmp *%rax` jmp 后面如果跟寄存器地址,则一定要加 `*`,地址存放在 `%rax` 中
### ds 寄存器
CPU 要读写一个内存单元时必须要给出这个内存单元的地址在8086中内存地址由段地址和偏移地址组成。
@@ -142,6 +227,8 @@ mov al, [2]
此时 al 的值为多少al 和 ax 的区别在于 ax = ah + al所以 al 的情况下直接从 10002H 开始取1个16位的数据所以 al 为 0022。
### 大小端序
小端序,指的是数据的高字节保存在内存的高地址中,数据的低字节保存在内存的低地址中
@@ -184,6 +271,8 @@ QA将 0x1122 存放在 0x40002 中,如何存储?
0x40003 0x11
### 指令操作明确 CPU 操作的内存
```shell
@@ -204,6 +293,8 @@ mov word ptr [0], 66h
指令执行后1000: 0000 66 22 00 00 00 00 00 00
## 栈
栈是一种后进先出特点的数据存储空间LIFO
@@ -222,7 +313,9 @@ SS: 栈的段地址
SP堆栈寄存器存放栈的偏移地址
#### push
### push
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StackPush.png)
@@ -234,7 +327,9 @@ SP堆栈寄存器存放栈的偏移地址
ax = ah + al所以 ax 中的数据入栈需要占据2个单位sp = sp - 2
#### pop
### pop
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/stackPop.png)
@@ -275,7 +370,9 @@ pop bx
对于栈段,将它的段地址存放在 SS 中,将栈顶单元的偏移地址存放在 SP 中,这样 CPU 在进行栈操作LIFO的时候比如 push、pop 指令,就可以操作 SP将我们定义的栈段当作栈空间来使用
### 中断
## 中断
中断是由于软件或者硬件的信号,使得 CPU 暂停当前的任务,转而去执行另一段子程序。
@@ -334,7 +431,9 @@ QA“全局变量的地址在编译那一刻就确定好了”怎么理解
全局变量存放在数据段,我们开发者写的代码存放在代码段,位置不一样,编译期就可以确定全局变量的地址。
### call 和 ret 指令
## call 和 ret 指令
实现打印3次 "Hello"
@@ -441,7 +540,9 @@ ret 会将栈顶的值出栈,赋值给 `CS:IP` ret 即 return
函数的3要素参数、返回值、局部变量
#### 返回值
### 返回值
函数运算的结果,一般是放在 ax 通用寄存器中。可以拿 Xcode 将下面的代码执行下,断点开启在 test 方法内的 return 处Debug - Debug WorkFlow - Always show Disassembly
@@ -461,7 +562,7 @@ int main(int argc, const char * argv[]) {
可以看到 return 的值是保存在 eax 寄存器中。为什么是 ee是32位的意思环境老款 MBP 电脑运行)。
#### 参数
### 参数
需要用的时候 push最后不用则 pop所以用栈来传参。
@@ -499,6 +600,8 @@ QAstack overflow
清楚函数调用原理 call、ret、stack 就知道函数调用函数,常见的递归或者循环,其实函数都在 stack 上进行操作比如函数参数、函数下一条指令也会入栈在递归或者函数内不断调用函数的过程中stack 不及时”栈平衡“,很容易出现栈溢出的情况,也就是 stack overflow。
### 内平栈/外平栈
外平栈
@@ -534,6 +637,8 @@ sum:
内平栈的好处是函数调用者不用去处理“栈平衡”
### 函数调用的约定
`__cdecl` 外平栈,参数从右到左入栈
@@ -544,6 +649,8 @@ sum:
寄存器传递参数效率更高速度更快iOS 平台函数采用6到8个 寄存器传参,剩余的从右到左入栈。
### c 代码可与汇编混合开发
验证函数的返回值是存放在 eax 寄存器中eax 和 ax 区别在于位数)
@@ -565,6 +672,8 @@ int main(int argc, const char * argv[]) {
// 10
```
## 函数局部变量
大多数情况下函数内部会存在局部变量,但是不知道局部变量到底有多少,如何保证局部变量不会被污染呢?
@@ -631,10 +740,284 @@ sum:
pop
```
## 栈帧
Stack Frame Layout代表一个函数的执行环境。包括参数、返回地址、局部变量和包括在本函数内部执行的所有内存操作等
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StackFrame.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CSStackFrame.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CSStackFrame.png)
## iOS 调用汇编
1. 在 Xcode 工程中创建文件,选择 Other -> empty保存为 `.s` 拓展名
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeCreateAssembleFile.png" style="zoom:25%">
2. 编写汇编代码
```assembly
.text
.global _test
_test:
movq $0x8, %rax;
ret;
```
创建一个名为 `test` 的函数,内部给 rax 寄存器赋值为8然后 ret 返回。
`.text` 是保存在 _TEXT 段上。并将函数暴露给全局,函数名为 test暴露的时候就要写 _test
3. 汇编函数给外部调用,就需要声明一个头文件 `Asm.h`,写好需要暴露的方法声明
4. 最后在使用的地方,引入暴露的汇编头文件 `Asm.h`,正常调用函数即可。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/iOSCallAssemble.png" style="zoom:25%">
## 汇编编写“函数”
上面的例子也顺带看了汇编是如何编写“函数”的,为什么加引号,因为这个概念是不存在的,汇编只有指令,这个函数概念是方便组织代码,参考定义的。类似给一段代码打了个标签。
1. 创建汇编文件
2. 编写代码
```assembly
.text
.global _test, _add, _sub
_test:
movq $0x8, %rax;
ret;
_add:
movq %rsi, %rax
movq %rdi, %rbx
addq %rbx, %rax
retq
_sub:
movq %rdi, %rax
movq %rsi, %rbx
subq %rbx, %rax
ret
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/iOSCallAssembleDemo1.png" style="zoom:25%">
说明:笔者编写平台是老款 MBPXcode 连接模拟器跑的代码,也就是 X86_64 架构的汇编。真机运行一般跑 arm64 汇编语法,会 X86_64 的话 arm64 类似,翻译下写法就好。
看这2个函数都是从 `rsi` `rdi` 寄存器里面获取函数参数,内部调用系统指令,做了减加运算逻辑后,将函数返回值保存到 `rax` 寄存器中,直接 return。不需要显示声明 `return rax`,汇编会自动将 `rax` 寄存器里的值,交给函数调用者。
3. 汇编函数给外部调用,就需要声明一个头文件 `Asm.h`,写好需要暴露的方法声明
4. 最后在使用的地方,引入暴露的汇编头文件 `Asm.h`,正常调用函数即可。
## iOS 源码探索
经常需要将黑盒的 iOS 代码结合 GNU 之外,还需要将源文件编译成汇编代码去分析。格式为:
`xcrun --sdk iphoneos clang -S -arch arm64 main.m -o main.s`
## arm64 汇编
### 寄存器
#### 通用寄存器
- 64位 x0x28
- 32位w0w28属于 x0x28 的低32位
- x0x7 经常用来存放函数的参数,更多的函数参数用堆栈来传递
- x0 经常用来存放函数的返回值
Demo汇编定义加减法OC 去调用
```assembly
// Asm.s
.text
.global _add, _sub
_add:
add x0, x0, x1
ret
_sub:
sub x0, x0, x1
ret
// ViewContoller.m
#import "Asm.h"
NSInteger sum = add(2, 4) // 6
NSInteger res = sub(4, 2) // 2
```
#### 程序计数器
pc(Program Counter)
#### 堆栈指针
- spStack Pointer
- fpFrame Pointer也就是 x29
#### 链接寄存器
lrlink register也就是 x30
#### 程序状态寄存器
- cpsrCurrent Program Status Register
- spsrSaved Program Status Register异常状态下使用
### 指令
- ret函数返回
- cmp将2个寄存器的值相减结果会影响 cpsr 寄存器的标志位
- b跳转指令。格式为`b{条件} 目标地址` 。b 指令是最简单的跳转指令,一旦遇到一个 B 指令ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。
条件跳转一般搭配 cmp 使用。条件跳转对应 `if...else...`
Demo定义一段汇编代码一个标签然后跳转执行。跳转前传递参数跳转后读取并相加
```assembly
.text
.global _jump
_jump:
movq $0x1, %rsi
jmp myCode
myCode:
movq %rsi, %rax
movq $0x2, %rbx
addq %rbx, %rax
ret
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssembleJMPDemo1.png" style="zoom:25%">
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AssembleJMPDemo2.png" style="zoom:25%">
上面是 x86_64 的汇编,`jmp` 跳转指令在 arm64 中对应 `b` 指令。类似下面代码
```assembly
.text
.global _jump:
_jump:
// ...
b myCode
myCode:
// ...
```
条件跳转:`bgt conditionJump`
```assembly
.text
.global _jump
_jump:
mov x0, #0x5
mov x1, #0x5
cmp x0, x1
bgt conditionJump
conditionJump:
mov x1, #0x6
ret
```
- bl带返回值的跳转指令。格式为`bl{条件} 目标地址`。bl 跳转前,会在寄存器 r14 中保存 pc 的当前内容,因此,可以通过将 r14 的内容重新加载到 pc 中,来返回到跳转指令之后的那个指令处执行。该指令是实现子程序调用的一个基本但常用的手段。在 x86_64 中就是 call 指令。
### 条件域
- EQequal 相等
- NEnot equal 不想等
- GTgreat than 大于
- GEgreater equal 大于等于
- LTless than 小于
- LEless equal 小于等于
### 内存操作
- load 从内存中装载数据
- ldr
`ldr x0, [x1]` 代表从地址 x1 处取8个字节的数据赋值给 x0会将 x1 寄存器中存储的内存地址所指向的值加载到 x0 寄存器中)。`ldr w0, [x1]` 代表从地址 x1 处取4个字节的数据赋值给 w0。一般会搭配 CPU 寻址能力一起使用。
- ldur
和 ldr 一样作用都是从一个寄存器中存储的内存地址所指向的值加载到某个寄存器上。ldr 搭配正数地址,如 `ldr x1, [sp, #0x28]` ldur 搭配负数地址,如 `ldur w8, [x29, #-0x8]`
- ldp `ldp w0, w1, [x2, #0x10]` 代表从 x2 + 0x10 计算结果对应的内存出取出前4个字节的值赋值给寄存器 w0后4个字节对应的值赋值给寄存器 w1
- store 往内存中存储数据
- str。`str w0, [x1, #0x5]` 将 w0 寄存器的值赋值给 `x1 + #0x5` 地址开始4个字节处。str 搭配正数地址偏移
- stur。`str w0, [x1, #-0x5]` 将 w0 寄存器的值赋值给 `x1 - #0x5` 地址开始4个字节处。stur 搭配正数地址偏移
- stp。`stp w0, w1, [x1, #0x5]` 将 w0 寄存器的值赋值给 `x1 + #0x5` 地址开始前4个字节处w1 寄存器的值赋值给后4个字节
- 零寄存器
- wzr32bit即 word zero register。
- xzr64bit
```objective-c
int a = 0
long b = 0;
```
转换为 arm64 汇编就是
```assembly
stur wzr, [x29, #-0x14]
stur xzr, [x29, #-0x24]
```
## 经验小结
- 内存地址格式为:`0x7ab60(%rip)` 一般是全局变量
- 内存地址格式为:`-0x50(%rbp)` 一般是局部变量
- 源代码 -> 汇编 -> 机器码,从机器码到汇编是可逆的。但是无法做到汇编到源代码的反编译,因为不同的源代码可能生成的汇编代码是一样的。