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 machines
ccache: store the compiled file after it have been deleted by make clean

PA1

overview

What should be done in PA:

1
2
3
4
5
6
7
8
9
10
11
12
13
                         +---------------------+  +---------------------+
| Super Mario | | "Hello World" |
+---------------------+ +---------------------+
| Simulated NES | | Simulated |
| hardware | | hardware |
+---------------------+ +---------------------+ +---------------------+
| "Hello World" | | NES Emulator | | NEMU |
+---------------------+ +---------------------+ +---------------------+
| GNU/Linux | | GNU/Linux | | GNU/Linux |
+---------------------+ +---------------------+ +---------------------+
| Real hardware | | Real hardware | | Real hardware |
+---------------------+ +---------------------+ +---------------------+
(a) (b) (c)

using riscv-32 as ISA!
CPU: central processing unit
运算器 operator
寄存器 容量小速度快; 存储器 容量大速度慢
执行程序 –> program counter(PC)
the nature of computer:

1
2
3
4
5
while (1) {
从PC指示的存储器位置取出指令;
执行指令;
更新PC;
}

计算机是一个状态机!

  • 时序逻辑部件(存储器,计数器,寄存器)
  • 组合逻辑部件(加法器)
    程序也是一个状态机!
    静态视角:代码
    动态视角:状态机的转换
    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
    60
    nemu
    ├── 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 system
make 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 the nemu/include/generated/autoconf.h (for reading C++ file) and nemu/include/config/auto.conf for reading makefile

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

  1. 调用init_monitor()函数(在nemu/src/monitor/monitor.c中定义) 来进行一些和monitor相关的初始化工作 (parse_args()init_rand()init_log(),init_mem())
  2. 调用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中定义一个全局变量cpu
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    pmem:
    CONFIG_MBASE RESET_VECTOR
    | |
    v v
    -----------------------------------------------
    | | |
    | | guest prog |
    | | |
    -----------------------------------------------
    ^
    |
    pc
    now, just run make run in the nemu directory.

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()体现

阅读代码

  1. 找到main函数: grep -n main $(find . -name "*.c") # RTFM: -n find . | xargs grep --color -nse '\<main\>'

基础设施

正则表达式

VScode 未定义数据类型 修改设置

settings.json中设置:

1
2
  "C_Cpp.intelliSenseEngineFallback": "Disabled",
  "C_Cpp.intelliSenseEngine": "Tag Parser"

如何调试

  • 使用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非常遥远了