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

NEMU 是什么?

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

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

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

实验指导书,提取码:9h8k

前言

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


实验环境

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;

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

输出结果

aa10n@aa10n-VirtualBox:~/NEMU2021$ 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

任务自查表

序号 是否已完成
必做任务1
必做任务2
必做任务3
必做任务4
必做任务5
必做任务6
nemu/src/monitor/monitor.c
选做任务1
选做任务2

思考题

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

思考题1:opcode_table 到底是一个什么类型的数组?

opcode_table 数组是一个函数指针数组。

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

-1是无符号类型最大的数字,所以函数里的for循环可以执行所以指令。

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

在此处的含义是静态全局变量,该变量只能被本文件中的函数调用,并且是全局变量,而不能被同一程序其他文件中的函数调用,使用static是为了避免它被误修改。

思考题3:

  • EFLAGS 寄存器中的 CF 位是什么意思?

i386手册里P34页中和参阅附录c提到,CF是进位标志。

  • ModR/M 字节是什么?

P241-243页。ModR/M 由 Mod,Reg/Opcode,R/M 三个部分组成。 Mod 是前两位,提供寄存器寻址和内存寻址, Reg/Opcode为3-5位,如果是Reg表示使用哪个寄存器,Opcode表示对group属性的Opcode进行补充; R/M为6-8位,与mod结合起来会得到8个寄存器和24个内存寻址。

  • mov 指令的具体格式是怎么样的?

P345页,格式是DEST ← SRC。

  • 完成 PA1 的内容之后, nemu 目录下的所有.c 和.h 和文件总 共有多少行代码? 你是使用什么命令得到这个结果的?和框架代码相 比, 你在 PA1 中编写了多少行代码?你可以把这条命令写入 Makefile 中, 随着实验进度的推进, 你可以很方便地统计工程的代码行数, 例如 敲入 make count 就会自动运行统计代码行数的命令。再来个难一点的, 除去空行之外, nemu 目录下的所有.c 和.h 文件总共有多少行代码?

通过find . -name “*[.h/.c]” | xargs wc -l命令,得到4803行。和框架代码4197行相比, 我在 PA1 中编写了606行代码。

通过find . -name “*[.h/.c]” | xargs grep “^.” | wc -l命令计算去除空行的所有.c .h文件得到了3900行代码。

make count指令如下:

make count

​ @find nemu/ -name “.c” -o -name “.h” | xargs cat | grep -v ^$$ | wc -l

  • 打开工程目录下的 Makefile 文件, 你会在 CFLAGS 变量中看到 gcc 的一些编译选项。请解释 gcc 中的-Wall 和-Werror 有什么作用? 为 什么要使用-Wall 和-Werror?

-Wall 使GCC编译后显示所有的警告信息。 -Werror 会将将所有的警告当成错误进行处理,并且取消编译操作。 使用 -Wall 和 -Werror就是为了找出可能存在的错误,尽可能地避免程序运行出错,优化程序。


后记

PA1只要学过c基本上实现没什么难度。