发布时间:2026-07-21阅读(1)
上一期已经讲述了操作系统用到的相关寄存器和一些位操作
那么我们这一期来看看程序的实现吧!

(GuEeOS启动界面)
链接之后,程序的代码段,数据段就保存好了。以上就是对ELF(32bits)的简单介绍,如果读者还想深入了解这个格式,可以去各种网站上一一了解。当然,笔者也找到了这个文献:http://www.skyfree.org/linux/references/ELF_Format.pdf(如果失效还可以在 Linux 系统的/usr/include/elf.h 中找到这些定义)。下面笔者将象征性地演示ELF文件的分析,首先需要一个二进制查看器,这种工具在Linux和windows上都非常易得,笔者用的是binary Editor(一个鬼子开发的软件)。打开编译后的kernel.bin文件:

(开发底层软件研究二进制数据很重要)
上面都是16进制码,画蓝色细线的地方是ELF Header(Elf32_Ehdr),画红色粗线的地方是Program header table(Elf32_Phdr)下面我们逐字节分析:
第一行:
第二行:
第三行:
第四行:
第五行:
第六行:
得知内核的各个数据后,我们就可以加载内核了。目前我们强制设定了Loader程序大小为4096字节,占用了4096/512=8个扇区,那么我们就从第9扇区开始读取内核。
我们先修改Makefile:
1#现在该明白这儿为什么写1、8了吧 2LOAD_SECTOR_OFFSET=1 3LOAD_SECTORS=8 4 5#从第9扇区开始读,设为9 6KERNEL_SECTOR_OFFEST=9 7#内核占用扇区数,根据内核大小设置,写得足够大就行 8KERNEL_SECTORS=348 910#这是笔者的gcc的目录,请读者另外自行设置(可无)11PREFIX=builder/1213NASM=nasm14CC=$(PREFIX)gcc15LD=ld16DD=dd17QEMU=qemu-system-i3861819BOOT_BIN=boot.bin20LOADER_BIN=loader.bin21KERNEL_FILE=kernel.bin22OS_IMG=os.img2324#编译参数25ASM_KERNEL_FLAGS=-felf3226#-fno-builtin指不使用gcc默认库,因为我们要自己实现所有功能-m32是32位模式-I是指头文件默认目录27C_KERNEL_FLAGS=-I./include-c-fno-builtin-m3228#内核_START(可自行设置,默认为_start)入口和地址29LD_FLAGS=-melf_i386-e_START-Ttext0x801000003031#默认执行os.img32.all:os.img3334#注意这些缩进是制表符[--->],不是空格[]35bootloader.bin:36$(NASM)boot.asm-o$(BOOT_BIN)37$(NASM)loader.asm-o$(LOADER_BIN)3839ASM_FILE:40$(NASM)$(ASM_KERNEL_FLAGS)_Start.asm-o_Start.o4142C_FILE:43$(CC)$(C_KERNEL_FLAGS)start.c-ostart.o4445kernel.bin:ASM_FILEC_FILE46$(LD)$(LD_FLAGS)-o$(KERNEL_FILE)_Start.ostart.o4748#执行os.img前,应该生成boot.bin和loader.bin49#seek为9,目的是跨过前9个扇区(第0~8个扇区),我们在第9个扇区写入。50#count为348,目的是一次往参数of指定的文件中写入348个扇区。51os.img:bootloader.binkernel.bin52$(DD)if=$(BOOT_BIN)of=$(OS_IMG)bs=512count=1conv=notrunc53$(DD)if=$(LOADER_BIN)of=$(OS_IMG)bs=512seek=$(LOAD_SECTOR_OFFSET)count=$(LOAD_SECTORS)conv=notrunc54$(DD)if=$(KERNEL_FILE)of=$(OS_IMG)bs=512seek=$(KERNEL_SECTOR_OFFEST)count=$(KERNEL_SECTORS)conv=notrunc5556#运行前,应该生成os.img57run:os.img58$(QEMU)-boota-fda$(OS_IMG)5960clean:61rm*.bin62rm*.o
里面有些提到的文件后面会讲。目前文件较少,内核目录也较简单,下节会有较大的改动,这是目前的文件树:

我们先把内核前奏的程序_Start.asm写好(可不写,记得设置内核入口函数名就行),这个内核是由另一个程序调用的,栈顶地址可自己按照需要修改:
1;File:_Start.asm 2;内核的栈顶地址 3KERNEL_STACK_TOPequ0x8009fc00 4 5[bits32] 6 7externmain 8 9[section.text]10global_START1112_START:13movax,0x1014movds,ax15moves,ax16movfs,ax17movgs,ax18movss,ax19movesp,KERNEL_STACK_TOP2021callmain;调用start.c的main()2223CPU_hlt:24hlt25jmpCPU_hlt
接下来我们修改一下loader.asm就可以了,这次修改内容不多,但是理解起来较为麻烦,笔者注释已经写清楚了。
1... 2jmpENTER_LOADER 3 4READ_SECTORequ9 5 6ENTER_LOADER: 7;该地址实际是0x10000当前处于实模式 8;一次只能加载128个扇区,一共384个扇区,因此分3次加载 9movax,0x1000 10movsi,READ_SECTOR 11movcx,128 12callload_file 13 14movax,0x2000 15movsi,READ_SECTOR 128 16movcx,128 17callload_file 18 19movax,0x3000 20movsi,READ_SECTOR 256 21movcx,128 22callload_file 23 24;跳过数据段 25jmpTest_0xE820 26... 27... 28 29;si:扇区逻辑区块地址,起点为0 30;cx:扇区数 31read_floppy_sector: 32pushax 33pushcx 34pushdx;保存缓冲内容 35pushbx 36 37movax,si 38xordx,dx 39movbx,18 40 41divbx 42incdx 43movcl,dl 44xordx,dx 45movbx,2 46 47divbx 48 49movdh,dl 50xordl,dl 51movch,al 52popbx 53.rp: 54moval,0x01 55movah,0x02 56int0x13 57jc.rp 58popdx 59popcx 60popax 61ret 62 63load_file: 64;段偏移 65moves,ax 66xorbx,bx 67.loop: 68callread_floppy_sector 69addbx,512 70incsi 71loop.loop 72ret 73 74[bits32] 75 76flush: 77... 78... 79point_in_paging_mode: 80;分页机制下寻址 81moveax,Page_Dir_Address 82movebx,Page_Table_Address 83addeax,ebx 84shleax,20 85addeax,0xb8000 86movdword[eax 160 2],P 87movdword[eax 160 3],0x6f 88movdword[eax 160 4],a 89movdword[eax 160 5],0x6f 90 91jmpenter_kernel 92 93KERNEL_BIN_BASE_ADDREQU0x10000 94KERNEL_ENTRYequ0x80100000 95 96enter_kernel: 97callinit_kernel 98;进入内核 99jmpKERNEL_ENTRY100101;这里引用胡同学的注释:102;遍历每一个ProgramHeader,根据ProgramHeader中的信息来确定把什么放进内存,放到什么位置,以及放多少。103init_kernel:104xoreax,eax105xorebx,ebx;记录每一个ProgramHeaderTable地址106xorecx,ecx;记录每一个ProgramHeaderTable数量107xoredx,edx;记录每一个ProgramHeaderTable的大小:e_phentsize108109movdx,[KERNEL_BIN_BASE_ADDR 42];偏移42字节:e_phentsize110movebx,[KERNEL_BIN_BASE_ADDR 28];偏移28字节:e_phoff,第一个programheader偏移量111addebx,KERNEL_BIN_BASE_ADDR112movcx,[KERNEL_BIN_BASE_ADDR 44];偏移44字节:e_phnum113114;遍历每个段115.EACH_SEGMENT:116cmpbyte[ebx 0],0;PT_NULL=0117je.PTNULL118119pushdword[ebx 16];p_filesz,memcpy第三个参数:size120moveax,[ebx 4];p_offset,本段在文件起始的偏移字节121addeax,KERNEL_BIN_BASE_ADDR;本程序段的起始地址122pusheax;memcpy第二个参数:source123pushdword[ebx 8];memcpy第一个参数:destination124callmemcpy125addesp,12;memcpy一共3个参数,故3*4=12126127.PTNULL:128addebx,edx;Edxistheprogramheadersize,iee_phentsize,whereebxpointstothenextprogramheader129loop.EACH_SEGMENT130131ret132133;逐字节拷贝134memcpy:135cld136pushebp137movebp,esp138pushecx;保存ecx内值139movedi,[ebp 8];dst140movesi,[ebp 12];src141movecx,[ebp 16];size142repmovsb;逐字节拷贝143144popecx145popebp146147ret148...
接下来就是激动人心的一刻:make run

编译非常流畅!运行结果也是成功的!

当然,Loader程序还有很多值得优化的地方,进入图形模式,输出调试信息等,但是现在,我们已经真正地在开始编写我们的OS内核了!那么,Loader程序编写就告一段落了!
关注"GuEes"公众号,了解更多消息!
Copyright © 2024 有趣生活 All Rights Reserve吉ICP备19000289号-5 TXT地图HTML地图XML地图