1. 引言
上一篇文章中,我们结合此前已经介绍过的一系列知识,成功的将内核载入内存并进入到了保护模式中。 实战操作系统 loader 编写(上) — 进入保护模式
但是,我们马上就遇到了一个十分重要的问题,那就是如何在内存中按照 ELF 文件所需要的方式放置我们的内核,从而让内核能够执行呢?别急,本文我们就来一探究竟。
2. 内存区域划分
经过一系列的文章,我们不断的在向物理内存中存放着我们的文件,从最初的引导扇区,到 loader.bin,再到 kernel.bin,整个物理内存到底被我们变成了什么样子呢? 下图展示了物理内存的样貌:
图中按照具体的物理内存使用情况划分了格子,但格子的大小是均等的,并没有按照实际的大小比例来绘制,不过左侧标注了物理内存地址,所以格子实际在内存中的大小是可以通过左侧的数字计算得到的。 回看之前的文章,你会发现上图的可用区域与通过 int 15h BIOS 中断获取到的可用信息是一样的: 实战分页机制实现 — 通过实际内存大小动态调整页表个数
如果我们实现了复杂的分页算法,让从 0h 到 FFFFFFFFh 的虚拟地址全部映射到 PDE 空间后面的未分配空间,那么,从进入保护模式,初始化 PDT 与 PDE 以后,我们就再也不需要考虑物理内存哪里可用哪里不可用的问题了。 但是,我们目前的分页机制启动代码是直接将物理地址与虚拟地址一对一映射实现的,因为我们的目标是尽早实现一个可用的操作系统,所以要避免过度深入某一环节。 那么,既然如此,即便我们已经进入到保护模式,开始使用虚拟地址,但我们依然必须要考虑物理内存的划分问题,从而避免使用不可用的内存区域。
3. ELF 分区信息
此前的文章中,我们曾经介绍过 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 的全部区域我们都可以覆盖使用。
4. 按照 ELF 载入内存规则移动 kernel.bin
接下来,我们就要将已经在内存中的 kernel.bin 按照 ELF 文件的规则进行分块移动了。
4.1. 内存拷贝函数
首先,我们需要一个能够复用的内存拷贝函数,你一定想到了 C 语言中的 memcpy 函数,没错,我们就用汇编语言仿写一个 memcpy:
代码语言:javascript复制; ------------------- 内存拷贝函数 -----------------
; 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
4.2. 放置 kernel
代码语言:javascript复制; ------------------- 放置 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
5. 从 loader 跳转到 kernel
既然 kernel 已经被放置在了我们想要的位置,直接跳转过去就可以了:
代码语言:javascript复制KernelEntryPointPhyAddr equ 010000h ; KERNEL ELF header e_entry 值,起始物理地址
jmp SelectorFlatC : KernelEntryPointPhyAddr ; 跳转进入内核
6. 编写 kernel demo
我们编写一个 kernel,简单的打印一行文字:
代码语言:javascript复制[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
7. 运行系统
下面,我们来运行我们的系统,可以看到:
8. 完整代码
本项目已开源:https://github.com/zeyu203/techlogOS。