寄存器结构体

寄存器是CPU中一个重要的组成部分, 在CPU中进行运算所用到的数据和结果都会存放在寄存器中. i386手册的第2.3节对i386中所用寄存器进行了简单的介绍. 在现阶段的NEMU中, 我们只会用到其中的两类寄存器: 首先是通用寄存器. 通用寄存器的结构如下图所示:

 31                23                15                7               0
+-----------------+-----------------+-----------------+-----------------+
|                                  EAX       AH       AX      AL        |
|-----------------+-----------------+-----------------+-----------------|
|                                  EDX       DH       DX      DL        |
|-----------------+-----------------+-----------------+-----------------|
|                                  ECX       CH       CX      CL        |
|-----------------+-----------------+-----------------+-----------------|
|                                  EBX       BH       BX      BL        |
|-----------------+-----------------+-----------------+-----------------|
|                                  EBP                BP                |
|-----------------+-----------------+-----------------+-----------------|
|                                  ESI                SI                |
|-----------------+-----------------+-----------------+-----------------|
|                                  EDI                DI                |
|-----------------+-----------------+-----------------+-----------------|
|                                  ESP                SP                |
+-----------------+-----------------+-----------------+-----------------+

其中

  • EAX , EDX , ECX , EBX , EBP , ESI , EDI , ESP 是32位寄存器;
  • AX , DX , CX , BX , BP , SI , DI , SP 是16位寄存器;
  • AL , DL , CL , BL , AH , DH , CH , BH 是8位寄存器.

但它们在物理上并不是相互独立的, 例如 EAX 的低16位是 AX , 而 AX 又分成 AHAL . 这样的结构有时候在处理数据时能提供一些便利. 至于如何实现这样的结构, 当然是难不倒聪明的你啦!

第二类在NEMU中用到的寄存器就是 EIP , 也就是大名鼎鼎的程序计数器(Program Counter). 你在程序设计课上已经知道, 程序执行就是执行一行一行的C代码; 在计算机硬件的世界里, 程序执行也有类似的表现, 就是执行一条一条的指令. 但计算机怎么知道程序已经执行到哪里呢? 肩负着这一重要使命的就是程序计数器了, i386给它起了一个名字叫 EIP .

 31                23                15                7               0
+-----------------+-----------------+-----------------+-----------------+
|                       EIP (INSTRUCTION POINTER)                       |
+-----------------+-----------------+-----------------+-----------------+

可别小看了这个32位的家伙, 你会在PA2中频繁地跟它打交道. 随着实验的推进, 更多的寄存器会加入到NEMU中.

实现正确的寄存器结构体

我们在PA0中提到, 运行NEMU会出现assertion fail的错误信息, 这是因为框架代码并没有正确地实现用于模拟寄存器的结构体 CPU_state , 现在你需要实现它了(结构体的定义在 nemu/include/cpu/reg.h 中). 关于i386寄存器的更多细节, 请查阅i386手册. Hint: 使用匿名union.

nemu/src/cpu/reg.c 中有一个 reg_test() 函数, 它会生成一些随机的数据, 来测试你的实现是否正确, 若不正确, 将会触发assertion fail. 实现正确之后, NEMU将不会在 reg_test() 中触发assertion fail, 同时会输出NEMU的命令提示符:

(nemu)

输入 c 之后, NEMU将会运行一个由 mov 指令组成的用户程序, 最后输出如下信息:

nemu: HIT GOOD TRAP at eip = 0x001002b1

这说明程序成功地结束运行. 键入 q 退出NEMU. 此时可以打开 log.txt 文件查看刚才程序执行的每一条指令.

解析命令

NEMU通过 readline 库与用户交互, 使用 readline() 函数从键盘上读入命令. 与 gets() 相比, readline() 提供了"行编辑"的功能, 最常用的功能就是通过上, 下方向键翻阅历史记录. 事实上, shell程序就是通过 readline() 读入命令的. 关于 readline() 的功能和返回值等信息, 请查阅

man readline

从键盘上读入命令后, NEMU需要解析该命令, 然后执行相关的操作. 解析命令的目的是识别命令中的参数, 例如在 si 10 的命令中识别出 si10 , 从而得知这是一条单步执行10条指令的命令. 解析命令的工作是通过一系列的字符串处理函数来完成的, 例如框架代码中的 strtok() . strtok() 是C语言中的标准库函数, 如果你从来没有使用过 strtok() , 并且打算继续使用框架代码中的 strtok() 来进行命令的解析, 请务必查阅

man strtok

另外, cmd_help() 函数中也给出了使用 strtok() 的例子. 事实上, 字符串处理函数有很多, 键入以下内容:

man 3 str<TAB><TAB>

其中 <TAB> 代表键盘上的TAB键. 你会看到很多以str开头的函数, 其中有你应该很熟悉的 strlen() , strcpy() 等函数. 你最好都先看看这些字符串处理函数的manual page, 了解一下它们的功能, 因为你很可能会用到其中的某些函数来帮助你解析命令. 当然你也可以编写你自己的字符串处理函数来解析命令.

另外一个值得推荐的字符串处理函数是 sscanf() , 它的功能和 scanf() 很类似, 不同的是 sscanf() 可以从字符串中读入格式化的内容, 使用它有时候可以很方便地实现字符串的解析. 如果你从来没有使用过它们, RTFM, 或者到互联网上查阅相关资料.

单步执行

单步执行的功能十分简单, 而且框架代码中已经给出了模拟CPU执行方式的函数, 你只要使用相应的参数去调用它就可以了. 如果你仍然不知道要怎么做, RTFSC.

打印寄存器

打印寄存器就更简单了, 执行 info r 之后, 直接用 printf() 输出所有寄存器的值即可. 如果你从来没有使用过 printf() , 请到互联网上搜索相关资料. 如果你不知道要输出什么, 你可以参考GDB中的输出.

扫描内存

扫描内存的实现也不难, 对命令进行解析之后, 先求出表达式的值. 但你还没有实现表达式求值的功能, 现在可以先实现一个简单的版本: 规定表达式 EXPR 中只能是一个十六进制数, 例如

x 10 0x100000

这样的简化可以让你暂时不必纠缠于表达式求值的细节. 解析出待扫描内存的起始地址之后, 你就使用循环将指定长度的内存数据通过十六进制打印出来. 如果你不知道要怎么输出, 同样的, 你可以参考GDB中的输出.

实现了扫描内存的功能之后, 你可以打印 0x100000 附近的内存, 你应该会看到程序的代码, 和用户程序的objdump结果进行对比(此时用户程序是 mov , 其dump结果在 obj/testcase/mov.txt 中), 看看你的实现是否正确.

实现单步执行, 打印寄存器, 扫描内存

熟悉了NEMU的框架之后, 这些功能实现起来都很简单, 同时我们对输出的格式不作硬性规定, 就当做是熟悉GNU/Linux编程的一次练习吧.

不知道如何下手? 嗯, 看来你需要再阅读一遍RTFSC小节的内容了. 不敢下手? 别怕, 放手去写! 编译运行就知道写得对不对. 代码改挂了, 就改回来呗; 代码改得面目全非, 还有 git 呀!

温馨提示

PA1阶段1到此结束.

results matching ""

    No results matching ""