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

2022-06-27 14:54:21 浏览数 (1)

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。

0 人点赞