从零开始写 OS 内核 - 加载可执行程序
Posted navi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始写 OS 内核 - 加载可执行程序相关的知识,希望对你有一定的参考价值。
系列目录
- 序篇
- 准备工作
- Bios 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- 全局描述符表 GDT
- 中断处理
- 虚拟内存完善
- 实现堆和 malloc
- 第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 进入用户态
- 进程的实现
- 系统调用
- 简单的文件系统
- 加载可执行程序
- 键盘驱动
- 运行 shell
exec 系统调用
有了前面几篇关于系统调用和文件系统的铺垫,本篇来实现 exec
系统调用,其实已经是万事俱备了。exec
的使用想必你应该熟悉,它在进程里调用后会读取给定的可执行文件,然后用这个文件里的程序覆盖当前 process 运行,这实际上就是完成了从磁盘上启动运行一个新程序的过程。
准备用户程序
首先我们需要准备几个用户程序,并将它们写入上一篇中我们定制的那个 naive_fs 磁盘镜像中去。我在项目里创建了一个 user 目录,并在里面也加了 user/src 目录存放用户程序的源文件,以及 user/prog
用以存放编译链接后的可执行二进制。例如我们可以简单地写一个用户程序:
int main(int argc, char** argv) {
while (1) {}
}
它非常简单只是一个死循环。当然你也可以写一个打印功能的程序,不过这里需要先实现打印,注意这是用户态的打印,你必须在让 kernel 提供一个打印功能的系统调用,比如叫 print
,供用户调用。我将这个功能封装在了 src/common/stdio.c
的 printf 函数里,它底层会使用 print
系统调用。这样类似于 C 标准库,它会被 link 到用户程序二进制里。
例如我写了一个用户程序 hello,里面用到了打印功能:
#include "common/common.h"
#include "common/stdio.h"
#include "syscall/syscall.h"
int main(uint32 argc, char* argv[]) {
printf("start user app: hello\\n");
printf("argc = %d\\n", argc);
for (uint32 i = 0; i < argc; i++) {
printf("argv[%d] = %s\\n", i, argv[i]);
}
return 0;
}
编译链接生成用户二进制程序后,就可以使用前一篇提到的 disk_image_writer 函数将它们写入磁盘镜像。
加载程序并 exec
接下来我们可以实现 exec
系统调用了,kernel 里的实现部分在 process_exec 函数。
首先是读取二进制文件,这里用到上一篇实现的文件系统的接口:
// Get elf binary file stat.
file_stat_t stat;
if (stat_file(path, &stat) != 0) {
monitor_printf("Command %s not found\\n", path);
return -1;
}
// Read elf binary file.
uint32 size = stat.size;
char* read_buffer = (char*)kmalloc(size);
if (read_file(path, read_buffer, 0, size) != size) {
monitor_printf("Failed to load cmd %s\\n", path);
kfree(read_buffer);
return -1;
}
然后就是解析 elf 文件,这个在加载并进入 kernel里已经实现过,这里用 C 语言重写一下,看起来会更清楚一点,代码在 src/elf/elf.c,它会将 elf 程序的各个 section 加载到内存,然后返回程序的入口地址。
接下来就是废弃原来的 process 的所有资源,因为 exec 是占据式的,原来的程序会被完全替代。这里主要是两项工作:
- 清理原来 process 的所有 threads,除了当前 thread;
- 释放原来的 user 空间的所有虚拟内存;
然后创建一个新的 thread,并使之以我们刚加载的新的 elf 二进制的入口函数为 entry 开始运行,这个新的 thread 就是新程序开始运行的主线程。
至于当前的执行线程,在 process_exec 的结尾,会直接调用 schedule_thread_exit
函数进入消亡。
总结
本篇比较简单,主要是我们各项准备工作都已经做的很充分了,exec
的实现只是一个拼接工作。不过有一些细节还是要注意的,比如说 exec 的参数需要提前 copy 到内核中,然后再释放 user 空间的虚拟内存,因为传进来的参数原本是存储在 user 空间的,一旦释放了内存,后面就无法再访问这些参数了。
以上是关于从零开始写 OS 内核 - 加载可执行程序的主要内容,如果未能解决你的问题,请参考以下文章