NEMU PA1

PA1 简易调试器

任务自查表

序号 是否已完成
必做任务1
必做任务2
必做任务3
必做任务4
必做任务5
必做任务6
必做任务7
选做任务1
选做任务2

思考题

  1. 思考题 1:opcode_table 到底是一个什么类型的数组?
    答:在 nemu/src/cpu/exec/exec.c 目录下中看到了opcode_table数组:
    NEMU PA1
    该数组为 helper_fun 类型,在同一个文件中看到 helper_fun 的定义为:
    NEMU PA1
    查阅资料后知道了typedef 返回类型(*新类型)(参数表)的这种使用方式,此处 typedef 的功能是定义新的 helper_fun 类型,并定义这种类型为指向某种函数的指针,这种函数以一个 swaddr_t 为参数并返回 int 类型。因此,opcode_table 数组是一个函数指针数组。
  2. 思考题 2:
    (1)在 cmd_c()函数中, 调用 cpu_exec()的时候传入了参数-1 , 你知道为什么吗?

    答:在cpu_exec.c文件中找到了函数 cpu_exec()。
    NEMU PA1n是无符号整型,所以-1就是无符号最大的数字,那么函数里的for循环可以执行最大次数的循环,从而让cpu处理之后的指令。

(2)框架代码中定义 wp_pool 等变量的时候使用了关键字 static,static 在此处的含义是什么? 为什么要在此处使用它?
答:框架代码中定义wp_pool等变量时使用了关键字static,在此处的含义是静态全局变量,该变量只能被本文件中的函数调用,并且是全局变量,而不能被同一程序其他文件中的函数调用。在此处使用static是为了避免它被误修改。

查阅资料 后对 static 有了更深的了解。

  1. 思考题 3
    1)查阅 i386 手册 :
    ① EFLAGS 寄存器中的 CF 位是什么意思?
    答:P34页中提到参阅附录c,CF是进位标志
    NEMU PA1
    P419页中写到CF位:在最高位发生进位或者借位的时候将其置1,否则清零。
    NEMU PA1

② ModR/M 字节是什么?
阅读P241-243页。ModR/MModReg/OpcodeR/M 三部分组成。
Mod 是前两位,提供寄存器寻址和内存寻址,
Reg/Opcode为3-5位,如果是Reg表示使用哪个寄存器,Opcode表示对group属性的Opcode进行补充;
R/M为6-8位,与mod结合起来查图得8个寄存器和24个内存寻址
NEMU PA1

③ mov 指令的具体格式是怎么样的?
答: 在P347页,格式是DEST ← SRC.

2) shell 命令
答:nemu 目录下的所有.c 和.h 和文件总共有4207行代码。通过find . -name "*[.h/.c]" | xargs wc -l命令得到了这个结果。和框架代码相比, 我在 PA1 中编写了 445行代码。在Makefile中添加了相应的代码,实现了make count命令。
NEMU PA1

除去空行之外, 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):
NEMU PA1
reg.h文件中的源代码里,用struct结构定义寄存器。查阅资料可以知道struct和union的区别:

StructUnion主要有以下区别:
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;  
}

实验结果:

NEMU PA1

打印寄存器

打印程序状态的命令格式为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;  
} 

实验结果:
NEMU PA1

扫描内存

查阅实验手册知道,访问内存的接口函数相关的源代码存在memory.c文件中,其中lnaddr_readlnaddr_write两个函数用来对内存进行读写,lnaddr_read函数需要传入两个参数,分别为起始地址和扫描长度。

NEMU PA1
内存扫描命令的格式为x N EXPR ,N表示扫描长度,EXPR为起始内存。因此得出解题步骤
1)传入cmd_x()函数的参数为字符串,需要利用strtok()函数分别得到 NEXPR 部分的字符串,再利用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;  
}

实验结果:
NEMU PA1
与mov.txt文件中的内容比较,结果一致(* ^ ▽ ^ *)~
NEMU PA1

表达式求值

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

想要求出表达式的值,第一步要解决的问题是识别字符串中的数字、符号、括号等等,解决方法是利用正则表达式刻画字符的组合规律,将字符串切割成一个个的有确定类型的token。

  • 表达式中可能出现的类型:
    ① 数字:十进制 ,十六进制 …
    ② 运算符:+,-,*,/,(…
    ③ 符号:test_case,…
    ④ 寄存器:$ eax,$ edx,…

利用正则表达式的规则补充rules[],其中要特别注意,如果识别的符号为正则表达式的元符号则需要加上 \ 符号,代码如下:

NEMU PA1

扩充完正则表达式规则以后,需要做的就是对输入的字符串进行分析,对每一个符号进行分类,再将各个类型存储在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 + 1q 的位置进行扫描,用变量 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()来读取该位置所存的内容。

最后附上实验结果ヾ(✿゚▽゚)ノ:
NEMU PA1

监视点

监视点的功能是监视一个表达式的值何时发生变化。查阅资料以后对监视点有了更深的了解,接下里就可以开始任务了~
NEMU PA1


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

首先,我们需要增加监视点结构体的成员。在watchpoint.h文件中有watchpoint结构体的定义。我在结构体中增加了两个成员:
① char类型的数组 exp[32] , 用来存储算数表达式的内容
② unit32_t类型的 value,用来存储算数表达式的结果NEMU PA1
接下来需要为了使用监视点池, 我们需要编写以下两个函数WP* new_wp()void free_wp(WP *wp)

init_wp_pool()函数会对两个链表 free_head 进行了初始化,初始化以后的结果如图所示:

NEMU PA1

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;
}

该代码的内容包含链表插入,实现的过程如下图所示:
NEMU PA1

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()
NEMU PA1

添加监视点

添加监视点的任务由cmd_w()函数来完成,该函数中调用new_wp()函数来存储新的监视点。
NEMU PA1

删除监视点

删除监视点需要由cmd_d()函数来实现,该函数中会调用delete_wp()函数,delete_wp() 函数的参数为int类型的监视点序号,在该函数中需要遍历head链表,从中找出对应序号的监视点,再调用free_wp()函数来把此监视点归还到free_链表中,实现了监视点的删除。

代码如下:
NEMU PA1

打印监视点

打印监视点由cmd_info()函数来实现,该函数中会调用print_wp()函数,print_wp()函数所要做的就是对head链表进行遍历
,然后输出每个监视点的NO, exp,value。

代码如下:
NEMU PA1
最后附上实验结果(ノ゚▽゚)ノ:
NEMU PA1

上一篇:2020/5/26-笔记:Oracle数据库表空间的管理


下一篇:DeRPnStiNK