写在前面的话
如果您对该系列感兴趣的话,推荐您先看一下南京大学的计算机组成原理实验(也就是PA)的讲义,然后再来看这篇文章可能有更多地收获。如果您是要完成该作业的学生,我推荐你先看讲义,或者好好听老师的讲课,然后自己独立完成这个作业,但是如果你没有听懂,或者你无论如何也无法理解讲义上面的字,又或者说对讲义上面的某点知识某个问题不了解而又觉得太简单不好意思问老师,那么您可能会从这篇文章里面获得一些你需要的信息。本篇文章将会包括笔者自己做PA的所有经过,希望你并不将该文章当成抄袭的根源,而是成为你思考的源泉。
从PA1开始我们就开始真正进行试验了,之前的PA0只是搭配环境,说白了就是输入几个指令,但是从现在开始就是真正运用起来我们自己的智慧的时刻,我们要自己动手写代码,用自己的脑洞来创造一个模拟器,所以相比于PA0,从今往后你对于每个任务都要仔细认真地思考并且尽量独自完成。
PA系列传送门
PA0:https://blog.csdn.net/qq_41983842/article/details/88921427
PA1.1:https://blog.csdn.net/qq_41983842/article/details/88934779
PA1.2:https://blog.csdn.net/qq_41983842/article/details/89714479
PA1.3:https://blog.csdn.net/qq_41983842/article/details/89714689
PA2.1:https://blog.csdn.net/qq_41983842/article/details/95232055
PA2.2&2.3:https://blog.csdn.net/qq_41983842/article/details/101164495
PA3.1:https://blog.csdn.net/qq_41983842/article/details/103094859
PA3.2:https://blog.csdn.net/qq_41983842/article/details/103843093
PA4:https://blog.csdn.net/qq_41983842/article/details/104667951
思考题
-
体验监视点
测试代码:
#include<stdio.h> int i = 0,j = 0; int main() { for(i = 0;i < 10;i++) j += 5; return 0; }
玩GDB:
-
此处可否使用
free
来作为头指针的名字呢?不能,c语言中
free
直接就把空间给释放了,所以编译的时候符号名就会重复,就像我给一个变量取名叫new
一样,这些都是在c语言中有其他意义的符号。 -
static
在此处的含义是什么?为什么要在此处使用它?这里定义了一个静态全局变量,如果不加
static
的话那么wp_pool[NR_WP],*head, *free_;
对整个工程可见,加上以后仅仅对本文件可见,这就保证了其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响,避免不同文件同名变量的冲突,且不会误使用。 -
在 my-x86 中,文章中的断点机制还可以正常工作吗?为什么?
int3
指令长度是一个字节,换成两个字节之后就不能正常工作了。这种形式是有价值的 , 因为它可以被用于代替第一指令字节中的任何一个的断点 , 包括另一字节指令 , 而无需重写其它代码。 -
把断点设置在指令的非首字节(中间或末尾),会发生什么?
GDB
没有办法将断点设置在指令的中间或者末尾,会出错。 -
模拟器 (Emulator) 和调试器 (Debugger) 有什么不同?更具体地,和
NEMU
相比,GDB
到底是如何调试程序的?调试器可以开始一些过程和系统进行调试 , 或者自己附加到现有进程。它可以单一步骤通过代码 , 设置断点和运行 ,检查变量值和堆栈跟踪。调试器有许多高级功能 ,比如执行表达式和函数的调用
debbugged
进程的地址空间 ,并且即使改变的过程的代码在实时观看的效果。模拟器跟调试器不同的地方在于环境的区别吧,实现断点的方式不是软硬件断点是模拟出来的。我们所说的模拟器都是利用软件模拟相关的硬件,相当于用程序去模拟,而在硬件上运行的是你运行这套模拟系统的程序,而虚拟机或者说调试器是把系统的指令送到机器里,用硬件来执行指令的,所以效率高并且还会快很多。 -
假设你现在需要了解一个叫
selector
的概念, 请通过i386手册的目录确定你需要阅读手册中的哪些地方.通过搜索定位到第五章5.1.3。手册的第96页。
-
你将会在调试上花费多少时间?简易调试器可以帮助你节省多少调试的时间?
500×0.9×30×20=270000s=75h,500×0.9×10×20=25h,可以节省50h时间。
-
EFLAGS
寄存器中的CF位是什么意思?ModR/M
字节是什么?mov
指令的具体格式是怎么样的?CF位是进位标志位(2.3.4.1,手册第34页标志位),
ModR/M
长度为1个字节(17.2,手册第240页),ModR/M
中Mod
决定操作数,R/M
表示Register (or/and) Mermory
,跟mod
一起确定源操作数。mov
指令格式:(手册414页的表)1.
MOV
寄存器, 寄存器/内存单元/段寄存器/立即数2.
MOV
内存单元 , 寄存器/段寄存器/立即数3.
MOV
段寄存器 , 寄存器/内存单元 -
shell
命令find ics2017/nemu/ -name "*[.h|.c]" |xargs cat|wc -l
命令统计有4032行代码find ics2017/nemu/ -name "*[.h|.c]" |xargs cat|grep -v ^$|wc -l
命令统计去除空格后有3340行代码返回到
master
分支以后,统计代码有3526行,差不多编写了不到500行吧。 -
-Wall
和-Werror
有什么作用? 为什么要使用-Wall
和-Werror
?-Wall
是打开gcc
的所有警告,-Werror
要求gcc
将所有的警告当成错误进行处理。-Wall
作用很明显,编译肯定要有警告嘛,如果没有警告万一影响了程序运行不就尴尬了。位-Werror
可以防止函数定义未使用,当定义未使用时,会报错,而不是警告,保证了程序的正确运行。他将程序中所有的warning
都指示成为error
,防止程序因为warning
造成程序的不稳定性。
实验内容
实现监视点结构体
添加三个变量
实现监视点池的管理
这个任务主要就是链表的操作,一共两个链表,一个head
一个free_
,WP* new_wp()
函数就是把free_
链表里面的空闲结点拿出来放到head
里面,而void free_wp(WP *wp)
就正好相反,所以每个函数最主要的核心功能就是删除链表元素和添加链表元素,根据讲义上面的指示,很快就可以写出来两个函数,但是这时候就出现了一个问题,什么时候调用结构里面写好的init_wp_pool()
函数呢?我就又加设了一个全局变量init
,初值位0,当他位0的时候就会初始化监视点池,然后每增加一个监视点,init
就加1,反之减一。而在free_wp
函数中就需要判断删除的位置:头结点、中间结点、没有监视点,三种情况分别判断就好了,我将函数返回类型改成了int
,并且将参数改成了需要删除的编号(为了更好在delete_watchpoint
中调用)
将监视点加入调试器功能
在写完这个基础的函数之后,就要开始实现真正的功能了,任务三和任务四是连在一起的,所以就一起实现了,实现这个任务要在ui.c
里面写三个相应的功能,cmd_w
cmd_d
和cmd_info
里面的w
子命令,分别对应添加监视点,删除监视点和显示所有监视点的功能。而他们分别对应着任务4的函数,所以基本上直接调用就行了。
cmd_w
函数只需要将参数直接传到set_watchpoint(args)
函数里面就好了,不需要分割,因为他本来就是一个表达式。
cmd_d
函数需要将得到的字符串分割,并且转换成数字,即编号,这样就可以传入delete_watchpoint()
函数处理了。
而cmd_info
里面的w
子命令,就是调用list_watchpoint()
就行了,所以任务三没什么难度,主要内容都是在任务4里面搞的。
实现监视点
实现4个函数,
int set_watchpoint(char *e); //给予一个表达式e,构造以该表达式为监视目标的监视点,并返回编号
bool delete_watchpoint(int NO); //给予一个监视点编号,从已使用的监视点中归还该监视点到池中
void list_watchpoint(void); //显示当前在使用状态中的监视点列表
WP* scan_watchpoint(void); //扫描所有使用中的监视点,返回触发的监视点指针,若无触发返回NULL
其实说白了全部都是链表的操作,链表的插入,删除,遍历等,讲义里面也给了思路,直接实现就好。
set_watchpoint(char *e)
函数,按照讲义上面的参考输出,首先从监视点池里面拿一个新的监视点,然后对这个监视点的各个变量进行赋值,表达式直接存进去就好,旧值通过调用表达式求值函数来实现,最后打印出来旧值就行了。
实现后的set_watchpoint(char *e)
函数:
delete_watchpoint(int NO)
这个函数相当的简单,直接调用free_wp
函数就行了。
list_watchpoint(void)
函数这个也很简单,就是遍历链表嘛,不多说了。
scan_watchpoint(void)
这个函数里面主要就是针对每个监视点判断他的值有没有发生变化,如果变化了就停止,如果不变化就继续运行,实现也非常的简单,就是链表的操作,打印地址我用的cpu.eip
,一开始我还把这断点和监视点两种搞混了,直接实现的断点的功能,后来发现监视点其实是不需要让程序暂停,而断点才会让程序暂停,然后就把任务5从这个函数里面搬到了cpu-exec()
函数里面。
使用模拟断点
就像上面说的一样,在cpu-exec()
函数里面有一个提示你写代码的地方,所以就决定写在这里了,直接调用scan_watchpoint()
函数,看他的返回值,如果不是空,就说明触发了监视点事件,那么就直接暂停就好。关于模拟断点和具体监视点实现的不同,我觉得讲义上面写的不清楚,或者没讲清楚他面两个的区别,后来问了助教,我才知道模拟断点不会输出old_val
new_val
等在监视点才会输出的信息,直接中断就行了。
但是为了可以用WP
这个结构体,必须给他加个头文件
#include "monitor/watchpoint.h"//加入watchpoint
可以实现所有的功能了:
将set_watchpoint
函数修改如下,主要就是将断点和监视点区分,利用strncmp
:
int set_watchpoint(char *e) {
WP *p;
p = new_wp();//拿一个新监视点
bool watchpoints = false;
strcpy(head->expr,e);//存入表达式
if (!strncmp(p->expr,"$eip == ",8)) {//是断点
printf("Set break point $eip == ADDR\n");
}
else {//是监视点
watchpoints= true;
printf("Set watchpoint #%d\n", p->NO);
printf("expr = %s\n", p->expr);
}
bool success = true;
p->old_val = expr(p->expr,&success);//存入旧值
if (!success) {
printf("Fail to eval\n");
return 0;
}
else if(watchpoints == true){
printf("Old value = %#x\n", p->old_val);
}
return 1;
}
同样,将scan_watchpoint
修改如下,同样是根据表达式来区分断点和监视点:
WP* scan_watchpoint(void) {
WP *p = head;
bool success = true;
if(p == NULL) {
printf("No Watchpoint\n");
return false;
}
else {
while (p) {
p->new_val = expr(p->expr,&success);//计算新值
if (!success)
printf("Fail to eval new_val in watchpoint %d\n",p->NO);
else {
if (p->new_val != p->old_val) {//值发生变化
if (!strncmp(p->expr,"$eip == ",8)) {//是断点表达式
printf("Hit break point,program paused\n");
return p;
}
else {
printf("Hit watchpoint %d at address %#8x\n", p->NO,cpu.eip);
printf("expr = %s\n", p->expr);
printf("old value = %#x\nnew value = %#x\n", p->old_val,p->new_val);
p->old_val = p->new_val;//旧值更新
printf("program paused\n");
return p;
}
}
}
}
}
return NULL;
}
最终实现效果如下: