mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
docs: clang 插件开发
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
|
||||
偏移地址为16位,16位地址的寻址能力位64kb,所以一个段的长度最大为64kb。
|
||||
|
||||
|
||||
|
||||
## CPU 的典型构成
|
||||
|
||||
- 寄存器:信息存储
|
||||
@@ -22,6 +24,81 @@
|
||||
|
||||
不同的 CPU,寄存器个数、结构是不同的(比如8086是16为结构的 CPU,8086有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位寄存器(0~63位)
|
||||
|
||||
- 64位:则 all in 全部使用
|
||||
- 32位:为了兼容低的32位寄存器,则拿出低的4字节(0~31位)当作 %eax 32位寄存器来使用
|
||||
- 16位:为了兼容16位的寄存器,则拿出低的2个字节(0~15位)当作 %ax 16位寄存器来使用;
|
||||
- 8位:为了兼容8位的寄存器,则拿出低的2个字节(0~15位)分为2段,高8位、低8位来使用,分别是 %ah、%al 寄存器。
|
||||
|
||||

|
||||

|
||||
|
||||
寄存器:
|
||||
- r 开头:64 bit,8 Byte
|
||||
- e 开头:32 bit,4 Byte
|
||||
- ax、bx、cx、dx:16 bit,2 Byte
|
||||
- ah、al、bh、bl...:8 bit,1 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
|
||||
|
||||

|
||||
|
||||
@@ -234,7 +327,9 @@ SP:堆栈寄存器存放栈的偏移地址
|
||||
|
||||
ax = ah + al,所以 ax 中的数据入栈需要占据2个单位(sp = sp - 2)
|
||||
|
||||
#### pop
|
||||
|
||||
|
||||
### pop
|
||||
|
||||

|
||||
|
||||
@@ -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 寄存器中。为什么是 e,e是32位的意思(环境:老款 MBP 电脑运行)。
|
||||
|
||||
#### 参数
|
||||
### 参数
|
||||
|
||||
需要用的时候 push,最后不用则 pop,所以用栈来传参。
|
||||
|
||||
@@ -499,6 +600,8 @@ QA:stack 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,代表一个函数的执行环境。包括:参数、返回地址、局部变量和包括在本函数内部执行的所有内存操作等
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
## 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%">
|
||||
|
||||
说明:笔者编写平台是老款 MBP,Xcode 连接模拟器跑的代码,也就是 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位: x0~x28
|
||||
- 32位:w0~w28(属于 x0~x28 的低32位)
|
||||
- x0~x7 经常用来存放函数的参数,更多的函数参数用堆栈来传递
|
||||
- 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)
|
||||
|
||||
|
||||
|
||||
#### 堆栈指针
|
||||
|
||||
- sp(Stack Pointer)
|
||||
- fp(Frame Pointer),也就是 x29
|
||||
|
||||
#### 链接寄存器
|
||||
|
||||
lr(link register),也就是 x30
|
||||
|
||||
|
||||
|
||||
#### 程序状态寄存器
|
||||
|
||||
- cpsr(Current Program Status Register)
|
||||
- spsr(Saved 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 指令。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 条件域
|
||||
|
||||
- EQ:equal 相等
|
||||
- NE:not equal 不想等
|
||||
- GT:great than 大于
|
||||
- GE:greater equal 大于等于
|
||||
- LT:less than 小于
|
||||
- LE:less 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个字节
|
||||
|
||||
- 零寄存器
|
||||
|
||||
- wzr(32bit)即 word zero register。
|
||||
- xzr(64bit)
|
||||
|
||||
```objective-c
|
||||
int a = 0;
|
||||
long b = 0;
|
||||
```
|
||||
|
||||
转换为 arm64 汇编就是
|
||||
|
||||
```assembly
|
||||
stur wzr, [x29, #-0x14]
|
||||
stur xzr, [x29, #-0x24]
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 经验小结
|
||||
|
||||
- 内存地址格式为:`0x7ab60(%rip)` 一般是全局变量
|
||||
- 内存地址格式为:`-0x50(%rbp)` 一般是局部变量
|
||||
- 源代码 -> 汇编 -> 机器码,从机器码到汇编是可逆的。但是无法做到汇编到源代码的反编译,因为不同的源代码可能生成的汇编代码是一样的。
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user