PA 2/3 实验报告

常子豪 changzihao17s@ict.ac.cn
2017/11/03

必答题:

  • 编译与链接nemu/include/cpu/rtl.h中, 你会看到由static inline开头定义的各种RTL指令函数. 选择其中一个函数, 分别尝试去掉static, 去掉inline或去掉两者, 然后重新进行编译, 你会看到发生错误. 请分别解释为什么会发生这些错误? 你有办法证明你的想法吗?

    答:
    1.去掉inline会出现如下错误

    2、去掉static不会出现报错
    3、去掉static和inline会出现如下错误

    首先为了提高代码的效率,对于经常使用的元操作可以通过inline定义的方式避免用函数调用从而提高效率,但是为了其能够复用,我们必须将其定义在头文件中,由于有多个文件包含了rtl.h这个头文件,这边便会导致函数多次定义的问题,同时在所有调用都可以展开的情况下会把函数原型删除。
    对于上述三种情况,第一种情况是由于prefic.c文件包含了该函数却没有使用而报错,第二种情况不会报错,第三种情况是由于去掉static和inline之后不同的.c文件include rtl.h导致出现了函数重定义。                    

  • 编译与链接
  1. nemu/include/common.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问重新编译后的NEMU含有多少个dummy变量的实体? 你是如何得到这个结果的?、
    答:有78个dummy变量实体,通过grep指令可以计算common.h在nemu/build/obj路径下一共出现77次加上在common.h的1次,一共78次。

  2. 添加上题中的代码后, 再在nemu/include/debug.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问此时的NEMU含有多少个dummy变量的实体? 与上题中dummy变量实体数目进行比较, 并解释本题的结果.
    答:有30个dummy变量实体,通过grep指令可以计算 debug.h在nemu/build/obj路径下一共出现29次加上在debug.h的1次,一共30次。

  3. 修改添加的代码, 为两处dummy变量进行初始化:volatile static int dummy = 0; 然后重新编译NEMU. 你发现了什么问题? 为什么之前没有出现这样的问题? (回答完本题后可以删除添加的代码.)
    屏幕快照 2017-11-04 14.53.49
    屏幕快照 2017-11-04 14.53.49
    在C语言中只声明不初始化是一种弱定义,当声明多个同名同类型的变量时,编译不会报错,但是到了链接阶段,由于全是弱符号,链接器会随便选择一个。但是,有了声明之后就不一样了,这变成了强定义,编译器无法忽略。
  • 了解Makefile 请描述你在nemu目录下敲入make 后, make程序如何组织.c和.h文件, 最终生成可执行文件nemu/build/nemu. (这个问题包括两个方面:Makefile的工作方式和编译链接的过程.) 关于Makefile工作方式的提示:

    • Makefile中使用了变量, 包含文件等特性
    • Makefile运用并重写了一些implicit rules
    • 在man make中搜索-n选项, 也许会对你有帮助
    • RTFM

    答:首先我们需要在nemu目录中编写一个Makefile文件,当我们执行make指令时,会首先找到名称满足要求的Makefile文件。随后才是执行Makefile文件,分为Makefile工作部分和编译和链接的本分。
    在Makefile工作的部分,Makefile首先会根据“include”指令把读取工作目录中的文件,处理内建的变量、明确规则和隐含规则,并建立所有目标和依赖之间的依赖关系结构链表。随后make会执行Makefile中的终极目标,根据之前的依赖关系进行一系列的编译工作。
    在编译和链接过程中,主要目标是围绕最终文件展开,从最终文件开始,make会一次递归的取寻找每一个依赖文件,是否有、是都需要更新,如果没有或者需要更新,make会按照指定的遍历指令和参数对目标文件进行编译生产依赖文件(例如.o),最终有了所有的依赖文件之后再根据定义好的链接规则将所有文件链接在一起最终生成我们的目标文件。

  • 文件读写的具体过程 仙剑奇侠传中有以下行为:

    • 在navy-apps/apps/pal/src/global/global.c的PAL_LoadGame()中通过fread()读取游戏存档
    • 在navy-apps/apps/pal/src/hal/hal.c的redraw()中通过NDL_DrawRect()更新屏幕

    请结合代码解释仙剑奇侠传, 库函数, libos, Nanos-lite, AM, NEMU是如何相互协助, 来分别完成游戏存档的读取和屏幕的更新.
    navy-apps/apps/pal/src/global/global.c中函数PAL_LoadGame()有一系列加载数据的函数,但是这些加载各类数据的函数最终都会通过调用fread()来实现加载数据。而fread()是在navy-apps/libs/libc/src/stdio/fread.c中实现的c语言库函数,在fread()中会调用定义在navy-apps/libs/libc/src/stdio/fread.c中实现,而fread()通过调用memcpy()读取文件内容(其中文件内容是在fopen()中处理)。而fopen()的实现与下面要介绍的fwrite()十分类似,这里不做详细介绍。
      
    答:在navy-apps/apps/pal/src/hal/hal.credraw()中通过NDL_DrawRect()更新屏幕。NDL_DrawRect()navy-apps/libs/libndl/src/ndl.c中定义,它会调用fwrite()c语言库函数,在navy-apps/libs/libc/src/stdio/fwrite.c实现,通过fwrite() -> __sfvwrite -> _write(),通过这样的路径,调用关系转接到了navy-apps/libs/libos/src/nanos.c系统调用接口,在nanos.c_write()再次通过宏函数_syscall_(SYS_write, fd, buf,count),这变成功转换到我们实现的中断处理部分,此时_syscall()执行int 0x80参数中断,并设定好相应参数,随后操作系统通过nanos-lite/src/ syscall.c中定义的_RegSet* do_syscall(_RegSet *r)捕获中断,判断出其为SYS_write,然后调用fs_write(),而我们在nanos-lite/src/fs.c中实现读取文件,通过参数fd判断出文件类型为FD_fb继而通过fb_write()更新屏幕。而fb_write()则是调用了之前在nexus-am/am/arch/x86-nemu/src/ioe.c中实现_draw_rect()来在更新显示内容所对用的内存。这便是其完成的技术路径。
     
    总的来看,应用程序的整体流程大致如下:在nanos-lite执行make run,首先程序编译navy-apps路径下我们指定的应用程序,而在编译应用程序时使用的系统调用有关的实现均在navy-apps/libs/下代码中实现,而这边本分代码会通过在nexus-am/am/arch/x86-nemu/src/中定义的与硬件十分相关的代码来实现对硬件的具体读写等操作,最终在应用程序、操作系统、lib、am等完成编译后生成镜像,在交给nemu执行。

感悟:

本次的报告涵盖了pa2、pa3两部分的内容,这两部分包含通过int 0x80中断机制将nemu的实现,am实现,操作系统的实现串联起来,环环相扣。在实现中遇到了很多难以找寻的bug,但是这大多与自己思考不够细致有关,而且较多集中在和操作数长度、符号拓展有关,这个两个方面可以说是bug的重灾区,比如push指令的符号拓展,判定SF位时要考虑操作数长度。这里着重要提一下SF设置的问题,这个bug是在成功运行仙剑之后才出现,当时的代码遍及nemu、am、nanos三个部分,寻找错误不易,而且之前在diff_test也没有暴露出来,最后无奈找来正确代码一步一步替换才定位到时rtl_update_SF出现错误,这也算是自己给自己挖下的一个大坑吧。
目前关于身边很多同学都经常在之前提到的操作数长度、符号拓展两块出现出错,而且是经常大家遇到同样的错误提示,同样的原因,个人觉得可以在pa的实验指导书适当提醒同学们,让同学少给自己挖坑。