从零开始写一个操作系统内核 笔记(五) 从汇编过渡到C语言

从Boot到loader到C语言

从 加载硬盘的第一扇区 到能使用C语言来编写内核程序,我们完成了如下3个部分

  1. Boot:将loader加载到 内存0x9000:0x0100处 576K处,跳转到Loader。
  2. Loader:将Kernel 加载到0x7000:0x0000 处、获取内存大小、打开分页、开启CPU保护模式、读取Kernel的ELF文件格式 将 将Kernel从0x7000:0x0000 处搬运到ELF指定的入口点处 我这里搬运到了 4K(0x0000:0x1000)处,然后跳转到 4K(Kernel)处。
  3. 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语言 打印出了字符串。
在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值