内联汇编
内联汇编用于在代码中穿插一些由高级语言无法直接完成或无法高效完成的逻辑.
而C语言内联汇编的语法会受到编译器的影响.
注: 汇编是平台架构高度绑定性的, 不同平台的助记符是完全不同的(如寄存器/操作码助记符等)
本教程仅以x86
平台的汇编进行演示 (包括IA32
X86_64
)
1. 定义语句
C语言中通常以 asm
__asm__
等来表示一个内联汇编语句, 后方会跟一个 volatile
关键字防止优化器对汇编进行重排, 括号内即可编写汇编语法
__asm__ volatile (
"指令模板"
: 输出操作数列表
: 输入操作数列表
: 被破坏的寄存器列表 (clobber)
: 可跳转标签列表 (仅 asm goto 用)
);
2
3
4
5
6
7
以上是一个内联汇编语法的结构, 指令模板编写实际的汇编代码.
- 输出操作数列表: 内联汇编语句向C代码中的变量写入值等操作
- 输入操作数列表: 内联汇编语句从C代码中的变量获取值等操作
- 被破坏的寄存器列表: 用于向说明哪些寄存器被内联汇编语句修改和使用了, 防止与C代码冲突
- 当该列表出现
"memory"
时, 代表内联汇编会对内存进行读写操作
- 当该列表出现
- 可跳转标签列表: 让内联汇编中的
jmp
跳转指令可以跳转到C语言标签处.
注意
内联汇编中, 每一条指令都需要通过 \n\t
相隔开, 否则编译器有可能将汇编混在一行里, 导致汇编器无法正确识别指令
2. 约束符号
用于输入输出操作数列表中对C语言变量的一种约束, 告知编译器该变量应该使用哪一个寄存器或内存方式.
输出约束以
=
为开头, 如=a
=r
约束符号 | 描述 | 示例 |
---|---|---|
r | 任意通用寄存器 | 由编译器决定用哪个寄存器 |
a | 约束为累加寄存器 | rax eax |
b | 约束为基址寄存器 | rbx ebx |
c | 约束为计数寄存器 | rcx ecx |
d | 约束为数据寄存器 | rdx edx |
S | 约束为源变址寄存器 | rsi esi |
D | 约束为目标变址寄存器 | rdi edi |
m | 约束为内存地址(指针) | void *ptr |
i | 约束为立即数 | 0x1000UL |
3. 更改汇编语法
一般通过一些特殊的汇编符号来更改到指定汇编语法, 不同编译器的默认汇编语法是不同的
gcc
clang
默认的汇编语法为AT&T
(不支持MSVC
风格)MSVC
icc
默认的汇编语法为intel
AT&T汇编语法
一般为 gcc
clang
等编译器默认的汇编语法, 通常情况下不需要额外的符号来更改
但在已有其他符号改变汇编语法后,需要通过 .att_syntax
来切换回 AT&T
语法
__asm__ volatile (
"<已被改变语法>\n\t"
".att_syntax\n\t"
"movq %rax, %rbx\n\t" // 切换回AT&T语法
);
2
3
4
5
英特尔汇编语法
通过 .intel_syntax noprefix
表示更改到英特尔汇编语法
__asm__ volatile (
".intel_syntax noprefix\n\t"
"mov eax, ebx\n\t"
);
2
3
4
4. 占位符
内联汇编语法中的格式化符号,用来在汇编字符串中插入 C 语言变量或其他元素的真实内容
其数据来源为输入输出操作数列表提供的C语言变量、常量或立即数等
在
AT&T
中%
是一个特殊符号, 如果你想真的表示一个%
字符的话可以采用%%
两个百分号的写法
%<number>
数字编号 - 表示输入输出约束的位置, 从0开始计数Cint a = 5; __asm__("mov %0, %%eax" : : "r"(a));
1
2%[<name>]
命名引用 - 可自行定义约束位置的命名, 用引用代表Cint a = 5; __asm__("mov %[input], %%eax" : : [input] "r"(a));
1
2%c<number>
常量 - 插入C语言中的常量或宏定义等立即数(不会加$
#
前缀)C#define VAL 42 __asm__("mov %c0, %%eax" : : "i"(VAL));
1
2%n<number>
纯值 - 同样用于插入立即数, 但是强调的是纯值(适用于宏等)C#define SIZE 64 __asm__("cmp $%n0, %%eax" : : "i"(SIZE));
1
2%l<number>
标签 - 用于asm goto
的标签跳转C__asm__ goto("jmp %l0" ::: : my_label); my_label:;
1
2
5. 裸函数
不由编译器自动生成函数前后栈帧管理代码的函数, 该函数通常情况下全是内联汇编汇编代码
gcc
clang
中采用__attribute__((naked))
修饰一个函数使其变成裸函数msvc
中采用__declspec(naked)
修饰
定义一个裸函数
__attribute__((naked)) void my_handler(void) {
__asm__ volatile (
"push %rbp\n"
"mov %rsp, %rbp\n"
// 你的手动代码...
"pop %rbp\n"
"ret\n"
);
}
2
3
4
5
6
7
8
9
警告
在裸函数中, 编译器不会有以下处理行为
- 不自动
push
pop
寄存器 - 不会自动设置
rbp
esp
rsp
寄存器 - 不会对函数的返回进行管理 (需要手动插入
ret
指令) - 不允许包含C语言代码
6. 示例
展示一些裸函数与内联汇编实际使用的场景代码.
- C
static inline uint8_t inb(uint16_t port) { uint8_t data; __asm__ volatile("inb %w1, %b0" : "=a"(data) : "Nd"(port)); return data; } static inline void outb(uint16_t port, uint8_t data) { __asm__ volatile("outb %b0, %w1" : : "a"(data), "Nd"(port)); }
1
2
3
4
5
6
7
8
9 - C
static void open_page(){ //打开分页机制 uint32_t cr0; __asm__ volatile("mov %%cr0, %0" : "=b"(cr0)); cr0 |= 0x80000000; __asm__ volatile("mov %0, %%cr0" : : "b"(cr0)); }
1
2
3
4
5
6 - C
__attribute__((naked)) void save_registers() { __asm__ volatile(".intel_syntax noprefix\n\t" "cli\n\t" "push 0\n\t" // 对齐 "push 0\n\t" // 对齐 "push r15\n\t" "push r14\n\t" "push r13\n\t" "push r12\n\t" "push r11\n\t" "push r10\n\t" "push r9\n\t" "push r8\n\t" "push rdi\n\t" "push rsi\n\t" "push rbp\n\t" "push rdx\n\t" "push rcx\n\t" "push rbx\n\t" "push rax\n\t" "mov rax, es\n\t" "push rax\n\t" "mov rax, ds\n\t" "push rax\n\t" "mov rdi, rsp\n\t" "call timer_handle\n\t" "mov rsp, rax\n\t" "pop rax\n\t" "mov ds, rax\n\t" "pop rax\n\t" "mov es, rax\n\t" "pop rax\n\t" "pop rbx\n\t" "pop rcx\n\t" "pop rdx\n\t" "pop rbp\n\t" "pop rsi\n\t" "pop rdi\n\t" "pop r8\n\t" "pop r9\n\t" "pop r10\n\t" "pop r11\n\t" "pop r12\n\t" "pop r13\n\t" "pop r14\n\t" "pop r15\n\t" "add rsp, 16\n\t" // 越过对齐 "sti\n\t" "iretq\n\t"); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51