上次课内容: C程序(指令序列)如何执行
实现更多指令, 就能得到一个功能更完整的模拟器/处理器
本次课内容: NEMU(加强版YEMU)代码导读
The make utility will determine automatically which pieces of a large program need
to be recompiled, and issue the commands to recompile them...
To prepare to use make, you must write a file called the makefile that describes
the relationships among files in your program, and the states the commands for
updating each file...
...The make program uses the makefile description and the last-modification times
of the files to decide which of the files need to be updated.
RTFM就可以了解make工具的基本功能和原理:
makefile
的文件来描述程序各种文件之间的依赖关系makefile
中相应的命令来更新这些文件
可以输出make工具的调试日志
也可以通过strace
了解make工具的行为
stat
系统调用可以查询文件信息
# 变量
BIN = yemu
USER_BIN = hello.bin
RV_CFLAGS = -march=rv32g -mabi=ilp32 -ffreestanding -nostdlib -static -Wl,-Ttext=0x0 -O2
SRCS = $(shell ls *.c) # 将shell命令的运行结果赋值给变量
OBJS = $(SRCS:.c=.o) # 字符串处理, SRCS变量中的.c替换成.o
CC = gcc
# 规则
prog/hello.elf: prog/hello.c
riscv64-linux-gnu-gcc $(RV_CFLAGS) -o $@ $^ # 自动变量
$(USER_BIN): prog/hello.elf
riscv64-linux-gnu-objcopy -j .text -O binary $^ $@
$(BIN): $(OBJS) # 隐含规则
run-yemu: $(BIN) $(USER_BIN) # 此规则并不生成目标run-yemu
./$(BIN) $(USER_BIN)
.PHONY: run-yemu clean # 定义伪目标
clean: # 用于清除编译结果
-@rm $(OBJS) $(BIN) prog/hello.elf $(USER_BIN) 2> /dev/null
变量 = C语言中的宏定义
subst
, shell
等规则中也可以引用变量
$@
- 规则的目标, $^
- 规则的依赖隐含规则: make可以自动推导一些常见的编译规则, 如
strace
和调试日志理解这一行为伪目标: 相应的规则不检查依赖关系, 总是执行规则的命令
更多细节: 《跟我一起写Makefile》(入门), 《GNU Make Manual》(精通)
更复杂了…
mk
文件
wildcard
, patsubst
,
filter-out
…ifeq ... endif
应该如何下手?
下手需要选择容易的方式: 看踪迹
踪迹也有不同的层次:
strace
- 系统调用踪迹,
查看程序/工具如何与操作系统交互make -d
- make工具的调试日志, 查看make如何进行决策?
- 面向上层用户的日志,
只想了解make执行哪些命令来编译NEMU
RTFM: man make
, 搜索command
关键字
可以找到两个相关的选项
-n
: 只打印命令但不执行(更合适)--trace
: 输出目标被构建的原因和执行的命令有同学反馈man make
中的内容很多, 一打开就不想读
一个普遍的误区: 读手册 = 读完整本手册
实际中的RTFM: 在FM中快速定位感兴趣的信息
例如, man make
中有30个选项
输出了好多信息, 在vim中稍作处理提升可读性
# 只保留gcc或g++开头的行
:%!grep "^\(gcc\|g++\)"
# 将环境变量$NEMU_HOME所指示字符串替换为$NEMU_HOME
:%!sed -e "s+$NEMU_HOME+\$NEMU_HOME+g"
# 将$NEMU_HOME/build/obj-riscv32-nemu-interpreter替换为$OBJ_DIR
:%s+\$NEMU_HOME/build/obj-riscv32-nemu-interpreter+$OBJ_DIR+g
# 将-c之前的内容替换为$CFLAGS
:%s/-O2.*=riscv32/$CFLAGS/g
# 将最后一行的空格替换成换行并缩进两格
:$s/ */\r /g
和编译YEMU很类似, 但多了一些编译和链接选项(老规矩: RTFM)
大致理解了make的行为, 就可以回过头来(连蒙带猜地)看Makefile了
nemu/Makefile
SRCS
: 和YEMU差不多, 是需要编译的源文件CFLAGS
: 刚才看到的编译选项include $(NEMU_HOME)/scripts/native.mk
:
包含其他文件nemu/scripts/native.mk
nemu/scripts/build.mk
-MMD
选项生成,
并通过fixdep
工具处理)来看看项目规模
find . -name "*.[ch]" | grep -E "^\./(src|include)" | \
grep -E -v "^\./include/config" | grep -v "mips32\|riscv64\|loongarch32r" | wc
find . -name "*.[ch]" | grep -E "^\./(src|include)" | \
grep -E -v "^\./include/config" | grep -v "mips32\|riscv64\|loongarch32r" | xargs wc
和NEMU直接相关的源文件: 50+个, 3500+行
其实项目规模很小, QEMU有25000+个源文件, 110000+行源代码 😂
框架代码有一个练习用的assert(0)
,
完成练习之后可成功运行
$ make run
+ LD /home/ysyx/nemu/build/riscv32-nemu-interpreter
/home/ysyx/nemu/build/riscv32-nemu-interpreter --log=/home/ysyx/nemu/build/nemu-log.t
xt
[src/utils/log.c:28 init_log] Log is written to /home/ysyx/nemu/build/nemu-log.txt
[src/memory/paddr.c:56 init_mem] physical memory area [0x80000000, 0x87ffffff]
[src/monitor/monitor.c:51 load_img] No image is given. Use the default build-in image
.
[src/monitor/monitor.c:28 welcome] Trace: ON
[src/monitor/monitor.c:29 welcome] If trace is enabled, a log file will be generated
to record the trace. This may lead to a large log file. If it is not necessary, you
can disable it in menuconfig
[src/monitor/monitor.c:32 welcome] Build time: 14:35:42, Sep 9 2023
Welcome to riscv32-NEMU!
For help, type "help"
[src/monitor/monitor.c:35 welcome] Exercise: Please remove me in the source code and
compile NEMU again.
(nemu) c
[src/cpu/cpu-exec.c:120 cpu_exec] nemu: HIT GOOD TRAP at pc = 0x8000000c
[src/cpu/cpu-exec.c:88 statistic] host time spent = 95 us
[src/cpu/cpu-exec.c:89 statistic] total guest instructions = 4
[src/cpu/cpu-exec.c:90 statistic] simulation frequency = 42,105 inst/s
(nemu) q
发现log
/trace
等关键字
[src/utils/log.c:28 init_log] Log is written to /home/ysyx/nemu/build/nemu-log.txt
[src/memory/paddr.c:56 init_mem] physical memory area [0x80000000, 0x87ffffff]
[src/monitor/monitor.c:51 load_img] No image is given. Use the default build-in image
.
[src/monitor/monitor.c:28 welcome] Trace: ON
[src/monitor/monitor.c:29 welcome] If trace is enabled, a log file will be generated
to record the trace. This may lead to a large log file. If it is not necessary, you
can disable it in menuconfig
[src/monitor/monitor.c:32 welcome] Build time: 14:35:42, Sep 9 2023
[src/monitor/monitor.c:35 welcome] Exercise: Please remove me in the source code and
compile NEMU again.
0x80000000: 80 00 02 b7 lui t0, 524288
0x80000004: 00 02 a0 23 sw zero, 0(t0)
0x80000008: 00 02 a5 03 lw a0, 0(t0)
0x8000000c: 00 10 00 73 ebreak
[src/cpu/cpu-exec.c:120 cpu_exec] nemu: HIT GOOD TRAP at pc = 0x8000000c
[src/cpu/cpu-exec.c:88 statistic] host time spent = 95 us
[src/cpu/cpu-exec.c:89 statistic] total guest instructions = 4
[src/cpu/cpu-exec.c:90 statistic] simulation frequency = 42,105 inst/s
只是多打印了几条指令, 但可以看出: 本质上和YEMU一样在执行指令!
大致了解NEMU的动态行为后, 就可以来阅读代码了
main()
函数开始啦
下一个问题: main()
函数在哪里呢?
nemu/src/nemu-main.c
,
直觉告诉你在里面grep -nr "\bmain\b" nemu/src
源代码是静态的, 状态机是动态的: 结合程序运行的行为更容易理解
一些常用的GDB命令(更多的命令RTFM)
r
- 重新开始执行程序s
- 单步执行一行源代码 / n
-
类似但不进入函数(可用于跳过库函数)finish
- 执行直到当前函数返回p
- 打印变量或寄存器的值x
- 扫描内存bt
- 查看调用栈b
- 设置断点 / watch
- 设置监视点help xxx
- 查看xxx
命令的帮助通过init_monitor
初始化NEMU的大部分功能
parse_args()
- 解析参数
getopt_long()
man getopt_long
, 还赠送了一个示例--log
参数
init_rand()
- 初始化随机数
init_log()
- 打开日志文件
Assert()
宏和YEMU中介绍的类似Log()
宏与printf()
类似, 但进行了如下增强:
__FILE__
/__LINE__
/__func__
-
C语言提供的预定义宏
__func__
其实是个预定义的变量man console_codes
)不换行可能无法及时输出到终端, RTFM: man setbuf
init_mem()
- 初始化状态机的\(M\)
init_isa()
- 设置状态机的初始状态
memcpy()
往\(M\)中拷贝一段内置程序cpu.pc = RESET_VECTOR
, 该值可通过menuconfig配置,
默认为0x80000000
cpu.gpr[0] = 0
, 0号寄存器恒为0load_img()
- 加载命令行指定的镜像文件
init_difftest()
- DiffTest测试相关, 目前未开启,
可忽略
init_sdb()
- 初始化SDB
init_disasm()
- 初始化LLVM提供的用于反汇编的库函数
welcome()
- 输出欢迎信息
engine_start()
-> sdb_mainloop()
在NEMU中输入c
命令, 将会开始执行程序
cpu_exec()
-> execute()
->
exec_once()
-> isa_exec_once()
核心功能与YEMU的inst_cycle()
非常类似
NEMU为大家准备了一个 “抄手册宏”
用它来实现指令, 真的只需要抄手册就可以了 😂
但这些宏的展开过程并不那么好读 😂
用笔在纸上推导一下展开过程, 人肉宏展开
使用工具查看宏展开结果
回顾: 使用gcc的-E
参数可以输出预处理结果
解决方案: 在Makefile文件的编译规则中添加命令
pattern_decode()
采用循环展开而不是for
循环来从字符串中计算出key
,
mask
和shift
属性
STRLEN(pattern)
而不是strlen(pattern)
nemu/include/macro.h
IFDEF
/IFNDEF
-
可以替代#ifdef
/#ifndef
, 写出更紧凑的代码MAP(c, f)
- 类似X-macroBITS(x, hi, lo)
-
类似Verilog的x[hi:lo]
SEXT(x, len)
-
借助struct
和位域进行符号扩展
建议手动展开来体会它们的诀窍
年份 | 指令实现方案 | 不言自明 | 不言自证 |
---|---|---|---|
2014 | Copy-Paste(反面教材😂) | ✖️✖️✖️ | ✖️✖️✖️ |
2015~2016 | 函数定义宏 + include | ✖️ | ✖️ |
2017~2019 | 填表宏 + IR | ✔️ | ✔️ |
2020 | 填switch宏 + IR | ✔️ | ✔️ |
2021 | 抄手册宏 + IR | ✔️✔️ | ✔️✔️ |
2022~2023 | 抄手册宏 | ✔️✔️✔️ | ✔️✔️✔️ |
准则: 越接近手册, 不言自明和不言自证就做得越好
译码的例子说明了编码和维护之间存在一定的矛盾
初学者比较容易感知的是编码的复杂度, 但对代码的可维护性感知较弱
宏定义多起来之后, 维护不方便
交给专业的工具来维护
nemu/Kconfig
)
但 “提供一部分满足依赖关系的配置选项”并不容易, 修改也不容易
menuconfig: 读取配置描述文件, 以菜单树的形式展示各种配置选项
nemu/.config
, 是个隐藏文件kconfig编译后生成如下文件:
nemu/include/generated/autoconf.h
)
CONFIG_xxx
nemu/include/config/auto.conf
)nemu/include/config/auto.conf.cmd
)nemu/include/config/
nemu/tools/fixdep
来使用,
用于在更新配置选项后节省不必要的文件编译需求: menuconfig的某些配置选项会决定某些源文件是否参与编译
SRCS-y
- 参与编译的源文件的候选集合SRCS-BLACKLIST-y
- 不参与编译的源文件的黑名单集合DIRS-y
- 参与编译的目录集合,
该目录下的所有文件都会被加入到SRCS-y
中DIRS-BLACKLIST-y
- 不参与编译的目录集合,
该目录下的所有文件都会被加入到SRCS-BLACKLIST-y
中
与kconfig协助
nemu/include/generated/autoconf.h
集合了所有由kconfig生成的宏定义
common.h
里面吧
common.h
fixdep: 一个聪明的小工具
CONFIG_xxx
宏,
将对autoconf.h
的依赖分解成对若干空文件的依赖
一个例子: 修改Testing and Debugging -> Only trace instructions when the condition is true
读代码的最终目的是理解程序
先从容易理解的方式下手(trace), 再结合程序的动态行为理解静态代码
make -n
-> Makefile
使用正确的工具提升工作效率
make -n
的信息方便理解