类似于Unix的OS开发-Shell。 结论(9)

现在该为我们的内核编写第一个单独的程序-shell。 它会存储在单独的.elf文件中,并在内核启动时由init进程启动。

这是我们操作系统开发周期中的最后一篇文章。

目录


构建系统(make,gcc,gas)。 初始引导(多次引导)。 启动(qemu)。 C库(strcpy,memcpy,strext)。

C库(sprintf,strcpy,strcmp,strtok,va_list ...)。 以内核模式和用户应用程序模式构建库。

内核系统日志。 显存 输出到终端(kprintf,kpanic,kassert)。
动态内存,堆(kmalloc,kfree)。

内存和中断处理的组织(GDT,IDT,PIC,syscall)。 例外情况
虚拟内存(页面目录和页面表)。

过程。 策划人 多任务处理。 系统调用(kill,exit,ps)。

字符设备驱动程序。 系统调用(ioctl,fopen,fread,fwrite)。 C库(fopen,fclose,fprintf,fscanf)。

内核(initrd),elf及其内部文件系统。 系统调用(执行)。 Shell作为内核的完整程序。

用户保护模式(ring3)。 任务状态段(tss)。

Shell作为完整的内核程序


在上一篇文章中,我们研究了字符设备驱动程序,并编写了一个终端驱动程序。

现在,我们拥有创建第一个控制台应用程序所需的一切。

我们将编写控制台应用程序本身,然后将其编译为单独的elf。

/* * Elf entry point */ void start() { u_int errno; stdio_init(); errno = main(); stdio_deinit(); exit(errno); } 

我们将需要初始化标准库,并将控制权转移到熟悉的main函数。

 int main() { char cmd[255]; while (1) { printf(prompt); flush(); scanf(cmd); if (!execute_command(cmd)) { break; } } return 0; } 

在循环的更深处,我们只需读取该行并执行命令即可。

如果有参数,则通过strtok_r进行Parsim命令。

 static bool execute_command(char* cmd) { if (!strcmp(cmd, cmd_ps)) { /* show tasks list */ struct clist_definition_t *task_list; task_list = ps(); printf(" -- process list\n"); clist_for_each(task_list, print_task_info); } else if (!strcmp(cmd, cmd_clear)) { /* clear screen */ clear(); flush(); } else if (!strncmp(cmd, cmd_kill, strlen(cmd_kill))) { /* kill task */ char* save_ptr = null; strtok_r(cmd, " ", &save_ptr); char* str_tid = strtok_r(null, " ", &save_ptr); u_short tid = atou(str_tid); if (!kill(tid)) { printf(" There is no process with pid %u\n", tid); }; } else if (!strncmp(cmd, cmd_exit, strlen(cmd_exit))) { /* exit */ clear(); printf(prompt); flush(); return false; } else if (!strncmp(cmd, cmd_exec, strlen(cmd_exec))) { /* exec file on intrd */ char* save_ptr = null; strtok_r(cmd, " ", &save_ptr); char* str_file = strtok_r(null, " ", &save_ptr); exec(str_file); } else if (!strncmp(cmd, cmd_dev, strlen(cmd_dev))) { /* show device list */ struct clist_definition_t *dev_list; dev_list = devs(); printf(" -- device list\n"); clist_for_each(dev_list, print_dev_info); } else { printf(" There is no such command.\n Available command list:\n"); printf(" %s %s %s <pid> %s <file.elf> %s %s\n", cmd_ps, cmd_exit, cmd_kill, cmd_exec, cmd_clear, cmd_dev); } return true; } 

实际上,我们只是在拉系统调用。
让我提醒您有关标准库的初始化。

在上一课中,我们在库中编写了以下函数:

 extern void stdio_init() { stdin = fopen(tty_dev_name, MOD_R); stdout = fopen(tty_dev_name, MOD_W); asm_syscall(SYSCALL_IOCTL, stdout, IOCTL_INIT); asm_syscall(SYSCALL_IOCTL, stdin, IOCTL_READ_MODE_LINE); asm_syscall(SYSCALL_IOCTL, stdin, IOCTL_READ_MODE_ECHO); } 

它只是打开用于读取和写入的特殊终端驱动程序文件,该文件对应于键盘输入和输出到屏幕。

在用外壳组装精灵之后,必须将其放置在原始内核文件系统(initrd)上。

初始ram磁盘由内核加载程序作为多重引导模块加载,因此我们知道initrd内存中的地址。

根据James Molloy的文章,仍然很容易为initrd组织文件系统。

因此,格式如下:

 extern struct initrd_node_t { unsigned char magic; /* magic number */ char name[8]; /* file name */ unsigned int offset; /* file base */ unsigned int length; /* file length */ }; extern struct initrd_fs_t { int count; /* files count */ struct initrd_node_t node[INITRD_MAX_FILES]; /* files headers */ }; 

接下来,请记住32位elf的格式。
 struct elf_header_t { struct elf_header_ident_t e_ident; u16 e_type; u16 e_machine; u32 e_version; u32 e_entry; /* virtual address of entry point */ u32 e_phoff; /* program headers table offset */ u32 e_shoff; /* program headers sections table offset */ u32 e_flags; u16 e_ehsize; /* file header size */ u16 e_phentsize; /* single header size */ u16 e_phnum; /* headers count */ u16 e_shentsize; /* section header size */ u16 e_shnum; /* sections headers count */ u16 e_shstrndx; }; 

在这里,我们对程序头表的入口和地址感兴趣。

代码和数据部分将是第一个标题,而堆栈部分将是第二个标题(根据通过objdump研究elf的结果)。

 struct elf_program_header_t { u32 p_type; /* segment type */ u32 p_offset; /* segment offset from file begin */ u32 p_vaddr; /* target virtual address */ u32 p_paddr; /* target physical address */ u32 p_filesz; /* segment size in file */ u32 p_memsz; /* segment size in memory */ u32 p_flags; /* permissions and etc */ u32 p_align; /* alignment */ } attribute(packed); 

该信息足以编写elf文件加载器。
我们已经知道如何为自定义流程选择页面。
因此,我们只需要为标题分配足够数量的页面并将内容复制到其中即可。
我们将编写一个函数,该函数将基于已解析的elf文件创建一个进程。
在视频教程中了解如何解析elfik。
我们只需要下载一个包含代码和数据的程序标头,因此我们不会一概而论,而只关注这种情况。

 /* * Api - execute elf as a task */ extern void elf_exec(struct elf_header_t* header) { assert(header->e_ident.ei_magic == EI_MAGIC); printf(MSG_KERNEL_ELF_LOADING, header->e_phnum); // elf_dump(header); size_t elf_base = (size_t)header; size_t entry_point = header->e_entry; struct task_mem_t task_mem; memset(&task_mem, 0, sizeof(struct task_mem_t)); // load sections in memory for (int i = 0; i < header->e_phnum; ++i) { struct elf_program_header_t* p_header = (void*)(header->e_phoff + elf_base + i * header->e_phentsize); task_mem.pages_count = (p_header->p_memsz / MM_PAGE_SIZE) + 1; if (p_header->p_memsz == 0) { continue; } // allocate pages assert(task_mem.pages_count > 0); assert(task_mem.pages == null); task_mem.pages = mm_phys_alloc_pages(task_mem.pages_count); void* section = (void*)(elf_base + p_header->p_offset); memcpy(task_mem.pages, section, p_header->p_memsz); // setup virtual memory task_mem.page_table = mmu_create_user_page_table(); task_mem.page_dir = mmu_create_user_page_directory(task_mem.page_table); for (int i = 0; i < task_mem.pages_count; ++i) { mmu_occupy_user_page(task_mem.page_table, (void*)((size_t)task_mem.pages + i * MM_PAGE_SIZE)); } } // create task u_short tid = next_tid++; assert(task_create(tid, (void*)entry_point, &task_mem)); // run task struct task_t* task; task = task_get_by_id(tid); task->status = TASK_RUNNING; strncpy(task->name, "elf", sizeof(task->name)); printf(MSG_KERNEL_ELF_LOADED); } 

这里最有趣的是创建页面目录和页面表。
请注意,首先我们选择物理页面(mm_phys_alloc_pages),然后将它们映射到逻辑页面(mmu_occupy_user_page)。
这里假设物理存储器中的页面是连续分配的。
仅此而已。 现在,您可以为内核实现自己的shell! 观看视频教程并深入研究细节。

结论


我希望您发现这一系列文章有用。
我们尚未考虑过保护环,但是由于该主题的相关性较低且评论不一,我们将继续中断。
我认为您现在已经准备好进行进一步的研究,因为您和我都检查了所有最重要的内容。

因此,束紧腰带,投入战斗! 通过编写自己的操作系统!
我花了大约一个月的时间(如果我们考虑全天工作6-8个小时)来实施您和我从头开始学到的一切。

因此,在2-3个月内,您将能够编写一个具有真实文件系统的成熟OS,而我们没有实现该文件系统。

只是知道qemu不知道如何使用任意格式的initrd并将其剪切为4kb,所以您将需要像在Linux中那样使它成为一个,或者使用borsch代替qemu。
如果您知道如何解决此问题,请写一封私人信件,我将非常感谢您。

仅此而已! 直到新的不再见面!

参考文献


观看视频教程以获取更多信息。

git存储库中的源代码(您需要lesson9分支)。

参考文献


1.詹姆斯·莫洛伊(James Molloy)。 滚动自己的玩具UNIX克隆操作系统。
2.牙齿。 DOS,Windows,Unix的汇编器
3.卡拉什尼科夫。 汇编程序很简单!
4. Tanenbaum。 操作系统。 实施与开发。
5.罗伯特·洛夫(Robert Love)。 Linux内核 开发过程的描述。

Source: https://habr.com/ru/post/zh-CN468719/


All Articles