Loading... 到现在为止,我们已经进入了保护模式并做好了虚拟地址映射、开启了分页模式,loader 的历史使命也差不多该完成了,现在它需要来引导我们的内核并移交控制权了。 内核较为复杂,全部用汇编实现显然是不现实的,类似于大多数的操作系统,我们使用 C 来完成开发。 ### 关于编译方式: 高版本的 GCC 在编译代码的时候开启了许多优化和保护,我的虚拟机为 Ubuntu 20.04,gcc 版本为 9.0,难以生成我们希望的汇编代码,解决方法为降级为 gcc 4.8,使用 ```shell gcc -c -o main.o main.c -m32 -fno-asynchronous-unwind-tables ``` 进行编译,可以获得希望最低程度改动的代码(指汇编代码和预期的基本一致)。 ### 关于文件格式: 现代操作系统基本都有对该操作系统的可执行文件的格式进行约定,Linux 下常用为 **ELF**(**Executable and Linkable Format**,可执行与可链接格式),Windows 下则为 **PE**(**Portable Executable**,可移植的可执行的文件)。我们的大象操作系统当然也可以约定一个格式,比如*大象格式*。 但是大可不必这样做,说到底来,格式不过是一种约定,浪费时间在约定格式上对我们的学习并无多少帮助,另一方面,使用 ELF 也代表我们可以直接用 Linux + gcc 进行开发,节省许多格式处理上的麻烦。最后 ELF 也是一个成熟的、标准化的格式,广为接受,直接拿来用完全没毛病。 ### 内核代码生成方式 我们的内核代码的入口地址需要我们自己指定,由于内核未来会比较小,所以可以直接放到 1M 空间以下,和书上相同,我也放在虚拟地址 0xC0001500 上,既然这样,就不能让 gcc 直接给我们链接掉,而是需要我们自己用 ld 链接。以 main.c 做例子,就是先用 gcc 生成目标文件 main.o ``` gcc -c -o main.o main.c -m32 -fno-asynchronous-unwind-tables ``` 然后用 ld 指定入口点和代码段基地址 ``` ld main.o -Ttext 0xc0001500 -e _start -o kernel.bin -m elf_i386 ``` 注意命令中的 `-e _start`,这是指定入口点符号为 _start,其实默认就是使用这个函数做入口点的。如果习惯用 main 函数做入口点函数的话(其实事实上一般来说 ELF 文件都不是真的以 main 函数作为入口点的),只要把 `-e _start` 改为 `-e main` 就可以正常链接了。得到的 kernel.bin 就是我们未来要引导的内核文件了。 然后需要写入磁盘,和书中的选择一样,我也是从 0x9 扇区(第十个扇区)开始写 200 个扇区的,也就是 ``` dd if=./kernel.bin of=/path/to/hd60M.img bs=512 count=200 seek=9 conv=notrunc ``` ### 读取 ELF 文件 之前的几步做好了准备工作,之后就是要 loader 来做引导了,首先先把 kernel.bin 的内容都读到内存里面来,避免频繁的磁盘 I/O 操作造成性能过低。和书上一样,我也在分页模式开启前读取,虽然其实开启前后读关系都不大。 这里的读取方式可以几乎直接沿用 mbr 中对 loader 的引用方式,只要改一下进行写入操作的寄存器为 32 位寄存器就可以了,看后面的代码就可以很容易理解。主要是读到内存的什么位置比较重要,其实也不是很重要,只要不会覆盖后面的页表,且在内核展开后不会被内核覆盖就可以。多次提到,底端 1M 的内存在未来会映射到自己身上,这 1M 我们准备防止内核代码,提一下其中 0x500 ~ 0x9FBFF 是没有被其他设备映射的,我们可以随便用。顺便提一下,其中 GDT 表处在 0x610 ~ 0x810 中,后面又跟了一些重要的变量。 内核代码放在虚拟地址 0xC00001500,也就是物理地址 0x1500 处。我们沿用 Linux 的习惯,代码从低地址开始向上增长,栈从高地址开始向下增长,中间余留一定空间保证不会交汇。我们可以把 kernel.bin 放在这中间的地方,和书上一样,我也放在了 0x60000 上。 ### 导出 ELF 文件中各段 导出的过程涉及 ELF 的结构,这个结构里面东西挺多的,我觉得没必要死记硬背,这里只要知道我们需要的一些东西就可以了,由于是 32 位系统,所以只考虑 ELF32 的格式。 ```cpp #define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr; ``` 这里 `Elf32_Half` 类型占 2 字节,`Elf32_Word`、`ELF32_Addr` 和 `Elf32_Off` 三个类型都是 4 字节,偏移可以自己计算。这里面对我们有用的是 `e_phoff`、`e_phentsize` 和 `e_phnum` 三个成员变量 ,分别代表段表的偏移,段表的大小,段表的总数。 每一个段表项的结构如下 ```cpp typedef struct { Elf32_Word p_type; \\ 段的类型 Elf32_Off p_offset; \\ 段距文件头的偏移 Elf32_Addr p_vaddr; \\ 该段应该处于的虚拟地址 Elf32_Addr p_paddr; Elf32_Word p_filesz; \\ 该段的文件长度(即在文件中的长度,下面哪个是段在内存中占的长度) Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; } Elf32_Phdr; ``` 这里有用到四个变量,已经注释出来了。 那么我们如何导出呢?其实比较容易,首先获得段表基地址和段表项总数,然后遍历段表,通过内存拷贝把对应的数据拷到对应的地址就可以了。 ### 总结 好吧我承认这里我没有说的很清楚,一方面是对 ELF 格式我虽然尝试学了很多次,但是一直没法记下来,所以也不是特别了解,另一方面我觉得说实话也不是很重要;-) ### 实现代码 之前虽然说的很简略,但是看着代码应该就可以理解了 boot.inc 中新增 ``` KERNEL_START_SECTOR equ 0x9 KERNEL_SUM_SECTOR equ 200 KERNEL_BIN_BASE_ADDR equ 0x60000 ; where we put the kernel.bin KERNEL_ENTER_POINT equ 0xC0001500 ; the kernel enter point addr ;--------- elf related ---------- PT_NULL equ 0 ; segment type ``` loader.S ``` %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR jmp LoaderStart ; 3 bytes db 0 dd 0,0,0 ; addr align ; offset 0x10 ; set up GOT and descriptor GDT_BASE: dd 0x00000000 dd 0x00000000 CODE_DESC: dd 0x0000FFFF ; low 32 bits dd DESC_CODE_HIGH4 ; high 32 bits DATA_STACK_DESC: dd 0x0000FFFF ; used by stack and data seg dd DESC_DATA_HIGH4 ; text-mode display ; limit = (0xBFFFF - 0xB8000) / 4K = 0x7 VIDEO_DESC: dd 0x80000007 dd DESC_VIDEO_HIGH4 GDT_SIZE equ $ - GDT_BASE GDT_LIMIT equ GDT_SIZE - 1 times 60 dq 0 ; reserve 60 GDTs TOTAL_MEM_BYTES dd 0 ; memory of the machine ; addr: LOADER_BASE_ADDR + 0x10 + 0x200 = 0x800 SELECTOR_CODE equ ((CODE_DESC - GDT_BASE) / 8) << 3 + TI_GDT + RPL0 SELECTOR_DATA equ ((DATA_STACK_DESC - GDT_BASE) / 8) << 3 + TI_GDT + RPL0 SELECTOR_VIDEO equ ((VIDEO_DESC - GDT_BASE) / 8) << 3 + TI_GDT + RPL0 ; pointer point to GDT gdt_ptr: dw GDT_LIMIT ; low 16 bits of GDT reg dd GDT_BASE ; high 32 bits of GDT reg ; end of GDT setup LoaderStart: ; ---------- first, get the total memory of the machine ---------- ; ---------- we must do it before enter the PE mode as we need the BIOS int ---------- ; use bios int 0x15 sub 0xE801 .LoaderStart_E801FailedRetry: mov ax,0xE801 int 0x15 jc .LoaderStart_E801FailedRetry ; calculate low 15MB memory mov cx,0x400 mul cx shl edx,16 and eax,0x0000FFFF or edx,eax add edx,0x100000 ; add 1MB, this is caused by the memory hole mov esi,edx xor eax,eax mov ax,bx mov ecx,0x10000 ; 64 * 1024 mul ecx add esi,eax ; esi store the mov [TOTAL_MEM_BYTES],esi ; now TOTAL_MEM_BYTES stores the total memory ; ---------- ready to enter Proctection mode ---------- ; 1 open A20 address line ; 2 load GDT reg ; 3 set pe of cr0 to 1 ; open A20 in al,0x92 or al,0000_0010B ; save existed status out 0x92,al ; load GDT reg lgdt [gdt_ptr] ; set cr0, let's roll! mov eax,cr0 or eax,0x00000001 ; save existed status mov cr0,eax ; enter Protection mode jmp dword SELECTOR_CODE:ProctectionModeStart ; reflesh assembly line ; ---------- end of function LoaderStart ---------- ; ---------- now we are in 32-bits PE mode ---------- [bits 32] ProctectionModeStart: ; set selectors mov ax,SELECTOR_DATA mov ds,ax mov es,ax mov ss,ax mov esp,LOADER_STACK_TOP mov ax,SELECTOR_VIDEO mov gs,ax mov byte [gs:2],'P' ; first thing we do is load the kernel.bin to the RAM mov esi,KERNEL_START_SECTOR mov edi,KERNEL_BIN_BASE_ADDR mov edx,KERNEL_SUM_SECTOR ; read this much sectors call ReadDiskSector_32 ; second thing we do is start the page mode ; 1 setup PDE and related PTE call SetupPage ; 2 modify the GDT to make it work in paging mode sgdt [gdt_ptr] mov ebx,[gdt_ptr + 2] or dword [ebx + 0x18 + 4],0xC0000000 ; modify the VIDEO_DESC add dword [gdt_ptr + 2],0xC0000000 ; pre modify the GDTR value add esp,0xC0000000 ; also modify the stack mov eax,PAGE_DIR_TABLE_POS mov cr3,eax mov eax,cr0 or eax,0x80000000 ; save existed status mov cr0,eax ; enable paging mode lgdt [gdt_ptr] ; change GDTR mov byte [gs:4],'V' ; last thing we do is extract the Ttext to where it belongs jmp SELECTOR_CODE:EnterKernel EnterKernel: call KernelInit mov esp,0xC009F000 ; set kernel stack jmp KERNEL_ENTER_POINT ; enter kernel ; end of ProctectionModeStart ; end of loader, thank you and goodbye! SetupPage: ; ---------- this function setup the Page Directory Entry and Page Table Entry ---------- ; clear PTE mov ecx,0x1000 ; 4K PDE mov esi,0 ; use this reg the clear .SetupPage_ClearPDE: mov byte [PAGE_DIR_TABLE_POS + esi],0 inc esi loop .SetupPage_ClearPDE ; setup PDE .SetupPage_CreatePDE: mov eax,PAGE_DIR_TABLE_POS add eax,0x1000 ; addr of the first PTE mov ebx,eax ; ebx is the base addr of PTEs ; make the PDE[0] and PDE[0xC00] point to the first PTE or eax,PG_US_U | PG_RW_RW | PG_P ; set user page status mov [PAGE_DIR_TABLE_POS + 0x0],eax ; the first PTE's place, mapping loader's addr to itself mov [PAGE_DIR_TABLE_POS + 0xC00],eax ; the first PTE used by kernel, mapping to low 1M ; 0xC0000000 ~ 0xFFFFFFFF belongs to kernel sub eax,0x1000 mov [PAGE_DIR_TABLE_POS + 0xFFC],eax ; make the last Entry point to PDE itself ; creat PTE for kernel mov ecx,256 ; 1M / 4K = 256 mov esi,0 mov edx,PG_US_U | PG_RW_RW | PG_P ; User, RW, P .SetupPage_CreatePTE: mov [ebx + esi * 4],edx add edx,0x1000 inc esi loop .SetupPage_CreatePTE mov eax,PAGE_DIR_TABLE_POS add eax,0x2000 ; second PTE or eax,PG_US_U | PG_RW_RW | PG_P mov ebx,PAGE_DIR_TABLE_POS mov ecx,254 ; 1022 - 769 + 1 mov esi,769 ; start from 769,the second PTE of kernel .SetupPage_CreateKernelPDE: mov [ebx + esi * 4],eax inc esi add eax,0x1000 loop .SetupPage_CreateKernelPDE ret ; ---------- end of function SetupPage ---------- ; ---------- start of function ReadDiskSector_32 ; function MBR_ReadDiskSector_32(LBA_addr, writing_addr, n), read n sectors from hard-disk in 32 bit mode ; esi: LBA addr of start sector ; edi: writing addr ; edx: n ReadDiskSector_32: ; read sectors mov ebx,edx ; bx keeps the n mov ax,bx ; n sectors mov dx,0x1F2 ; set reg Sector count out dx,al ; read n sectors ; set LBA addr mov eax,esi mov dx,0x1F3 ; set reg LBA low out dx,al ; write low 8 bits mov cl,8 shr eax,cl mov dx,0x1F4 ; set reg LBA mid out dx,al ; write LBA mid shr eax,cl mov dx,0x1F5 ; set reg LBA high out dx,al ; write LBA high shr eax,cl and al,0xF ; only 4 bits or al,0xE0 ; 1110b: LBA mode, disk: master mov dx,0x1F6 ; set reg device out dx,al ; set mode and LBA addr ; ready to read mov dx,0x1F7 ; set reg command mov al,0x20 ; mode: read out dx,al ; do read ; check disk status .ReadDiskSector_32_DiskNotReady: in al,dx ; get disk status and al,0x88 ; result 0x8 => disk is read ; result 0x80 => disk is busy cmp al,0x08 jnz .ReadDiskSector_32_DiskNotReady ; read data mov ax,bx ; get n mov dx,256 ; read by word, so dx = 512 / 2 mul dx ; assum this mul won't overflow mov cx,ax ; sum of words need to read mov dx,0x1F0 ; set reg data .ReadDiskSector_32_ReadingLoop: in ax,dx ; read a word mov [edi],ax ; write a word add edi,2 loop .ReadDiskSector_32_ReadingLoop ret ; end of function ReadDiskSector_32 FatalKernelBroken: mov byte [gs:0],'F' mov byte [gs:1],0xA4 mov byte [gs:2],'A' mov byte [gs:3],0xA4 mov byte [gs:4],'T' mov byte [gs:5],0xA4 mov byte [gs:6],'A' mov byte [gs:7],0xA4 mov byte [gs:8],'L' mov byte [gs:9],0xA4 mov byte [gs:10],':' mov byte [gs:11],0xA4 mov byte [gs:12],' ' mov byte [gs:14],'K' mov byte [gs:16],'E' mov byte [gs:18],'R' mov byte [gs:20],'N' mov byte [gs:22],'E' mov byte [gs:24],'L' mov byte [gs:26],' ' mov byte [gs:28],'B' mov byte [gs:30],'R' mov byte [gs:32],'O' mov byte [gs:34],'K' mov byte [gs:36],'E' mov byte [gs:38],'N' jmp $ KernelInit: mov eax,[KERNEL_BIN_BASE_ADDR] ; check the magic number cmp eax,0x464c457f jne FatalKernelBroken mov al,[KERNEL_BIN_BASE_ADDR + 4] ; make sure it is a 32 bits elf cmp al,1 jne FatalKernelBroken mov al,[KERNEL_BIN_BASE_ADDR + 5] ; make sure it is a LSB elf cmp al,1 jne FatalKernelBroken ; check done mov ebx,[KERNEL_BIN_BASE_ADDR + 28] ; offset of program header table add ebx,KERNEL_BIN_BASE_ADDR ; address of program header table xor edx,edx mov dx,[KERNEL_BIN_BASE_ADDR + 42] ; program header size mov cx,[KERNEL_BIN_BASE_ADDR + 44] ; sum of segments .LoadKernelEachSegment: cmp byte [ebx],PT_NULL ; skip th null segment je .LoadKernelEachSegment_PT_NULL push dword [ebx + 16] ; nbytes, p_filesz mov eax,[ebx + 4] add eax,KERNEL_BIN_BASE_ADDR ; src push eax push dword [ebx + 8] ; dst call mem_cpy add esp,12 ; unpush 3 .LoadKernelEachSegment_PT_NULL: add ebx,edx ; skip the header loop .LoadKernelEachSegment ret ; ---------- function mem_cpy(dst,src,nbytes) ---------- mem_cpy: push ebp mov ebp,esp push edi push esi push ecx mov edi,[ebp + 8] ; dst mov esi,[ebp + 12] ; src mov ecx,[ebp + 16] ; nbytes cld rep movsb pop ecx pop esi pop edi leave ret ``` kernel/main.c ```cpp int _start() { int i = 0; while(1) { i++; asm volatile( "movb $\'K\',%gs:6" ); }; return 0; } ``` 到现在为止,我们向屏幕输出了四个字符:"MPVK",分别在 mbr,保护模式,分页模式,内核中输出,代表四模式的成功进入。 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/05/1894295228.png "></div> 之后我们就可以以 C 为主进行开发了。 另外说一下,这里的引导应该说是不太完整的,相应的段属性都没有设置,之后应该会逐渐完善。 最后修改:2021 年 05 月 18 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧