实战操作系统 loader 编写(下) -- 进军内核

2020-07-18 20:21:09   最后更新: 2020-07-18 20:27:29   访问数量:163




上一篇文章中,我们结合此前已经介绍过的一系列知识,成功的将内核载入内存并进入到了保护模式中

实战操作系统 loader 编写(上) -- 进入保护模式

 

但是,我们马上就遇到了一个十分重要的问题,那就是如何在内存中按照 ELF 文件所需要的方式放置我们的内核,从而让内核能够执行呢?别急,本文我们就来一探究竟

 

经过一系列的文章,我们不断的在向物理内存中存放着我们的文件,从最初的引导扇区,到 loader.bin,再到 kernel.bin,整个物理内存到底被我们变成了什么样子呢?

下图展示了物理内存的样貌:

 

 

图中按照具体的物理内存使用情况划分了格子,但格子的大小是均等的,并没有按照实际的大小比例来绘制,不过左侧标注了物理内存地址,所以格子实际在内存中的大小是可以通过左侧的数字计算得到的

回看之前的文章,你会发现上图的可用区域与通过 int 15h BIOS 中断获取到的可用信息是一样的:

实战分页机制实现 -- 通过实际内存大小动态调整页表个数

 

如果我们实现了复杂的分页算法,让从 0h 到 FFFFFFFFh 的虚拟地址全部映射到 PDE 空间后面的未分配空间,那么,从进入保护模式,初始化 PDT 与 PDE 以后,我们就再也不需要考虑物理内存哪里可用哪里不可用的问题了

但是,我们目前的分页机制启动代码是直接将物理地址与虚拟地址一对一映射实现的,因为我们的目标是尽早实现一个可用的操作系统,所以要避免过度深入某一环节

那么,既然如此,即便我们已经进入到保护模式,开始使用虚拟地址,但我们依然必须要考虑物理内存的划分问题,从而避免使用不可用的内存区域

 

此前的文章中,我们曾经介绍过 ELF 文件的分区信息:

详解 Linux 可执行文件 ELF 文件的内部结构

 

我们通过 xxd 命令查看一下我们编译好的 kernel.bin:

xxd -u -a -g 1 -c 16 kernel.bin

 

 

结合上文,图中被圈出的字段就是 e_entry,也就是 ELF 程序的入口地址,值为 804A010h,也就是内存的 128M 以上的位置,既然上图的物理地址分区图中,低地址的内存范围有很多可用区域,我们为什么不把 kernel 安排在那里呢?

答案当然是可以的,ld 命令中,通过 -Ttext 参数可以指定 elf 文件执行的起始地址:

ld -m elf_i386 -s -Ttext 0x10000 -o main asm.o main.o

 

编译后,我们再次查看 kernel.bin 的 ELF header:

 

 

可以看到,新编译后的 kernel.bin 的 ELF header 中,e_entry 的值已经变成了10000h

事实上,既然我们已经从 BIOS 加载起始扇区,到跳转进入 loader,并且不会再次回去执行 BIOS 或其实扇区的代码,从 0h 到 7FFFFh 的全部区域我们都可以覆盖使用

 

接下来,我们就要将已经在内存中的 kernel.bin 按照 ELF 文件的规则进行分块移动了

 

内存拷贝函数

首先,我们需要一个能够复用的内存拷贝函数,你一定想到了 C 语言中的 memcpy 函数,没错,我们就用汇编语言仿写一个 memcpy:

; ------------------- 内存拷贝函数 ----------------- ; void* MemCpy(void* es:pDest, void* ds:pSrc, int iSize); ; -------------------------------------------------- MemCpy: push ebp mov ebp, esp push esi push edi push ecx mov edi, [ebp + 8] ; Destination mov esi, [ebp + 12] ; Source mov ecx, [ebp + 16] ; Counter ; 参数校验 cmp ecx, 0 jz .memcpy_end .memcpy_loop: ; 逐字节移动内存 mov al, [ds:esi] inc esi mov byte [es:edi], al inc edi loop .memcpy_loop .memcpy_end: mov eax, [ebp + 8] ; 返回值 pop ecx pop edi pop esi mov esp, ebp pop ebp ret

 

 

放置 kernel

; ------------------- 放置 kernel ----------------- InitKernel: xor esi, esi mov cx, word [BaseOfKernelFilePhyAddr + 2Ch] ; cx 存储 ELF header e_phnum 字段,program header 条目数 movzx ecx, cx ; 将 cx 扩展为 ecx mov esi, [BaseOfKernelFilePhyAddr + 1Ch] ; esi 存储 ELF header e_phoff 字段,program header 偏移量 add esi, BaseOfKernelFilePhyAddr ; esi 存储 program header 物理地址 .Begin: ; program header 空条目处理 mov eax, [esi + 0] cmp eax, 0 jz .NoAction ; 拷贝 program header 描述的内存段到目标内存地址 push dword [esi + 010h] ; p_filesz 内存段大小 mov eax, [esi + 04h] ; p_offset 段在文件中的偏移 add eax, BaseOfKernelFilePhyAddr ; eax 存储内存段在 elf 文件中的起始物理地址 push eax push dword [esi + 08h] ; p_vaddr 内存段目标虚拟地址 call MemCpy add esp, 12 .NoAction: add esi, 020h ; 跳到下一条目 loop .Begin ret

 

 

既然 kernel 已经被放置在了我们想要的位置,直接跳转过去就可以了:

KernelEntryPointPhyAddr equ 010000h ; KERNEL ELF header e_entry 值,起始物理地址 jmp SelectorFlatC : KernelEntryPointPhyAddr ; 跳转进入内核

 

 

我们编写一个 kernel,简单的打印一行文字:

[section .data] randstr db "Welcome to kernel by techlog.cn", 0 [section .text] global _start _start: push dword randstr call DispStr add esp, 4 jmp $ DispStr: push ebp mov ebp, esp push ebx push esi push edi mov esi, [ebp + 8] ; pszInfo mov edi, (80 * 7) * 2 mov ah, 0Fh .1: lodsb test al, al jz .2 .3: mov [gs:edi], ax add edi, 2 jmp .1 .2: pop edi pop esi pop ebx pop ebp ret

 

 

下面,我们来运行我们的系统,可以看到:

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 

本项目已开源:https://github.com/zeyu203/techlogOS

 

实现一个操作系统






技术帖      操作系统      os      linux      elf      techlogos     


京ICP备15018585号