从零开始写 OS 内核 - 加载可执行程序

系列目录

exec 系统调用

有了前面几篇关于系统调用和文件系统的铺垫,本篇来实现 exec 系统调用,其实已经是万事俱备了。exec 的使用想必你应该熟悉,它在进程里调用后会读取给定的可执行文件,然后用这个文件里的程序覆盖当前 process 运行,这实际上就是完成了从磁盘上启动运行一个新程序的过程。

准备用户程序

首先我们需要准备几个用户程序,并将它们写入上一篇中我们定制的那个 naive_fs 磁盘镜像中去。我在项目里创建了一个 user 目录,并在里面也加了 user/src 目录存放用户程序的源文件,以及 user/prog 用以存放编译链接后的可执行二进制。例如我们可以简单地写一个用户程序:

int main(int argc, char** argv) {
    while (1) {}
}

它非常简单只是一个死循环。当然你也可以写一个打印功能的程序,不过这里需要先实现打印,注意这是用户态的打印,你必须在让 kernel 提供一个打印功能的系统调用,比如叫 print,供用户调用。我将这个功能封装在了 src/common/stdio.cprintf 函数里,它底层会使用 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 空间的,一旦释放了内存,后面就无法再访问这些参数了。

你可能感兴趣的:(操作系统cexec进程)