從零開始寫一個操作系統內核 筆記(五) 從彙編過渡到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語言 打印出了字符串。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章