上一期已经讲述了操作系统用到的相关寄存器和一些位操作
那么我们这一期来看看程序的实现吧!
(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 = -f elf3226#-fno-builtin指不使用gcc默认库,因为我们要自己实现所有功能 -m32是32位模式 -I是指头文件默认目录27C_KERNEL_FLAGS = -I ./include -c -fno-builtin -m3228#内核_START(可自行设置,默认为_start)入口和地址29LD_FLAGS = -m elf_i386 -e _START -Ttext 0x801000003031#默认执行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 -o start.o4445kernel.bin: ASM_FILE C_FILE46 $(LD) $(LD_FLAGS) -o $(KERNEL_FILE) _Start.o start.o4748#执行os.img前,应该生成boot.bin和loader.bin49#seek为9,目的是跨过前 9 个扇区(第0~8个扇区),我们在第9个扇区写入。50#count为348,目的是一次往参数 of 指定的文件中写入348个扇区。51os.img: bootloader.bin kernel.bin52 $(DD) if=$(BOOT_BIN) of=$(OS_IMG) bs=512 count=1 conv=notrunc53 $(DD) if=$(LOADER_BIN) of=$(OS_IMG) bs=512 seek=$(LOAD_SECTOR_OFFSET) count=$(LOAD_SECTORS) conv=notrunc54 $(DD) if=$(KERNEL_FILE) of=$(OS_IMG) bs=512 seek=$(KERNEL_SECTOR_OFFEST) count=$(KERNEL_SECTORS) conv=notrunc5556#运行前,应该生成os.img57run: os.img58 $(QEMU) -boot a -fda $(OS_IMG)5960clean:61 rm *.bin62 rm *.o
里面有些提到的文件后面会讲。目前文件较少,内核目录也较简单,下节会有较大的改动,这是目前的文件树:
我们先把内核前奏的程序_Start.asm写好(可不写,记得设置内核入口函数名就行),这个内核是由另一个程序调用的,栈顶地址可自己按照需要修改:
1;File: _Start.asm 2;内核的栈顶地址 3KERNEL_STACK_TOP equ 0x8009fc00 4 5[bits 32] 6 7extern main 8 9[section .text]10global _START1112_START:13 mov ax,0x1014 mov ds,ax15 mov es,ax16 mov fs,ax17 mov gs,ax18 mov ss,ax19 mov esp,KERNEL_STACK_TOP2021 call main ;调用start.c的main()2223CPU_hlt:24 hlt25 jmp CPU_hlt
接下来我们修改一下loader.asm就可以了,这次修改内容不多,但是理解起来较为麻烦,笔者注释已经写清楚了。
1... 2 jmp ENTER_LOADER 3 4READ_SECTOR equ 9 5 6ENTER_LOADER: 7 ;该地址实际是0x10000当前处于实模式 8 ;一次只能加载128个扇区,一共384个扇区,因此分3次加载 9 mov ax, 0x1000 10 mov si, READ_SECTOR 11 mov cx, 128 12 call load_file 13 14 mov ax,0x2000 15 mov si,READ_SECTOR+128 16 mov cx,128 17 call load_file 18 19 mov ax,0x3000 20 mov si,READ_SECTOR+256 21 mov cx,128 22 call load_file 23 24 ;跳过数据段 25 jmp Test_0xE820 26... 27... 28 29;si: 扇区逻辑区块地址,起点为0 30;cx: 扇区数 31read_floppy_sector: 32 push ax 33 push cx 34 push dx ;保存缓冲内容 35 push bx 36 37 mov ax, si 38 xor dx, dx 39 mov bx, 18 40 41 div bx 42 inc dx 43 mov cl, dl 44 xor dx, dx 45 mov bx, 2 46 47 div bx 48 49 mov dh, dl 50 xor dl, dl 51 mov ch, al 52 pop bx 53.rp: 54 mov al, 0x01 55 mov ah, 0x02 56 int 0x13 57 jc .rp 58 pop dx 59 pop cx 60 pop ax 61 ret 62 63load_file: 64 ;段偏移 65 mov es, ax 66 xor bx, bx 67.loop: 68 call read_floppy_sector 69 add bx, 512 70 inc si 71 loop .loop 72 ret 73 74[bits 32] 75 76flush: 77... 78... 79point_in_paging_mode: 80 ;分页机制下寻址 81 mov eax,Page_Dir_Address 82 mov ebx,Page_Table_Address 83 add eax,ebx 84 shl eax,20 85 add eax,0xb8000 86 mov dword [eax+160+2],'P' 87 mov dword [eax+160+3],0x6f 88 mov dword [eax+160+4],'a' 89 mov dword [eax+160+5],0x6f 90 91 jmp enter_kernel 92 93KERNEL_BIN_BASE_ADDR EQU 0x10000 94KERNEL_ENTRY equ 0x80100000 95 96enter_kernel: 97 call init_kernel 98 ;进入内核 99 jmp KERNEL_ENTRY100101;这里引用胡同学的注释:102;遍历每一个 Program Header,根据 Program Header 中的信息来确定把什么放进内存,放到什么位置,以及放多少。103init_kernel:104 xor eax,eax105 xor ebx,ebx ;记录每一个Program Header Table地址106 xor ecx,ecx ;记录每一个Program Header Table数量107 xor edx,edx ;记录每一个Program Header Table的大小:e_phentsize108109 mov dx,[KERNEL_BIN_BASE_ADDR+42] ;偏移42字节:e_phentsize110 mov ebx,[KERNEL_BIN_BASE_ADDR+28] ;偏移28字节:e_phoff,第一个program header偏移量111 add ebx,KERNEL_BIN_BASE_ADDR112 mov cx,[KERNEL_BIN_BASE_ADDR+44] ;偏移44字节: e_phnum113114;遍历每个段115.EACH_SEGMENT:116 cmp byte [ebx+0],0 ;PT_NULL = 0117 je .PTNULL118119 push dword [ebx+16] ;p_filesz,memcpy第三个参数: size120 mov eax,[ebx+4] ;p_offset, 本段在文件起始的偏移字节121 add eax,KERNEL_BIN_BASE_ADDR ;本程序段的起始地址122 push eax ;memcpy第二个参数: source123 push dword [ebx+8] ;memcpy第一个参数: destination124 call memcpy125 add esp,12 ;memcpy一共3个参数,故3*4=12126127.PTNULL:128 add ebx,edx ;Edx is the program header size, ie e_phentsize, where ebx points to the next program header129 loop .EACH_SEGMENT130131 ret132133;逐字节拷贝134memcpy:135 cld136 push ebp137 mov ebp,esp138 push ecx ;保存ecx内值139 mov edi,[ebp+8] ;dst140 mov esi,[ebp+12] ;src141 mov ecx,[ebp+16] ;size142 rep movsb ;逐字节拷贝143144 pop ecx145 pop ebp146147 ret148...
接下来就是激动人心的一刻:make run
编译非常流畅!运行结果也是成功的!
当然,Loader程序还有很多值得优化的地方,进入图形模式,输出调试信息等,但是现在,我们已经真正地在开始编写我们的OS内核了!那么,Loader程序编写就告一段落了!
关注"GuEes"公众号,了解更多消息!