PA1 简易调试器
任务自查表
序号 | 是否已完成 |
---|---|
必做任务1 | 是 |
必做任务2 | 是 |
必做任务3 | 是 |
必做任务4 | 是 |
必做任务5 | 是 |
必做任务6 | 是 |
必做任务7 | 是 |
选做任务1 | 是 |
选做任务2 | 是 |
思考题
-
思考题 1:opcode_table 到底是一个什么类型的数组?
答:在 nemu/src/cpu/exec/exec.c 目录下中看到了opcode_table数组:
该数组为 helper_fun 类型,在同一个文件中看到 helper_fun 的定义为:
查阅资料后知道了typedef 返回类型(*新类型)(参数表)
的这种使用方式,此处 typedef 的功能是定义新的 helper_fun 类型,并定义这种类型为指向某种函数的指针,这种函数以一个 swaddr_t 为参数并返回 int 类型。因此,opcode_table 数组是一个函数指针数组。 -
思考题 2:
(1)在 cmd_c()函数中, 调用 cpu_exec()的时候传入了参数-1 , 你知道为什么吗?
答:在cpu_exec.c文件中找到了函数 cpu_exec()。
n是无符号整型,所以-1就是无符号最大的数字,那么函数里的for循环可以执行最大次数的循环,从而让cpu处理之后的指令。
(2)框架代码中定义 wp_pool 等变量的时候使用了关键字 static,static 在此处的含义是什么? 为什么要在此处使用它?
答:框架代码中定义wp_pool等变量时使用了关键字static,在此处的含义是静态全局变量,该变量只能被本文件中的函数调用,并且是全局变量,而不能被同一程序其他文件中的函数调用。在此处使用static是为了避免它被误修改。
查阅资料 后对 static 有了更深的了解。
- 思考题 3
1)查阅 i386 手册 :
① EFLAGS 寄存器中的 CF 位是什么意思?
答:P34页中提到参阅附录c,CF是进位标志
P419页中写到CF位:在最高位发生进位或者借位的时候将其置1,否则清零。
② 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 指令的具体格式是怎么样的?
答: 在P347页,格式是DEST ← SRC.
2) shell 命令
答:nemu 目录下的所有.c 和.h 和文件总共有4207行代码。通过find . -name "*[.h/.c]" | xargs wc -l
命令得到了这个结果。和框架代码相比, 我在 PA1 中编写了 445行代码。在Makefile中添加了相应的代码,实现了make count命令。
除去空行之外, nemu 目录下的所有.c 和.h 文件总共有3418代码。 通过find . -name "*[.h/.c]" | xargs grep "^." | wc -l
命令得到了这个结果。
3)Make文件 :-Wall 和-Werror 有什么作用? 为 什么要使用-Wall 和-Werror?
答:-Wall 使GCC产生尽可能多的警告信息,取消编译操作,打印出编译时所有错误或警告信息。
-Werror 要求GCC将所有的警告当成错误进行处理,取消编译操作。
使用 -Wall 和 -Werror就是为了找出存在的错误,尽可能地避免程序运行出错,优化程序。
寄存器结构体
必做任务 1:实现正确的寄存器结构体
在现阶段的NEMU 中通用寄存器为:
32位寄存器:EAX , EDX , ECX , EBX , EBP , ESI , EDI , ESP
16位寄存器:AX , DX , CX , BX , BP , SI , DI , SP
8 位寄存器:AL , DL , CL , BL , AH , DH , CH , BH
但它们在物理上并不是相互独立的, 例如 EAX 的低 16 位是 AX , 而 AX 又分成 AH 和 AL。因此EAX寄存器结构图如下(图中没有标出AH):
在reg.h文件中的源代码里,用struct结构定义寄存器。查阅资料可以知道struct和union的区别:
Struct与Union主要有以下区别:
1)struct和union都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存放了一个被选中的成员, 而struct的所有成员都存在。在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总长度等于所有成员长度之和。在Union中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union变量的长度等于最长的成员的长度。
2)对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于struct的不同成员赋值是互不影响的。
参考网页.
由此可以看出寄存器的特征符合联合体,修改后的代码为:
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;
必做任务 2:实现单步执行、打印寄存器、扫描内存
实现单步执行
单步执行的格式为si [N] ,程序单步执行N条指令后暂停, 当N没有给出时, 缺省为默认为1。根据单步执行的说名得出解题步骤:
1)传入cmd_si()函数的参数为字符串,现在需要利用一些方法将其分解为两部分,分别为“si (空格)”和“N”(N是字符串类型的数字),N的部分存到字符串arg中,此过程中需要用到strtok()库函数,查阅资料知道:
strtok( ) 的使用
C 库函数 char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。
2)根据字符串arg来判断需要执行的指令数 i,需要使用sscanf()库函数,将字符串arg改为int型的数字 i,查阅资料知道:
sscanf( ) 的使用
C 库函数 int sscanf(const char *str, const char *format, …) 从字符串读取格式化输入。
str – 这是 C 字符串,是函数检索数据的源。
format – 这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符和format 说明符。
① 若arg为NULL,默认 i = 1
② 若 i < -1, 提示输入错误
③ 若 i >= -1, 调用次 cpu_exec(steps) (当steps > 10时没能输出结果,询问同学后才知道,cpu_exce.c文件中的#define MAX_INSTR_TO_PRINT 10 中的10改为最大值就可以了)
最后附上代码:
static int cmd_si(char *args){
char *arg = strtok(NULL," ");
int steps = 0;
if(arg == NULL){
cpu_exec(1);
return 0;
}
sscanf(arg, "%d", &steps);
if(i<-1){
printf("Error,N is an integer greater than or equal to -1\n");
return 0;
}
cpu_exec(steps);
}
return 0;
}
实验结果:
打印寄存器
打印程序状态的命令格式为info SUBCMD ,当SUBCMD为 r 时info r打印印寄存器状态,只需要printf每一个寄存器的状态。
代码如下:
static int cmd_info(char *args) {
char *arg=strtok(NULL," ");
if(strcmp(arg,"r") == 0){
for(int i=0;i<8;i++)
printf("%s \t%x \t%d\n",regsl[i],cpu.gpr[i]._32,cpu.gpr[i]._32);
printf("$eip \t%x \t%d\n", cpu.eip, cpu.eip);
}
return 0;
}
实验结果:
扫描内存
查阅实验手册知道,访问内存的接口函数相关的源代码存在memory.c文件中,其中lnaddr_read和lnaddr_write两个函数用来对内存进行读写,lnaddr_read函数需要传入两个参数,分别为起始地址和扫描长度。
内存扫描命令的格式为x N EXPR ,N表示扫描长度,EXPR为起始内存。因此得出解题步骤:
1)传入cmd_x()函数的参数为字符串,需要利用strtok()函数分别得到 N 和 EXPR 部分的字符串,再利用sscanf()函数将字符串 N 转化为十进制整型数 len,把字符串EXPR转化为十六进制的数address。
2)任务中要求以16进制 形式输出连续的N个4字节,因此,将address和4传入lnaddr_read函数就可以得到,再用for循环循环len次,每次循环时起始地址加4,就可以实现内存的扫描。
代码如下:
static int cmd_x(char *args){
char *N = strtok(NULL," ");
char *EXPR = strtok(NULL," ");
int len;
lnaddr_t address;
sscanf(N, "%d", &len);
sscanf(EXPR, "%x", &address);
printf("0x%x:",address);
int i;
for(i = 0; i < len; i ++){
printf("%08x ",lnaddr_read(address,4));
address += 4;
}
printf("\n");
return 0;
}
实验结果:
与mov.txt文件中的内容比较,结果一致(* ^ ▽ ^ *)~
表达式求值
必做任务 3:实现算术表达式的词法分析
想要求出表达式的值,第一步要解决的问题是识别字符串中的数字、符号、括号等等,解决方法是利用正则表达式刻画字符的组合规律,将字符串切割成一个个的有确定类型的token。
- 表达式中可能出现的类型:
① 数字:十进制 ,十六进制 …
② 运算符:+,-,*,/,(…
③ 符号:test_case,…
④ 寄存器:$ eax,$ edx,…
利用正则表达式的规则补充rules[],其中要特别注意,如果识别的符号为正则表达式的元符号则需要加上 \ 符号,代码如下:
扩充完正则表达式规则以后,需要做的就是对输入的字符串进行分析,对每一个符号进行分类,再将各个类型存储在tokens[]数组中,完成此操作的函数为make_token()函数。已给出代码的部分可以成功识别得到该字符或者字符串的对应规则,而我们需要补充的部分是switch语句,switch语句将表达式中每一个部分用对应的类型及具体值存储到tokens[nr_token].str中(如NUM类型里存具体的数字,RESITER类型里存具体的寄存器的名字等等),此过程中用到了strncpy()函数来复制具体的值(如NUM类型的具体值等等)。
必做任务 4:实现算术表达式的递归求值
通过任务3,我们已将token存入到了tokens[]数组中,接下来需要用递归的方法求出表达式的值,此功能在eval()函数中实现。实验手册中给出了eval()函数的代码框架,任务4和5中,我们需要做的就是补充eval()函数,在实现eval()函数的过程中我们还会需要其它的函数,如:① 括号匹配函数check_parentheses() ② 寻找dominant operator的函数dominant_operator()
括号的匹配
为了判断表达式中的括号是否匹配对,我们需要写一个bool类型的bool check_parentheses()函数,若括号匹配返回true,否则,返回false。基本思路如下:
1)若表达式的左右两端为匹配的括号,进入循环从 p + 1 到 q 的位置进行扫描,用变量 counter 记录括号的数目,遇到右括号 counter++, 遇到左括号则 counter–,若在进入下一个循环之前出现counter不为零的情况说明括号不匹配直接返回 false 。循环结束时若有 counter = 0 则说明括号匹配,返回 true。
2)不满足1)中第一个if语句的情况有以下三种:
(1)(4 + 3 * (2 - 1)
(2)4 + 3 * (2 - 1)
(3)4 + 3 * 2 - 1
为了区分这三种情况在这里我又写了两个函数,check_only_parentheses()仅仅查明表达式中括号是否匹配, check_if_parentheses()判断一个符号是不是被一个括号包围着。(2),(3)情况会在eval()函数中视为同一种情况处理。
寻找dominant operator
为了寻找 dominant operator,在这里我定义了一个dominant_ operator()函数,此函数完成的实现过程如下:
1)在函数中定义了一个 数组candidate[] (初始值为-1)来存储除数字,寄存器,括号以外的符号的位置
2)对数组candidate[]进行倒序遍历,再按优先级降序对tokens[ candidate[j] ].type判断进行判断,遇到优先级最高的运算符就return其所在的位置。核心代码如下:
for(i = j - 1;i >= 0; --i){
if(tokens[candidate[i]].type==OR)
return candidate[i];
if(tokens[candidate[i]].type==AND)
return candidate[i];
if(tokens[candidate[i]].type==EQUAL||tokens[candidate[i]].type==NOTEQUAL)
return candidate[i];
if(tokens[candidate[i]].type=='+'||tokens[candidate[i]].type=='-')
return candidate[i];
if(tokens[candidate[i]].type=='*'||tokens[candidate[i]].type=='/')
return candidate[i];
}
for(i = 0; i < j; ++i)
if(tokens[candidate[i]].type == NEG||tokens[candidate[i]].type=='!'||tokens[candidate[i]].type==DEREF)
return candidate[i];
必做任务 5:实现更复杂的表达式求值
通过任务4,我们已经做好了表达式运算的基本准备工作。在此任务中我们需要实现完整的表达式求值功能更,因此需要完成 expr.c 中的eval(),expr()函数,和 ui.c 中的cmd_p()函数。
eval () 函数的实现
eval(p, q)是实现BNF算法的函数。eval函数会对表达式的首尾地址的合理性进行判断,若不合理assert(0),若合理则用递归的方法实现表达式求值,具体步骤如下:
1)若 p > q,说明表达式有误,直接assert(0)。
2)若 p = q,说明表达式为十进制数字,十六进制数字或者寄存器。根据tokens[p].type判断表达式为以上三种情况中的哪一种,再按对应格式输出即可。其中,寄存器最为特殊,在输出寄存器的值时会用到swaddr_read()函数来获取寄存器的值。
3)若check_parentheses(p, q) == true,则直递归调用eval()函数。
4)若check_parentheses(p, q) == false,可能的情况有三种(在任务4中已做分析),通过调用check_only_parentheses()函数来判断属于哪一种哪一种情况,对每一种情况给出相应的处理方式:
① 若check_only_parentheses()为false,说明表达式本身存在错误,如:(4 + 3 * (2 - 1),直接assert(0)即可。
②若check_only_parentheses()为true,说明是任务四中分析的第(2),(3)种情况,这两种情况的解决方法一样,需要调用dominant_operator()函数来判断dominant operator的位置,从该位置将表达式分为两部分,再对每一个部分递归调用eval()函数。
expr () 函数的实现
expr()是实现表达式求值的函数。该函数中完成了两个选做任务,判断了 “-” 和 “ * ”的具体意义,然后再调用eval()函数对表达式进行递归求值。
选做任务 1:实现带有负数的算术表达式的求值
想要判断-为减号还是负号,只需要判断该符号前面的负号是否为数字或者寄存器,若-的前一个符号为数字或者寄存器说明是减号,若不是则说明是负号。(*的判断方法也一样)
具体代码如下:
if(tokens[i].type == '*' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i-1].type!= HEXNUM && tokens[i-1].type!= REGNAME && tokens[i-1].type!=')')) )
tokens[i].type = DEREF;
if(tokens[i].type == '-' && (i == 0 || (tokens[i - 1].type != NUM && tokens[i-1].type!= HEXNUM && tokens[i-1].type != REGNAME && tokens[i-1].type!=')')) )
tokens[i].type = NEG;
选做任务 2:实现指针解引用
在expr()函数中已经实现了- 和 *的具体含义的判断,在eval()函数只需要加入对应的运算规则即可:
switch(tokens[op].type){
case NEG:
return -val;
case DEREF:
return swaddr_read(val, 4);
case '!':
return !val;
default:
assert(0);
}
需要注意的是,当 *作为指针解引时需要通过 swaddr_read()来读取该位置所存的内容。
最后附上实验结果ヾ(✿゚▽゚)ノ:
监视点
监视点的功能是监视一个表达式的值何时发生变化。查阅资料以后对监视点有了更深的了解,接下里就可以开始任务了~
必做任务 6:实现监视点池的管理
首先,我们需要增加监视点结构体的成员。在watchpoint.h文件中有watchpoint结构体的定义。我在结构体中增加了两个成员:
① char类型的数组 exp[32] , 用来存储算数表达式的内容
② unit32_t类型的 value,用来存储算数表达式的结果
接下来需要为了使用监视点池, 我们需要编写以下两个函数WP* new_wp() 和 void free_wp(WP *wp)。
init_wp_pool()函数会对两个链表 free_ 和 head 进行了初始化,初始化以后的结果如图所示:
new_wp()函数的实现
new_wp()从 free_链表中返回一个空闲的监视点结构给head链表,且将表达式,表达式的值赋给该监视点结构,具体代码如下:
WP* new_wp(char * exp){
assert(free_ != NULL);
WP *temp = free_;
free_ = free_->next;
temp->next = NULL;
bool success = false;
strcpy(temp->exp, exp);
temp->value = expr(temp->exp, &success);
assert(success);
if(head==NULL)
head = temp;
else{
WP *p = head;
while(p->next)
p = p->next;
p->next = temp;
}
return temp;
}
该代码的内容包含链表插入,实现的过程如下图所示:
free_wp( )函数的实现
free_wp() 函数的参数为WP 类型的指针wp,free_wp() 的作用是将wp所指的结点归还到free_链表中。具体步骤如下:
① 若 wp = NULL ,则说明输入有误
② 若 wp = head ,说明wp指向head链表的头结点,只需让head指针指向下一个结点,再将wp所指的结点连到free_链表的第一个位置,并让free_指针指向该节点
③ 若 wp 为其它结点,则需要对head链表进行遍历找出wp所指的结点,再根据②中的步骤,将该结点归还到free_链表中
是遍历head链表直到找出对应NO的结点,从head中删除该节点,添加到free_链表中。
具体代码如下:
int free_wp(WP *wp){
if(wp == NULL)
printf("Input something!\n");
if(wp == head)
head = head -> next;
else{
WP *p = head;
while(p->next!=wp)
p = p->next;
if(pos == NULL)
return 0;
pos -> next = wp -> next;
}
wp -> next = free_;
free_ = wp;
return 1;
}
必做任务 7:实现监视点
test_change()函数
该函数为int类型的函数。 此函数对每个监视点进行遍历,取出每个结点的exp,利用expr()函数对其进行求值,再将求出来的值与结点的value值进行比较,若相同则返回1,否则返回0。每当cpu_exec()执行完一条指令,需要调用test_change()函数来判断监视点的值是否发生改变,若发生改变了则需要将nemu_state变量设置为STOP来达到暂停的效果。因此需要在cup-exec.c文件中添加test_change()。
添加监视点
添加监视点的任务由cmd_w()函数来完成,该函数中调用new_wp()函数来存储新的监视点。
删除监视点
删除监视点需要由cmd_d()函数来实现,该函数中会调用delete_wp()函数,delete_wp() 函数的参数为int类型的监视点序号,在该函数中需要遍历head链表,从中找出对应序号的监视点,再调用free_wp()函数来把此监视点归还到free_链表中,实现了监视点的删除。
代码如下:
打印监视点
打印监视点由cmd_info()函数来实现,该函数中会调用print_wp()函数,print_wp()函数所要做的就是对head链表进行遍历
,然后输出每个监视点的NO, exp,value。
代码如下:
最后附上实验结果(ノ゚▽゚)ノ: