计算机系统基础综合实践 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct { union { union { uint32_t _32; uint16_t _16; uint8_t _8[2 ]; } gpr[8 ]; struct { uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi; }; }; swaddr_t eip; } CPU_state;
这是关于匿名结构体和联合体的使用。我们可以在结构体 中使用匿名 的方式声明某个联合体(或结构体)。之后就可以直接利用结构体访问成员的方式一样访问结构体中已经声明过的匿名联合体(或结构体)的成员,使用这种方式可以让代码更加简洁。
输出结果
1 2 3 4 5 6 7 8 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
1 2 3 4 5 6 7 8 9 10 11 12 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 }, };
在相应位置填写所需要的指令。
单步执行
nemu/src/monitor/debug/ui.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
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 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
则代表要被分解的一组字符串。该函数会有一个返回值,若没有可检索的字符串,则返回一个空指针,否则返回第一个子字符串。
输出结果
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 (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
1 2 3 4 5 6 7 8 9 10 11 12 13 enum { NOTYPE = 256 , NUM = 1 , RESGISTER = 2 , HEX = 3 , EQ = 4 , NOTEQ = 5 , OR = 6 , AND = 7 , POINT, NEG };
首先在添加表达式相应的 token 类型。
nemu/src/monitor/debug/expr.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 static struct rule { char *regex; int token_type; } rules[] = { {" +" , NOTYPE}, {"\\+" , '+' }, {"\\-" , '-' }, {"\\*" , '*' }, {"\\/" , '/' }, {"\\$[a-z]+" , RESGISTER}, {"0[xX][0-9a-fA-F]+" , HEX}, {"[0-9]+" , NUM}, {"==" , EQ}, {"!=" , NOTEQ}, {"\\(" , '(' }, {"\\)" , ')' }, {"\\|\\|" , OR}, {"&&" , AND}, {"!" , '!' }, };
在结构体 rule 中添加相应的规则,利用正则表达式来判断输入的字符。
正则表达式
– 语法 | 菜鸟教程 (runoob.com)
nemu/src/monitor/debug/expr.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 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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
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 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
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 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
1 2 3 4 5 6 7 8 9 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
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 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
1 2 3 4 5 6 7 8 9 10 11 12 13 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
1 2 3 4 5 6 7 8 9 static int cmd_p (char *args) { bool *success = false ; int i; i = expr(args, success); if (!success){ printf ("%d\n" , i); } return 0 ; }
添加表达式求值打印的指令。
输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 (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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; }
编写一个 newwp(),该函数用于从代码框架中的
free 链表返回一个闲置的监视点结构。有两种情况,一种是 head
链表为空时,直接 head = temp,否则的话要设置一个变量用来查找 head
最后一个节点,利用尾插法把闲置的节点插上。
nemu/src/monitor/debug/watchpoint.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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' ; }
编写一个 freewp(),将 wp 归还至
free 链表当中。有三种情况,第一种如果当前返回的节点为空,直接
assert(0),第二种的情况 head 就是 wp,否则的话要在 head
中找到与之相对应的 wp,之后用头插法把 wp 插到 free,最后把 wp
的属性清空。
必做任务 7:实现监视点
实现类似 GDB 的监视点功能。
nemu/src/monitor/debug/watchpoint.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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
1 2 3 4 5 bool change = checkWP();if (change){ nemu_state = STOP; }
checkWP()返回值用来判断是否触发监视点,如果触发了就更改 nemu_state
的状态。
nemu/src/monitor/debug/watchpoint.c
1 2 3 4 5 6 7 8 9 10 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
1 2 3 4 5 6 7 8 9 10 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
1 2 3 4 5 6 7 8 9 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ; }
添加删除指令。
输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (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
文件中,有助于实现数据封装,防止其他文件直接修改这些变量,确保数据的完整性和一致性。
查阅 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
类型的四维数组 dram
来模拟的。
1 uint8_t dram[NR_RANK][NR_BANK][NR_ROW][NR_COL];
1 2 3 4 #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
是通过 1 << COL_WIDTH
计算出来的,其中 COL_WIDTH
是 10,因此 NR_COL
的值是 1024。
uint8_t 表示一个无符号的 8
位整数(即一个字节),这是计算机内存的基本存储单位。使用
uint8_t
可以精确地模拟内存中的每个字节。uint8_t
只占用一个字节的空间,比其他数据类型(如 int 或
float)更节省内存。这对于模拟大容量的主存非常重要。uint8_t
是无符号的,这意味着它只能表示非负整数(0 到
255)。这符合内存地址和数据的实际情况,因为内存中的每个字节通常表示为无符号值。
nemu 是如何开始执行用户程序的?
通过函数 load_entry 来加载用户的程序,读取一个名字为 entry
的文件,以二进制只读模式读取,获取文件大小之后,将程序的 data 和 text
字段放入 ENTRY_START 的模拟内存的位置。然后 cpu.eip 置为
ENTRY_START,虚拟机开始执行程序。