文章目录
-
- [1. 编译流程](#1. 编译流程)
- [2. 可执行文件](#2. 可执行文件)
-
- [2.1 可重定位目标文件与可执行目标文件](#2.1 可重定位目标文件与可执行目标文件)
- [3. 汇编语法](#3. 汇编语法)
-
- [3.1 注释](#3.1 注释)
- [3.2 标签和符号](#3.2 标签和符号)
- [3.3 指令伪指令](#3.3 指令伪指令)
-
- [3.3.1 对齐伪指令](#3.3.1 对齐伪指令)
- [3.3.2 数据定义伪指令](#3.3.2 数据定义伪指令)
- [3.3.3 函数相关伪指令](#3.3.3 函数相关伪指令)
- [3.3.4 与段相关的伪指令](#3.3.4 与段相关的伪指令)
- [3.3.5 宏](#3.3.5 宏)
- [3.3.6 文件相关的伪指令](#3.3.6 文件相关的伪指令)
- [4. RISCV 汇编编译选项](#4. RISCV 汇编编译选项)
1. 编译流程
通常情况下,编译器编译一个可执行文件包含以下几个步骤:
- 预处理
- 编译
- 汇编
- 链接
输入层
gcc -E
gcc -S
gcc -c
gcc -o
阶段4: 链接 (Linker)
合并多个 .o 文件
解析符号引用
跨文件函数调用
链接标准库
libc / libm / libstdc++
链接启动代码
crt0 / crt1
地址重定位
分配最终内存地址
输出: 可执行文件
ELF / PE / Mach-O
阶段3: 汇编 (Assembler)
汇编指令 → 机器码
二进制操作码
生成符号表
函数/变量地址映射
生成重定位信息
未解析的外部引用
输出: 目标文件
*.o / *.obj
阶段2: 编译 (Compiler Proper)
词法分析
Token化
语法分析
生成 AST/GENERIC
语义分析
类型检查
中间表示转换
GENERIC → GIMPLE → RTL
优化 Pass
SSA、内联、循环优化等
代码生成
输出汇编指令
输出: 汇编代码
*.s 文件
阶段1: 预处理 (Preprocessor)
处理 #include
头文件展开
处理 #define
宏替换
条件编译 #ifdef
选择性保留代码
删除注释
添加行号标记
输出: 纯净C代码
*.i 文件
源代码文件
*.c / *.cpp / *.h
2. 可执行文件
可执行与可链接格式(Executable and Linkable Format, ELF),一种文件格式
常见的段:
- 代码段(.text):存放程序的可执行机器指令(编译后的二进制代码)。
- 只读数据段(.rodata):存放只读常量数据。
- 数据段(.data):存放已初始化的全局变量和静态变量。
- BSS段(.bss):存放未初始化或初始化为 0 的全局/静态变量。
- 符号段(.symtab):记录程序中的符号信息(函数名、变量名及其地址)。
- 可重定位代码段(.rel.text):记录代码中需要重定位的位置。
- 可重定位数据段(.rel.data):记录数据段中需要重定位的位置。
- 调试符号段(.debug):存放调试信息,供 GDB 等调试器使用。
2.1 可重定位目标文件与可执行目标文件
可重定位目标文件:在链接阶段和其他可重定位目标文件合并成一个可执行文件
- 包含代码和数据
- 所有段的起始地址不确定,用0填充
- 可重定位代码段(.rel.text)
- 可重定位数据段(.rel.data)
可执行目标文件:可以执行的目标文件
- 包含代码和数据
- 所有段的起始地址都已经确定
3. 汇编语法
3.1 注释
在.S文件中支持三种注释:
- "//" 单行注释
- "#" 在一行开始表示注释整行
- "/**/" 多行注释
注: .S文件会经过预处理器处理,可以使用C语言中的注释方式,经过预处理以后会被丢弃,.s文件中只能用汇编注释#。
3.2 标签和符号
- 标签 (Label):后面紧跟冒号 :,代表所在位置的地址。常用于跳转或函数入口。start: 或 loop_start: 都是标签。
- 符号 (Symbol):指代一段数据或代码的名字,但不一定需要紧跟冒号。通过 .equ、.set、#define(仅 .S)定义的常量,或用 .globl 导出的函数名,都是符号。
asm
label_name: # 普通标签(冒号结尾)
instruction
1: # 局部标签(纯数字,用于临时跳转)
instruction
jmp 1f # 1f = 向前(forward)找下一个 "1:"
jmp 1b # 1b = 向后(backward)找上一个 "1:"
| 类型 | 示例 | 说明 |
|---|---|---|
| 全局符号 | main:、_start: |
默认局部可见,需 .global 导出 |
| 局部符号 | main:、_start: |
默认局部可见,通常用.local声明 |
| 局部标签 | 1:、2: |
临时标记,配合 f/b 使用 |
| 符号赋值 | symbol = 0x1000 |
等价于常量,不占内存 |
| 等值伪指令 | .set name, 0x100 |
定义符号常量 |
符号代表所在的地址,也可以当作变量或者函数来使用,符号由字母,数字,'_' 和'.'和"$"组成。
局部标签不一定是符号(数字标签就不是),局部符号一定是符号表中的条目。
3.3 指令伪指令
指令是真正的机器指令,汇编后直接生成对应的机器码,并且寄存器必须小写,指令可以是全大写或者全小写。
txt
[前缀] 操作码 [源操作数], [目的操作数]
伪指令以点 . 开头,是汇编器的命令,不生成机器码,控制汇编过程。
伪指令可以实现如下功能:
- 符号定义
- 数据定义和对齐
- 汇编控制
- 汇编宏
- 段描述
3.3.1 对齐伪指令
- .align 对齐,填充数据来实现对齐,可以填入0或者nop指令
- 告诉汇编器,.align 后面的汇编必须从下一个能被2^n整除的地址开始分配
- RISCV中第一个参数表示2^n大小
txt
.align expression [, fill_value [, max_bytes]]
| 参数 | 说明 | 默认值 |
|---|---|---|
expression |
对齐边界(含义因架构而异) | 必需 |
fill_value |
填充字节值 | 0(x86)/ 0x00 或 nop(其他) |
max_bytes |
最大填充字节数,超过则跳过对齐 | 无限制 |
3.3.2 数据定义伪指令
基本数据定义伪指令:
| 伪指令 | 数据大小 | 说明 | 示例 |
|---|---|---|---|
.byte |
8-bit (1 字节) | 定义字节 | .byte 0xFF, -1, 'A' |
.short / .hword |
16-bit (2 字节) | 定义半字 | .short 0x1234, -300 |
.long / .int/.word |
32-bit (4 字节) | 定义字 | .long 0x12345678 |
.quad |
64-bit (8 字节) | 定义四字 | .quad 0x123456789ABCDEF0 |
.octa |
128-bit (16 字节) | 定义八字 | .octa 0x... |
.single / .float |
32-bit | 单精度浮点 | .float 3.14, -0.5 |
.double |
64-bit | 双精度浮点 | .double 3.1415926535 |
字符串定义伪指令:
| 伪指令 | 自动补 \0 |
说明 | 示例 |
|---|---|---|---|
.ascii |
❌ 否 | ASCII 字符串(原始) | .ascii "Hello" |
.asciz |
✅ 是 | ASCII + 自动补零 | .asciz "Hello" |
.string |
✅ 是 | 同 .asciz |
.string "Hello" |
.string8 |
✅ 是 | 8-bit 字符 | .string8 "Hello" |
.string16 |
✅ 是 | 16-bit 宽字符 | .string16 "Hello" |
.string32 |
✅ 是 | 32-bit 宽字符 | .string32 "Hello" |
3.3.3 函数相关伪指令
.global 定义一个全局符号
.include 引用头文件
.if , .else, .endif 控制语句
3.3.4 与段相关的伪指令
.section
txt
.section name,"flags"
.section: 表示接下来的数据或者指令会链接到哪个段中,每一个段以段名为开始,以下一个段名或者文件末尾为结束。
| 字符 | 全称 | 含义 | ELF 段头标志 |
|---|---|---|---|
a |
Allocatable | 段在运行时分配内存 | SHF_ALLOC |
e |
Exclude | 链接时排除该段 | SHF_EXCLUDE |
w |
Writable | 段内容可写 | SHF_WRITE |
x |
Executable | 段内容可执行 | SHF_EXECINSTR |
M |
Mergeable | 相同内容段可合并 | SHF_MERGE |
S |
Strings | 段包含 null 结尾字符串 | SHF_STRINGS |
G |
Group | 段属于 COMDAT 组 | SHF_GROUP |
T |
TLS | 线程局部存储段 | SHF_TLS |
? |
保留 | 未知/保留标志 | --- |
.pushsection和.popsection
txt
.pushsection name, "flags", @type
# ... 在新段中定义内容 ...
.popsection
| 伪指令 | 作用 |
|---|---|
.pushsection |
保存当前段上下文,切换到指定新段 |
.popsection |
恢复之前保存的段上下文 |
.section和.previous
txt
初始状态: 当前在 .text 段
.section .data, "aw", @progbits ──► 切换到 .data
记住: 上一个段是 .text
[在 .data 中定义数据]
.previous ──► 回到 .text(上一个段)
[继续在 .text 写代码]
.section .rodata, "a", @progbits ──► 切换到 .rodata
记住: 上一个段是 .text(.previous 后的当前段)
[定义只读数据]
.previous ──► 回到 .text
| 特性 | .section + .previous |
.pushsection + .popsection |
|---|---|---|
| 记忆层数 | 仅一层(上一个段) | 多层栈(支持嵌套) |
| 切换方式 | 直接切换,覆盖记忆 | 压栈保存,精确恢复 |
| 嵌套支持 | ❌ 不支持嵌套(会丢失更早的段) | ✅ 支持任意深度嵌套 |
| 适用场景 | 简单两段切换 | 复杂多段嵌套、编译器生成 |
| 安全性 | 容易误用(忘记当前段) | 栈结构,成对使用更安全 |
3.3.5 宏
- 汇编中的宏通过
.macro和.endm定义 .macro后面是宏的名称,然后是跟宏的参数- 在宏里面使用参数要加"\"
asm
.macro add_1 p1 p2
add x0, \p1, \p2
.endm
- 宏定义的时候可以设置一个初始值:
asm
.macro reserve_str p0=0,p2
.endm
# 由于第一个参数有一个默认值,可以省略默认值
reserve_str ,b
reserve_str a,b
- 宏参数后面加
.req表示在宏调用过程中必须传递一个值,否则会编译出错
3.3.6 文件相关的伪指令
.incbin 伪指令可以把文件的二进制数据嵌入到当前位置。
.include 伪指令可以把汇编代码插入到当前指令所在位置。
4. RISCV 汇编编译选项
| 选项 | 作用 | 常用值 |
|---|---|---|
-fpic / -fPIC |
位置无关代码(共享库) | -fPIC(大模型) |
-fno-pic |
绝对地址代码(裸机/静态) | 默认裸机 |
-mabi= |
整数/浮点/指针宽度 | lp64d, ilp32 |
-march= |
指令集扩展 | rv64gc, rv32imac |
-misa-spec= |
ISA 规范版本 | 20191213 |
-mlittle-endian |
小端存储(默认) | - |
-mbig-endian |
大端存储 | - |