有趣生活

当前位置:首页>科技>bootloader目标代码是什么格式操作系统开发loader程序编写

bootloader目标代码是什么格式操作系统开发loader程序编写

发布时间:2026-07-21阅读(1)

导读上一期已经讲述了操作系统用到的相关寄存器和一些位操作那么我们这一期来看看程序的实现吧!(GuEeOS启动界面)链接之后,程序的代码段,数据段就保存好了。以上....

上一期已经讲述了操作系统用到的相关寄存器和一些位操作

那么我们这一期来看看程序的实现吧!

(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)下面我们逐字节分析:

第一行:

  • 一整条线都(就)是e_ident[],每一个字节的意义对应了上面的表,读者可以用上面的表格进行查看,这里的意义为:0x7f, E, L, F, 32位对象, 小端字节序有效版本后面都为不需要设置的内容均为0

第二行:

  • 第一条线就是e_type:可执行文件
  • 第二条线就是e_machine:Intel 80386
  • 第三条线就是e_version:有效版本
  • 第四条线就是e_entry:程序的虚拟入口地址为0x80100000
  • 第五条线就是e_phoff:程序头表在文件中的偏移量是0x34

第三行:

  • 第一条线就是e_shoff:节区头部表在文件内的偏移量,这里的值为0x10f8(如果没有节区头部表则为0)
  • 第二条线就是e_flags:e_flags的具体属性
  • 第三条线就是e_ehsize:ELF header的字节大小为0x34
  • 第四条线就是e_phentsize:Elf32_Phdr 的字节大小0x20字节
  • 第五条线就是e_phnum:程序头表中段的个数为2个段
  • 第六条线就是e_shentsize:节区头部表中各个节的大小

第四行:

  • 第一条线就是e_shnum:节头表中节的个数为8
  • 第二条线就是e_shstrndx:字符串表在节区头表中的索引为5
  • 第三条线就是p_type:可加载程序段
  • 第四条线就是p_offset:本段在文件起始的偏移字节为0x1000
  • 第五条线就是p_vaddr:本段被加载到内存后的起始虚拟地址为0x80100000

第五行:

  • 第一条线就是p_paddr:(此项暂且保留,未设定)
  • 第二条线就是p_filesz:本段在文件中的字节大小为0x79
  • 第三条线就是p_memsz:本段在内存中的字节大小(和p_filesz相等)
  • 第四条线就是p_flags:该值为5=4 1=PF_R PF_X,表示可读,可执行

第六行:

  • 第一条线就是p_align:本段对齐的方式

得知内核的各个数据后,我们就可以加载内核了。目前我们强制设定了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地图