寄存器结构体
寄存器是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
又分成 AH
和 AL
. 这样的结构有时候在处理数据时能提供一些便利. 至于如何实现这样的结构, 当然是难不倒聪明的你啦!
第二类在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
的命令中识别出 si
和 10
, 从而得知这是一条单步执行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到此结束.