2019年5月17日 星期五

uefi os loader (4) - 把所有的努力集合起來, 載入 os kernel

凡經我手, 必屬佳作
經歷過前面幾個階段的努力之後, 再來便是把他們集合起來, 準備發大財, 阿 ... 不是, 是載入 kernel。

最早是以 binary 的方式載入 kernel, 到最近修改的版本, 已經可以載入 elf64 kernel 了, 為什麼堅持要載入 elf64, 並不是我太閒, 想要展示自己的本事, 而是我希望可以作到 relocation 的 kernel, 在simple os 時, 我還沒有理解 relocation, 所以 simple os 需要放在特定位址才能執行, 現在我想做一個可以 relocation 的 kernel, 放到那個位址, 都可以正常執行, 酷吧! 所以需要 elf64 的其他資訊來幫忙作到這件事情, 不過本篇不會提到 elf64, 這會讓事情變得複雜一點, 還是以 bin 的 kernel 來解說。

先來看段影片, 這是在真實機器上載入 kernel 的畫面, kernel 使用 GOP 來畫出文字, 看到 i am 64bit kernel 時, 心中真是感動, 你一定無法體會我吃了多少苦頭, 才讓這個字串正常出現。

影片看起來成功執行在真實機器上, 可惜並不是每台真實機器都可以正常執行, 我在別台機器上, 就無法順利看到最後的 i am 64bit kernel 畫面, 還有努力的空間。

大抵上就是先讀入 kernel binary 檔案, 再來是使用 GOP, 然後取得 memory map, 最後就是跳到 kernel 位址, 步驟其實並不複雜。

把之前介紹的 3 段程式組合起來即可,
  1. uefi os loader (1) - read a file
  2. uefi os loader (2) - 如何秀字
  3. uefi os loader (3) - get memory map
這個 uefi os loader 並不是很完整, 例如讀取 kernel 的程式碼就不嚴謹, kernel 超過某個大小, 就無法讀取完整。

memory map 的資料我也沒存起來給 kernel, 而 system table 的位址我也沒存起來, 之後 kernel 要使用 uefi runtime service 就無法使用。

但作為練習, 她已經夠用, 也足夠讓我繼續開發 os kernel 了。

這是另外一個 uefi os loader, 也是類似的作法。
https://github.com/naoki9911/edk2/tree/f6392fdaa72cf3f54d43a00b26a74ce0e1184027/xv6_bootloader

我可不是抄他的作法, 在我對於 uefi os loader 有了實做想法之後, 我才發現這個 uefi xv6 的 uefi os loader, 這是英雄所見略同, 我們都不想自己寫硬碟讀取程式。



還沒提到的部份是怎麼跳到 kernel 的位址, kernel 的載入位址我存在 (CONFIG_SYS_TEXT_BASE - 8), 取出來之後, 轉型成 function pointer 執行即可 (loader.c L92)。

另外有一個 goto 的用法是 gnu extension, 我之前傻傻的不知道, 還以為自己發現了一個 goto 的特殊用法, 原來是 gnu extension, loader.c L92。

loader.c
 1 #include "types.h"
 2 #include "const.h"
 3 
 
73 int loader_main()
74 {

81   com1_puts("jump to kernel ...\n");

86   u64 addr = *(u64*)(CONFIG_SYS_TEXT_BASE-8);
87   char str[32];
88   itoa_32((int)addr, str);
89   com1_puts("\njump address: ");
90   com1_puts(str);
91   com1_puts("\n");
92   (*(void(*)())addr)();
92   //goto *(u64*)addr; // gcc extension: 6.3 Labels as Values 
93   while(1);
94 }

kernel 的程式碼很簡單, 印出字串而已, 然後就無窮迴圈。而為了簡單, 當時只用 com1 印出字串, 為了在真實機器上驗證, 我還特地用了另一台 PC, 連上 com1 線, 打開 minicom 連入, 看看字串有沒正常秀出, 結果當然是沒有, 模擬器正常的行為, 在真實機器上是錯誤的, 原因是我需要正常操作 com1 暫存器, 等有資料時, 才能去讀取資料, qemu 模擬器自然沒這問題。

uefi os loader 告一個段落, 再來就是 os kernel 的難題了, 重複的東西又要再來一次了, 這伴隨著新痛苦的到來, 有點累了, 先這樣吧!

k.c 沒有 c runtime, 所以某些 c 語法可能會有令人意想不到的錯誤, 不過作為一個測試用的 kernel, 它已經表現的很好了。

k.c
 1 
 2 void k_main();
 3 void _start()
 4 {
 5   k_main();
 6 }
 7 
 8 #include "uart.h"
 9 
16 void k_main()
17 {
18 #ifdef OS_64BIT
19   const char *s = "\ni am 64bit kernel\n";
20 #else
21   const char *s = "\ni am 32bit kernel\n";
22 #endif
25   com1_puts(s);
<
32   while(1);
33 }

由於在 x86_64, 可以執行 16/32/64 bit code, 而在 uefi 的環境中, 已經切到 long mode, 可以執行 32bit, 64bit 程式碼, 所以 k.c 才準備了 32/64 bit 2 個版本的 kernel, 不過目前以 64 bit kernel 為主。

由於 simple os 是 32bit, 所以我才費心研讀了如何載入 32bit kernel, 不想重新寫一個 os kernel, 不過在我深入理解之後, 就算是 32bit kernel, 在處理 isr 時, 還是得使用 64 bit code, 所以, 還是得碰 64bit 的部份, 閃不掉。

list 1 qemu gdt
gdt: 47, addr: 07ED7F18
0: 0  0
8: CF9200  FFFF
10: CF9F00  FFFF
18: CF9300  FFFF
20: CF9A00  FFFF
28: 0  0
30: CF9300  FFFF
38: AF9A00  FFFF
40: 0  0
32-bit code segment: 10

list 1 是 qemu uefi 環境的 gdt, 要載入 32bit kernel, 需要選擇不支援 64 bit 的 selector (0x10), 要載入 64bit kernel, 需要選擇支援 64 bit 的 selector (0x38), 但由於 uefi 預設就使用 0x38, 所以要載入 64bit kernel, 並不用做什麼特別處理。

gdt descriptor bit 53 是 L bit, 決定是不是使用 64 bit 模式來執行, fig 1 0x10 selector gdt descriptor 值是 0xcf9f000000ffff, bit 53 不是 1, 所以是執行在 32bit mode; fig 2 selector 0x38 gdt descriptor 值是 0xaf9a000000ffff, bit 53 是 1, 所以是執行在 64bit mode。

要載入 32bit kernel 當然不是只有選用這個 selector 還有其他後續的東西要設定, 有點複雜, 所以先不管, 先處理 64bit kernel。

其實我測試過用 0x10 32bit selector 執行 64bit kernel, 看起來可以執行, 但是在 lea _DYNAMIC(%rip), %rsi 這樣的指令時, 似乎會出錯, 而這種 lea 用法, 也的確是在 64bit 環境下的用法。

uefi os loader 系列到此先告一個段落, 接下來不是急著寫一個 x64 os kernel, 要讓自己輕鬆一下, 再次安排日本自助行。

fig 1 selector 0x10, bit 53 不是 1
fig 2 selector 0x38, bit 53 是 1

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。