基础设施(2)
AM作为基础设施
编写klib, 然后在NEMU上运行string程序, 看其是否能通过测试.
表面上看, 这个做法似乎没什么不妥当, 然而如果测试不通过,
你在调试的时候肯定会思考: 究竟是klib写得不对, 还是NEMU有bug呢?
如果这个问题得不到解决, 调试的难度就会上升:
很有可能在NEMU中调了一周, 最后发现是klib的实现有bug.
之所以会有这个问题, 是因为软件(klib)和硬件(NEMU)都是你编写的,
它们的正确性都是不能100%保证的.
大家在中学的时候都学习过控制变量法:
如果能把其中一方换成是认为正确的实现, 就可以单独测试另一方的正确性了!
比如我们在真机上对klib进行测试, 如果测试没通过,
那就说明是klib的问题, 因为我们可以相信真机的硬件实现永远是对的;
相反, 如果测试通过了, 那就说明klib没有问题, 而是NEMU有bug.
一个新的问题是, 我们真的可以很容易地把软件移植到其它硬件上进行测试吗?
聪明的你应该想起来AM的核心思想了: 把程序和架构解耦.
AM的思想保证了运行在AM之上的代码(包括klib)都是架构无关的,
这恰恰增加了代码的可移植性.
想象一下, 如果string.c的代码中有一条只能在NEMU中执行的nemu_trap指令,
那么它就无法在真机上运行.
nexus-am中有一个特殊的架构叫native, 是用GNU/Linux默认的运行时环境来实现的AM API.
例如我们通过gcc hello.c编译程序时, 就会编译到GNU/Linux提供的运行时环境;
你在PA1试玩的超级玛丽, 也是编译到native上并运行.
和$ISA-nemu相比, native有如下好处:
直接运行在真机上, 可以相信真机的行为永远是对的
就算软件有bug, 在native上调试也比较方便(例如可以使用GDB, 比NEMU的monitor方便很多)
因此, 与其在$ISA-nemu中直接调试软件, 还不如在native上把软件调对,
然后再换到$ISA-nemu中运行, 来对NEMU进行测试.
在nexus-am中, 我们可以很容易地把程序编译到另一个架构上运行,
例如在nexus-am/tests/cputest/目录下执行
make ALL=string ARCH=native run
即可将string程序编译到native并运行.
由于我们会将程序编译到不同的架构中, 因此你需要注意make命令中的ARCH参数.
如何生成native的可执行文件
阅读相关Makefile, 尝试理解nexus-am是如何生成native的可执行文件的.
与NEMU中运行程序不同, 由于cputest中的测试不会进行任何输出,
我们只能通过程序运行的返回值来判断测试是否成功.
如果string程序通过测试, 终端将不会输出任何信息;
如果测试不通过, 终端将会输出
make[1]: *** [run] Error 1
当然也有可能输出段错误等信息.
奇怪的错误码
为什么错误码是1呢? 你知道make程序是如何得到这个错误码的吗?
别高兴太早了, 框架代码编译到native的时候默认链接到glibc,
我们需要把这些库函数的调用链接到我们编写的klib来进行测试.
我们可以通过在nexus-am/libs/klib/include/klib.h
中控制是否定义宏__NATIVE_USE_KLIB__, 来控制是否把库函数链接到klib.
若不定义这个宏, 库函数将会链接到glibc, 可以作为正确的参考实现来进行对比.
好了, 现在你就可以在native上测试/调试你的klib实现了,
还可以使用_putc()进行字符输出来帮助你调试, 甚至是GDB.
实现正确后, 再将程序编译到$ISA-nemu(记得移除调试时插入的_putc()), 对NEMU进行测试.
编写可移植的程序
为了不损害程序的可移植性, 你编写程序的时候不能再做一些架构相关的假设了,
比如"指针的长度是4字节"将不再成立, 因为在native上指针长度是8字节,
按照这个假设编写的程序, 在native上运行很有可能会触发段错误.
当然, 解决问题的方法还是有的, 至于要怎么做, 老规矩, STFW吧.
Differential Testing
理解指令的执行过程之后, 添加各种指令更多的是工程实现.
工程实现难免