从Boot到loader到C语言
从 加载硬盘的第一扇区 到能使用C语言来编写内核程序,我们完成了如下3个部分
- Boot:将loader加载到 内存0x9000:0x0100处 576K处,跳转到Loader。
- Loader:将Kernel 加载到0x7000:0x0000 处、获取内存大小、打开分页、开启CPU保护模式、读取Kernel的ELF文件格式 将 将Kernel从0x7000:0x0000 处搬运到ELF指定的入口点处 我这里搬运到了 4K(0x0000:0x1000)处,然后跳转到 4K(Kernel)处。
- Kernel:内核程序 主要有C语言 编译后 和 内核汇编程序 编译后 链接而成,使用C语言是因为可以更方便的编写更复杂的逻辑 脱离汇编的低级难调试,同时有些 低级的功能 C语言能调用 汇编。
ELF文件格式
一个标准的ELF文件,是由文件头(ELF Header),程序头(Segment Header),节头(Section Header),符号表( Symbol Table),动态符号表(Dynamic Symbol Table)等组成。
我们需要完成的是 从 文件头中 找到 2个值,程序头表的起始位置,和程序头表的数量,由程序头表之间是连续的 那么只要遍历 他的长度 32字节,就能找到所有的段表,在段表里 有 程序段的数据的起始地址 程序长度 和 要写入的虚拟地址 有了这些就可以将ELF文件里的汇编内容读取出来。
我们为什么需要了解ELF
如果我们想用到C语言编译的程序作为内核,而我们想要和它产生交流,就要按照它定义的规则来,当然 如果你不想遵循读取ELF的方式去调用内核,你可以手动的找到C语言 生成汇编代码,然后 黏贴到你的代码中 然后直接调用.
1.一个c源程序f1.c经过c预处理器(cpp)进行预处理后,变成f1.i(上图省略此步)
2.c编译器(ccl)将f1.i文件翻译成一个ASCII汇编语言文件f1.s
3.汇编器(as)将f1.s翻译成一个可重定位目标文件f1.o
4.链接器(ld)将所有文件链接,最终生成一个可执行文件p
从上图可以看出,一个源程序最终是要转成汇编程序最后才能生成一个可执行目标文件,写过汇编的都知道,汇编每一段开头都有不同的声明,表示接下来这一段的内容是什么,如下图,这就是section,也就是说section本身的作用就是来自于汇编中声明
那么segment的作用是什么呢? 多个可重定向文件最终要整合成一个可执行的文件的时候,链接器吧目标文件中相同的 section 整合成一个segment,在程序运行的时候,方便加载器的加载。
解读ELF
在ELF文件里面,程序头 包含了每个段的描述信息32位下每个段占用32字节, 在linux 系统下 我们使用 readelf -h [filename] 来查看 ELF文件头信息:
程序头包含了很多重要的信息,每个字段的含义可参考ELF结构文档。主要看下:
- Entry point address:程序的入口地址,这是没有链接的目标文件所以值是0x00
- Start of section headers:段表开始位置的首字节
- Size of section headers:段表的长度(字节为单位)
- Number of section headers:段表中项数,也就是有多少段
- Start of program headers:程序头表的起始位置(对于可执行文件重要,现在为0)
- Size of program headers:程序头大小(对于可执行文件重要,现在为0)
- Number of program headers:程序头表中的项数,也就是多少Segment(和Section有区别,后面介绍)
- Size of this header:当前ELF文件头的大小,这里是52字节
段表是什么
,ELF文件中把指令和数据分成了很多段,比如.text比如.data等等,其实还有一些辅助的段没有被显示出来,比如符号表,比如字符串表等等,而ELF文件中所有的段的信息,都会存在一个段表中,然后文件头中的e_shoff 成员来指示这个段表在哪,这样,我们得到了文件头,从文件头得到段表位置,从段表中获得段的位置,最后从段中可以找到对应数据。
我们 需要的是 段表 里面的 sh_addr sh_offset 和 sh_size 分别表示这 程序在内存中的起始位置(位于:段表起始位置 + 4) 要被拷贝到的位置 和 拷贝多少个字节(位于:段表起始位置 + 16)。
InitKernelnelInMemory:
xor esi,esi
xor ecx,ecx
mov cx,word[KERNEL_PHY_ADDR + 44] ;程序头表数量
mov esi,[KERNEL_PHY_ADDR + 28] ;程序头表 在文件中的偏移量(字节)
add esi,KERNEL_PHY_ADDR ;第一个程序头 位置
.Begin:
mov eax,[esi + 0]
cmp eax,0
je .NoAction ;e_type == 0 不可用段
;e_type != 0 说明它是一个可用段
push dword [esi + 16] ;压入参数 拷贝字节大小
mov eax,[esi + 4] ;sh_addr 段将要被被加载进的虚拟地址
add eax,KERNEL_PHY_ADDR ;这里加上实际物理地址
push eax ;压入参数 拷贝到的地址
push dword[esi + 8] ;压入参数 要被拷贝的源地址 sh_offset 该段位于文件中的偏移
call MemoryCpy ;调用 函数 拷贝内存 传入3个参数 源起始地址,目标地址,拷贝大小
add esp,4 * 3 ;清理堆栈
.NoAction:
add esi,32 ;指向下一个程序头表 一个表占32字节
dec ecx
cmp ecx,0 ;判断 是否已经循环完所有程序
jnz .Begin
ret
;==========================================
; 拷贝内存(按字节)
;函数原型:void *MemoryCpy(void *es:dest,void *ds:src,int size)
;
;==========================================
MemoryCpy:
push esi
push edi
push ecx
mov edi,[esp+ 4 * 4]
mov esi,[esp+ 4 * 5]
mov ecx,[esp+ 4 * 6]
.Copy:
cmp ecx,0
jz .CmpEnd
mov al,[ds:esi]
inc esi
mov [es:edi],al
inc edi
loop .Copy
.CmpEnd:
mov eax,[esp + 4 * 4] ;返回拷贝后 数据所在位置指针
pop ecx
pop edi
pop esi
ret
链接命令:
gcc -c -m32 -o main.o main.c //c语言编译指令
nasm -f elf -o [out] [filename]//汇编编译指令
ld -m elf_i386 -Ttext 0x1000 -o [out] [in1] [in2] [...] //将 C语言 和汇编链接到一起 -Ttext 0x1000 指定 入口点 -m elf_i386指定 32位
我们 看看 C语言 文件的 定义:
int dis_position = (80 * 5 + 0) * 2; //显示位置
void low_print(char* str); //外部函数引用
void rabbit_main(void){ //内核主函数
low_print("Hello Rabbit os !!!\n");//调用汇编打印
while(1){}//死循环
}
kernel.asm 内核文件
;==================================================
; 内核数据段
;==================================================
;导入C语言编写出的内核主函数
extern rabbit_main
;导出函数
global _start
[section .data]
bits 32
nop
;-------------------------------------------------
;=======================================
; 内核堆栈段
;=======================================
[section .bss]
StackSpace : resb 0x1000 ;分配4KB栈空间
StackTop:
;=======================================
; 内核代码段
;=======================================
[section .text]
_start:
mov ax,ds
mov es,ax
mov fs,ax
mov ss,ax
mov esp,StackTop;设置栈顶
jmp rabbit_main ;跳转到C语言 内核程序
;实际上并不会跳转到这里来
SysEnd:
HLT
jmp SysEnd
打印函数:
i386_function.asm
extern dis_position ;导入变量
global low_print ;导出函数
;============================
; 打印字符
; 函数原型 : void low_print(char* str),以0结尾
;=============================
low_print:
push esi
push edi
mov esi,[esp + 0xc] ;指向栈向前第六个指针 字符串指针
mov edi,[dis_position] ;输出起始位置
mov ah,0xf ;白底黑字
.s1:
lodsb
test al,al
jz .closePrint
cmp al,10 ;换行符
jz .s2
mov [gs:edi],ax;往屏幕打印
add edi,2 ;下一列
jmp .s1
.s2: ; 处理换行符 '\n'
push eax
mov eax,edi
mov bl,160 ;每一行 80个字符 一个字符栈2个字节 所以 =160字节
div bl ;计算当前行的下一行
inc eax
mov bl,160
mul bl ;将下一行 乘以 每列字数 计算出 下一行起始位置
mov edi,eax ;指向
pop eax
jmp .s1
.closePrint:
mov dword [dis_position], edi ; 打印完毕,更新显示位置
pop edi
pop esi
ret
将上面 的 汇编文件 用:
nasm -f elf -o kernel.o kernel.asm
nasm -f elf -o i386_function.o i386_function.asm
gcc -c -m32 -o main.o main.c
ld -m elf_i386 -o kernel.bin kernel.o main.o i386_kernel.o
最后 生成一个 kernel.bin 文件 写入文件系统就可以了。
最后看看,成果 我们成果使用C语言 打印出了字符串。