计算机系统基础综合实践 PA1

NEMU 是什么?

PA的目的是要实现NEMU, 一款经过简化的全系统模拟器.

但什么是模拟器呢?你小时候应该玩过红白机, 超级玛丽, 坦克大战, 魂斗罗… 它们的画面是否让你记忆犹新? (希望我们之间没有代沟…) 随着时代的发展, 你已经很难在市场上看到红白机的身影了. 当你正在为此感到苦恼的时候, 模拟器的横空出世唤醒了你心中尘封已久的童年回忆. 红白机模拟器可以为你模拟出红白机的所有功能. 有了它, 你就好像有了一个真正的红白机, 可以玩你最喜欢的红白机游戏. 这里是jyy移植的一个小型项目LiteNES, PA工程里面已经带有这个项目, 你可以在如今这个红白机难以寻觅的时代, 再次回味你儿时的快乐时光, 这实在是太神奇了!

这是实验指导书中的一段描述。

前言

理解程序如何在计算机上运行的根本途径是实现一个完整的计算机系统。


实验环境

NEMU 是基于 Linux/GNU 实验环境,所需要的环境如下:

  • 操作系统:Ubuntu18.04
  • 编译器:GCC-4.4.7

实验内容

  • 阶段 1:实现“单步、打印寄存器状态、扫描内存”三个调试功能
  • 阶段 2:实现调试功能的表达式求值
  • 阶段 3:实现监视点

开始实验


必做任务 1:实现正确的寄存器结构体

nemu/include/cpu/reg.h

typedef struct {
union {
union {
uint32_t _32;
uint16_t _16;
uint8_t _8[2];
} gpr[8];

/* Do NOT change the order of the GPRs' definitions. */

struct {
uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
};
};

swaddr_t eip;
// ...
} CPU_state;

这是关于匿名结构体和联合体的使用。我们可以在结构体中使用匿名的方式声明某个联合体(或结构体)。之后就可以直接利用结构体访问成员的方式一样访问结构体中已经声明过的匿名联合体(或结构体)的成员,使用这种方式可以让代码更加简洁。

输出结果

nemu@nemu-VirtualBox:~/NEMU$ make run
objcopy -S -O binary obj/kernel/kernel entry
obj/nemu/nemu obj/testcase/mov-c
Welcome to NEMU!
The executable is obj/testcase/mov-c.
For help, type "help"
(nemu) c
nemu: HIT GOOD TRAP at eip = 0x001012db

必做任务2:实现单步执行、打印寄存器、扫描内存

这次的任务主要是模拟GDB相关的功能。

nemu/src/monitor/debug/ui.c

static struct {
char *name;
char *description;
int (*handler) (char *);
} cmd_table [] = {
{ "help", "Display informations about all supported commands", cmd_help },
{ "c", "Continue the execution of the program", cmd_c },
{ "q", "Exit NEMU", cmd_q },
{ "si", "One step", cmd_si },
{ "info", "Display all informations of regisiters", cmd_info },
/* TODO: Add more commands */
};

在相应位置填写所需要的指令。

单步执行

nemu/src/monitor/debug/ui.c

static int cmd_si(char *args){
char *sencondWord = strtok(NULL," ");
int step = 0;
int i;
if (sencondWord == NULL){
cpu_exec(1);
return 0;
}
sscanf(sencondWord, "%d", &step);
if (step <= 0){
printf("MISINIPUT\n");
return 0;
}
for (i = 0; i < step; i++){
cpu_exec(1);
}
return 0;
}

添加单步执行的相关代码。这里用了for循环,一条一条指令执行。因为cpu_exec()函数中的宏MAX_INSTR_TO_PRINT限制为10,更改宏或者for循环后,两种方法都可以解决无法执行10条以上指令的问题。

打印寄存器

nemu/src/monitor/debug/ui.c

static int cmd_info(char *args){
char *sencondWord = strtok(NULL," ");
int i;
if (strcmp(sencondWord, "r") == 0){
for (i = 0; i < 8; i++){
printf("%s\t\t", regsl[i]);
printf("0x%08x\t\t%d\n", cpu.gpr[i]._32, cpu.gpr[i]._32);
}
printf("eip\t\t0x%08x\t\t%d\n", cpu.eip, cpu.eip);
return 0;
}
printf("MISINPUT\n");
return 0;
}

添加打印寄存器的相关代码。

扫描内存

nemu/src/monitor/debug/ui.c

static int cmd_x(char *args){
char *sencondWord = strtok(NULL," ");
char *thirdWord = strtok(NULL, " ");

int step = 0;
swaddr_t address;

sscanf(sencondWord, "%d", &step);
sscanf(thirdWord, "%x", &address);

int i, j = 0;
for (i = 0; i < step; i++){
if (j % 4 == 0){
printf("0x%x:", address);
}
printf("0x%08x ", swaddr_read(address, 4));
address += 4;
j++;
if (j % 4 == 0){
printf("\n");
}
}
printf("\n");
return 0;
}

添加扫描内存的相关代码,我把要输出的地址分割成一行输出五个。

这里全部使用到了char *strtok(char *str, const char *delim) 库函数,delim代表了分隔符,str则代表要被分解的一组字符串。该函数会有一个返回值,若没有可检索的字符串,则返回一个空指针,否则返回第一个子字符串。

输出结果

(nemu) si
100000: bd 00 00 00 00 movl $0x0,%ebp
(nemu) si 5
100005: bc 00 00 00 08 movl $0x8000000,%esp
10000a: e9 11 12 00 00 jmp 101220
101220: 55 pushl %ebp
101221: b8 60 12 10 00 movl $0x101260,%eax
101226: 89 e5 movl %esp,%ebp
(nemu) si 15
101228: 83 ec 18 subl $0x18,%esp
10122b: ff e0 jmp *%eax
101260: 55 pushl %ebp
101261: 89 e5 movl %esp,%ebp
101263: 83 ec 18 subl $0x18,%esp
101266: c7 44 24 0c a3 19 10 00 movl $0x1019a3,0xc(%esp)
10126e: c7 44 24 08 4a 00 00 00 movl $0x4a,0x8(%esp)
101276: c7 44 24 04 5c 19 10 00 movl $0x10195c,0x4(%esp)
10127e: c7 04 24 70 19 10 00 movl $0x101970,(%esp)
101285: e8 c6 fe ff ff call 101150
101150: 55 pushl %ebp
101151: 89 e5 movl %esp,%ebp
101153: 5d popl %ebp
101154: ret
10128a: e8 d1 fe ff ff call 101160
(nemu) info r
eax 0x00101260 1053280
ecx 0x26365f3f 641097535
edx 0x5123097b 1361250683
ebx 0x7d3f57d7 2101303255
esp 0x07ffffc4 134217668
ebp 0x07ffffe0 134217696
esi 0x1d0c876a 487360362
edi 0x4d2976e8 1294563048
eip 0x00101160 1053024
(nemu) x 10 0x100000
0x100000:0x000000bd 0x0000bc00 0x11e90800 0x90000012
0x100010:0x56e58955 0x08458b53 0x2d0c5d8b 0x40000000
0x100020:0xeac1da89 0xf0002516

输出可能会和我稍有不同。


必做任务 3:实现算术表达式的词法分析

这里主要是完成表达式的计算和对正则表达式的理解。

nemu/src/monitor/debug/expr.c

enum {
NOTYPE = 256,
NUM = 1,
RESGISTER = 2,
HEX = 3,
EQ = 4,
NOTEQ = 5,
OR = 6,
AND = 7,
POINT, NEG
/* TODO: Add more token types */

};

首先在添加表达式相应的token类型。

nemu/src/monitor/debug/expr.c

static struct rule {
char *regex;
int token_type;
} rules[] = {

/* TODO: Add more rules.
* Pay attention to the precedence level of different rules.
*/

{" +", NOTYPE}, // spaces

{"\\+", '+'}, // plus
{"\\-", '-'},
{"\\*", '*'},
{"\\/", '/'},

{"\\$[a-z]+", RESGISTER},
{"0[xX][0-9a-fA-F]+", HEX},
{"[0-9]+", NUM},

{"==", EQ}, // equal
{"!=", NOTEQ},

{"\\(", '('},
{"\\)", ')'},

{"\\|\\|", OR},
{"&&", AND},
{"!", '!'},
};

在结构体rule中添加相应的规则,利用正则表达式来判断输入的字符。

正则表达式 – 语法 | 菜鸟教程 (runoob.com)

nemu/src/monitor/debug/expr.c

static bool make_token(char *e) {
//...
int j;
for (j = 0; j < 32; j++){ //清空
tokens[nr_token].str[j] = '\0';
}

switch(rules[i].token_type) {
case 256:
break;
case 1:
tokens[nr_token].type = 1;
strncpy(tokens[nr_token].str, &e[position - substr_len], substr_len);
nr_token++;
break;
case 2:
tokens[nr_token].type = 2;
strncpy(tokens[nr_token].str, &e[position - substr_len], substr_len);
nr_token++;
break;
case 3:
tokens[nr_token].type = 3;
strncpy(tokens[nr_token].str, &e[position - substr_len], substr_len);
nr_token++;
break;
case 4:
tokens[nr_token].type = 4;
strcpy(tokens[nr_token].str, "==");
nr_token++;
break;
case 5:
tokens[nr_token].type = 5;
strcpy(tokens[nr_token].str, "!=");
nr_token++;
break;
case 6:
tokens[nr_token].type = 6;
strcpy(tokens[nr_token].str, "||");
nr_token++;
break;
case 7:
tokens[nr_token].type = 7;
strcpy(tokens[nr_token].str, "&&");
nr_token++;
break;
case '+':
tokens[nr_token].type = '+';
nr_token++;
break;
case '-':
tokens[nr_token].type = '-';
nr_token++;
break;
case '*':
tokens[nr_token].type = '*';
nr_token++;
break;
case '/':
tokens[nr_token].type = '/';
nr_token++;
break;
case '!':
tokens[nr_token].type = '!';
nr_token++;
break;
case '(':
tokens[nr_token].type = '(';
nr_token++;
break;
case ')':
tokens[nr_token].type = ')';
nr_token++;
break;
default:
assert(0);
}
//...
}

利用代码框架中的switch语句,规定token的类型,nr_token代表了token的数量,strcpy()和strncpy()负责函数将表达式复制到tokens.str中。需要注意区分**strcpy()strncpy()**两种函数,前者是复制整个字符串,后者是复制前n个字符。每次要把str清空,不然计算表达式的时候会答案会累加。


必做任务 4:实现算术表达式的递归求值

nemu/src/monitor/debug/expr.c

bool check_parentheses(int p, int q){
int a;
int j = 0, k = 0;
if (tokens[p].type == '(' || tokens[q].type == ')'){
for (a = p; a <= q; a++){
if (tokens[a].type == '('){
j++;
}
if (tokens[a].type == ')'){
k++;
}
if (a != q && j == k){
return false;
}
}
if (j == k){
return true;
} else {
return false;
}
}
return false;
}

写了一个新的函数check_parentheses(),该函数是用来识别表达式中的左括号和右括号是否匹配。这里我依照实验指导书的指示在一开始加了一个判断式,用于判断表达式是否被一对匹配的括号所包围。

nemu/src/monitor/debug/expr.c

int dominant_operator(int p, int q){
int step = 0;
int i;
int op = -1;
int pri = 0;

for (i = p; i <= q; i++){
if (tokens[i].type == '('){
step++;
}
if (tokens[i].type == ')'){
step--;
}

if (step == 0){
if (tokens[i].type == OR){
if (pri < 51){
op = i;
pri = 51;
}
} else if (tokens[i].type == AND){
if (pri < 50){
op = i;
pri = 50;
}
} else if (tokens[i].type == EQ || tokens[i].type == NOTEQ){
if (pri < 49){
op = i;
pri = 49;
}
} else if (tokens[i].type == '+' || tokens[i].type == '-'){
if (pri < 48){
op = i;
pri = 48;
}
} else if (tokens[i].type == '*' || tokens[i].type == '/'){
if (pri < 46){
op = i;
pri = 46;
}
}
else if (step < 0){
return -2;
}
}
}
return op;
}

这里同样写了一个新的函数dominant_operator(),该函数是用于区分运算符的优先级。首先判断表达式中括号数量是否正确,之后进入if语句根据运算符确定相应的优先级。返回值op为-1时则代表有两个token,是用于后续任务所加的判断。

nemu/src/monitor/debug/expr.c

uint32_t eval(int p, int q){
int result = 0;
int op;
int val1, val2;
if (p > q){
assert(0);
} else if (p == q){
if (tokens[p].type == NUM){
sscanf(tokens[p].str, "%d", &result);
return result;
} else if (tokens[p].type == HEX){
int i = 2;
while(tokens[p].str[i] != 0){
result *= 16;
result += tokens[p].str[i] < 58 ? tokens[p].str[i] - '0' : tokens[p].str[i] - 'a' + 10;
i++;
}
} else if (tokens[p].type == RESGISTER){
if (!strcmp(tokens[p].str, "$eax")){
return cpu.eax;
} else if (!strcmp(tokens[p].str, "$ecx")){
return cpu.ecx;
} else if (!strcmp(tokens[p].str, "$edx")){
return cpu.edx;
} else if (!strcmp(tokens[p].str, "$ebx")){
return cpu.ebx;
} else if (!strcmp(tokens[p].str, "$esp")){
return cpu.esp;
} else if (!strcmp(tokens[p].str, "$ebp")){
return cpu.ebp;
} else if (!strcmp(tokens[p].str, "$esi")){
return cpu.esi;
} else if (!strcmp(tokens[p].str, "$edi")){
return cpu.edi;
} else if (!strcmp(tokens[p].str, "$eip")){
return cpu.eip;
} else {
return 0;
}
} else {
assert(0);
}
} else if (check_parentheses(p, q) == true){
return eval(p + 1, q - 1);
} else {
op = dominant_operator(p, q);
if (op == -2){
assert(0);
} else if (tokens[p].type == '!'){
sscanf(tokens[q].str, "%d", &result);
return !result;
} else if (tokens[p].type == RESGISTER) {
if (!strcmp(tokens[p].str, "$eax")){
result = cpu.eax;
return result;
} else if (!strcmp(tokens[p].str, "$ecx")){
result = cpu.ecx;
return result;
} else if (!strcmp(tokens[p].str, "$edx")){
result = cpu.edx;
return result;
} else if (!strcmp(tokens[p].str, "$ebx")){
result = cpu.ebx;
return result;
} else if (!strcmp(tokens[p].str, "$esp")){
result = cpu.esp;
return result;
} else if (!strcmp(tokens[p].str, "$ebp")){
result = cpu.ebp;
return result;
} else if (!strcmp(tokens[p].str, "$esi")){
result = cpu.esi;
return result;
} else if (!strcmp(tokens[p].str, "$edi")){
result = cpu.edi;
return result;
} else if (!strcmp(tokens[p].str, "$eip")){
result = cpu.eip;
return result;
} else {
assert(0);
return 0;
}
}
}
val1 = eval(p, op - 1);
val2 = eval(op + 1, q);

switch (tokens[op].type){
case '+' : return val1 + val2;
case '-' : return val1 - val2;
case '*' : return val1 * val2;
case '/' : return val1 / val2;
case OR : return val1 || val2;
case AND : return val1 && val2;
case EQ :
if (val1 == val2){
return 1;
} else {
return 0;
}
case NOTEQ :
if (val1 != val2){
return 1;
} else {
return 0;
}
default : assert(0);
}
}
return 0;
}

编写eval()函数,该函数用于表达式求值。如果p > q的话直接assert(0),把表达式里可能包含的类型全部解释出来之后,先对分裂出来的两个子表达式进行递归求值,然后再根据优先级对两个子表达式的值进行运算。


选做任务 1:实现带有负数的算术表达式的求值

nemu/src/monitor/debug/expr.c

uint32_t eval(int p, int q){
if (op == -2){
assert(0);
} else if (op == -1){
} else if (tokens[p].type == NEG){
sscanf(tokens[q].str, "%d", &result);
return -result;
// ...
}

当op == -1的时候,如果token的类型是NEG,则取出表达式并且放在result,最后返回-result即可。


必做任务 5:实现更复杂的表达式求值

已完成。


选做任务 2:实现指针解引用

nemu/src/monitor/debug/expr.c

uint32_t eval(int p, int q){
// ...
if (op == -2){
assert(0);
} else if (op == -1){
// ...
if(tokens[p].type == POINT){
if (!strcmp(tokens[p + 2].str, "$eax")){
result = swaddr_read(cpu.eax, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$ecx")){
result = swaddr_read(cpu.ecx, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$edx")){
result = swaddr_read(cpu.edx, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$ebx")){
result = swaddr_read(cpu.ebx, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$esp")){
result = swaddr_read(cpu.esp, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$ebp")){
result = swaddr_read(cpu.ebp, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$esi")){
result = swaddr_read(cpu.esi, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$edi")){
result = swaddr_read(cpu.edi, 4);
return result;
} else if (!strcmp(tokens[p + 2].str, "$eip")){
result = swaddr_read(cpu.eip, 4);
return result;
}
// ...
}

当op == -1的时候,如果token的类型是POINT,判断是哪一个寄存器后,取出的值放在result,最后返回result。

nemu/src/monitor/debug/expr.c

uint32_t expr(char *e, bool *success) {
// ...
int i;
for (i = 0; i < nr_token; i++){
if (tokens[i].type == '*' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i - 1].type != HEX && tokens[i - 1].type != ')'))){
tokens[i].type = POINT;
}
if (tokens[i].type == '-' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i - 1].type != HEX && tokens[i - 1].type != ')'))){
tokens[i].type = NEG;
}
}
return eval(0, nr_token - 1);
}

判断token属于POINT还是NEG,只要token前一个运算符不是十进制、十六进制以及左括号就把token解释为POINT和NEG。

nemu/src/monitor/debug/ui.c

static int cmd_p(char *args){
bool *success = false;
int i;
i = expr(args, success);
if (!success){
printf("%d\n", i);
}
return 0;
}

添加表达式求值打印的指令。

输出结果

(nemu) x 10 0x1234
0x00000000 0x99848b66 0x00002000 0x0099a48a 0x8b000020
0x00123415 0x158b6600 0x00001234 0x1234158a 0x358a0000
(nemu) p 0xc0100000 - (($edx+0x1234-10) * 16) / 4
-1072711848
(nemu) p (!($ecx != 0x00008000) && ($eax == 0x00000000)) + 0x12345678
305419897
(nemu) p -5 + *($eip)
536904070
(nemu) w ($eip==0x100224)
Watch point 0: ($eip==0x100224)
(nemu) c
Hint watchpoint 0 at address 0x00100224

上述测试命令是启动 nemu 并运行 100 步(si 100)之后输入的命令


必做任务 6:实现监视点池的管理

这次的任务主要是对链表的相关操作进行一个复习。

nemu/src/monitor/debug/watchpoint.c

WP* new_wp(){
WP *temp;
temp = free_;
free_ = free_->next;
temp->next = NULL;
if (head == NULL){
head = temp;
} else {
WP* temp2;
temp2 = head;
while (temp2->next != NULL){
temp2 = temp2->next;
}
temp2->next = temp;
}
return temp;
}

编写一个new_wp(),该函数用于从代码框架中的free_链表返回一个闲置的监视点结构。有两种情况,一种是head链表为空时,直接head = temp,否则的话要设置一个变量用来查找head最后一个节点,利用尾插法把闲置的节点插上。

nemu/src/monitor/debug/watchpoint.c

void free_wp(WP *wp){
if (wp == NULL){
assert(0);
}
if (wp == head){
head = head->next;
} else {
WP* temp = head;
while (temp != NULL && temp->next != wp){
temp = temp->next;
}
temp->next = temp->next->next;
}
wp->next =free_;
free_ = wp;
wp->result = 0;
wp->expr[0] = '\0';
}

编写一个free_wp(),将wp归还至free_链表当中。有三种情况,第一种如果当前返回的节点为空,直接assert(0),第二种的情况head就是wp,否则的话要在head中找到与之相对应的wp,之后用头插法把wp插到free,最后把wp的属性清空。


必做任务 7:实现监视点

实现类似GDB的监视点功能。

nemu/src/monitor/debug/watchpoint.c

bool checkWP(){
bool check = false;
bool *success = false;
WP *temp = head;
int expr_temp;
while(temp != NULL){
expr_temp = expr(temp->expr, success);
if (expr_temp != temp->result){
check = true;
printf ("Hint watchpoint %d at address 0x%08x\n", temp->NO, cpu.eip);
temp = temp->next;
continue;
}
printf ("Watchpoint %d: %s\n",temp->NO,temp->expr);
printf ("Old value = %d\n",temp->result);
printf ("New value = %d\n",expr_temp);
temp->result = expr_temp;
temp = temp->next;
}
return check;
}

编写一个checkWP()函数,该函数用于判断监视点是否触发。首先进行表达式求值,每当NEMU执行完一条指令,则若触发了用户所设的监视点,程序便会暂停下来,否则打印监视点、旧值和新值。

nemu/src/monitor/debug/cpu-exec.c

/* TODO: check watchpoints here. */
bool change = checkWP();
if (change){
nemu_state = STOP;
}

checkWP()返回值用来判断是否触发监视点,如果触发了就更改nemu_state的状态。

nemu/src/monitor/debug/watchpoint.c

void printf_wp(){
WP *temp = head;
if (temp == NULL){
printf("No watchpoints\n");
}
while (temp != NULL){
printf("Watch point %d: %s\n", temp->NO, temp->expr);
temp = temp->next;
}
}

简单的链表输出操作。

nemu/src/monitor/debug/ui.c

WP* delete_wp(int p, bool *key){
WP *temp = head;
while (temp != NULL && temp->NO != p){
temp = temp->next;
}
if (temp == NULL){
*key = false;
}
return temp;
}

简单的链表删除操作。

nemu/src/monitor/debug/ui.c

static int cmd_info(char *args){
// ...
if (strcmp(sencondWord, "w") == 0){
printf_wp();
return 0;
}
printf("MISINPUT\n");
return 0;
}

如果分隔后的第一个字符是w就打印监视点的功能。这里貌似不能定义另一个函数来打印监视点,和之前的会有冲突,所以直接在cmd_info添加判断。

nemu/src/monitor/debug/ui.c

static int cmd_d(char *args){
int p;
bool key = true;
sscanf(args, "%d", &p);
WP* q = delete_wp(p, &key);
if (key){
printf("Delete watchpoint %d: %s\n", q->NO, q->expr);
free_wp(q);
return 0;
} else {
printf("No found watchpoint %d\n", p);
return 0;
}
return 0;
}

添加删除指令。

输出结果

(nemu) info w
No watchpoints
(nemu) w 0x100000
[nemu/src/monitor/debug/expr.c,98,make_token] match rules[6] = "0[xX][0-9a-fA-F]+" at position 0 with len 8: 0x100000
Watch point 0: 0x100000
(nemu) si
100000: bd 00 00 00 00 movl $0x0,%ebp
[nemu/src/monitor/debug/expr.c,98,make_token] match rules[6] = "0[xX][0-9a-fA-F]+" at position 0 with len 8: 0x100000
Watchpoint 0: 0x100000
Old value = 0
New value = 0
(nemu) w 0x888888
[nemu/src/monitor/debug/expr.c,98,make_token] match rules[6] = "0[xX][0-9a-fA-F]+" at position 0 with len 8: 0x888888
Watch point 1: 0x888888
(nemu) info w
Watch point 0: 0x100000
Watch point 1: 0x888888
(nemu) d 0
Delete watchpoint 0: 0x100000
(nemu) info w
Watch point 1: 0x888888

思考题

思考题仅作为个人思考,若有错误欢迎指出。

opcode_table 到底是一个什么类型的数组?

opcode_table 是一个函数指针数组。具体来说,它是一个包含 256 个元素的数组,每个元素都是一个指向 helper_fun 类型函数的指针。helper_fun 类型的函数是一个接受一个swaddr_t 类型参数并返回一个 int 类型值的函数。

在 cmd_c()函数中, 调用 cpu_exec()的时候传入了参数-1 , 你知道为什么吗?

-1作为无符号数是正无穷大,保证了你的程序能够执行完

框架代码中定义 wp_pool 等变量的时候使用了关键字 static,static 在此处的含义是什么? 为什么要在此处使用它?

可以避免命名冲突,static 变量在程序的整个生命周期内保持存在,并且在程序开始时分配内存,程序结束时释放内存。它们在第一次初始化后,其值在后续函数调用中保持不变。使用static可以将这些变量限制在 [watchpoint.c](vscode-file://vscode-app/c:/Program Files/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 文件中,有助于实现数据封装,防止其他文件直接修改这些变量,确保数据的完整性和一致性。

查阅 i386 手册

EFLAGS 寄存器中的 CF 位(Carry Flag)是 EFLAGS 寄存器中的一个标志位,用于指示进位情况。在加法操作中,CF 为 1 表示有进位发生;在减法操作中,CF 为 1 表示有借位发生。具体描述见 i386 手册的 “EFLAGS Register” 部分。 Page 33-34

ModR/M 字节用于指定操作数的寻址模式,包括 MOD、REG 和 R/M 三个部分。它定义了操作数的具体位置和类型。详细信息见 i386 手册的 “Instruction Formats” 章节。Page 241-242

mov 指令的具体格式 MOV 指令用于在寄存器和内存之间传输数据。其格式包括操作码、ModR/M 字节、SIB 字节(如需要)、立即数(如有)。具体格式及其操作方式见 i386 手册的 “Instruction Set Reference” 章节。 Page 247-248

shell 命令

count:

find nemu -name ‘.c’ -o -name '.h’ | xargs awk ‘NF’ | wc -l

Make 文件

-Wall 和 -Werror 是 GCC 编译器中的两个选项,用于控制编译器发出的警告信息和错误处理方式。它们的作用如下:
-Wall

-Wall 选项用于启用编译器的所有常见警告。它代表“Warn all”,尽管它并不是启用所有可能的警告选项,而是启用了一组常见且有用的警告。使用 -Wall 可以帮助开发者发现潜在的问题和代码中的潜在错误,这些警告通常是编译器检测到的可能的错误或不良编程习惯。
-Werror

-Werror 选项将所有的警告视为错误。这意味着编译器在遇到任何警告时都会停止编译过程,并返回一个错误。这个选项可以确保所有的警告都被处理和修复,因为它们会阻止程序编译成功,直到问题被解决为止。

nemu用什么类型来模拟主存?为什么使用这种类型?

主存是使用一个四维数组来模拟的。具体来说,主存是用一个 [uint8_t](vscode-file://vscode-app/c:/Program Files/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 类型的四维数组 [dram](vscode-file://vscode-app/c:/Program Files/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 来模拟的。

uint8_t dram[NR_RANK][NR_BANK][NR_ROW][NR_COL];
#define NR_COL (1 << COL_WIDTH)
#define NR_ROW (1 << ROW_WIDTH)
#define NR_BANK (1 << BANK_WIDTH)
#define NR_RANK (1 << RANK_WIDTH)

这些宏定义通过位移操作计算出每个维度的大小。例如,[NR_COL](vscode-file://vscode-app/c:/Program Files/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 是通过 [1 << COL_WIDTH](vscode-file://vscode-app/c:/Program Files/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 计算出来的,其中 [COL_WIDTH](vscode-file://vscode-app/c:/Program Files/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 是 10,因此 [NR_COL](vscode-file://vscode-app/c:/Program Files/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 的值是 1024。

uint8_t 表示一个无符号的8位整数(即一个字节),这是计算机内存的基本存储单位。使用 uint8_t 可以精确地模拟内存中的每个字节。uint8_t 只占用一个字节的空间,比其他数据类型(如 intfloat)更节省内存。这对于模拟大容量的主存非常重要。uint8_t 是无符号的,这意味着它只能表示非负整数(0 到 255)。这符合内存地址和数据的实际情况,因为内存中的每个字节通常表示为无符号值。

nemu是如何开始执行用户程序的?

通过函数load_entry来加载用户的程序,读取一个名字为entry的文件,以二进制只读模式读取,获取文件大小之后,将程序的data和text字段放入ENTRY_START的模拟内存的位置。然后cpu.eip置为ENTRY_START,虚拟机开始执行程序。