PA1-notes
advanced make
lscpu to see how many CPUs have in your computer
then, add -j? after the make to make the project run in multiple machinesccache: store the compiled file after it have been deleted by make clean
PA1
overview
What should be done in PA:
1 | +---------------------+ +---------------------+ |
using riscv-32 as ISA!
CPU: central processing unit
运算器 operator
寄存器 容量小速度快; 存储器 容量大速度慢
执行程序 –> program counter(PC)
the nature of computer:
1 | while (1) { |
计算机是一个状态机!
- 时序逻辑部件(存储器,计数器,寄存器)
- 组合逻辑部件(加法器)
程序也是一个状态机!
静态视角:代码
动态视角:状态机的转换
monitor: 方便地监控客户计算机的运行状态
most of the files’ function in the filelist: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
52
53
54
55
56
57
58
59
60nemu
├── configs # 预先提供的一些配置文件
├── include # 存放全局使用的头文件
│ ├── common.h # 公用的头文件
│ ├── config # 配置系统生成的头文件, 用于维护配置选项更新的时间戳
│ ├── cpu
│ │ ├── cpu.h
│ │ ├── decode.h # 译码相关
│ │ ├── difftest.h
│ │ └── ifetch.h # 取指相关
│ ├── debug.h # 一些方便调试用的宏
│ ├── device # 设备相关
│ ├── difftest-def.h
│ ├── generated
│ │ └── autoconf.h # 配置系统生成的头文件, 用于根据配置信息定义相关的宏
│ ├── isa.h # ISA相关
│ ├── macro.h # 一些方便的宏定义
│ ├── memory # 访问内存相关
│ └── utils.h
├── Kconfig # 配置信息管理的规则
├── Makefile # Makefile构建脚本
├── README.md
├── resource # 一些辅助资源
├── scripts # Makefile构建脚本
│ ├── build.mk
│ ├── config.mk
│ ├── git.mk # git版本控制相关
│ └── native.mk
├── src # 源文件
│ ├── cpu
│ │ └── cpu-exec.c # 指令执行的主循环
│ ├── device # 设备相关
│ ├── engine
│ │ └── interpreter # 解释器的实现
│ ├── filelist.mk
│ ├── isa # ISA相关的实现
│ │ ├── mips32
│ │ ├── riscv32
│ │ ├── riscv64
│ │ └── x86
│ ├── memory # 内存访问的实现
│ ├── monitor
│ │ ├── monitor.c
│ │ └── sdb # 简易调试器
│ │ ├── expr.c # 表达式求值的实现
│ │ ├── sdb.c # 简易调试器的命令处理
│ │ └── watchpoint.c # 监视点的实现
│ ├── nemu-main.c # 你知道的...
│ └── utils # 一些公共的功能
│ ├── log.c # 日志文件相关
│ ├── rand.c
│ ├── state.c
│ └── timer.c
└── tools # 一些工具
├── fixdep # 依赖修复, 配合配置系统进行使用
├── gen-expr
├── kconfig # 配置系统
├── kvm-diff
├── qemu-diff
└── spike-diff
system configuration
kconfig, a set of language to manage the systemmake menuconfig:
- 检查
nemu/tools/kconfig/build/mconf程序是否存在, 若不存在, 则编译并生成mconf - 检查
nemu/tools/kconfig/build/conf程序是否存在, 若不存在, 则编译并生成conf - 运行命令
mconf nemu/Kconfig, 此时mconf将会解析nemu/Kconfig中的描述, 以菜单树的形式展示各种配置选项, 供开发者进行选择 - 退出菜单时,
mconf会把开发者选择的结果记录到nemu/.config文件中 - 运行命令
conf --syncconfig nemu/Kconfig, 此时conf将会解析nemu/Kconfig中的描述, 并读取选择结果nemu/.config, 结合两者来生成如下文件:- 可以被包含到C代码中的宏定义(
nemu/include/generated/autoconf.h), 这些宏的名称都是形如CONFIG_xxx的形式 - 可以被包含到Makefile中的变量定义(
nemu/include/config/auto.conf) - 可以被包含到Makefile中的, 和”配置描述文件”相关的依赖规则(
nemu/include/config/auto.conf.cmd), 为了阅读代码, 我们可以不必关心它 - 通过时间戳来维护配置选项变化的目录树
nemu/include/config/, 它会配合另一个工具nemu/tools/fixdep来使用, 用于在更新配置选项后节省不必要的文件编译, 为了阅读代码, 我们可以不必关心它
we only need to care for thenemu/include/generated/autoconf.h(for reading C++ file) andnemu/include/config/auto.conffor reading makefile
- 可以被包含到C代码中的宏定义(
project building
与配置系统进行关联
通过包含nemu/include/config/auto.conf, 与kconfig生成的变量进行关联. 因此在通过menuconfig更新配置选项后, Makefile的行为可能也会有所变化.
文件列表(filelist)
通过文件列表(filelist)决定最终参与编译的源文件. 在nemu/src及其子目录下存在一些名为filelist.mk的文件, 它们会根据menuconfig的配置对如下4个变量进行维护:
SRCS-y- 参与编译的源文件的候选集合SRCS-BLACKLIST-y- 不参与编译的源文件的黑名单集合DIRS-y- 参与编译的目录集合, 该目录下的所有文件都会被加入到SRCS-y中DIRS-BLACKLIST-y- 不参与编译的目录集合, 该目录下的所有文件都会被加入到SRCS-BLACKLIST-y中
Makefile会包含项目中的所有filelist.mk文件, 对上述4个变量的追加定义进行汇总, 最终会过滤出在SRCS-y中但不在SRCS-BLACKLIST-y中的源文件, 来作为最终参与编译的源文件的集合.
编译规则:1
2
3
4
5$(OBJ_DIR)/%.o: %.c
@echo + CC $<
@mkdir -p $(dir $@)
@$(CC) $(CFLAGS) -c -o $@ $<
$(call call_fixdep, $(@:.o=.d), $@)1
2
3
4$(CC) -> gcc
$@ -> /home/user/ics2024/nemu/build/obj-riscv32-nemu-interpreter/src/utils/timer.o
$< -> src/utils/timer.c
$(CFLAGS) -> 剩下的内容
the first user program
- 调用
init_monitor()函数(在nemu/src/monitor/monitor.c中定义) 来进行一些和monitor相关的初始化工作 (parse_args(),init_rand(),init_log(),init_mem()) - 调用
init_isa()函数(在nemu/src/isa/$ISA/init.c中定义), 来进行一些ISA相关的初始化工作.
- 将客户程序(
nemu/src/isa/$ISA/init.c)读到内存(uint8_t数组模拟)里(固定位置RESET_VECTOR)现实生活中–bios初始化,再读程序运行
- 初始化寄存器(
restart) :寄存器结构体CPU_state的定义放在nemu/src/isa/$ISA/include/isa-def.h中, 并在nemu/src/cpu/cpu-exec.c中定义一个全局变量cpunow, just run1
2
3
4
5
6
7
8
9
10
11
12pmem:
CONFIG_MBASE RESET_VECTOR
| |
v v
-----------------------------------------------
| | |
| | guest prog |
| | |
-----------------------------------------------
^
|
pcmake runin thenemudirectory.
start running the first client program
Monitor的初始化工作结束后, main()函数会继续调用engine_start()函数 (在nemu/src/engine/interpreter/init.c中定义). 代码会进入简易调试器(Simple Debugger)的主循环sdb_mainloop() (在nemu/src/monitor/sdb/sdb.c中定义)
在命令提示符后键入c后, NEMU开始进入指令执行的主循环cpu_exec() (在nemu/src/cpu/cpu-exec.c中定义)
q to quit the loop
- 三个对调试有用的宏(在
nemu/include/debug.h中定义)Log()是printf()的升级版, 专门用来输出调试信息, 同时还会输出使用Log()所在的源文件, 行号和函数. 当输出的调试信息过多的时候, 可以很方便地定位到代码中的相关位置Assert()是assert()的升级版, 当测试条件为假时, 在assertion fail之前可以输出一些信息panic()用于输出信息并结束程序, 相当于无条件的assertion fail
- 内存通过在
nemu/src/memory/paddr.c中定义的大数组pmem来模拟. 在客户程序运行的过程中, 总是使用vaddr_read()和vaddr_write()(在nemu/src/memory/vaddr.c中定义)来访问模拟的内存. vaddr, paddr分别代表虚拟地址和物理地址. 这些概念在将来才会用到, 目前不必深究, 但从现在开始保持接口的一致性可以在将来避免一些不必要的麻烦.
to sum up, - 存储器是个在
nemu/src/memory/paddr.c中定义的大数组 - PC和通用寄存器都在
nemu/src/isa/$ISA/include/isa-def.h中的结构体中定义 - 加法器在… 嗯, 这部分框架代码有点复杂, 不过它并不影响我们对TRM的理解, 我们还是在PA2里面再介绍它吧
- TRM的工作方式通过
cpu_exec()和exec_once()体现
阅读代码
- 找到
main函数:grep -n main $(find . -name "*.c") # RTFM: -n find . | xargs grep --color -nse '\<main\>'
基础设施
正则表达式
VScode 未定义数据类型 修改设置
settings.json中设置:
1 | "C_Cpp.intelliSenseEngineFallback": "Disabled", |
如何调试
- 使用
assert()设置检查点, 拦截非预期情况- 例如
assert(p != NULL)就可以拦截由空指针解引用引起的段错误
- 例如
- 结合对程序执行行为的理解, 使用
printf()查看程序执行的情况(注意字符串要换行)printf()输出任意信息可以检查代码可达性: 输出了相应信息, 当且仅当相应的代码块被执行printf()输出变量的值, 可以检查其变化过程与原因
- 使用GDB观察程序的任意状态和行为
- 打印变量, 断点, 监视点, 函数调用栈…
一些软件工程相关的概念:
- Fault: 实现错误的代码, 例如
if (p = NULL) - Error: 程序执行时不符合预期的状态, 例如
p被错误地赋值成NULL - Failure: 能直接观测到的错误, 例如程序触发了段错误
调试其实就是从观测到的failure一步一步回溯寻找fault的过程, 找到了fault之后, 我们就很快知道应该如何修改错误的代码了. 但从上面的例子也可以看出, 调试之所以不容易, 恰恰是因为:
- fault不一定马上触发error
- 触发了error也不一定马上转变成可观测的failure
- error会像滚雪球一般越积越多, 当我们观测到failure的时候, 其实已经距离fault非常遥远了
