课题05:RISC-V汇编代码美化器
难度:低 | 类型:项目实战 | 源文件:
scratchv/backend/asm_beautifier.py| 行数:~535 状态:✅ 已完成
概述
开发独立工具,读取编译器生成的.s汇编文件,输出格式整洁、带注释、对齐良好的版本。该工具自动对RISC-V汇编代码进行三项处理:列对齐(标签、操作码、操作数分别对齐到固定宽度)、语义注释(为每条指令自动生成人类可读的解释)、分段标记(识别代码段/数据段并插入分隔标题),大幅降低汇编代码的阅读门槛。
理解背景
是什么?
汇编美化器(asm_beautifier.py)把机器生成的"裸"RISC-V 汇编代码变成人类友好的格式。它做三件事:
- 列对齐:标签、操作码、操作数分别对齐到固定宽度列
- 语义注释:给每条指令自动加上"这句在做什么"的注释
- 分段标记:识别代码段/数据段,插入分隔标题
效果对比:
# 美化前(机器生成的原始汇编)
main:
addi sp,sp,-32
sw ra,28(sp)
li a5,3
li a4,5
add a5,a5,a4
# 美化后(人类友好)
# --- Function: main ---
main: addi sp, sp, -32 # sp = sp + -32
sw ra, 28(sp) # MEM[sp + 28] = ra
li a5, 3 # a5 = 3
li a4, 5 # a4 = 5
add a5, a5, a4 # a5 = a5 + a4
为什么?
初学 RISC-V 汇编时,你经常需要对照手册才能看懂 addi sp, sp, -32 在干什么。美化器把每条指令翻译成人类可读的公式,大大降低阅读门槛。
对编译器开发者来说,美化后的汇编更容易: - 人工检查代码生成是否正确 - 对比优化前后的汇编差异 - 教学演示时让学生一眼看懂
核心概念
1. 汇编行的结构
一条 RISC-V 汇编行可以拆成 4 个部分:
[label:] [opcode] [operands] [# comment]
标签 操作码 操作数 注释
例如:main: add a0, a1, a2 # a0 = a1 + a2
2. 语义注释模板
美化器内置了 80+ 条 RISC-V 指令的"翻译模板"。例如:
| 指令 | 模板 | 效果 |
|---|---|---|
add rd, rs1, rs2 |
{rd} = {rs1} + {rs2} |
a0 = a1 + a2 |
lw rd, imm(rs1) |
{rd} = MEM[{rs1} + {imm}] |
a0 = MEM[sp + 16] |
beq rs1, rs2, label |
if {rs1} == {rs2} goto {label} |
if a0 == a1 goto .L1 |
jal rd, imm |
{rd} = PC+4; goto {imm} |
ra = PC+4; goto 0x100 |
li rd, imm |
{rd} = {imm} |
a0 = 42 |
mv rd, rs1 |
{rd} = {rs1} |
a0 = a1 |
3. 函数/段检测
美化器自动识别:
- 段切换:.text → 代码段,.data → 数据段,.bss → 未初始化数据段
- 函数入口:不以 .、L、loop 开头的标签视为函数
详细任务
- 解析汇编行,识别标签、指令、操作数、注释。
- 对齐字段:标签左对齐,指令助记符占固定宽度(如8字符),操作数左对齐。
- 为每条指令自动添加注释:如
addi x1, x0, 5 # x1 = x0 + 5。提供指令注释模板库。
- 添加段注释:
.text、.data、函数入口前插入分隔线和描述。
- 支持命令行参数:输入文件、输出文件、是否添加注释、是否对齐。
- 输出美化后的汇编文件。
交付产物
- Python脚本
asm_beautifier.py
- 示例输入输出文件
- 文档:使用方法、自定义注释模板
代码走读
使用美化器
addi x1, x0, 5 # x1 = x0 + 5。提供指令注释模板库。.text、.data、函数入口前插入分隔线和描述。- Python脚本
asm_beautifier.py - 示例输入输出文件
- 文档:使用方法、自定义注释模板
代码走读
使用美化器
Step 1: 从 Python 调用
from scratchv.backend.asm_beautifier import beautify_asm
raw_asm = """
main:
addi sp,sp,-32
sw ra,28(sp)
li a5,3
add a5,a5,a5
"""
pretty = beautify_asm(raw_asm)
print(pretty)
Step 2: 从命令行使用
# 美化汇编文件并输出到终端
python -m scratchv.backend.asm_beautifier output.s
# 保存到文件
python -m scratchv.backend.asm_beautifier output.s -o output_pretty.s
# 关闭对齐
python -m scratchv.backend.asm_beautifier output.s --no-align
# 关闭自动注释
python -m scratchv.backend.asm_beautifier output.s --no-comments
Step 3: 编译 CNN 模型并美化汇编
# 先生成汇编
python scratchv/standalone/onnx_to_riscv_standalone.py models/graph/cnn.onnx \
-o /tmp/cnn.bin --asm /tmp/cnn_raw.s
# 再美化
python -m scratchv.backend.asm_beautifier /tmp/cnn_raw.s -o /tmp/cnn_pretty.s
# 对比看看效果
head -50 /tmp/cnn_pretty.s
核心函数:beautify_asm()
def beautify_asm(asm_text: str, align: bool = True,
add_comments: bool = True) -> str:
# 1. 按行解析 → 每行拆成 {label, opcode, operands, comment}
lines = asm_text.strip().split("\n")
parsed_lines = [_parse_line(ln) for ln in lines]
# 2. 第一遍扫描:计算每列的最大宽度
max_label = max(len(p["label"]) for p in parsed_lines)
max_opcode = max(len(p["opcode"]) for p in parsed_lines)
# ... clamp 宽度避免溢出 ...
# 3. 第二遍扫描:按对齐宽度格式化,加注释
for p in parsed_lines:
formatted = _format_line(p, align, max_label, max_opcode, ...)
output_lines.append(formatted)
return "\n".join(output_lines)
注释生成逻辑:_gen_comment()
def _gen_comment(opcode: str, operands: list[str]) -> str:
template = _INST_COMMENTS.get(opcode) # 查模板表
if template is None:
return "" # 未知指令不加注释
# 替换模板变量:{rd}→实际寄存器, {rs1}→实际寄存器, ...
return template.format(rd=operands[0], rs1=operands[1], ...)
关键细节
- 内存操作数处理:
lw a0, 16(sp) 需要把 16 提取为 imm,sp 提取为 rs1
- 分支跳转指令:目标标签通常是最后一个操作数(不是第一个)
动手练习
练习 1: 美化一段手写汇编
def beautify_asm(asm_text: str, align: bool = True,
add_comments: bool = True) -> str:
# 1. 按行解析 → 每行拆成 {label, opcode, operands, comment}
lines = asm_text.strip().split("\n")
parsed_lines = [_parse_line(ln) for ln in lines]
# 2. 第一遍扫描:计算每列的最大宽度
max_label = max(len(p["label"]) for p in parsed_lines)
max_opcode = max(len(p["opcode"]) for p in parsed_lines)
# ... clamp 宽度避免溢出 ...
# 3. 第二遍扫描:按对齐宽度格式化,加注释
for p in parsed_lines:
formatted = _format_line(p, align, max_label, max_opcode, ...)
output_lines.append(formatted)
return "\n".join(output_lines)
_gen_comment()
def _gen_comment(opcode: str, operands: list[str]) -> str:
template = _INST_COMMENTS.get(opcode) # 查模板表
if template is None:
return "" # 未知指令不加注释
# 替换模板变量:{rd}→实际寄存器, {rs1}→实际寄存器, ...
return template.format(rd=operands[0], rs1=operands[1], ...)
关键细节
- 内存操作数处理:
lw a0, 16(sp) 需要把 16 提取为 imm,sp 提取为 rs1
- 分支跳转指令:目标标签通常是最后一个操作数(不是第一个)
动手练习
练习 1: 美化一段手写汇编
lw a0, 16(sp) 需要把 16 提取为 imm,sp 提取为 rs1练习 1: 美化一段手写汇编
创建 test.s:
.text
main:
addi sp,sp,-16
li a0,10
li a1,20
add a0,a0,a1
sw a0,0(sp)
lw a0,0(sp)
ret
然后运行美化器观察效果。
练习 2: 添加新的指令模板
在 _INST_COMMENTS 字典中添加一条新指令的注释模板。比如添加 max(ScratchV 伪指令)的模板。
练习 3: 对比美化前后的可读性
用 make bench-cnn 生成汇编,分别看美化前后的效果。哪种更容易理解?
常见坑
| 坑 | 说明 |
|---|---|
| 操作数顺序混淆 | RISC-V 中 add rd, rs1, rs2 和 sw rs2, imm(rs1) 的操作数"角色"不同,美化器需要区分 |
| 伪指令 | li、mv、call 等不是真正的 RISC-V 指令,是汇编器的"语法糖",模板需要单独处理 |
| 列宽 clamp | 标签列宽最大 30、操作码最大 12、操作数最大 40,太长会被截断避免排版溢出 |
| 浮点指令 | fadd.s、fmul.s 等浮点指令注释模板已内置但较少使用(ScratchV 目前主要用整数路径) |
进阶阅读
- RISC-V 汇编语法参考:RISC-V Assembly Programmer's Manual
- ABI 寄存器命名约定:RISC-V ELF psABI
- 相关 topic: 课题07:编译器日志增强器 | 课题13:窥孔优化器
12周每周目标
- W1:学习RISC-V基础指令集,阅读项目生成的
.s文件样例,分析格式问题。
- W2:编写正则表达式,从一行汇编中提取标签、指令、操作数(逗号分割)、注释。
- W3:实现字段对齐:设定指令助记符宽度8,操作数宽度20,左对齐或右对齐。
- W4:输出对齐后的汇编行,保留空行和纯注释。生成第一版美化脚本。
- W5:构建指令注释字典,为常见指令(
add, sub, lw, sw, beq, jal)撰写人类可读解释模板。
- W6:实现自动注释生成:根据指令操作数填充模板中的寄存器名(如
x1→ra可选)。
- W7:在代码段前添加段注释:
.text前加# ===== CODE SECTION =====,.data前加类似标记。
- W8:处理伪指令(
li, mv)的特殊注释,确保注释不会过长。
- W9:添加命令行参数(
argparse):输入文件、输出文件、--no-comments、--no-align。
- W10:美化错误处理:遇到无法解析的行原样输出并警告,支持批量处理多个文件。
- W11:测试至少10个不同的汇编文件(包括错误格式),对比美化前后可读性,编写测试脚本。
- W12:撰写文档(安装依赖、使用示例、正则表达式规则),准备演示。
- W1:学习RISC-V基础指令集,阅读项目生成的
.s文件样例,分析格式问题。 - W2:编写正则表达式,从一行汇编中提取标签、指令、操作数(逗号分割)、注释。
- W3:实现字段对齐:设定指令助记符宽度8,操作数宽度20,左对齐或右对齐。
- W4:输出对齐后的汇编行,保留空行和纯注释。生成第一版美化脚本。
- W5:构建指令注释字典,为常见指令(
add, sub, lw, sw, beq, jal)撰写人类可读解释模板。 - W6:实现自动注释生成:根据指令操作数填充模板中的寄存器名(如
x1→ra可选)。 - W7:在代码段前添加段注释:
.text前加# ===== CODE SECTION =====,.data前加类似标记。 - W8:处理伪指令(
li,mv)的特殊注释,确保注释不会过长。 - W9:添加命令行参数(
argparse):输入文件、输出文件、--no-comments、--no-align。 - W10:美化错误处理:遇到无法解析的行原样输出并警告,支持批量处理多个文件。
- W11:测试至少10个不同的汇编文件(包括错误格式),对比美化前后可读性,编写测试脚本。
- W12:撰写文档(安装依赖、使用示例、正则表达式规则),准备演示。