loader实现ELF格式内核的加载

从这节开始,激动可以进入内核的世界啦~
Let’s step into the world of operating system kernels!

Q&A
1、为什么用C语言编写内核?

因为汇编好难用,其他语言又不好控制内存。

2、用C语言编写和汇编有什么不同(在最后生成的二进制文件上)?

一言以蔽之:用C语言编写,计算机最后执行的是ELF格式的二进制文件(linux)
这里的细节值得好好说道说道。
1、C语言生成可执行目标文件的过程(盗用CSAPP里面的一张图片):
在这里插入图片描述
原先我们用汇编语言写MBR和loader,到交给计算机执行,实际上只进行了汇编器的处理——将汇编转换成机器码,生成纯二进制文件。这样的好处是cpu拿来就直接能跑,缺点就是每个这样的文件都要单独进行从硬盘到内存的加载
文件格式的产生就是为了应对这一问题,计算机对每个文件按照约定好的格式进行加载,这样就不用单独为每个文件都写一个加载程序了。
在linux下,用gcc来对.c文件进行处理,最后会封装成带有程序头的ELF可执行文件,而不是直接可执行的二进制文件。linux操作系统按照规定格式对文件进行解析,加载到内存的指定位置,将CS:IP跳转到程序入口,执行文件。
关于ELF文件格式详细的解析,可以参考我之前写的这篇blog:https://blog.csdn.net/ToryYang/article/details/104990253。

上图中所示的这一系列步骤中的最后一步,链接会将一个或多个可重定位目标文件进行组合,将节组合成段,最后生成能提供给计算机执行的可执行目标文件。
使用readelf -e命令查看可执行目标文件,我们能看到段是由哪些节组成的:
在这里插入图片描述
这个程序中又两个段,第一个段是由三个节组成的,分别是file1data,file1text和file2text,他们来自两个可重定位目标文件。

在下面写内核加载程序的时候,需要我们手动执行链接,并指定内核程序的入口地址,方法是在用ld命令进行链接时,Ttext指定程序入口地址,我们把内核程序的入口地址定为0xc0001500,因为已经实现了二级页表,而0xc0000000上的1MB的内存映射到了物理内存的0-1MB,因此,内核的实际入口物理地址是0x1500。

3、ELF文件格式?
具体细节参考我之前写的一篇博客哈,https://blog.csdn.net/ToryYang/article/details/104990253。

简单的说就是elf header + program header + segment,program header是描述segment信息的结构数组,通过遍历program header,可以获取到加载segment所需的信息,完成ELF文件格式的加载。

4、ELF文件加载的具体流程?
流程如下:
在这里插入图片描述

5、格式问题
因为我的ubuntu是64位的,所以gcc -c 生成的也是64位的可重定位目标文件,而我们的内核加载器是32位的,无法加载64位格式。
所以在使用gcc生成可重定位目标文件时要加 -m32参数。
nasm : nasm -f elf print.S -o print.o
ld命令:ld -m elf_i386 main.o print.o -e main -Ttext 0xc0001500

**

内核1.0

**
生成ELF可执行文件,注入磁盘

int main(void)
{
while(1);
return 0;
}

就是一个死循环,用gcc生成先生成可重定位目标文件,
gcc -c main.c -o main.o
再使用ld命令指定入口地址生成可执行目标文件。
ld main.o -Ttext 0xc0001500 -e main -o kernel.bin
注意:因为并没有直接gcc main.c生成可执行目标文件,所以必须加上-e main指定入口符号,否则就把源程序中的main改成_start,因为ld默认把 _start作为入口符号。
dd if=kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc
从LBA编号为9的扇区开始加载,共计200个扇区。

实现内核的加载:

kernel_init:
   xor eax, eax
   xor ebx, ebx		;ebx记录程序头表地址
   xor ecx, ecx		;cx记录程序头表中的program header数量
   xor edx, edx		;dx 记录program header尺寸,即e_phentsize

   mov dx, [KERNEL_BIN_BASE_ADDR + 42]	  ; 表示program header大小
   mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 第1 个program header的偏移量
   add ebx, KERNEL_BIN_BASE_ADDR
   mov cx, [KERNEL_BIN_BASE_ADDR + 44]    ; program header的数量
.each_segment:
   cmp byte [ebx + 0], PT_NULL ; 判断段是否为NULL
   je .PTNULL

; memcpy(dst,src,size)
   push dword [ebx + 16]		  ; 压入函数memcpy的第三个参数size
   mov eax, [ebx + 4]			  
   add eax, KERNEL_BIN_BASE_ADDR	  ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
   push eax				   ; 压入函数memcpy的第二个参数:源地址
   push dword [ebx + 8]			  ; 压入函数memcpy的第一个参数:目的地址
   call mem_cpy				  ; 调用mem_cpy完成段复制
   add esp,12				   ; 清理参数
.PTNULL:
   add ebx, edx				  ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header 
   loop .each_segment
   ret

;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:		      
   cld
   push ebp
   mov ebp, esp
   push ecx		   ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
   mov edi, [ebp + 8]	   ; dst
   mov esi, [ebp + 12]	   ; src
   mov ecx, [ebp + 16]	   ; size
   rep movsb		   ; 逐字节拷贝

   ;恢复环境
   pop ecx		
   pop ebp
   ret

mem_cpy的代码直接照搬了书上的,不想研究汇编了,想赶快钻入C的怀抱,哈哈。
激动人心的在下一节——实现自己的打印函数。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值