pa3.2 用户程序和系统调用
如何加载用户程序?
操作系统中, 加载用户程序是由loader模块负责的.
加载的过程:
可执行文件中的 *代码和数据* 放置在正确的 **内存** 位置
NanOS -> AM -> NEmu
loader() -> ramdisk_write()
memcpy()
movl -> vaddr_write -> paddr_write -> pmem[addr]=val
跳转到程序入口
EIP := addr
为了实现loader()函数, 我们需要解决以下问题:
- 可执行文件在哪里? ramdisk偏移为0处
- 代码和数据在可执行文件的哪个位置?
- 代码和数据有多少?
- "正确的内存位置"在哪里? ((for x86) 0x300,0000)
操作系统的用户程序(Navy-apps) dummy.c OS._start -> dummy.main() -> OS.exit()
complie (linked to Newlib) navy-apps/tests/dummy/build/dummy-**x86**
<=> nanos-lite/build/ramdisk.img
compile (embed) into NanOS (**x86-NEmu**)
ELF中采用program header table来管理segment, program header table的一个表项描述了一个segment的所有属性, 包括类型, 虚拟地址, 标志, 对齐方式, 以及文件内偏移量和segment大小. 根据这些信息, 我们就可以知道需要加载可执行文件的哪些字节了, 同时我们也可以看到, 加载一个可执行文件并不是加载它所包含的所有内容, 只要加载那些与运行时刻相关的内容就可以了, 例如调试信息和符号表就不必加载. 我们可以通过判断segment的Type属性是否为PT_LOAD来判断一个segment是否需要加载.
你需要找出每一个需要加载的segment的
Offset, VirtAddr, FileSiz, MemSiz
这些参数. 其中相对文件偏移Offset
指出相应segment的内容从ELF文件的第Offset
字节开始, 在文件中的大小为FileSiz
, 它需要被分配到以VirtAddr
为首地址的虚拟内存位置, 在内存中它占用大小为MemSiz
. 也就是说, 这个segment使用的内存就是[VirtAddr, VirtAddr + MemSiz)
这一连续区间, 然后将segment的内容从ELF文件中读入到这一内存区间, 并将[VirtAddr + FileSiz, VirtAddr + MemSiz)
对应的物理区间清零.
ELF文件在ramdisk中, 框架代码提供了一些ramdisk相关的函数(在nanos-lite/src/ramdisk.c
中定义), 你可以使用它们来实现loader的功能
TODO: 实现loader的功能, 来把用户程序加载到正确的内存位置, 然后执行用户程序
loader()
函数在nanos-lite/src/loader.c
中定义, 其中的pcb
参数目前暂不使用, 可以忽略, 而因为ramdisk中目前只有一个文件,filename
参数也可以忽略. 在下一个阶段实现文件系统之后,filename
就派上用场了.
实现后, 在init_proc()
中调用naive_uload(NULL, NULL)
, 它会调用你实现的loader来加载第一个用户程序, 然后跳转到用户程序中执行. 如果你的实现正确, 你会看到执行dummy程序时在Nanos-lite中触发了一个未处理的4号事件. 这说明loader已经成功加载dummy, 并且成功地跳转到dummy中执行了.