从这节开始,激动可以进入内核的世界啦~
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的怀抱,哈哈。
激动人心的在下一节——实现自己的打印函数。