NEMU PA 2-2 实验报告

课程地址:https://www.bilibili.com/video/BV1f7411D7P6

一、实验目的

在PA2-1中,我们实现了了解了程序的装载和对指令的解码和执行,在这一章节我们将继续深入了解程序的装载,以不同于原本直接copy的方式实现对程序的装载。

二、实验步骤

首先我们熟悉ELF可执行目标文件的结构,即认识ELF文件和ELF Header:

其中比较重要的有:

  • 节头表,这个表记录了文件分为多少个节,每个节(如.symtab节)在文件中的什么地方;
  • 程序头表,这个表记录了文件的哪一部分应该被搬到内存的哪个位置,我们在后面装载的实现中需要用到程序头表给出的信息。
  • 符号表和字符串表,PA2-3符号解析中需要用到,它们记录了某全局变量或函数对应内存的哪个地址。

我们在这一章需要做的,就是改变原本程序的装载方式,通过对ELF文件中程序头表的解析来装载程序。

  • 原本的装载方式:把ELF文件和镜像文件分别copy到DAMdisk区和物理内存区;

  • 新的装载方式:通过NEMU的操作系统,来将(已经copy到)DAMdisk区的ELF文件装载到物理内存区。这意味着我们不仅要装载样例文件,还要先把操作系统(kernel文件)先装载到内存中。(kernel的装载就直接copy,同原本的样例镜像文件的装载方式)

装载的步骤为:

  1. 由kernel解析测试用例的ELF文件并装载,装载起始位置为0X100000;
  2. 由kernel调整CPU的EIP到装载的起始位置0X100000;

现在,我们可以进行一下要点的梳理

  • 现阶段哪些文件是待装载的ELF文件?样例ELF文件。
  • ELF文件是由谁装载的?Kernel负责。
  • 现阶段在NEMU启动后待装载的ELF文件位于何处?Kernel的后面,0X100000处。
  • NEMU开机后执行的第一条指令是哪个地址?是哪个软件的指令?Kernel的起始地址,0X30000。
  • ELF文件被装载到哪个地址?0X100000。
  • 为了保证上述装载和执行地址的正确,需要修改Makefile。

接下来是重头戏,我们要实现对程序头表的解析:

  • 首先,我们可以通过readelf命令来观察需要装载的ELF文件的程序头表;

  • 上文我们提到,程序头表记录了文件的哪一部分应该被搬到内存的哪个位置,我们观察readelf给出的信息:

    可以发现Type为LOAD的节是需要装载的——ELF文件中从文件Offset开始位置,连续FileSiz个字节的内容需要被装载,装载到内存VirtAddr开始,连续MemSiz个字节的区域中,其中MemSiz可能大于FileSiz(.bss节的原因,这时候需要把内存中 VirtAddr + [FileSiz, MemSiz] 的区域清零)。

  • 好,我们现在进行装载。装载由kernel的loader程序来完成。其装载逻辑如下:

  • 通过阅读kernel/src/elf/elf.c处的代码(主要是loader()函数),我们可以再进行一次要点的梳理

    • Kernel中访问变量语句编译后对应什么指令?mov指令或者lea指令。

    • 这条指令由谁执行?访问的地址是多少?由cpu执行,访问地址需要看变量类型,但都在hw_mem[]数组地址的范围内。

    • 指令中的地址如何转换成对hw_mem[]的访问的?

      首先kernel里面的代码会编译成一条条指令(如memcpy会被编译成一条条mov指令),而这些指令操作的执行不是通过现实的机器来执行,而是像PA2-1一样,是依靠你自己实现的虚拟CPU来执行的,由你手搓的CPU进行指令的解码和执行。然后解码执行过程中,对地址的操作是由自己实现的operand_read/write来读/写的,operand_read/write调用了memory.c中封装好的读写函数进行转换(看最底层的hw_mem_read/write,这个时候的读写操作的解码执行才是现实CPU做的,要分清实机和虚拟机),避免了直接访问当前地址而造成的访问到了实际机器上的内存的问题。

      在这里我还要提醒自己:ELF文件放在实际机器可以靠实际机器的CPU运行,放在手搓虚拟机上就需要靠手搓的CPU运行。kernel也一样,这里的kernel也是一个img文件,它的执行环境决定了它需要靠你自己手搓的对ISA的实现上运行。一定要注意运行环境的不同!不然容易混糊,就像这一问。

  • 我们需要实现的代码部分位于该loader()函数中:

都到这一步了,我们来看看Kernel是如何执行loader()的:

kernel/start/start.S开始 --> 跳转到kernel/src/main.c中的init() --> 跳转到kernel/src/main.c中的init_cond()

--> 执行loader()函数,并由其返回可执行文件的EIP(通过ELF头得到) --> 得到EIP后我们就可以直接将它包装成函数指针,去跑装载好的测试用例。

整体的PA2-2代码要求如下:

  1. 修改testcase/MakefileLDFLAGS并make clean

    #LDFLAGS := -m elf_i386 -e start -Ttext=0x30000

    LDFLAGS := -m elf_i386 -e start -Ttext=0x100000

    -Ttext的功能就是指定程度段的起始地址。程序链接后的地址空间要与其运行环境的地址空间相匹配,现在将样例文件的起始地址指定为0x100000,那么在kernel根据ELF程序头表来装载文件的时候,这个样例文件程序头表项就会显示应该从0x100000处开始装载。

    可以看一下博文:https://blog.csdn.net/chuanzhilong/article/details/53020351

  2. 实现Kernel中的loader();该代码位于kernel/src/elf/elf.c

  3. 使用make test_pa-2-2执行测试用例并通过。

三、思考题

这章节没有明确需要回答的思考题,以下是理解PA2-2过程中一些给出的要点的解答:

  • 现阶段哪些文件是待装载的ELF文件?样例ELF文件。

  • ELF文件是由谁装载的?Kernel负责。

  • 现阶段在NEMU启动后待装载的ELF文件位于何处?Kernel的后面,0X100000处。

  • NEMU开机后执行的第一条指令是哪个地址?是哪个软件的指令?Kernel的起始地址,0X30000。

  • ELF文件被装载到哪个地址?0X100000。

  • Kernel中访问变量语句编译后对应什么指令?mov指令或者lea指令。

  • 这条指令由谁执行?访问的地址是多少?由cpu执行,访问地址需要看变量类型,但都在hw_mem[]数组地址的范围内。

  • 指令中的地址如何转换成对hw_mem[]的访问的?

    首先kernel里面的代码会编译成一条条指令(如memcpy会被编译成一条条mov指令),而这些指令操作的执行不是通过现实的机器来执行,而是像PA2-1一样,是依靠你自己实现的虚拟CPU来执行的,由你手搓的CPU进行指令的解码和执行。然后解码执行过程中,对地址的操作是由自己实现的operand_read/write来读/写的,operand_read/write调用了memory.c中封装好的读写函数进行转换(看最底层的hw_mem_read/write,这个时候的读写操作的解码执行才是现实CPU做的,要分清实机和虚拟机),避免了直接访问当前地址而造成的访问到了实际机器上的内存的问题。

posted @ 2023-02-24 22:29  GrapefruitCat  阅读(340)  评论(0编辑  收藏  举报