监视点
监视点的功能是监视一个表达式的值何时发生变化. 如果你从来没有使用过监视点, 请在GDB中体验一下它的作用.
简易调试器允许用户同时设置多个监视点, 删除监视点, 因此我们最好使用链表将监视点的信息组织起来.
框架代码中已经定义好了监视点的结构体(在nemu/include/monitor/watchpoint.h
中):
typedef struct watchpoint {
int NO;
struct watchpoint *next;
/* TODO: Add more members if necessary */
} WP;
但结构体中只定义了两个成员: NO
表示监视点的序号, next
就不用多说了吧.
为了实现监视点的功能, 你需要根据你对监视点工作原理的理解在结构体中增加必要的成员.
同时我们使用"池"的数据结构来管理监视点结构体,
框架代码中已经给出了一部分相关的代码(在nemu/src/monitor/debug/watchpoint.c
中):
static WP wp_pool[NR_WP];
static WP *head, *free_;
代码中定义了监视点结构的池wp_pool
, 还有两个链表head
和free_
,
其中head
用于组织使用中的监视点结构, free_
用于组织空闲的监视点结构,
init_wp_pool()
函数会对两个链表进行了初始化.
为了使用监视点池, 你需要编写以下两个函数(你可以根据你的需要修改函数的参数和返回值):
WP* new_wp();
void free_wp(WP *wp);
其中new_wp()
从free_
链表中返回一个空闲的监视点结构, free_wp()
将wp
归还到free_
链表中,
这两个函数会作为监视点池的接口被其它函数调用.
需要注意的是, 调用new_wp()
时可能会出现没有空闲监视点结构的情况,
为了简单起见, 此时可以通过assert(0)
马上终止程序.
框架代码中定义了32个监视点结构, 一般情况下应该足够使用,
如果你需要更多的监视点结构, 你可以修改NR_WP
宏的值.
这两个函数里面都需要执行一些链表插入, 删除的操作, 对链表操作不熟悉的同学来说, 这可以作为一次链表的练习.
框架代码中定义wp_pool
等变量的时候使用了关键字static
,
static
在此处的含义是什么? 为什么要在此处使用它?
实现了监视点池的管理之后, 我们就可以考虑如何实现监视点的相关功能了. 具体的, 你需要实现以下功能:
- 当用户给出一个待监视表达式时, 你需要通过
new_wp()
申请一个空闲的监视点结构, 并将表达式记录下来. 每当cpu_exec()
执行完一条指令, 就对所有待监视的表达式进行求值(你之前已经实现了表达式求值的功能了), 比较它们的值有没有发生变化, 若发生了变化, 程序就因触发了监视点而暂停下来, 你需要将nemu_state
变量设置为NEMU_STOP
来达到暂停的效果. 最后输出一句话提示用户触发了监视点, 并返回到ui_mainloop()
循环中等待用户的命令. - 使用
info w
命令来打印使用中的监视点信息, 至于要打印什么, 你可以参考GDB中info watchpoints
的运行结果. - 使用
d
命令来删除监视点, 你只需要释放相应的监视点结构即可.
你需要实现上文描述的监视点相关功能, 实现了表达式求值之后, 监视点实现的重点就落在了链表操作上. 如果你仍然因为链表的实现而感到调试困难, 请尝试学会使用assertion.
在同一时刻触发两个以上的监视点也是有可能的, 你可以自由决定如何处理这些特殊情况, 我们对此不作硬性规定.
断点
断点的功能是让程序暂停下来, 从而方便查看程序某一时刻的状态. 事实上, 我们可以很容易地用监视点来模拟断点的功能:
w $eip == ADDR
其中ADDR
为设置断点的地址.
这样程序执行到ADDR
的位置时就会暂停下来.
调试器设置断点的工作方式和上述通过监视点来模拟断点的方法大相径庭. 事实上, 断点的工作原理, 竟然是三十六计之中的"偷龙转凤"! 如果你想揭开这一神秘的面纱, 你可以阅读这篇文章. 了解断点的工作原理之后, 可以尝试思考下面的两个问题.
我们知道int3
指令不带任何操作数, 操作码为1个字节, 因此指令的长度是1个字节. 这是必须的吗?
假设有一种x86体系结构的变种my-x86, 除了int3
指令的长度变成了2个字节之外, 其余指令和x86相同.
在my-x86中, 文章中的断点机制还可以正常工作吗? 为什么?
如果把断点设置在指令的非首字节(中间或末尾), 会发生什么? 你可以在GDB中尝试一下, 然后思考并解释其中的缘由.
你已经对NEMU的工作方式有所了解了. 事实上在NEMU诞生之前, NEMU曾经有一段时间并不叫NEMU, 而是叫NDB(NJU Debugger), 后来由于某种原因才改名为NEMU. 如果你想知道这一段史前的秘密, 你首先需要了解这样一个问题: 模拟器(Emulator)和调试器(Debugger)有什么不同? 更具体地, 和NEMU相比, GDB到底是如何调试程序的?