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

2020-07-14 10:17:21   最后更新: 2020-07-14 10:17:21   访问数量:73




此前的文章中,我们详细介绍了从引导扇区跳转到 loader 的工作:

从启动扇区跳转到 loader

 

引导扇区的工作已经告一段落,接下来我们的工作就是编写我们的 loader 了

loader 的工作只有两个:

  1. 将内核载入内存
  2. 跳入保护模式

 

本文我们就来详细介绍一下

 

有了通过引导扇区加载 loader 的经验,让 loader 加载内核就简单的多了

从原理上来说,loader 加载内核也同样是从 FAT12 的软盘文件系统上找到内核入口文件,这与引导扇区做的事情并没有很大的区别,这里也不进行详细的介绍,只是分块大致讲解一下

但是,我们的内核将编译成 ELF 文件,因为只有这样,我们才能够接下来实现用 C 语言编写内核的目的,那么,如何让 loader 将内核 ELF 文件载入内存呢?其原理上一篇文章已经介绍过:

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

 

ELF 文件是在 unix 环境上编译生成的可执行可连接文件,他通过多个 section 来组织编译后的可执行代码,若干个 section 构成一个段,由 program header table 描述如何载入内存

因此,通过 elf header 与 program header table 中每一个条目的指引,我们就能够将 ELF 文件载入内存了

但是别急,本文我们先不急于去把 ELF 放在他应该在的内存位置上,因为 ELF 文件必须在保护模式下执行,所以我们先把内核放到一整块内存中,然后进入保护模式,再在保护模式中对他进行调整,根据 ELF 内部的一系列信息将他放到他应该位于的虚拟地址上,然后才能通过跳转指令,将控制权从 loader 再交给 kernel

本文,我们就来实现将内核载入内存并启动保护模式,也许你会有些失望,本文描述的内容都是此前文章已经介绍过的,不存在新的知识点,但不是有句话说“温故而知新”嘛

 

我们首先来看看如何让 loader 能够在软盘上找到 kernel,这里的 kernel,我们暂且先使用之前我们写好的快速排序的程序:

如何实现汇编语言与 C 语言之间的相互调用

 

定义 FAT12 磁盘头及相关信息

因为 boot 扇区需要读取 FAT12 磁盘头及相关信息,而 loader 也同样需要,所以我们需要定义一个公共依赖:

; FAT12 磁盘头 BS_OEMName DB 'ForrestY' ; OEM String, 必须 8 个字节 BPB_BytsPerSec DW 512 ; 每扇区字节数 BPB_SecPerClus DB 1 ; 每簇多少扇区 BPB_RsvdSecCnt DW 1 ; Boot 记录占用多少扇区 BPB_NumFATs DB 2 ; 共有多少 FAT 表 BPB_RootEntCnt DW 224 ; 根目录文件数最大值 BPB_TotSec16 DW 2880 ; 逻辑扇区总数 BPB_Media DB 0xF0 ; 媒体描述符 BPB_FATSz16 DW 9 ; 每FAT扇区数 BPB_SecPerTrk DW 18 ; 每磁道扇区数 BPB_NumHeads DW 2 ; 磁头数(面数) BPB_HiddSec DD 0 ; 隐藏扇区数 BPB_TotSec32 DD 0 ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数 BS_DrvNum DB 0 ; 中断 13 的驱动器号 BS_Reserved1 DB 0 ; 未使用 BS_BootSig DB 29h ; 扩展引导标记 (29h) BS_VolID DD 0 ; 卷序列号 BS_VolLab DB 'OrangeS0.02'; 卷标, 必须 11 个字节 BS_FileSysType DB 'FAT12 ' ; 文件系统类型, 必须 8个字节 ; 根目录占用空间 RootDirSectors = ((BPB_RootEntCnt*32)+(BPB_BytsPerSec–1))/BPB_BytsPerSec RootDirSectors equ 14 ; Root Directory 的第一个扇区号 = BPB_RsvdSecCnt + (BPB_NumFATs * BPB_FATSz16) SectorNoOfRootDirectory equ 19 ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt SectorNoOfFAT1 equ 1 ; DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * BPB_FATSz16) - 2 ; 文件的开始Sector号 = DirEntry中的开始Sector号 + 根目录占用Sector数目 + DeltaSectorNo DeltaSectorNo equ 17

 

 

在软盘中寻找 kernel.bin

想了解更加详细的内容,参考此前引导扇区加载 loader 的代码:

从启动扇区跳转到 loader

 

主要步骤仍然是:

  1. 循环读取根目录区的一个扇区
  2. 循环读取当前扇区内的一个条目
  3. 比较文件名是否为 KERNEL.BIN,相同则表示已找到

 

详细代码见附录

 

将 kernel.bin 读取到内存

如果上一步骤中找到了 kernel.bin 则读取文件内容载入到内存:

  1. 根据 FAT12 头信息,计算出数据区起始扇区
  2. 根据根目录条目中的文件起始簇号,读取文件首个簇
  3. 根据 FAT 项信息定位到文件下一个簇号
  4. 循环读取直到完成整个文件的读取

 

同样,代码放在附录

 

运行程序

下面我们编译上一篇文章中的快速排序代码,并把结果命名为 kernel.bin 然后放在 boot.img 的根目录下

运行我们的系统,就可以看到下图,表示 kernel.bin 已经成功被载入到内存中了:

 

 

如上文所说,loader 的另一个极为重要的工作就是跳转进入保护模式中

此前我们对保护模式的工作原理、执行方式及相关代码已经有了非常详尽的介绍,我们可以直接复用那些已经写好的代码

回忆一下,从实地址模式跳转到保护模式需要做哪些事呢?

  1. 创建 GDT 及对应的段选择子
  2. 在段内编写保护模式代码
  3. 将 GDT 首地址通过 lgdt 指令载入 gdtr
  4. 关闭硬件中断
  5. 打开 A20 地址总线
  6. 置位 cr0 寄存器的保护模式标志位
  7. 长跳转进入保护模式

 

这里需要说明的是,由于此前我们没有编写自己的 booter,而是使用 freedos 系统作为启动扇区拉起我们的系统,所以我们无法预期 freedos 会把我们的代码放在物理内存的哪个位置上,所以我们需要在跳转前动态计算保护模式代码所在的起始位置,然后去覆盖上述最后一步的长跳转指令操作数,这看起来是如此 treak

如今,我们自己编写的 boot 可以直接指定 loader 被载入内存的起始物理地址,这样,我们在代码编写时就可以计算出进入保护模式的起始位置,因此,再也不需要之前那种 treak 的方法,直接可以在代码中编写操作数实现上述操作了

 

执行我们的系统,可以看到:

 

 

本文详细介绍了 loader 中关键性的两个步骤:

  1. 将内核载入内存
  2. 进入保护模式

 

正所谓“厚积薄发”,此前我们关于保护模式原理的一系列介绍和总结所积累的大量代码终于派上用场,本文的代码也就显得非常简单易懂了

然而,事实上,第一步中,我们只是开辟了一块连续的空间来存储“内核”,实际上并没有对 ELF 文件进行处理,所以 ELF 并没有达到可执行的状态,我们也就更没有实现内核的执行了

敬请期待下一篇文章,让我们在保护模式下,重新放置我们已经载入到内存的内核 ELF 文件,实现通往内核的最后一跳

 

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

 

 

由于本文对 boot 代码、kernel 代码均没有任何修改,我们只是使用了此前已经编写、编译好的代码,所以在这部分不再贴出相应代码,Makefile 中也不再包含他们的编译指令

 

Makefile

LDR:=loader.asm KERNEL_BIN:=kernel.bin LDR_BIN:=$(subst .asm,.bin,$(LDR)) IMG:=boot.img FLOPPY:=/mnt/floppy/ .PHONY : everything everything : $(LDR_BIN) sudo mount -o loop $(IMG) $(FLOPPY) sudo cp $(LDR_BIN) $(FLOPPY) -v sudo cp $(KERNEL_BIN) $(FLOPPY) -v sudo umount $(FLOPPY) clean : rm -f $(LDR_BIN) *.o $(LDR_BIN) : $(LDR) nasm $< -o $@

 

 

loader.asm

org 0100h jmp LABEL_START ; ---------------- 内存段描述符宏 ------------- ; usage: Descriptor Base, Limit, Attr ; Base: dd ; Limit: dd (low 20 bits available) ; Attr: dw (lower 4 bits of higher byte are always 0) %macro Descriptor 3 dw %2 & 0FFFFh ; 段界限1 dw %1 & 0FFFFh ; 段基址1 db (%1 >> 16) & 0FFh ; 段基址2 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2 db (%1 >> 24) & 0FFh ; 段基址3 %endmacro BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的段地址 OffsetOfLoader equ 0100h ; LOADER.BIN 被加载到的偏移地址 BaseOfLoaderPhyAddr equ BaseOfLoader*10h ; LOADER.BIN 被加载到的物理地址 BaseOfKernelFile equ 08000h ; KERNEL.BIN 被加载到的位置段地址 OffsetOfKernelFile equ 0h ; KERNEL.BIN 被加载到的位置偏移地址 BaseOfStack equ 0100h PageDirBase equ 100000h ; 页目录开始地址: 1M PageTblBase equ 101000h ; 页表开始地址: 1M + 4K ; -------------- GDT ------------- ; 段基址 段界限, 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, 0c09Ah ; 4GB 可执行代码段 LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, 0c092h ; 4GB 可读写数据段 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, 0f2h ; 显存段 GdtLen equ $ - LABEL_GDT GdtPtr dw GdtLen - 1 ; 段界限 dd BaseOfLoaderPhyAddr + LABEL_GDT ; 基地址 ; ----------- GDT 选择子 ---------- SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT + 3 ; 下面是 FAT12 磁盘的头, 之所以包含它是因为下面用到了磁盘的一些信息 %include "fat12hdr.asm" LABEL_START: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, BaseOfStack mov dh, 0 ; "Loading " call DispStrRealMode ; 显示字符串 ; ----------- 获取内存信息 ------------- mov ebx, 0 mov di, _MemChkBuf ; es:di 存储地址范围描述符结构 ARDS .MemChkLoop: mov eax, 0E820h ; eax = 0000E820h mov ecx, 20 ; ecx = 地址范围描述符结构大小 mov edx, 0534D4150h ; edx = 'SMAP' int 15h jc .MemChkFail add di, 20 inc dword [_dwMCRNumber] ; dwMCRNumber = ARDS 的个数 cmp ebx, 0 jne .MemChkLoop jmp .MemChkOK .MemChkFail: mov dword [_dwMCRNumber], 0 .MemChkOK: ; ----- 在 A 盘根目录寻找 KERNEL.BIN ----- mov word [wSectorNo], SectorNoOfRootDirectory ; 软盘复位 xor ah, ah xor dl, dl int 13h LABEL_SEARCH_IN_ROOT_DIR_BEGIN: ; 根目录已读取完成,未找到 kernel.bin cmp word [wRootDirSizeForLoop], 0 jz LABEL_NO_KERNELBIN ; 读取根目录区一个扇区 dec word [wRootDirSizeForLoop] mov ax, BaseOfKernelFile mov es, ax ; es <- BaseOfKernelFile mov bx, OffsetOfKernelFile ; bx <- OffsetOfKernelFile mov ax, [wSectorNo] ; ax <- Root Directory 中的某 Sector 号 mov cl, 1 call ReadSector mov si, KernelFileName ; ds:si = "KERNEL BIN" mov di, OffsetOfKernelFile ; es:di = BaseOfKernelFile:OffsetOfKernelFile cld ; df = 0 ; 循环读取目录条目 mov dx, 10h ; 当前扇区所有目录条目循环次数 LABEL_SEARCH_FOR_KERNELBIN: ; 已读取完该扇区 cmp dx, 0 ; `. jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR dec dx ; 比较文件名 mov cx, 11 LABEL_CMP_FILENAME: cmp cx, 0 jz LABEL_FILENAME_FOUND ; 已找到 kernel dec cx lodsb cmp al, byte [es:di] jz LABEL_GO_ON jmp LABEL_DIFFERENT LABEL_GO_ON: inc di jmp LABEL_CMP_FILENAME ; 跳转到下一条目 LABEL_DIFFERENT: and di, 0FFE0h ; 让 es:di 指向当前条目起始位置 add di, 20h ; 跳至下一条目 mov si, KernelFileName jmp LABEL_SEARCH_FOR_KERNELBIN ; 跳转到下一扇区 LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR: add word [wSectorNo], 1 jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN ; 未找到,显示字符串,终止流程 LABEL_NO_KERNELBIN: mov dh, 2 ; "No KERNEL." call DispStrRealMode ; 显示字符串 jmp $ ; 找到 kernel,加载 LABEL_FILENAME_FOUND: ; 保存 kernel.bin 的文件大小 mov eax, [es : di + 01Ch] mov dword [dwKernelSize], eax ; 获取 loader.bin 对应的数据区簇号,保存在栈中 and di, 0FFF0h add di, 01Ah mov cx, word [es:di] push cx ; 获取文件所在扇区号,保存在 cx 中 mov ax, RootDirSectors add cx, ax add cx, DeltaSectorNo ; es:bx = kernel.bin 将要被加载到的内存物理地址 mov ax, BaseOfKernelFile mov es, ax mov bx, OffsetOfKernelFile ; 循环读取 kernel.bin mov ax, cx LABEL_GOON_LOADING_FILE: ; 打点,表示准备读取一个扇区,展示 Booting.... push ax push bx mov ah, 0Eh mov al, '.' mov bl, 0Fh int 10h pop bx pop ax ; 根据 FAT 项值循环读取簇 mov cl, 1 call ReadSector pop ax call GetFATEntry cmp ax, 0FFFh jz LABEL_FILE_LOADED push ax mov dx, RootDirSectors add ax, dx add ax, DeltaSectorNo add bx, [BPB_BytsPerSec] jmp LABEL_GOON_LOADING_FILE ; 加载完成 LABEL_FILE_LOADED: ; 关闭软驱 call KillMotor ; 显示字符串 mov dh, 1 call DispStrRealMode ; ------------ 跳转进入保护模式 ------------- ; 加载 GDTR lgdt [GdtPtr] ; 关中断 cli ; 打开地址线A20 in al, 92h or al, 00000010b out 92h, al ; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式 jmp dword SelectorFlatC:(BaseOfLoaderPhyAddr + LABEL_PM_START) jmp $ ; ---- 显示一个字符串, 函数开始时 dh 中存储字符串序号(0-based) ---- DispStrRealMode: mov ax, MessageLength mul dh add ax, LoadMessage mov bp, ax ; ┓ mov ax, ds ; ┣ ES:BP = 串地址 mov es, ax ; ┛ mov cx, MessageLength ; CX = 串长度 mov ax, 01301h ; AH = 13, AL = 01h mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h) mov dl, 0 add dh, 3 ; 从第 3 行往下显示 int 10h ret ; ------------- 关闭软驱 ----------- KillMotor: push dx mov dx, 03F2h mov al, 0 out dx, al pop dx ret ; ----- 从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中 ----- ReadSector: push bp mov bp, sp sub esp, 2 ; 开辟两个字节的堆栈区域存储扇区数 mov byte [bp-2], cl push bx mov bl, [BPB_SecPerTrk] ; bl: 每磁道扇区数 div bl ; 商保存在 al 中,余数保存在 ah 中 inc ah ; 获取其实扇区号 mov cl, ah ; cl <- 起始扇区号 mov dh, al shr al, 1 ; 获取柱面号 mov ch, al ; ch <- 柱面号 and dh, 1 ; 获取磁头号 pop bx mov dl, [BS_DrvNum] ; 驱动器号 (0 表示 A 盘) .GoOnReading: mov ah, 2 ; 读 mov al, byte [bp-2] ; 读 al 个扇区 int 13h jc .GoOnReading ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止 add esp, 2 pop bp ret ; ---- 读取序号为 ax 的 Sector 在 FAT 中的条目, 放在 ax 中 ---- GetFATEntry: push es push bx push ax ; 在 BaseOfKernelFile 后面留出 4K 空间用于存放 FAT mov ax, BaseOfKernelFile sub ax, 0100h mov es, ax ; 判断 ax 奇偶性,赋值 bOdd 变量 pop ax mov byte [bOdd], 0 ; bOdd 变量用于存放当前是奇数次读取还是偶数次读取 mov bx, 3 mul bx ; dx:ax = ax * 3 mov bx, 2 div bx ; dx:ax / 2 ==> ax <- 商, dx <- 余数 cmp dx, 0 jz LABEL_EVEN mov byte [bOdd], 1 ; 奇数 LABEL_EVEN: ; 计算 FAT 项所在扇区号 xor dx, dx mov bx, [BPB_BytsPerSec] div bx ; dx:ax / BPB_BytsPerSec ; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号) ; dx <- 余数 (FATEntry 在扇区内的偏移) push dx mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfKernelFile - 100):00 add ax, SectorNoOfFAT1 ; ax = FAT1 起始扇区号 + 指定读取扇区号 = FATEntry 所在的扇区号 mov cl, 2 call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个 ; 赋值结果给 ax 并矫正结果 pop dx add bx, dx mov ax, [es:bx] cmp byte [bOdd], 1 jnz LABEL_EVEN_2 shr ax, 4 LABEL_EVEN_2: and ax, 0FFFh LABEL_GET_FAT_ENRY_OK: pop bx pop es ret ; --------------- 变量 ---------------- wRootDirSizeForLoop dw RootDirSectors ; Root Directory 占用的扇区数 wSectorNo dw 0 ; 要读取的扇区号 bOdd db 0 ; 奇数还是偶数 dwKernelSize dd 0 ; KERNEL.BIN 文件大小 ; -------------- 字符串 ---------------- KernelFileName db "KERNEL BIN", 0 ; KERNEL.BIN 文件名 MessageLength equ 9 LoadMessage: db "Loading " Message1 db "Ready. " Message2 db "No KERNEL" ; ------------ 32 位代码段 ------------- [SECTION .s32] ALIGN 32 [BITS 32] LABEL_PM_START: mov ax, SelectorVideo mov gs, ax mov ax, SelectorFlatRW mov ds, ax mov es, ax mov fs, ax mov ss, ax mov esp, TopOfStack add esp, 4 call DispMemInfo call SetupPaging push szMemChkTitle call DispStr jmp $ %include "lib.asm" ; ----------------- 获取内存信息 ------------------ DispMemInfo: push esi push edi push ecx ; 循环获取 ARDS 4 个成员 mov esi, MemChkBuf ; 寻址缓存区 mov ecx, [dwMCRNumber] ; 获取循环次数 ARDS 个数 .loop: mov edx, 5 ; 循环遍历 ARDS 的 4 个成员 mov edi, ARDStruct .1: ; 将缓冲区中成员赋值给 ARDStruct mov eax, dword [esi] stosd add esi, 4 dec edx cmp edx, 0 jnz .1 ; Type 是 AddressRangeMemory 赋值 dwMemSize cmp dword [dwType], 1 jne .2 mov eax, [dwBaseAddrLow] add eax, [dwLengthLow] xchg bx, bx cmp eax, [dwMemSize] jb .2 mov [dwMemSize], eax .2: loop .loop pop ecx pop edi pop esi ret ; 启动分页机制 -------------------------------------------------------------- SetupPaging: ; 根据内存大小计算应初始化多少PDE以及多少页表 xor edx, edx mov eax, [dwMemSize] mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小 div ebx mov ecx, eax ; 此时 ecx 为页表的个数,也即 PDE 应该的个数 test edx, edx jz .no_remainder inc ecx ; 如果余数不为 0 就需增加一个页表 .no_remainder: push ecx ; 暂存页表个数 ; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞. ; 首先初始化页目录 mov ax, SelectorFlatRW mov es, ax mov edi, PageDirBase ; 此段首地址为 PageDirBase xor eax, eax mov eax, PageTblBase | 7 .1: stosd add eax, 4096 ; 为了简化, 所有页表在内存中是连续的 loop .1 ; 再初始化所有页表 pop eax ; 页表个数 mov ebx, 1024 ; 每个页表 1024 个 PTE mul ebx mov ecx, eax ; PTE个数 = 页表个数 * 1024 mov edi, PageTblBase ; 此段首地址为 PageTblBase xor eax, eax mov eax, 7 .2: stosd add eax, 4096 ; 每一页指向 4K 的空间 loop .2 mov eax, PageDirBase mov cr3, eax mov eax, cr0 or eax, 80000000h mov cr0, eax jmp short .3 .3: nop ret ; ---------------- 32 位数据段 ------------------ [SECTION .data1] ALIGN 32 LABEL_DATA: ; 实模式下使用这些符号 ; 字符串 _szMemChkTitle: db "Welcome to loader by techlog.cn", 0Ah, 0 _szReturn: db 0Ah, 0 ; 变量 _dwMCRNumber: dd 0 ; Memory Check Result _dwMemSize: dd 0 _ARDStruct: ; Address Range Descriptor Structure _dwBaseAddrLow: dd 0 _dwBaseAddrHigh: dd 0 _dwLengthLow: dd 0 _dwLengthHigh: dd 0 _dwType: dd 0 _MemChkBuf: times 256 db 0 ; 保护模式下使用这些符号 szMemChkTitle equ BaseOfLoaderPhyAddr + _szMemChkTitle szReturn equ BaseOfLoaderPhyAddr + _szReturn dwMemSize equ BaseOfLoaderPhyAddr + _dwMemSize dwMCRNumber equ BaseOfLoaderPhyAddr + _dwMCRNumber ARDStruct equ BaseOfLoaderPhyAddr + _ARDStruct dwBaseAddrLow equ BaseOfLoaderPhyAddr + _dwBaseAddrLow dwBaseAddrHigh equ BaseOfLoaderPhyAddr + _dwBaseAddrHigh dwLengthLow equ BaseOfLoaderPhyAddr + _dwLengthLow dwLengthHigh equ BaseOfLoaderPhyAddr + _dwLengthHigh dwType equ BaseOfLoaderPhyAddr + _dwType MemChkBuf equ BaseOfLoaderPhyAddr + _MemChkBuf ; 堆栈空间 StackSpace: times 1024 db 0 TopOfStack equ BaseOfLoaderPhyAddr + $

 

 

实现一个操作系统






操作系统      内核      nasm      汇编      保护模式      oranges      loader     


京ICP备15018585号