操作系统脚本编程教程(操作系统开发loader程序编写)
操作系统脚本编程教程(操作系统开发loader程序编写)第三行:第二行:(开发底层软件研究二进制数据很重要)上面都是16进制码,画蓝色细线的地方是ELF Header(Elf32_Ehdr),画红色粗线的地方是Program header table(Elf32_Phdr)下面我们逐字节分析:第一行:
上一期已经讲述了操作系统用到的相关寄存器和一些位操作
那么我们这一期来看看程序的实现吧!
(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
9
10#这是笔者的gcc的目录,请读者另外自行设置(可无)
11PREFIX=builder/
12
13NASM=nasm
14CC=$(PREFIX)gcc
15LD=ld
16DD=dd
17QEMU=qemu-system-i386
18
19BOOT_BIN=boot.bin
20LOADER_BIN=loader.bin
21KERNEL_FILE=kernel.bin
22OS_IMG=os.img
23
24#编译参数
25ASM_KERNEL_FLAGS=-felf32
26#-fno-builtin指不使用gcc默认库,因为我们要自己实现所有功能-m32是32位模式-I是指头文件默认目录
27C_KERNEL_FLAGS=-I./include-c-fno-builtin-m32
28#内核_START(可自行设置,默认为_start)入口和地址
29LD_FLAGS=-melf_i386-e_START-Ttext0x80100000
30
31#默认执行os.img
32.all:os.img
33
34#注意这些缩进是制表符[--->],不是空格[]
35bootloader.bin:
36$(NASM)boot.asm-o$(BOOT_BIN)
37$(NASM)loader.asm-o$(LOADER_BIN)
38
39ASM_FILE:
40$(NASM)$(ASM_KERNEL_FLAGS)_Start.asm-o_Start.o
41
42C_FILE:
43$(CC)$(C_KERNEL_FLAGS)start.c-ostart.o
44
45kernel.bin:ASM_FILEC_FILE
46$(LD)$(LD_FLAGS)-o$(KERNEL_FILE)_Start.ostart.o
47
48#执行os.img前,应该生成boot.bin和loader.bin
49#seek为9,目的是跨过前9个扇区(第0~8个扇区),我们在第9个扇区写入。
50#count为348,目的是一次往参数of指定的文件中写入348个扇区。
51os.img:bootloader.binkernel.bin
52$(DD)if=$(BOOT_BIN)of=$(OS_IMG)bs=512count=1conv=notrunc
53$(DD)if=$(LOADER_BIN)of=$(OS_IMG)bs=512seek=$(LOAD_SECTOR_OFFSET)count=$(LOAD_SECTORS)conv=notrunc
54$(DD)if=$(KERNEL_FILE)of=$(OS_IMG)bs=512seek=$(KERNEL_SECTOR_OFFEST)count=$(KERNEL_SECTORS)conv=notrunc
55
56#运行前,应该生成os.img
57run:os.img
58$(QEMU)-boota-fda$(OS_IMG)
59
60clean:
61rm*.bin
62rm*.o
里面有些提到的文件后面会讲。目前文件较少,内核目录也较简单,下节会有较大的改动,这是目前的文件树:
我们先把内核前奏的程序_Start.asm写好(可不写,记得设置内核入口函数名就行),这个内核是由另一个程序调用的,栈顶地址可自己按照需要修改:
1;File:_Start.asm
2;内核的栈顶地址
3KERNEL_STACK_TOPequ0x8009fc00
4
5[bits32]
6
7externmain
8
9[section.text]
10global_START
11
12_START:
13movax 0x10
14movds ax
15moves ax
16movfs ax
17movgs ax
18movss ax
19movesp KERNEL_STACK_TOP
20
21callmain;调用start.c的main()
22
23CPU_hlt:
24hlt
25jmpCPU_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_ENTRY
100
101;这里引用胡同学的注释:
102;遍历每一个ProgramHeader,根据ProgramHeader中的信息来确定把什么放进内存,放到什么位置,以及放多少。
103init_kernel:
104xoreax eax
105xorebx ebx;记录每一个ProgramHeaderTable地址
106xorecx ecx;记录每一个ProgramHeaderTable数量
107xoredx edx;记录每一个ProgramHeaderTable的大小:e_phentsize
108
109movdx [KERNEL_BIN_BASE_ADDR 42];偏移42字节:e_phentsize
110movebx [KERNEL_BIN_BASE_ADDR 28];偏移28字节:e_phoff,第一个programheader偏移量
111addebx KERNEL_BIN_BASE_ADDR
112movcx [KERNEL_BIN_BASE_ADDR 44];偏移44字节:e_phnum
113
114;遍历每个段
115.EACH_SEGMENT:
116cmpbyte[ebx 0] 0;PT_NULL=0
117je.PTNULL
118
119pushdword[ebx 16];p_filesz,memcpy第三个参数:size
120moveax [ebx 4];p_offset 本段在文件起始的偏移字节
121addeax KERNEL_BIN_BASE_ADDR;本程序段的起始地址
122pusheax;memcpy第二个参数:source
123pushdword[ebx 8];memcpy第一个参数:destination
124callmemcpy
125addesp 12;memcpy一共3个参数,故3*4=12
126
127.PTNULL:
128addebx edx;Edxistheprogramheadersize iee_phentsize whereebxpointstothenextprogramheader
129loop.EACH_SEGMENT
130
131ret
132
133;逐字节拷贝
134memcpy:
135cld
136pushebp
137movebp esp
138pushecx;保存ecx内值
139movedi [ebp 8];dst
140movesi [ebp 12];src
141movecx [ebp 16];size
142repmovsb;逐字节拷贝
143
144popecx
145popebp
146
147ret
148...
接下来就是激动人心的一刻:make run
编译非常流畅!运行结果也是成功的!
当然,Loader程序还有很多值得优化的地方,进入图形模式,输出调试信息等,但是现在,我们已经真正地在开始编写我们的OS内核了!那么,Loader程序编写就告一段落了!
关注"GuEes"公众号,了解更多消息!