NEMU

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

首先展示运行make run时的输出,里面许多内容会在后面的代码中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xiaoma@xiaoma-virtual-machine:~/Downloads/ics/ics_pa/nemu$ make run
/home/xiaoma/Downloads/ics/ics_pa/nemu/build/riscv32-nemu-interpreter --log=/home/xiaoma/Downloads/ics/ics_pa/nemu/build/nemu-log.txt
[src/utils/log.c:30 init_log] Log is written to /home/xiaoma/Downloads/ics/ics_pa/nemu/build/nemu-log.txt
[src/memory/paddr.c:50 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: 04:58:17, Apr 6 2024
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 = 119 us
[src/cpu/cpu-exec.c:89 statistic] total guest instructions = 4
[src/cpu/cpu-exec.c:90 statistic] simulation frequency = 33,613 inst/s

nemu-main.c

NEMU开始运行的时候,自然要找到main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <common.h>

void init_monitor(int, char *[]);
void am_init_monitor();
void engine_start();
int is_exit_status_bad();

int main(int argc, char *argv[]) {
/* Initialize the monitor. */
#ifdef CONFIG_TARGET_AM
am_init_monitor();
#else
init_monitor(argc, argv);
#endif

/* Start engine. */
engine_start();

return is_exit_status_bad();
}

可以看到调用了一些初始化函数,下面会展示其源代码

monitor.c

在运行客户机时,首先要讲程序读入客户机,这是通过monitor完成的。在monitor.c中:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <isa.h>
#include <memory/paddr.h>

void init_rand();
void init_log(const char *log_file);
void init_mem();
void init_difftest(char *ref_so_file, long img_size, int port);
void init_device();
void init_sdb();
void init_disasm(const char *triple);

static void welcome() {
Log("Trace: %s", MUXDEF(CONFIG_TRACE, ANSI_FMT("ON", ANSI_FG_GREEN), ANSI_FMT("OFF", ANSI_FG_RED)));
IFDEF(CONFIG_TRACE, Log("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"));
Log("Build time: %s, %s", __TIME__, __DATE__);
printf("Welcome to %s-NEMU!\n", ANSI_FMT(str(__GUEST_ISA__), ANSI_FG_YELLOW ANSI_BG_RED));
printf("For help, type \"help\"\n");
Log("Exercise: Please remove me in the source code and compile NEMU again.");
// assert(0);
}

#ifndef CONFIG_TARGET_AM
#include <getopt.h>

void sdb_set_batch_mode();

static char *log_file = NULL;
static char *diff_so_file = NULL;
static char *img_file = NULL;
static int difftest_port = 1234;

static long load_img() {
if (img_file == NULL) {
Log("No image is given. Use the default build-in image.");
return 4096; // built-in image size
}

FILE *fp = fopen(img_file, "rb");
Assert(fp, "Can not open '%s'", img_file);

fseek(fp, 0, SEEK_END);
long size = ftell(fp);

Log("The image is %s, size = %ld", img_file, size);

fseek(fp, 0, SEEK_SET);
int ret = fread(guest_to_host(RESET_VECTOR), size, 1, fp);
assert(ret == 1);

fclose(fp);
return size;
}

static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"batch" , no_argument , NULL, 'b'},
{"log" , required_argument, NULL, 'l'},
{"diff" , required_argument, NULL, 'd'},
{"port" , required_argument, NULL, 'p'},
{"help" , no_argument , NULL, 'h'},
{0 , 0 , NULL, 0 },
};
int o;
while ( (o = getopt_long(argc, argv, "-bhl:d:p:", table, NULL)) != -1) {
switch (o) {
case 'b': sdb_set_batch_mode(); break;
case 'p': sscanf(optarg, "%d", &difftest_port); break;
case 'l': log_file = optarg; break;
case 'd': diff_so_file = optarg; break;
case 1: img_file = optarg; return 0;
default:
printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
printf("\t-b,--batch run with batch mode\n");
printf("\t-l,--log=FILE output log to FILE\n");
printf("\t-d,--diff=REF_SO run DiffTest with reference REF_SO\n");
printf("\t-p,--port=PORT run DiffTest with port PORT\n");
printf("\n");
exit(0);
}
}
return 0;
}

void init_monitor(int argc, char *argv[]) {
/* Perform some global initialization. */

/* Parse arguments. */
parse_args(argc, argv);

/* Set random seed. */
init_rand();

/* Open the log file. */
init_log(log_file);

/* Initialize memory. */
init_mem();

/* Initialize devices. */
IFDEF(CONFIG_DEVICE, init_device());

/* Perform ISA dependent initialization. */
init_isa();

/* Load the image to memory. This will overwrite the built-in image. */
long img_size = load_img();

/* Initialize differential testing. */
init_difftest(diff_so_file, img_size, difftest_port);

/* Initialize the simple debugger. */
init_sdb();

#ifndef CONFIG_ISA_loongarch32r
IFDEF(CONFIG_ITRACE, init_disasm(
MUXDEF(CONFIG_ISA_x86, "i686",
MUXDEF(CONFIG_ISA_mips32, "mipsel",
MUXDEF(CONFIG_ISA_riscv,
MUXDEF(CONFIG_RV64, "riscv64",
"riscv32"),
"bad"))) "-pc-linux-gnu"
));
#endif

/* Display welcome message. */
welcome();
}
#else // CONFIG_TARGET_AM
static long load_img() {
extern char bin_start, bin_end;
size_t size = &bin_end - &bin_start;
Log("img size = %ld", size);
memcpy(guest_to_host(RESET_VECTOR), &bin_start, size);
return size;
}

void am_init_monitor() {
init_rand();
init_mem();
init_isa();
load_img();
IFDEF(CONFIG_DEVICE, init_device());
welcome();
}
#endif

init_monitor()

NEMU开始运行的时候会执行函数void init_monitor(int argc, char *argv[])。可以看到,这个函数调用了parse_args(argc, argv);来解析参数:

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
static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"batch" , no_argument , NULL, 'b'},
{"log" , required_argument, NULL, 'l'},
{"diff" , required_argument, NULL, 'd'},
{"port" , required_argument, NULL, 'p'},
{"help" , no_argument , NULL, 'h'},
{0 , 0 , NULL, 0 },
};
int o;
while ( (o = getopt_long(argc, argv, "-bhl:d:p:", table, NULL)) != -1) {
switch (o) {
case 'b': sdb_set_batch_mode(); break;
case 'p': sscanf(optarg, "%d", &difftest_port); break;
case 'l': log_file = optarg; break;
case 'd': diff_so_file = optarg; break;
case 1: img_file = optarg; return 0;
default:
printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
printf("\t-b,--batch run with batch mode\n");
printf("\t-l,--log=FILE output log to FILE\n");
printf("\t-d,--diff=REF_SO run DiffTest with reference REF_SO\n");
printf("\t-p,--port=PORT run DiffTest with port PORT\n");
printf("\n");
exit(0);
}
}
return 0;
}

可以看到,首先定义了一个选项表格,用于创建命令的不同选项以及是否需要参数,比如-b、-d等等。表格中的四项值对应的是option结构体(文档):

1
2
3
4
5
6
7
struct option {
const char *name; // 选项的名称
int has_arg; // 是否需要参数(no_argument, required_argument, optional_argument)
int *flag; // 指向一个整数,用于表示选项是否出现以及选项的值
int val; // 选项对应的短选项字符
};

"-bhl:d:p:"是对选项解析规则的定义:

  • -b: 表示有一个短选项b,它不需要参数。
  • -h: 表示有一个短选项h,它不需要参数。
  • -l: 表示有一个短选项l,它后面需要跟一个参数。
  • -d: 表示有一个短选项d,它后面需要跟一个参数。
  • -p: 表示有一个短选项p,它后面需要跟一个参数。

最后通过switch语句对实现各个选项对应命令的调用

这里用到了函数getopt_long():这是用于解析命令行参数的函数。文档参考https://www.gnu.org/software/libc/manual/html_node/Getopt-Long-Options.html


接下来是一些简单的初始化,包括随机数种子、日志文件、所选ISA依赖的初始化等等

最后是条件编译,根据预处理器宏的定义选择不同的代码(第一次见到的操作!)

IFDEF宏用于判断另一个宏是否被定义,如果被定义,则执行init_disasm函数的调用;否则,跳过这个调用。

接下来是嵌套的MUXDEF宏,用于根据不同的预处理器宏选择不同的字符串参数。根据CONFIG_ISA_x86CONFIG_ISA_mips32CONFIG_ISA_riscv这些宏的定义情况,选择不同的架构名称字符串( “i686”、”mipsel”、”riscv64”、”riscv32”等)作为参数传递给init_disasm函数。

整个嵌套的MUXDEFIFDEF的结构用于在编译时根据不同的配置选择合适的参数传递给init_disasm函数。最后,"-pc-linux-gnu"作为额外的字符串参数传递给init_disasm函数。

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
#ifndef CONFIG_ISA_loongarch32r
IFDEF(CONFIG_ITRACE, init_disasm(
MUXDEF(CONFIG_ISA_x86, "i686",
MUXDEF(CONFIG_ISA_mips32, "mipsel",
MUXDEF(CONFIG_ISA_riscv,
MUXDEF(CONFIG_RV64, "riscv64",
"riscv32"),
"bad"))) "-pc-linux-gnu"
));
#endif

/* Display welcome message. */
welcome();
}
#else // CONFIG_TARGET_AM
static long load_img() {
extern char bin_start, bin_end;
size_t size = &bin_end - &bin_start;
Log("img size = %ld", size);
memcpy(guest_to_host(RESET_VECTOR), &bin_start, size);
return size;
}

void am_init_monitor() {
init_rand();
init_mem();
init_isa();
load_img();
IFDEF(CONFIG_DEVICE, init_device());
welcome();
}
#endif

条件编译块外的代码,展示了两个函数:load_imgam_init_monitor。在这段代码中,它们被定义为静态函数或者条件编译块外的函数。这两个函数用于初始化监视器或者加载镜像等操作,根据具体的条件编译块内的配置不同来选择执行不同的初始化流程。

首先,load_img将一个有意义的客户程序从镜像文件读入到内存, 覆盖刚才的内置客户程序. 这个镜像文件是运行NEMU的一个可选参数, 在运行NEMU的命令中指定. 如果运行NEMU的时候没有给出这个参数, NEMU将会运行内置客户程序.

1
2
3
4
5
6
7
static long load_img() {
extern char bin_start, bin_end;
size_t size = &bin_end - &bin_start;
Log("img size = %ld", size);
memcpy(guest_to_host(RESET_VECTOR), &bin_start, size);
return size;
}

A disk image is a snapshot of a storage device’s structure and data typically stored in one or more computer files on another storage device.[1][2] Traditionally, disk images were bit-by-bit copies of every sector on a hard disk often created for digital forensic purposes, but it is now common to only copy allocated data to reduce storage space.[3][4] Compression and deduplication are commonly used to reduce the size of the image file set.[3][5] Disk imaging is done for a variety of purposes including digital forensics,[6][2] cloud computing,[7] system administration,[8] as part of a backup strategy,[1] and legacy emulation as part of a digital preservation strategy.[9] Disk images can be made in a variety of formats depending on the purpose. Virtual disk images (such as VHD and VMDK) are intended to be used for cloud computing,[10][11] ISO images are intended to emulate optical media[12] and raw disk images) are used for forensic purposes.[2] Proprietary formats are typically used by disk imaging software. Despite the benefits of disk imaging the storage costs can be high,[3] management can be difficult[6] and they can be time consuming to create.[13][9]

磁盘映像是存储设备结构和数据的快照,通常存储在另一存储设备上的一个或多个计算机文件中。传统上,磁盘映像是硬盘上每个扇区的逐位副本,通常用于数字取证目的,但现在通常只复制分配的数据以减少存储空间。压缩和去重通常用于减小映像文件集的大小。进行磁盘映像有各种目的,包括数字取证、云计算、系统管理、作为备份策略的一部分,以及作为数字保存策略的一部分的传统仿真。根据目的,可以用各种格式制作磁盘映像。虚拟磁盘映像(如VHD和VMDK)旨在用于云计算,ISO映像旨在模拟光盘,原始磁盘映像用于取证目的。专有格式通常由磁盘映像软件使用。尽管磁盘映像有诸多好处,存储成本可能很高,管理可能很困难,创建可能耗时。

最后,调用welcome函数,用于显示欢迎消息。

isa

接下来,monitor会调用init_isa()函数,进行一些isa相关的初始化操作

init.c

这里存放默认的初始化客户程序

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
#include <isa.h>
#include <memory/paddr.h>

// this is not consistent with uint8_t
// but it is ok since we do not access the array directly
static const uint32_t img [] = {
0x00000297, // auipc t0,0
0x00028823, // sb zero,16(t0)
0x0102c503, // lbu a0,16(t0)
0x00100073, // ebreak (used as nemu_trap)
0xdeadbeef, // some data
};

static void restart() {
/* Set the initial program counter. */
cpu.pc = RESET_VECTOR;

/* The zero register is always 0. */
cpu.gpr[0] = 0;
}

void init_isa() {
/* Load built-in image. */
memcpy(guest_to_host(RESET_VECTOR), img, sizeof(img));

/* Initialize this virtual computer system. */
restart();
}

img

其中静态数组img的解释:

  1. 0x00000297:这是一条RISC-V指令的机器码表示,具体是auipc t0,0,表示将当前PC加上一个立即数(0)并将结果存入寄存器t0中。这是一条用于实现相对寻址的指令。
  2. 0x00028823:这是另一条RISC-V指令的机器码表示,对应sb zero,16(t0),表示将寄存器zero(通常用于存放常数0)的值存储到地址为t0+16的内存中,此处s代表存储字节(byte)。
  3. 0x0102c503:这是一条加载指令,对应lbu a0,16(t0),表示从地址为t0+16的内存中加载一个字节并零扩展(load byte unsigned)到寄存器a0中。
  4. 0x00100073:这是一条特殊的指令,对应ebreak,在这里用作nemu_trap,表示触发一个NEMU的陷阱,可能是用于中断虚拟机执行或者其他调试目的。
  5. 0xdeadbeef:这是一个常数值,用于填充数组中的最后一个元素,没有特定的指令含义,可能只是用作占位符或者标记。

显然,这就是我们NEMU内置的客户程序

restart()函数用于初始化虚拟机的状态,实际上是初始化寄存器。它将程序计数器(cpu.pc)设置为一个重置向量(RESET_VECTOR)指定的地址,并将通用寄存器组(cpu.gpr[])中的零寄存器(cpu.gpr[0])设为0。

init_isa()

init_isa()函数是虚拟机的初始化函数。它首先通过memcpy函数将img数组中的机器码指令加载到虚拟机的内存中,加载的起始地址是RESET_VECTOR。然后调用restart()函数初始化虚拟机的状态,准备运行。

下面的init_isa()函数就通过memcpy()函数将客户程序img写入主机中

1
2
3
4
5
6
7
void init_isa() {
/* Load built-in image. */
memcpy(guest_to_host(RESET_VECTOR), img, sizeof(img));

/* Initialize this virtual computer system. */
restart();
}

为什么使用uint32_t类型来存储这些32位的机器码,而不是更加精确的uint8_t类型?实际上,这些指令的每一部分并不都是完整的32位,有的只需要8位或16位,但为了简化处理,将它们统一存储为32位。同时,由于程序中并没有直接对数组进行按字节操作,因此这种类型不一致的存储方式在这里是可以接受的。

第一项工作就是将一个内置的客户程序读入到内存中.

为了理解这项工作, 我们还需要理清三个问题:

  1. 客户程序是什么? 我们知道, 程序是由指令构成的, 而不同ISA的指令也各不相同(想象一下用不同的语言来表达”你好”的意思), 因而程序本身肯定是ISA相关的. 因此, 我们把内置客户程序放在nemu/src/isa/$ISA/init.c中. 内置客户程序的行为非常简单, 它只包含少数几条指令, 甚至算不上在做一些有意义的事情.
  2. 内存是什么? 我们可以把内存看作一段连续的存储空间, 而内存又是字节编址的(即一个内存位置存放一个字节的数据), 在C语言中我们就很自然地使用一个uint8_t类型的数组来对内存进行模拟. NEMU默认为客户计算机提供128MB的物理内存(见nemu/src/memory/paddr.c中定义的pmem),
  3. 需要将客户程序读入到内存的什么位置? 为了让客户计算机的CPU可以执行客户程序, 因此我们需要一种方式让客户计算机的CPU知道客户程序的位置. 我们采取一种最简单的方式: 约定. 具体地, 我们让monitor直接把客户程序读入到一个固定的内存位置RESET_VECTOR. RESET_VECTOR的值在nemu/include/memory/paddr.h中定义.

另外,寄存器是结构化特征较强的存储部件,并且结构与特定ISA相关。寄存器结构体CPU_state的定义在isa-def.h中。另外,cpu-exec.c中定义了一个全局变量cpu。初始化寄存器的一个重要工作就是设置cpu.pc的初值, 我们需要将它设置成刚才加载客户程序的内存位置, 这样就可以让CPU从我们约定的内存位置开始执行客户程序了

x86的物理内存是从0开始编址的, 但对于一些ISA来说却不是这样, 例如mips32和riscv32的物理地址均从0x80000000开始. 因此对于mips32和riscv32, 其CONFIG_MBASE将会被定义成0x80000000. 将来CPU访问内存时, 我们会将CPU将要访问的内存地址映射到pmem中的相应偏移位置, 这是通过nemu/src/memory/paddr.c中的guest_to_host()函数实现的. 例如如果mips32的CPU打算访问内存地址0x80000000, 我们会让它最终访问pmem[0], 从而可以正确访问客户程序的第一条指令. 这种机制有一个专门的名字, 叫地址映射

interpreter

Monitor的初始化工作结束后, main()函数会继续调用engine_start()函数 (在engine/interpreter/init.c中定义). 代码会进入简易调试器(Simple Debugger)的主循环sdb_mainloop() (在nemu/src/monitor/sdb/sdb.c中定义)

engine_start()

这里声明的engine_start()函数是在sdb.c中定义的,cpu_exec()是在cpu-exec.c中定义的,而cpu_exec()又调用了exec_once()

代码将在一个for循环中不断调用exec_once()函数, 这个函数的功能就是我们在上一小节中介绍的内容: 让CPU执行当前PC指向的一条指令, 然后更新PC.

engine_start()定义了条件编译,如果定义了宏CONFIG_TARGET_AM,则调用函数cpu_exec(-1)。否则调用sdb_mainloop()进入主循环

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <cpu/cpu.h>

void sdb_mainloop();

void engine_start() {
#ifdef CONFIG_TARGET_AM
cpu_exec(-1);
#else
/* Receive commands from user. */
sdb_mainloop();
#endif
}

在命令提示符后键入c后, NEMU开始进入指令执行的主循环cpu_exec() (在nemu/src/cpu/cpu-exec.c中定义). cpu_exec()又会调用execute(), 后者模拟了CPU的工作方式: 不断执行指令. 具体地, 代码将在一个for循环中不断调用exec_once()函数, 这个函数的功能就是让CPU执行当前PC指向的一条指令, 然后更新PC.

cpu.c

用于模拟cpu的执行,实现了cpu一步步执行指令的过程

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <cpu/cpu.h>
#include <cpu/decode.h>
#include <cpu/difftest.h>
#include <locale.h>

/* The assembly code of instructions executed is only output to the screen
* when the number of instructions executed is less than this value.
* This is useful when you use the `si' command.
* You can modify this value as you want.
*/
#define MAX_INST_TO_PRINT 10

CPU_state cpu = {};
uint64_t g_nr_guest_inst = 0;
static uint64_t g_timer = 0; // unit: us
static bool g_print_step = false;

void device_update();

static void trace_and_difftest(Decode *_this, vaddr_t dnpc) {
#ifdef CONFIG_ITRACE_COND
if (ITRACE_COND) {
log_write("%s\n", _this->logbuf);
}
#endif
if (g_print_step) {
IFDEF(CONFIG_ITRACE, puts(_this->logbuf));
}
IFDEF(CONFIG_DIFFTEST, difftest_step(_this->pc, dnpc));
}

static void exec_once(Decode *s, vaddr_t pc) {
s->pc = pc;
s->snpc = pc;
isa_exec_once(s);
cpu.pc = s->dnpc;
#ifdef CONFIG_ITRACE
char *p = s->logbuf;
p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
int ilen = s->snpc - s->pc;
int i;
uint8_t *inst = (uint8_t *)&s->isa.inst.val;
for (i = ilen - 1; i >= 0; i--) {
p += snprintf(p, 4, " %02x", inst[i]);
}
int ilen_max = MUXDEF(CONFIG_ISA_x86, 8, 4);
int space_len = ilen_max - ilen;
if (space_len < 0)
space_len = 0;
space_len = space_len * 3 + 1;
memset(p, ' ', space_len);
p += space_len;

#ifndef CONFIG_ISA_loongarch32r
void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);
#else
p[0] = '\0'; // the upstream llvm does not support loongarch32r
#endif
#endif
}

static void execute(uint64_t n) {
Decode s;
for (; n > 0; n--) {
exec_once(&s, cpu.pc);
g_nr_guest_inst++;
trace_and_difftest(&s, cpu.pc);
if (nemu_state.state != NEMU_RUNNING)
break;
IFDEF(CONFIG_DEVICE, device_update());
}
}

static void statistic() {
IFNDEF(CONFIG_TARGET_AM, setlocale(LC_NUMERIC, ""));
#define NUMBERIC_FMT MUXDEF(CONFIG_TARGET_AM, "%", "%'") PRIu64
Log("host time spent = " NUMBERIC_FMT " us", g_timer);
Log("total guest instructions = " NUMBERIC_FMT, g_nr_guest_inst);
if (g_timer > 0)
Log("simulation frequency = " NUMBERIC_FMT " inst/s", g_nr_guest_inst * 1000000 / g_timer);
else
Log("Finish running in less than 1 us and can not calculate the simulation frequency");
}

void assert_fail_msg() {
isa_reg_display();
statistic();
}

/* Simulate how the CPU works. */
void cpu_exec(uint64_t n) {
g_print_step = (n < MAX_INST_TO_PRINT);
switch (nemu_state.state) {
case NEMU_END:
case NEMU_ABORT:
printf("Program execution has ended. To restart the program, exit NEMU and run again.\n");
return;
default:
nemu_state.state = NEMU_RUNNING;
}

uint64_t timer_start = get_time();

execute(n);

uint64_t timer_end = get_time();
g_timer += timer_end - timer_start;

switch (nemu_state.state) {
case NEMU_RUNNING:
nemu_state.state = NEMU_STOP;
break;

case NEMU_END:
case NEMU_ABORT:
Log("nemu: %s at pc = " FMT_WORD,
(nemu_state.state == NEMU_ABORT ? ANSI_FMT("ABORT", ANSI_FG_RED) : (nemu_state.halt_ret == 0 ? ANSI_FMT("HIT GOOD TRAP", ANSI_FG_GREEN) : ANSI_FMT("HIT BAD TRAP", ANSI_FG_RED))),
nemu_state.halt_pc);
// fall through
case NEMU_QUIT:
statistic();
}
}

cpu_exec()

主循环cpu_exec()模拟了CPU执行命令的核心过程。该函数接收一个无符号64位整数并单步执行相应个数个指令,通过nemu_state.state判断NEMU当前所处的状态是否是结束或中止。调用execute(n),通过get_time()显示执行指令用了多长时间。

在指令执行后,它再次检查状态。如果CPU仍在运行(NEMU_RUNNING),它会将状态更改为NEMU_STOP,表示当前一批指令已经执行完毕。 如果状态是NEMU_END或NEMU_ABORT,它会记录一条消息。这条消息包括程序是正常结束(HIT GOOD TRAP),出现错误(HIT BAD TRAP),还是被中止,并打印达到该状态时的程序计数器(pc)。

在NEMU_END、NEMU_ABORT的情况下,或者状态转换为NEMU_QUIT时,它会调用statistic(),一个负责打印出模拟统计数据(如执行时间、执行的指令数量等)的函数。

从前面cmd_q看到的调用中明显可以看到,单步执行指令的就是cpu_exec()函数,这也就实现了sdb中si方法

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
/* Simulate how the CPU works. */
void cpu_exec(uint64_t n) {
g_print_step = (n < MAX_INST_TO_PRINT);
switch (nemu_state.state) {
case NEMU_END:
case NEMU_ABORT:
printf("Program execution has ended. To restart the program, exit NEMU and run again.\n");
return;
default:
nemu_state.state = NEMU_RUNNING;
}

uint64_t timer_start = get_time();

execute(n);

uint64_t timer_end = get_time();
g_timer += timer_end - timer_start;

switch (nemu_state.state) {
case NEMU_RUNNING:
nemu_state.state = NEMU_STOP;
break;

case NEMU_END:
case NEMU_ABORT:
Log("nemu: %s at pc = " FMT_WORD,
(nemu_state.state == NEMU_ABORT ? ANSI_FMT("ABORT", ANSI_FG_RED) : (nemu_state.halt_ret == 0 ? ANSI_FMT("HIT GOOD TRAP", ANSI_FG_GREEN) : ANSI_FMT("HIT BAD TRAP", ANSI_FG_RED))),
nemu_state.halt_pc);
// fall through
case NEMU_QUIT:
statistic();
}
}

execute()

显然主要逻辑就是调用了n次exec_once()函数,然后判断NEMU的当前状态。

1
2
3
4
5
6
7
8
9
10
static void execute(uint64_t n) {
Decode s;
for (;n > 0; n--) {
exec_once(&s, cpu.pc);
g_nr_guest_inst++;
trace_and_difftest(&s, cpu.pc);
if (nemu_state.state != NEMU_RUNNING) break;
IFDEF(CONFIG_DEVICE, device_update());
}
}

exec_once()

调用的exec_once()代码如下。

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
static void exec_once(Decode *s, vaddr_t pc) {
// 将当前PC值赋给Decode的PC指针和Staic Next PC指针
s->pc = pc;
s->snpc = pc;
// 调用执行当前PC处的指令并更新dnpc
isa_exec_once(s);
cpu.pc = s->dnpc;
#ifdef CONFIG_ITRACE
char *p = s->logbuf;
p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
int ilen = s->snpc - s->pc;
int i;
uint8_t *inst = (uint8_t *)&s->isa.inst.val;
for (i = ilen - 1; i >= 0; i --) {
p += snprintf(p, 4, " %02x", inst[i]);
}
int ilen_max = MUXDEF(CONFIG_ISA_x86, 8, 4);
int space_len = ilen_max - ilen;
if (space_len < 0) space_len = 0;
space_len = space_len * 3 + 1;
memset(p, ' ', space_len);
p += space_len;

#ifndef CONFIG_ISA_loongarch32r
void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);
#else
p[0] = '\0'; // the upstream llvm does not support loongarch32r
#endif
#endif
}

首先来看Decode结构体:

1
2
3
4
5
6
7
typedef struct Decode {
vaddr_t pc;
vaddr_t snpc; // static next pc
vaddr_t dnpc; // dynamic next pc
ISADecodeInfo isa;
IFDEF(CONFIG_ITRACE, char logbuf[128]);
} Decode;

结构体包含了执行一条指令所需的信息,包括当前指令的程序计数器(PC)地址、静态和动态下一条指令的PC地址(snpcdnpc),以及一个ISADecodeInfo类型的成员,后者在文件isa.h中定义。如果启用了指令跟踪(CONFIG_ITRACE),还会包含一个日志缓冲区。

其中ISADecodeInfo的定义在isa.h

1
typedef concat(__GUEST_ISA__, _ISADecodeInfo) ISADecodeInfo;

更具体的信息在isa-def.h

1
2
3
4
5
6
// decode
typedef struct {
union {
uint32_t val;
} inst;
} MUXDEF(CONFIG_RV64, riscv64_ISADecodeInfo, riscv32_ISADecodeInfo);

isa_exec_once()

模拟单条指令的执行:从内存中取回下一条指令。inst_fetch函数的第一个参数是一个指向存储下一条指令地址(s->snpc)的指针,第二个参数4表示要取回的指令长度,单位是字节。这里假设是一个固定长度的ISA,每条指令长度为4字节(如32位RISC架构)。取回的指令存储在s->isa.inst.val中。

1
2
3
4
int isa_exec_once(Decode *s) {
s->isa.inst.val = inst_fetch(&s->snpc, 4);
return decode_exec(s);
}

decode_exec()

对取回的指令进行解码并执行。这个函数采用了一种模式匹配的方法来识别和执行指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int decode_exec(Decode *s) {
int rd = 0;
word_t src1 = 0, src2 = 0, imm = 0;
s->dnpc = s->snpc;

#define INSTPAT_INST(s) ((s)->isa.inst.val)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
__VA_ARGS__ ; \
}

INSTPAT_START();
INSTPAT("0001110 ????? ????? ????? ????? ?????" , pcaddu12i, 1RI20 , R(rd) = s->pc + imm);
INSTPAT("0010100010 ???????????? ????? ?????" , ld.w , 2RI12 , R(rd) = Mr(src1 + imm, 4));
INSTPAT("0010100110 ???????????? ????? ?????" , st.w , 2RI12 , Mw(src1 + imm, 4, R(rd)));

INSTPAT("0000 0000 0010 10100 ????? ????? ?????", break , N , NEMUTRAP(s->pc, R(4))); // R(4) is $a0
INSTPAT("????????????????? ????? ????? ?????" , inv , N , INV(s->pc));
INSTPAT_END();

R(0) = 0; // reset $zero to 0

return 0;
}

memory

paddr.c

内存是通过paddr.c中定义的大数组pmem模拟的。内存访问的操作vaddr_read()vaddr_write()的操作则是在vaddr.c中定义

vaddr代表虚拟地址,paddr代表物理地址

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
#include <memory/host.h>
#include <memory/paddr.h>
#include <device/mmio.h>
#include <isa.h>

#if defined(CONFIG_PMEM_MALLOC)
static uint8_t *pmem = NULL;
#else // CONFIG_PMEM_GARRAY
static uint8_t pmem[CONFIG_MSIZE] PG_ALIGN = {};
#endif

uint8_t* guest_to_host(paddr_t paddr) { return pmem + paddr - CONFIG_MBASE; }
paddr_t host_to_guest(uint8_t *haddr) { return haddr - pmem + CONFIG_MBASE; }

static word_t pmem_read(paddr_t addr, int len) {
word_t ret = host_read(guest_to_host(addr), len);
return ret;
}

static void pmem_write(paddr_t addr, int len, word_t data) {
host_write(guest_to_host(addr), len, data);
}

static void out_of_bound(paddr_t addr) {
panic("address = " FMT_PADDR " is out of bound of pmem [" FMT_PADDR ", " FMT_PADDR "] at pc = " FMT_WORD,
addr, PMEM_LEFT, PMEM_RIGHT, cpu.pc);
}

void init_mem() {
#if defined(CONFIG_PMEM_MALLOC)
pmem = malloc(CONFIG_MSIZE);
assert(pmem);
#endif
IFDEF(CONFIG_MEM_RANDOM, memset(pmem, rand(), CONFIG_MSIZE));
Log("physical memory area [" FMT_PADDR ", " FMT_PADDR "]", PMEM_LEFT, PMEM_RIGHT);
}

word_t paddr_read(paddr_t addr, int len) {
if (likely(in_pmem(addr))) return pmem_read(addr, len);
IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));
out_of_bound(addr);
return 0;
}

void paddr_write(paddr_t addr, int len, word_t data) {
if (likely(in_pmem(addr))) { pmem_write(addr, len, data); return; }
IFDEF(CONFIG_DEVICE, mmio_write(addr, len, data); return);
out_of_bound(addr);
}

headers

这里放一些头文件作为参考。

isa.h

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
#ifndef __ISA_H__
#define __ISA_H__

#if defined(CONFIG_ISA_mips32)
#define ISA_QEMU_BIN "qemu-system-mipsel"
#define ISA_QEMU_ARGS "-machine", "mipssim",\
"-kernel", NEMU_HOME "/resource/mips-elf/mips.dummy",
#elif defined(CONFIG_ISA_riscv) && !defined(CONFIG_RV64)
#define ISA_QEMU_BIN "qemu-system-riscv32"
#define ISA_QEMU_ARGS "-bios", "none",
#elif defined(CONFIG_ISA_riscv) && defined(CONFIG_RV64)
#define ISA_QEMU_BIN "qemu-system-riscv64"
#define ISA_QEMU_ARGS
#elif defined(CONFIG_ISA_x86)
#define ISA_QEMU_BIN "qemu-system-i386"
#define ISA_QEMU_ARGS
#else
#error Unsupport ISA
#endif

union isa_gdb_regs {
struct {
#if defined(CONFIG_ISA_mips32)
uint32_t gpr[32];
uint32_t status, lo, hi, badvaddr, cause, pc;
#elif defined(CONFIG_ISA_riscv) && !defined(CONFIG_RV64)
uint32_t gpr[32];
uint32_t pc;
#elif defined(CONFIG_ISA_riscv) && defined(CONFIG_RV64)
uint64_t gpr[32];
uint64_t fpr[32];
uint64_t pc;
#elif defined(CONFIG_ISA_x86)
uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
uint32_t eip, eflags;
uint32_t cs, ss, ds, es, fs, gs;
#endif
};
struct {
uint32_t array[77];
};
};

#endif

paddr.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef __MEMORY_PADDR_H__
#define __MEMORY_PADDR_H__

#include <common.h>

#define PMEM_LEFT ((paddr_t)CONFIG_MBASE)
#define PMEM_RIGHT ((paddr_t)CONFIG_MBASE + CONFIG_MSIZE - 1)
#define RESET_VECTOR (PMEM_LEFT + CONFIG_PC_RESET_OFFSET)

/* convert the guest physical address in the guest program to host virtual address in NEMU */
uint8_t* guest_to_host(paddr_t paddr);
/* convert the host virtual address in NEMU to guest physical address in the guest program */
paddr_t host_to_guest(uint8_t *haddr);

static inline bool in_pmem(paddr_t addr) {
return addr - CONFIG_MBASE < CONFIG_MSIZE;
}

word_t paddr_read(paddr_t addr, int len);
void paddr_write(paddr_t addr, int len, word_t data);

#endif

cpu.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef __CPU_CPU_H__
#define __CPU_CPU_H__

#include <common.h>

void cpu_exec(uint64_t n);

void set_nemu_state(int state, vaddr_t pc, int halt_ret);
void invalid_inst(vaddr_t thispc);

#define NEMUTRAP(thispc, code) set_nemu_state(NEMU_END, thispc, code)
#define INV(thispc) invalid_inst(thispc)

#endif

Macro

这里只列举一些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/ macro testing
// See https://stackoverflow.com/questions/26099745/test-if-preprocessor-symbol-is-defined-inside-macro
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
// define placeholders for some property
#define __P_DEF_0 X,
#define __P_DEF_1 X,
#define __P_ONE_1 X,
#define __P_ZERO_0 X,
// define some selection functions based on the properties of BOOLEAN macro
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define MUXNDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, Y, X)
#define MUXONE(macro, X, Y) MUX_MACRO_PROPERTY(__P_ONE_, macro, X, Y)
#define MUXZERO(macro, X, Y) MUX_MACRO_PROPERTY(__P_ZERO_,macro, X, Y)

全部的宏内容在macro.h中:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/***************************************************************************************
* Copyright (c) 2014-2022 Zihao Yu, Nanjing University
*
* NEMU is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
*
* See the Mulan PSL v2 for more details.
***************************************************************************************/

#ifndef __MACRO_H__
#define __MACRO_H__

#include <string.h>

// macro stringizing
#define str_temp(x) #x
#define str(x) str_temp(x)

// strlen() for string constant
#define STRLEN(CONST_STR) (sizeof(CONST_STR) - 1)

// calculate the length of an array
#define ARRLEN(arr) (int)(sizeof(arr) / sizeof(arr[0]))

// macro concatenation
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define concat3(x, y, z) concat(concat(x, y), z)
#define concat4(x, y, z, w) concat3(concat(x, y), z, w)
#define concat5(x, y, z, v, w) concat4(concat(x, y), z, v, w)

// macro testing
// See https://stackoverflow.com/questions/26099745/test-if-preprocessor-symbol-is-defined-inside-macro
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
// define placeholders for some property
#define __P_DEF_0 X,
#define __P_DEF_1 X,
#define __P_ONE_1 X,
#define __P_ZERO_0 X,
// define some selection functions based on the properties of BOOLEAN macro
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define MUXNDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, Y, X)
#define MUXONE(macro, X, Y) MUX_MACRO_PROPERTY(__P_ONE_, macro, X, Y)
#define MUXZERO(macro, X, Y) MUX_MACRO_PROPERTY(__P_ZERO_,macro, X, Y)

// test if a boolean macro is defined
#define ISDEF(macro) MUXDEF(macro, 1, 0)
// test if a boolean macro is undefined
#define ISNDEF(macro) MUXNDEF(macro, 1, 0)
// test if a boolean macro is defined to 1
#define ISONE(macro) MUXONE(macro, 1, 0)
// test if a boolean macro is defined to 0
#define ISZERO(macro) MUXZERO(macro, 1, 0)
// test if a macro of ANY type is defined
// NOTE1: it ONLY works inside a function, since it calls `strcmp()`
// NOTE2: macros defined to themselves (#define A A) will get wrong results
#define isdef(macro) (strcmp("" #macro, "" str(macro)) != 0)

// simplification for conditional compilation
#define __IGNORE(...)
#define __KEEP(...) __VA_ARGS__
// keep the code if a boolean macro is defined
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
// keep the code if a boolean macro is undefined
#define IFNDEF(macro, ...) MUXNDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
// keep the code if a boolean macro is defined to 1
#define IFONE(macro, ...) MUXONE(macro, __KEEP, __IGNORE)(__VA_ARGS__)
// keep the code if a boolean macro is defined to 0
#define IFZERO(macro, ...) MUXZERO(macro, __KEEP, __IGNORE)(__VA_ARGS__)

// functional-programming-like macro (X-macro)
// apply the function `f` to each element in the container `c`
// NOTE1: `c` should be defined as a list like:
// f(a0) f(a1) f(a2) ...
// NOTE2: each element in the container can be a tuple
#define MAP(c, f) c(f)

#define BITMASK(bits) ((1ull << (bits)) - 1)
#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1)) // similar to x[hi:lo] in verilog
#define SEXT(x, len) ({ struct { int64_t n : len; } __x = { .n = x }; (uint64_t)__x.n; })

#define ROUNDUP(a, sz) ((((uintptr_t)a) + (sz) - 1) & ~((sz) - 1))
#define ROUNDDOWN(a, sz) ((((uintptr_t)a)) & ~((sz) - 1))

#define PG_ALIGN __attribute((aligned(4096)))

#if !defined(likely)
#define likely(cond) __builtin_expect(cond, 1)
#define unlikely(cond) __builtin_expect(cond, 0)
#endif

// for AM IOE
#define io_read(reg) \
({ reg##_T __io_param; \
ioe_read(reg, &__io_param); \
__io_param; })

#define io_write(reg, ...) \
({ reg##_T __io_param = (reg##_T) { __VA_ARGS__ }; \
ioe_write(reg, &__io_param); })

#endif

我看不懂,但我大受震撼