顯示具有 uefi 標籤的文章。 顯示所有文章
顯示具有 uefi 標籤的文章。 顯示所有文章

2019年6月14日 星期五

uefi os loader (5.1) - 載入 relocatable kernel (0) - 找出 entry point

三更燈火五更雞
之前開發的 simple os 並不是 relocatable kernel, 所以她必須被載入到特定位址才能執行。

而在《[code] 自己移動自己 - relocation》之中, 我獲得了 relocation 的技能, 現在結合 uefi os loader, 來打造一個 relocatable kernel。

也許你會問 relocatable kernel 和 os loader 會有關係嗎?

的確, relocation 的程式碼都在 kernel 上, 我所設計的方式並不是由 os loader 來做 relocation 的修改, kernel 一開始就會對自己做 relocation 的動作 (這段程式得保證是 relocation, 因為沒有人會來修正這段程式的 relocation), 不過由於之前 os loader 載入 kernel 的方式和這個不太一樣, 所以還是和 os loader 有點關係。

先提這次遇到的幾個問題:
在 debian/gcc 8.3 平台的環境編譯的 k64.elf, Dynamic section 的 RELASZ 是非 0 值, 這是正常的, 這個數字就是代表要修改幾個 relocation 數值; 但是在 Ubuntu 16.04.1 LTS, gcc (Ubuntu 8.1.0-5ubuntu1~16.04) 8.1.0/GNU ld (GNU Binutils for Ubuntu) 2.26.1 環境下編譯的 k64.elf, RELASZ 是 0, 所以程式不會做任何的 relocation 動作, 但是會多了一個 RELACOUNT, 紀錄 R_X86_64_RELATIVE 個數, 也可以拿來計算要做幾次 relocation 的修改。

Program Headers 在某個 linker script 會產生 2 個 LOAD 標籤的 segment, 有時候只有一個, 我是想要有 2 個, 將 text, rodata section 集合成一個 segment, 另一個是 data section 集合的 segment, 不過目前還不知道怎麼寫出這種 linker script。

先把上述 2 個問題記錄下來, 也許以後有機會可以解除疑惑。

[載入 elf64 kernel]

首先要載入 elf64 kernel, 直接載入一個 elf64 kernel, 比較麻煩的是要算出那個位址才是 kernel 的進入點。和載入固定位址的 kernel 不同, 我打算算出這個 kernel entry, 直接跳到該位址。

這要從 elf program header 來觀察, 需要 segment 的資訊。list 0 L14, L16 是這個 elf 的可執行的相關內容, 要載入一個 elf 執行檔, 就是把這部份載入到對應的記憶體位址, 在跳到 entry 就可以執行這個 elf, 寫一個 elf loader 比想像中還容易吧!

list 0 readelf -l k64.elf
 1 
 2 Elf file type is EXEC (Executable file)
 3 Entry point 0x5010
 4 There are 6 program headers, starting at offset 64
 5 
 6 Program Headers:
 7   Type           Offset             VirtAddr           PhysAddr
 8                  FileSiz            MemSiz              Flags  Align
 9   PHDR           0x0000000000000040 0x0000000000004040 0x0000000000004040
10                  0x0000000000000150 0x0000000000000150  R      0x8
11   INTERP         0x0000000000006020 0x000000000000a020 0x000000000000a020
12                  0x000000000000000f 0x000000000000000f  R      0x1
13       [Requesting program interpreter: /lib/ld64.so.1]
14   LOAD           0x0000000000000000 0x0000000000004000 0x0000000000004000
15                  0x000000000000299d 0x000000000000299d  R E    0x1000
16   LOAD           0x0000000000003000 0x0000000000007000 0x0000000000007000
17                  0x000000000000442c 0x0000000000004448  RW     0x1000
18   DYNAMIC        0x0000000000003000 0x0000000000007000 0x0000000000007000
19                  0x0000000000000110 0x0000000000000110  RW     0x8
20   GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
21                  0x0000000000000000 0x0000000000000000  RWE    0x10
22 
23  Section to Segment mapping:
24   Segment Sections...
25    00     
26    01     .interp 
27    02     .hash .text 
28    03     .dynamic .rela .dynsym .dynstr .interp .gnu.hash .rodata.k_relocate_elf32.str1.1 .eh_frame .rodata.test_func.str1.1 .rodata.rel_dyn_test.str1.1 .rodata.k_main.str1.1 .data .data.rel.local.test_func_val .data.test_val .bss 
29    04     .dynamic 
30    05     

以下的欄位就是載入/執行 elf 的關鍵。

table 1. segment 屬性
offset virtAddr filesiz
0 4000 299d
3000 7000 442c

這個 elf 的 entry point 是 5010。

只要參照 list 1 的 pseudo code, 就可以寫出載入/執行 elf 的程式, 也就是 elf loader。

list 1 pseudo code
memcpy (0x4000, 0, 0x299d)
memcpy (0x7000, 0x3000, 0x442c)
goto 0x5010

這樣就跳到這個 elf 執行檔並且開始執行。

fig 1 對應到 list 0 的 LOAD segment, 目標要算出 entry point X 是多少?
fig 1 左邊的圖 0, 3000 是 elf file 的 offset, 右邊的圖是在記憶體的絕對位址。
fig 1 左邊的圖 0x1000 是我假設的載入到記憶體的位址。

假設 elf 被載入到 0x1000 的位址,

先用
entry 5010
offset: 0
virtAddr: 4000
來計算:

X - 0 = 5010 - 4000
X = 1010

第二組
entry 5010
offset: 3000
virtAddr: 7000

3000 - X = 7000 - 5010
X = 1010

不管用那一組 segment 值做計算, 都會得到 1010, 所以不用擔心要用那一組 segment 的值來計算。

X 就是在 elf 開始的 offset, 加上 elf 的載入位址 0x1000 就是要跳去執行的 entry point, 也就是 1010 + 1000 = 2010。

fig 1 elf load segment layout

所以在載入這個 elf 到 0x1000 位址後, 跳到 2010 的位址執行就可以了。
如果這個 elf 是載入到 0x6000 位址後, 則跳到 7010 的位址執行。

而 elf 是透過 uefi boot service api 載入的。

這樣就不用一定要把 segment 複製到 0x3000, 0x7000 在跳去 0x5010, 當然, 前提是這個 elf 執行檔, 可以在任意位址執行才行。

下篇讓我們看看怎麼讓 elf 執行檔有這個能力。

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

2019年4月26日 星期五

uefi os loader (3) - get memory map

藏拙於巧、用晦而明、寓清於濁、以屈為伸
這個 api 用來取得記憶體配置資訊, 雖然有關的 api 只有 GetMemoryMap(), 但因為記憶體不同的分類而顯得複雜。除了需要知道這些記憶體的分類的開頭位址和大小, 也要知道他們可以被如何利用。

而 GetMemoryMap() 本身也不是很容易使用的函式。
EFI_BOOT_SERVICES.GetMemoryMap() Prototype
Summary Returns the current memory map.

typedef EFI_STATUS (EFIAPI *EFI_GET_MEMORY_MAP) 
(
  IN OUT UINTN *MemoryMapSize,
  IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap,
  OUT UINTN *MapKey,
  OUT UINTN *DescriptorSize,
  OUT UINT32 *DescriptorVersion
);

uefi runtime service 可以在進入 os 之後繼續提供服務, 所以需要知道 runtime service 的記憶體位址, 在進入 os 之後, map 出來讓 os 繼續使用。

如果需要在 os 中呼叫 runtime service, 參考以下 uefi spec 2.7.pdf 節錄的要點 (沒有完全列出)
2.3.4 x64 Platforms
For an operating system to use any UEFI runtime services, it must:
  • Preserve all memory in the memory map marked as runtime code and runtime data
  • Call the runtime service functions, with the following conditions:
  • In long mode, in 64-bit mode
  • Paging enabled
  • All selectors set to be flat with virtual = physical address. If the UEFI OS loader or OS
  • used SetVirtualAddressMap() to relocate the runtime services in a virtual address space, then this condition does not have to be met. See description of SetVirtualAddressMap() for details of memory map after this function has been called.
  • Direction flag in EFLAGs clear
  • ...
EFI_GET_MEMORY_MAP 的用法需要自己傳入夠大的 MemoryMap, 而不是該函式自己配好傳回來, 問題是我怎麼知道我要 alloc 多大的記憶體才夠用, 這是第一個難用的地方。

第一次呼叫 EFI_GET_MEMORY_MAP, 如果給的 MemoryMap 空間太小, 會得到 EFI_BUFFER_TOO_SMALL 錯誤, 並會得知所需要的空間大小 (透過 MemoryMapSize 傳回), 所以可以用 AllocatePool() 重新配置一個大小, 再次呼叫 EFI_GET_MEMORY_MAP。

第二個難點是計算有幾個 EFI_MEMORY_DESCRIPTOR, (MemoryMapSize/DescriptorSize) 就是所有的 EFI_MEMORY_DESCRIPTOR, 一一的把這些 EFI_GET_MEMORY_MAP 印出來, 就可以找到所有的 memory map。

另外的難點是要搞清楚這些記憶體是幹什麼用的, 並不是只知道開始位址, 大小就可以了。

uefi shell memmap 指令可以列出 memmap, 可以用來對照自己抓出來的記憶體範圍有無正確。

typedef struct 
{
  UINT32 Type;
  EFI_PHYSICAL_ADDRESS PhysicalStart;
  EFI_VIRTUAL_ADDRESS VirtualStart;
  UINT64 NumberOfPages;
  UINT64 Attribute;
} EFI_MEMORY_DESCRIPTOR

page 大小是 4k
NumberOfPages: Number of 4 KiB pages in the memory region. NumberOfPages must not be 0, and must not be any value that would represent a memory page with a start address, either physical or virtual, above 0xfffffffffffff000

有這 2 個屬性:
UINT32 Type;
UINT64 Attribute;

Type
typedef enum 
{
  EfiReservedMemoryType,
  EfiLoaderCode,
  EfiLoaderData,
  EfiBootServicesCode,
  EfiBootServicesData,
  EfiRuntimeServicesCode,
  EfiRuntimeServicesData,
  EfiConventionalMemory,
  EfiUnusableMemory,
  EfiACPIReclaimMemory,
  EfiACPIMemoryNVS,
  EfiMemoryMappedIO,
  EfiMemoryMappedIOPortSpace,
  EfiPalCode,
  EfiPersistentMemory,
  EfiMaxMemoryType
} EFI_MEMORY_TYPE

ExitBootServices() 之後, EfiBootServicesCode, EfiBootServicesData 就不是給 boot service 用的, 可以拿來自由運用。

Attribute 有一個 EFI_MEMORY_RUNTIME
EFI_MEMORY_RUNTIME Runtime memory attribute: The memory region needs to be given a virtual mapping by the operating system when SetVirtualAddressMap() is called (described in Section 8.4)

以下提供一個範例程式:
get_memmap.c
  1 // http://www.lab-z.com/51gfxuefi1/
  2 // http://f.osdev.org/viewtopic.php?f=1&t=25587&start=0
  3  
  4 // the code is reference: http://forum.osdev.org/viewtopic.php?f=1&t=26796
  5 
  6 #include "efi.h"
  7 #include "efi_api.h"
  8 
  9 #define MEMMAP_SIZE 400
 10 
 11 //struct efi_mem_desc mem_desc_pool[MEMMAP_SIZE];
 12 unsigned char mem_desc_pool[MEMMAP_SIZE*sizeof(struct efi_mem_desc)];
 13 struct efi_mem_desc *mem_desc;
 14 efi_uintn_t key, desc_size, size;
 15 u32 ver;
 16 typedef int (*PrintFp)(char *str);
 17 
 18 PrintFp print;
 19 
 20 struct efi_system_table *g_sys_table;
 21 
 22 char* itoa(int n, char* str, int radix) 
 23 {
 24   char digit[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 25   char* p=str;
 26   char* head=str;
 27 
 28 //  if(!p || radix < 2 || radix > 36)
 29 //    return p;
 30   if (n==0)
 31   {
 32     *p++='0';
 33     *p=0;
 34     return str;
 35   }
 36 #if 0
 37   if (radix == 10 && n < 0)
 38   {
 39     *p++='-';
 40     n= -n;
 41   }
 42 #endif
 43   while(n)
 44   {
 45     *p++=digit[n%radix];
 46     n/=radix;
 47   }
 48   *p=0;
 49 #if 1
 50   for (--p; head < p ; ++head, --p)
 51   {
 52     char temp=*head;
 53     *head=*p;
 54     *p=temp;
 55   }
 56 #endif
 57   return str;
 58 }
 59 
 60 // output to com1 for debug
 61 int print_char(char ch)
 62 {
 63   asm (
 64        "push %%rdx\n"
 65        "push %%rax\n"
 66        "mov   $0x3f8,%%rdx\n"
 67        "out   %%al,(%%dx)\n"
 68        "pop %%rax\n"
 69        "pop %%rdx\n\t"
 70        :
 71        :"a"(ch)
 72       );
 73   return 0;
 74 }
 75 
 76 int print_str(char *str)
 77 {
 78   char *s = str;
 79   while(*s)
 80   {
 81     print_char(*s);
 82     ++s;
 83   }
 84   return 0;
 85 }
 86 
 87 
 88 // ref: http://wiki.osdev.org/Inline_Assembly/Examples#OUTx
 89 void outb(u16 port, u8 val)
 90 {
 91     asm volatile ( "outb %0, %1" : : "a"(val), "Nd"(port) );
 92     /* There's an outb %al, $imm8  encoding, for compile-time constant port numbers that fit in 8b.  (N constraint).
 93      * Wider immediate constants would be truncated at assemble-time (e.g. "i" constraint).
 94      * The  outb  %al, %dx  encoding is the only option for all other cases.
 95      * %1 expands to %dx because  port  is a uint16_t.  %w1 could be used if we had the port number a wider C type */
 96 }
 97 
 98 
 99 static inline u8 inb(u16 port)
100 {
101     u8 ret;
102     asm volatile ( "inb %1, %0"
103                    : "=a"(ret)
104                    : "Nd"(port) );
105     return ret;
106 }
107 
108 static efi_guid_t gEfiSimpleFileSystemProtocolGuid = EFI_GUID(0x964E5B22, 0x6459, 0x11D2,  0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B);
109 #define PORT 0x3f8
110 
111 // ref: http://wiki.osdev.org/Serial_Ports#Baud_Rate 
112 void init_serial()
113 {
114    outb(PORT + 1, 0x00);    // Disable all interrupts
115    outb(PORT + 3, 0x80);    // Enable DLAB (set baud rate divisor)
116    outb(PORT + 0, 0x03);    // Set divisor to 3 (lo byte) 38400 baud
117    outb(PORT + 1, 0x00);    //                  (hi byte)
118    outb(PORT + 3, 0x03);    // 8 bits, no parity, one stop bit
119    outb(PORT + 2, 0xC7);    // Enable FIFO, clear them, with 14-byte threshold
120    //outb(PORT + 4, 0x0B);  // IRQs enabled, RTS/DSR set
121    outb(PORT + 4, 0x03);    // RTS/DSR set
122 }
123 
124 void uefi_print_char(char c)
125 {
126   char str[4] = {0};
127   str[0] = c;
128   g_sys_table->con_out->output_string(g_sys_table->con_out, str);
129 }
130 
131 void uefi_print(const char *str)
132 {
133   char *s = str;
134   while(*s)
135   {
136     uefi_print_char(*s);
137     ++s;
138   }
139   return 0;
140 }
141 
142 int print_memory_type(u32 memory_type)
143 {
144   if (0 <= memory_type && memory_type <= sizeof(memory_type_str)/sizeof(const char*))
145   {
146     print(memory_type_str[memory_type]);
147     print("\r\n");
148     return 0;
149   }
150   else
151   {
152     print("not found\r\n");
153     return -1;
154   }
155 }
156 
157 const char * mystr="abc123";
158 const char * mystr_array[]={"mnq", "12345678"};
159 
160 efi_status_t EFIAPI efi_main(efi_handle_t image, struct efi_system_table *sys_table)
161 {   
162   efi_uintn_t mode_num;
163   efi_status_t status;
164   mem_desc = mem_desc_pool;
165   //static struct efi_mem_desc desc[200];
166 
167   efi_status_t ret;
168 
169   g_sys_table = sys_table;
170   sys_table->con_out->output_string(sys_table->con_out, L"\r\nBBB\n\r");
171 
172   print_str("\r\naaa\r\n");
173   uefi_print("\r\nuefi print\r\n");
174   uefi_print(mystr);
175 
176   print = uefi_print;
177   print("\r\n");
178   print_str(mystr_array[0]);
179   print("\r\n");
180   print_str(mystr_array[1]);
181   print("\r\n");
182 
183 
184 
185   print("\r\n");
186   print(memory_type_str[0]);
187   print("\r\n");
188   print(memory_type_str[1]);
189   print("\r\n");
190   print(memory_type_str[2]);
191   print("\r\n");
192 
193   //init_serial();
194 
195   print("test get memory map\r\n");
196 
197   /* Get the memory map so we can switch off EFI */
198   //size = 0;
199   size = sizeof(mem_desc_pool);
200   void *buf = NULL;
201   char str[20];
202 
203   itoa(sizeof(struct efi_mem_desc), str, 10);
204   print_str("\r\nsizeof (struct efi_mem_desc): ");
205   print_str(str);
206   print_str("\r\n");
207 
208   itoa(size, str, 10);
209   print_str("\r\nsize: ");
210   print_str(str);
211   print_str("\r\n");
212 
213   //while(1)
214   for (int i=0 ; i < 2 ; ++i)
215   {
216     ret = sys_table->boottime->get_memory_map(&size, mem_desc, &key, &desc_size, &ver);
217     if (ret != EFI_SUCCESS)
218     {
219       //break;
220       if (ret == EFI_BUFFER_TOO_SMALL) 
221       {
222         // use allocate_pool()
223         size += 1024;
224         print_str("\r\nxx EFI_BUFFER_TOO_SMALL\r\n");
225         //ret = sys_table->boottime->allocate_pool(EfiBootServicesData, size, &buf);
226         itoa(size, str, 10);
227         print_str("size: \r\n");
228         print_str(str);
229         print_str("\r\n");
230       }
231       else 
232       {
233         sys_table->con_out->output_string(sys_table->con_out, L"\r\nget_memory_map fail,  the error is not EFI_BUFFER_TOO_SMALL\n\r");
234         //break;
235       }
236     }
237     else
238     {
239       itoa(size, str, 10);
240       print("size: \r\n");
241       print(str);
242       print("\r\n");
243 
244       itoa(desc_size, str, 10);
245       print("desc_size: \r\n");
246       print(str);
247       print("\r\n");
248 
249       itoa(ver, str, 10);
250       print("ver: \r\n");
251       print(str);
252       print("\r\n");
253 
254       print("\r\nget_memory_map ok\r\n");
255       break;
256     }
257   }
258   print("abc\r\n");
259   #if 1
260   for (int i=0 ; i < (size/desc_size) ; ++i)
261   {
262     struct efi_mem_desc *md = (struct efi_mem_desc *)(mem_desc_pool + (desc_size * i));
263     itoa(i, str, 10);
264     print(str);
265     print("\r\n");
266 
267     print_memory_type(md->type);
268     itoa(md->type, str, 10);
269     print("type: ");
270     print(str);
271     print("\r\n");
272 
273     itoa(md->physical_start, str, 16);
274     print("physical_start: ");
275     print(str);
276     print("\r\n");
277 
278     itoa(md->virtual_start, str, 16);
279     print("virtual_start: ");
280     print(str);
281     print("\r\n");
282 
283     print("physical end: ");
284     u64 end = md->physical_start + (md->num_pages * 4096) - 1;
285     itoa(end, str, 16);
286     print(str);
287     print("\r\n");
288 
289     itoa(md->num_pages, str, 16);
290     print("num_pages: ");
291     print(str);
292     print("\r\n");
293 
294     itoa(md->attribute, str, 16);
295     print("attribute: ");
296     print(str);
297     print("\r\n");
298   }
299   #endif
300   return 0;
348 }

這個資訊需要傳給 os kernel, 這樣 os kernel 才會知道記憶體的分佈範圍。

list 1. qemu uefi shell memmap command
 1 Type       Start            End              # Pages          Attributes
 2 BS_Code    0000000000000000-0000000000000FFF 0000000000000001 000000000000000F
 3 Available  0000000000001000-000000000009FFFF 000000000000009F 000000000000000F
 4 Available  0000000000100000-00000000007FFFFF 0000000000000700 000000000000000F
 5 ACPI_NVS   0000000000800000-0000000000807FFF 0000000000000008 000000000000000F
 6 Available  0000000000808000-000000000080FFFF 0000000000000008 000000000000000F
 7 ACPI_NVS   0000000000810000-00000000008FFFFF 00000000000000F0 000000000000000F
 8 BS_Data    0000000000900000-00000000013FFFFF 0000000000000B00 000000000000000F
 9 Available  0000000001400000-0000000003F35FFF 0000000000002B36 000000000000000F
10 BS_Data    0000000003F36000-0000000003F55FFF 0000000000000020 000000000000000F
11 Available  0000000003F56000-0000000006602FFF 00000000000026AD 000000000000000F
12 LoaderCode 0000000006603000-00000000066D8FFF 00000000000000D6 000000000000000F
13 Available  00000000066D9000-0000000006727FFF 000000000000004F 000000000000000F
14 BS_Data    0000000006728000-0000000006745FFF 000000000000001E 000000000000000F
15 Available  0000000006746000-0000000006763FFF 000000000000001E 000000000000000F
16 BS_Data    0000000006764000-0000000006770FFF 000000000000000D 000000000000000F
17 Available  0000000006771000-0000000006773FFF 0000000000000003 000000000000000F
18 BS_Data    0000000006774000-000000000679AFFF 0000000000000027 000000000000000F
19 Available  000000000679B000-000000000679BFFF 0000000000000001 000000000000000F
20 BS_Data    000000000679C000-00000000068A6FFF 000000000000010B 000000000000000F
21 ACPI_NVS   00000000068A7000-00000000068B4FFF 000000000000000E 000000000000000F
22 Reserved   00000000068B5000-00000000068CCFFF 0000000000000018 000000000000000F
23 RT_Data    00000000068CD000-00000000068CDFFF 0000000000000001 800000000000000F
24 BS_Code    00000000068CE000-00000000069EDFFF 0000000000000120 000000000000000F
25 RT_Data    00000000069EE000-0000000006AB4FFF 00000000000000C7 800000000000000F
26 RT_Code    0000000006AB5000-0000000006B1AFFF 0000000000000066 800000000000000F
27 BS_Data    0000000006B1B000-0000000007A1AFFF 0000000000000F00 000000000000000F
28 BS_Code    0000000007A1B000-0000000007B9AFFF 0000000000000180 000000000000000F
29 RT_Code    0000000007B9B000-0000000007BCAFFF 0000000000000030 800000000000000F
30 RT_Data    0000000007BCB000-0000000007BEEFFF 0000000000000024 800000000000000F
31 Reserved   0000000007BEF000-0000000007BF2FFF 0000000000000004 000000000000000F
32 ACPI_Recl  0000000007BF3000-0000000007BFAFFF 0000000000000008 000000000000000F
33 ACPI_NVS   0000000007BFB000-0000000007BFEFFF 0000000000000004 000000000000000F
34 BS_Data    0000000007BFF000-0000000007DFFFFF 0000000000000201 000000000000000F
35 Available  0000000007E00000-0000000007EF3FFF 00000000000000F4 000000000000000F
36 BS_Data    0000000007EF4000-0000000007F13FFF 0000000000000020 000000000000000F
37 BS_Code    0000000007F14000-0000000007F38FFF 0000000000000025 000000000000000F
38 BS_Data    0000000007F39000-0000000007F41FFF 0000000000000009 000000000000000F
39 BS_Code    0000000007F42000-0000000007F57FFF 0000000000000016 000000000000000F
40 RT_Data    0000000007F58000-0000000007F77FFF 0000000000000020 800000000000000F
41 ACPI_NVS   0000000007F78000-0000000007FFFFFF 0000000000000088 000000000000000F
42  
43   Reserved  :             28 Pages (114,688 Bytes)
44   LoaderCode:            214 Pages (876,544 Bytes)
45   LoaderData:              0 Pages (0 Bytes)
46   BS_Code   :            732 Pages (2,998,272 Bytes)
47   BS_Data   :          7,591 Pages (31,092,736 Bytes)
48   RT_Code   :            150 Pages (614,400 Bytes)
49   RT_Data   :            268 Pages (1,097,728 Bytes)
50   ACPI_Recl :              8 Pages (32,768 Bytes)
51   ACPI_NVS  :            402 Pages (1,646,592 Bytes)
52   MMIO      :              0 Pages (0 Bytes)
53   MMIO_Port :              0 Pages (0 Bytes)
54   PalCode   :              0 Pages (0 Bytes)
55   Available :         23,279 Pages (95,350,784 Bytes)
56   Persistent:              0 Pages (0 Bytes)
57               -------------- 
58 Total Memory:            127 MB (133,709,824 Bytes)

list 2. my_getmap
  1 get_memory_map ok
  2 abc
  3 0
  4 EFI_BOOT_SERVICES_CODE
  5 type: 3
  6 physical_start: 0
  7 virtual_start: 0
  8 physical end: FFF
  9 num_pages: 1
 10 attribute: F
 11 1
 12 EFI_CONVENTIONAL_MEMORY
 13 type: 7
 14 physical_start: 1000
 15 virtual_start: 0
 16 physical end: 9FFFF
 17 num_pages: 9F
 18 attribute: F
 19 2
 20 EFI_CONVENTIONAL_MEMORY
 21 type: 7
 22 physical_start: 100000
 23 virtual_start: 0
 24 physical end: 7FFFFF
 25 num_pages: 700
 26 attribute: F
 27 3
 28 EFI_ACPI_MEMORY_NVS
 29 type: 10
 30 physical_start: 800000
 31 virtual_start: 0
 32 physical end: 807FFF
 33 num_pages: 8
 34 attribute: F
 35 4
 36 EFI_CONVENTIONAL_MEMORY
 37 type: 7
 38 physical_start: 808000
 39 virtual_start: 0
 40 physical end: 80FFFF
 41 num_pages: 8
 42 attribute: F
 43 5
 44 EFI_ACPI_MEMORY_NVS
 45 type: 10
 46 physical_start: 810000
 47 virtual_start: 0
 48 physical end: 8FFFFF
 49 num_pages: F0
 50 attribute: F
 51 6
 52 EFI_BOOT_SERVICES_DATA
 53 type: 4
 54 physical_start: 900000
 55 virtual_start: 0
 56 physical end: 13FFFFF
 57 num_pages: B00
 58 attribute: F
 59 7
 60 EFI_CONVENTIONAL_MEMORY
 61 type: 7
 62 physical_start: 1400000
 63 virtual_start: 0
 64 physical end: 3F35FFF
 65 num_pages: 2B36
 66 attribute: F
 67 8
 68 EFI_BOOT_SERVICES_DATA
 69 type: 4
 70 physical_start: 3F36000
 71 virtual_start: 0
 72 physical end: 3F55FFF
 73 num_pages: 20
 74 attribute: F
 75 9
 76 EFI_CONVENTIONAL_MEMORY
 77 type: 7
 78 physical_start: 3F56000
 79 virtual_start: 0
 80 physical end: 6602FFF
 81 num_pages: 26AD
 82 attribute: F
 83 10
 84 EFI_LOADER_CODE
 85 type: 1
 86 physical_start: 6603000
 87 virtual_start: 0
 88 physical end: 66D8FFF
 89 num_pages: D6
 90 attribute: F
 91 11
 92 EFI_CONVENTIONAL_MEMORY
 93 type: 7
 94 physical_start: 66D9000
 95 virtual_start: 0
 96 physical end: 6727FFF
 97 num_pages: 4F
 98 attribute: F
 99 12
100 EFI_BOOT_SERVICES_DATA
101 type: 4
102 physical_start: 6728000
103 virtual_start: 0
104 physical end: 6745FFF
105 num_pages: 1E
106 attribute: F
107 13
108 EFI_CONVENTIONAL_MEMORY
109 type: 7
110 physical_start: 6746000
111 virtual_start: 0
112 physical end: 674FFFF
113 num_pages: A
114 attribute: F
115 14
116 EFI_LOADER_CODE
117 type: 1
118 physical_start: 6750000
119 virtual_start: 0
120 physical end: 675CFFF
121 num_pages: D
122 attribute: F
123 15
124 EFI_CONVENTIONAL_MEMORY
125 type: 7
126 physical_start: 675D000
127 virtual_start: 0
128 physical end: 6763FFF
129 num_pages: 7
130 attribute: F
131 16
132 EFI_BOOT_SERVICES_DATA
133 type: 4
134 physical_start: 6764000
135 virtual_start: 0
136 physical end: 6770FFF
137 num_pages: D
138 attribute: F
139 17
140 EFI_CONVENTIONAL_MEMORY
141 type: 7
142 physical_start: 6771000
143 virtual_start: 0
144 physical end: 6773FFF
145 num_pages: 3
146 attribute: F
147 18
148 EFI_BOOT_SERVICES_DATA
149 type: 4
150 physical_start: 6774000
151 virtual_start: 0
152 physical end: 68A6FFF
153 num_pages: 133
154 attribute: F
155 19
156 EFI_ACPI_MEMORY_NVS
157 type: 10
158 physical_start: 68A7000
159 virtual_start: 0
160 physical end: 68B4FFF
161 num_pages: E
162 attribute: F
163 20
164 EFI_RESERVED_MEMORY_TYPE
165 type: 0
166 physical_start: 68B5000
167 virtual_start: 0
168 physical end: 68CCFFF
169 num_pages: 18
170 attribute: F
171 21
172 EFI_RUNTIME_SERVICES_DATA
173 type: 6
174 physical_start: 68CD000
175 virtual_start: 0
176 physical end: 68CDFFF
177 num_pages: 1
178 attribute: F
179 22
180 EFI_BOOT_SERVICES_CODE
181 type: 3
182 physical_start: 68CE000
183 virtual_start: 0
184 physical end: 69EDFFF
185 num_pages: 120
186 attribute: F
187 23
188 EFI_RUNTIME_SERVICES_DATA
189 type: 6
190 physical_start: 69EE000
191 virtual_start: 0
192 physical end: 6AB4FFF
193 num_pages: C7
194 attribute: F
195 24
196 EFI_RUNTIME_SERVICES_CODE
197 type: 5
198 physical_start: 6AB5000
199 virtual_start: 0
200 physical end: 6B1AFFF
201 num_pages: 66
202 attribute: F
203 25
204 EFI_BOOT_SERVICES_DATA
205 type: 4
206 physical_start: 6B1B000
207 virtual_start: 0
208 physical end: 7A1AFFF
209 num_pages: F00
210 attribute: F
211 26
212 EFI_BOOT_SERVICES_CODE
213 type: 3
214 physical_start: 7A1B000
215 virtual_start: 0
216 physical end: 7B9AFFF
217 num_pages: 180
218 attribute: F
219 27
220 EFI_RUNTIME_SERVICES_CODE
221 type: 5
222 physical_start: 7B9B000
223 virtual_start: 0
224 physical end: 7BCAFFF
225 num_pages: 30
226 attribute: F
227 28
228 EFI_RUNTIME_SERVICES_DATA
229 type: 6
230 physical_start: 7BCB000
231 virtual_start: 0
232 physical end: 7BEEFFF
233 num_pages: 24
234 attribute: F
235 29
236 EFI_RESERVED_MEMORY_TYPE
237 type: 0
238 physical_start: 7BEF000
239 virtual_start: 0
240 physical end: 7BF2FFF
241 num_pages: 4
242 attribute: F
243 30
244 EFI_ACPI_RECLAIM_MEMORY
245 type: 9
246 physical_start: 7BF3000
247 virtual_start: 0
248 physical end: 7BFAFFF
249 num_pages: 8
250 attribute: F
251 31
252 EFI_ACPI_MEMORY_NVS
253 type: 10
254 physical_start: 7BFB000
255 virtual_start: 0
256 physical end: 7BFEFFF
257 num_pages: 4
258 attribute: F
259 32
260 EFI_BOOT_SERVICES_DATA
261 type: 4
262 physical_start: 7BFF000
263 virtual_start: 0
264 physical end: 7DFFFFF
265 num_pages: 201
266 attribute: F
267 33
268 EFI_CONVENTIONAL_MEMORY
269 type: 7
270 physical_start: 7E00000
271 virtual_start: 0
272 physical end: 7EF3FFF
273 num_pages: F4
274 attribute: F
275 34
276 EFI_BOOT_SERVICES_DATA
277 type: 4
278 physical_start: 7EF4000
279 virtual_start: 0
280 physical end: 7F13FFF
281 num_pages: 20
282 attribute: F
283 35
284 EFI_BOOT_SERVICES_CODE
285 type: 3
286 physical_start: 7F14000
287 virtual_start: 0
288 physical end: 7F38FFF
289 num_pages: 25
290 attribute: F
291 36
292 EFI_BOOT_SERVICES_DATA
293 type: 4
294 physical_start: 7F39000
295 virtual_start: 0
296 physical end: 7F41FFF
297 num_pages: 9
298 attribute: F
299 37
300 EFI_BOOT_SERVICES_CODE
301 type: 3
302 physical_start: 7F42000
303 virtual_start: 0
304 physical end: 7F57FFF
305 num_pages: 16
306 attribute: F
307 38
308 EFI_RUNTIME_SERVICES_DATA
309 type: 6
310 physical_start: 7F58000
311 virtual_start: 0
312 physical end: 7F77FFF
313 num_pages: 20
314 attribute: F
315 39
316 EFI_ACPI_MEMORY_NVS
317 type: 10
318 physical_start: 7F78000
319 virtual_start: 0
320 physical end: 7FFFFFF
321 num_pages: 88
322 attribute: F

list 1 是 uefi shell memmap 指令列出的, list 2 是 get_memmap.c 程式的輸出, 我用以下指令將 ouput 存檔。

qemu-system-x86_64 -drive if=ide,file=fat:rw:hda-contents,index=0,media=disk -bios OVMF.fd -net none -serial file:com1_file

要確認是不是有抓到正確的 memory map, 可以對照 memmap 的結果, 除了 virtual_start 是 0 之外, 開始位址, 結束位置, memory type, attribute 都是對的。總共有 40 塊 EFI_MEMORY_DESCRIPTOR。

ref:
  1. ACPI之 系统地址映射接口
  2. Step to UEFI Shell (5)----获得Shell下内存分配状况
  3. A Tour beyond BIOS Memory Map Design in UEFI BIOS
  4. Get EFI/UEFI Memory Map(有討論什麼是 Multiboot)
  5. Boot Services vs Runtime Services
  6. kernel 如何调用uefi的runtime service
  7. UEFI Test Tools For Linux Developers

2019年3月14日 星期四

uefi os loader (2) - 使用 GOP 來秀字

囝仔放尿泉過溪,老人放尿滴著鞋。
在 uefi 環境下, 有對應的 print 可以使用, uefi 提供的服務有 boot service, runtime service, print 相關的函式屬於 boot service, 但是一旦呼叫 ExitBootServices() 之後, 就無法使用 print function 這些 boot service, 需要自己使用 framebuffer 來秀字。而在載入 kernel 之前, 需要呼叫 ExitBootServices()。

Graphics Output Protocol (GOP) 就是提供繪圖模式的功能, 所以可以拿來畫圖形, 自然也能拿來畫字。

PI_Spec_1_6.pdf 11.2.3 Additional Classifications
Boot service drivers provide services that are available until the ExitBootServices() function is called. When ExitBootServices() is called, all the memory used by boot service drivers is released for use by an operating system.

UEFI OS loader 需要呼叫 ExitBootServices() 之後才能載入 os kernel, 所以在 os kernel 中, 該怎麼秀訊息?

在 legacy bios 下, 文字模式可以寫入 0xb8000 來印出字元, 繪圖模式是寫入 0xa0000, 這些在 uefi 都已經不能用, 所以連秀字都會是一個問題, 有一個簡單的方式是使用 com1, uefi 會出初始化 com1, 設定 baud rate 115200, 但需要用個 minicom 來看輸出, 會有點麻煩, 而且 notebook 也沒有 com1 可以用。

就算在 uefi loader 階段可以接受 com1 輸出, 但你不想在 os kernel 也用 com1 輸出吧! 感覺這 os kernel 很弱, 觀感不好。所以在這階段, 要把 frame buffer 資訊存起來, 傳遞給 os kernel。

這裡使用的流程是:
  1. 使用 gEfiGraphicsOutputProtocolGuid protocol 取得 framebuffer 相關資訊。
  2. 使用 Gop->QueryMode 找出想要設定的顯示模式。
  3. 再用 Gop->SetMode 設定顯示模式。
vga.c L117 ~ L146 就是在做這樣的事情, 以這個範例來說, 會設定為螢幕為 640X480X32bit color, 3 原色順序是: blue, green, red, reserve, 共 4 byte。

vga.c L276 gop->mode->fb_base 就是 framebuffer 開始位址, 對應到 (0,0) 這個螢幕座標。這就是 legacy bios 0xa0000 的對應值, 把顏色值寫到這區, 就可以畫圖了。

vga.c
  1 // http://www.lab-z.com/51gfxuefi1/
  2 // http://f.osdev.org/viewtopic.php?f=1&t=25587&start=0
  3  
  4 // the code is reference: http://forum.osdev.org/viewtopic.php?f=1&t=26796
  5 
 81 
 82 efi_status_t efi_main(efi_handle_t image, struct efi_system_table *sys_table)
 83 {   
 84   efi_uintn_t mode_num;
 85   efi_status_t status;
 86   efi_uintn_t key, desc_size, size;
 87   struct efi_mem_desc *desc;
 88   //static struct efi_mem_desc desc[200];
 89   efi_handle_t op_handle;
 90 
 91     efi_uintn_t handle_count = 0;
 92     efi_handle_t* handle_buffer;
 93     struct efi_gop* gop;
 94     struct efi_gop_mode_info* gop_mode_info;
 95     efi_uintn_t size_of_info;
 96 
 97   u32 version;
 98   efi_status_t ret;
 99 
100   init_serial();
101 #if 1
102   efi_guid_t loaded_image_guid = LOADED_IMAGE_PROTOCOL_GUID;
103   struct efi_loaded_image *loaded_image;
104 
105   ret = sys_table->boottime->open_protocol(image, &loaded_image_guid,
106       (void **)&loaded_image, image,
107       NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
108 
109   if (ret) 
110   {
111     sys_table->con_out->output_string(sys_table->con_out, L"Failed to get loaded image protocol\n\r");
112 
113     return ret;
114   }
115 #endif
116 
117 #if 1
118     status = sys_table->boottime->locate_handle_buffer( BY_PROTOCOL,
119                                       &EFI_GOP_GUID,
120                                       NULL,
121                                       &handle_count,
122                                       &handle_buffer );
123 
124     if (status != EFI_SUCCESS)
125         PreBootHalt( sys_table->con_out, L"LocateHandleBuffer() failed" );
126     status = sys_table->boottime->handle_protocol( handle_buffer[0],
127                                   &EFI_GOP_GUID,
128                                   (void **)&gop );
129     if (status != EFI_SUCCESS)
130         PreBootHalt( sys_table->con_out, L"HandleProtocol() failed" );
131 
132     for (mode_num = 0;
133          (status = gop->query_mode( gop, mode_num, &size_of_info, &gop_mode_info )) == EFI_SUCCESS;
134          mode_num++) {
135         if (gop_mode_info->width == DESIRED_HREZ &&
136               gop_mode_info->height == DESIRED_VREZ &&
137               gop_mode_info->pixel_format == DESIRED_PIXEL_FORMAT)
138             break;
139     }
140 
141     if (status != EFI_SUCCESS)
142         PreBootHalt( sys_table->con_out, L"Failed to find desired mode" );
143 
144     if (gop->set_mode( gop, mode_num) != EFI_SUCCESS)
145         PreBootHalt( sys_table->con_out, L"SetMode() failed" );
146 #endif
147 
148   sys_table->con_out->output_string(sys_table->con_out, L"005 test vga\n\r");
149 
150   /* Get the memory map so we can switch off EFI */
151   size = 0;
152 
153   ret = sys_table->boottime->get_memory_map(&size, NULL, &key, &desc_size, &version);
154   if (ret != EFI_BUFFER_TOO_SMALL) 
155   {
156     sys_table->con_out->output_string(sys_table->con_out, L"\r\nret is not EFI_BUFFER_TOO_SMALL\n\r");
157     return ret;
158   }
159 
160 #if 1
161         void *buf = NULL;
162         ret = sys_table->boottime->allocate_pool(loaded_image->image_data_type, size, &buf);
163 
164  desc = buf;
165  if (!desc) 
166         {
167           sys_table->con_out->output_string(sys_table->con_out, L"\r\nNo memory for memory descriptor\n\r");
168           return ret;
169  }
170 #endif
176 
177   size += 1024; /* Since doing a malloc() may change the memory map! */
178         //ret = sys_table->boottime->get_memory_map(&size, &desc[0], &key, &desc_size, &version);
179         ret = sys_table->boottime->get_memory_map(&size, desc, &key, &desc_size, &version);
180 
181         if (ret != EFI_SUCCESS) 
182         {
183           sys_table->con_out->output_string(sys_table->con_out, L"\r\nget memory map fail\n\r");
184           return ret;
185         }
186 
187         if (sys_table && image)
188           ret = sys_table->boottime->exit_boot_services(image, key);
189         #if 1
190  if (ret) 
191         {
192   /*
193    * Unfortunately it happens that we cannot exit boot services
194    * the first time. But the second time it work. I don't know
195    * why but this seems to be a repeatable problem. To get
196    * around it, just try again.
197    */
198             print_str("11 exit_boot_services fail\r\n");
199   size = sizeof(desc);
200   ret = sys_table->boottime->get_memory_map(&size, desc, &key, &desc_size,
201         &version);
202   if (ret) {
203             print_str("22 get_memory_map fail\r\n");
204    return ret;
205   }
206   ret = sys_table->boottime->exit_boot_services(image, key);
207   if (ret) {
208             print_str("22 exit_boot_services fail\r\n");
209    return ret;
210   }
211  }
212         #endif
213         print_str("exit_boot_services ok\r\n");
214 
271   draw_rect(gop->mode->fb_base, 0x0000ff00);
272   u8 ch = 'A';
273   for (int y = 0 ; y < 30 ; ++y)
274     for (int x = 0 ; x < 80 ; ++x)
275     {
276       draw_char(gop->mode->fb_base, 0 + x*8, 0+y*16, 0x00ff0000, ch);
277       ch = ch + 1;
278       if ('Z' <  ch)
279         ch = 'A';
280     }
283   while(1);
284   return 0; // cannot return, after call exit_boot_services, cannot return to uefi environment,
285             // in qemu, it will dump fault message.
286             // so I put while(1); before return 0;
287 }
288 

既然可以畫一個點, 就可以畫任何東西, 那該怎麼畫中/英文字呢? 中文麻煩了不只一點, 但英文已經有人做了, 我從 u-boot/include/video_font.h 取得 8X16 英文字形檔, 然後畫出所有英文字母。

fig1/fig2 分別是在模擬器和真實機器上的測試畫面。

fig 1 qemu 環境, 640*480*32bit color

fig 2 在實際的 pc 上測試

ref:
  1. Ditching legacy BIOS in favor of UEFI
  2. UEFI Video after ExitBootServices()
  3. Get EFI/UEFI Memory Map
  4. set 80 x 25 text mode by UEFI
  5. Check Available Text And Graphic Modes From UEFI Shell
  6. Replacing VGA, GOP implementation for UEFI

2018年3月9日 星期五

uefi os loader (1) - read a file

耳聞之不如目見之, 目見之不如足踐之
繼上次可以很單純的《不使用 edk2 來開發 uefi 程式》之後, 讓我對於 uefi 程式的撰寫有了動力, 畢竟要用 edk2 這種我不熟悉的開發工具, 實在是提不上勁。之後的範例沒特別說明的話, 都是以此方式來開發, 不再使用 edk2。

這次要練習使用 uefi api 來讀取檔案, 為什麼要練習這個, 因為 uefi os loader 應該會需要有讀檔案的能力, 我不打算自己撰寫磁碟驅動程式, 能先在 uefi 環境載入檔案在方便不過。我打算取讀 os kernel, 然後跳到哪裡去執行, 所以先來個讀檔練習。

讀取檔案程式員應該不陌生, 不管是 fopen/fread, open/read 都是類似的概念, uefi 也是, 不過在這之前得先取得一個叫 protocol 的東西。這是很特別的概念, 我在其他開發環境沒看過類似的用法, 通常要讀寫檔案, 直接呼叫 read api 即可, 不過在 uefi 環境, 要先取得檔案相關的 protocol, 並用其 protocol 的功能來處理檔案。

取得 protocol 有好幾個不同的 api 可以使用, 而且這些 API 不太好用, 很難理解其用法 我使用的是 LocateProtocol(), 你說我的怎麼是 locate_protocol, 這有點難解釋, 就不解釋了, 因為不是用 edk2, 這是自己宣告的函式名稱。

L16, 43, 就是在取得檔案的 protocol。L16 定義了一組數字, 這組數字就是對應到檔案相關的操作; 每個 protocol 都有定義一組數字, 圖形相關的 protocol 的數字則是

  define EFI_GOP_GUID EFI_GUID(0x9042a9de, 0x23dc, 0x4a38, 0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a)

所以要取得某個服務, 要知道其數字, edk2 沒問題, 這些都定義好了, 自己打造的環境就需要把這些數字自己定義上去。不難, 看 edk2 定義的數字, 依樣畫葫蘆即可。

L52, 66, 78 則是程式員比較習慣的檔案操作, open, read, 多了一個 open_volume (edk2 的名稱是 OpenVolume())。其參數 EFI_FILE_PROTOCOL 提供了相關的檔案操作, open, read ... 你可能發現和 edk2 文件不同, edk2 Open 我用的是 open, Read 我是用 read, 有點奇怪, 這是因為 uefi 程式是在被載入後才使用這些函式, 而不是在 link 階段就把這些函式都定位好, 所以才可以這麼做。

OpenVolume() Prototype
typedef
EFI_STATUS
(EFIAPI *EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_OPEN_VOLUME) (
  IN EFI_SIMPLE_FILE_SYSTEM PROTOCOL *This,
  OUT EFI_FILE_PROTOCOL **Root);
Parameters

Provides file based access to supported file systems.

Contents

 [hide]

read_file.c
  1 /*
  2  * Copyright (c) 2015 Google, Inc
  3  *
  4  * SPDX-License-Identifier: GPL-2.0+
  5  *
  6  * EFI information obtained here:
  7  * http://wiki.phoenix.com/wiki/index.php/EFI_BOOT_SERVICES
  8  *
  9  * Loads a payload (U-Boot) within the EFI environment. This is built as an
 10  * EFI application. It can be built either in 32-bit or 64-bit mode.
 11  */
 12 
 13 #include "efi.h"
 14 #include "efi_api.h"
 15 
 16 static efi_guid_t gEfiSimpleFileSystemProtocolGuid = EFI_GUID(0x964E5B22, 0x6459, 0x11D2,  0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B);
 17 
 18 
 19 #if 0
 20 
 21 // File attributes
 22 //
 23 #define EFI_FILE_READ_ONLY  0x0000000000000001
 24 #define EFI_FILE_HIDDEN     0x0000000000000002
 25 #define EFI_FILE_SYSTEM     0x0000000000000004
 26 #define EFI_FILE_RESERVED   0x0000000000000008
 27 #define EFI_FILE_DIRECTORY  0x0000000000000010
 28 #define EFI_FILE_ARCHIVE    0x0000000000000020
 29 #define EFI_FILE_VALID_ATTR 0x0000000000000037
 30 #endif
 31 
 32 efi_status_t efi_main(efi_handle_t image, struct efi_system_table *sys_table)
 33 {
 34   efi_status_t ret;
 35 
 36   struct efi_simple_text_output_protocol *con = sys_table->con_out;
 37 
 38   con->output_string(con, L"22 test read file\n\r");
 39 
 40 
 41   struct efi_simple_file_system_protocol *sfs;
 42 
 43   ret = sys_table->boottime->locate_protocol(&gEfiSimpleFileSystemProtocolGuid, 0, (void **)&sfs);
 44 
 45   if (ret != EFI_SUCCESS) 
 46   {
 47     con->output_string(con, L"\r\nLocateProtocol failed\n\r");
 48   }
 49 
 50   struct efi_file_handle *root = 0;
 51 
 52   ret = sfs->open_volume(sfs, &root);
 53   if (ret != EFI_SUCCESS) 
 54   {
 55     con->output_string(con, L"\r\nsfs->open_volume failed\n\r");
 56   }
 57 
 58   struct efi_file_handle *fh = 0;
 59   u8 Buf[66]={0};
 60   u16 u16_buf[66]={0};
 61   u64 BufSize = 64;
 62   s16 fn[20]={'.', '\\', 't', '.', 't', 'x', 't', 0};
 63   con->output_string(con, L"open ");
 64   con->output_string(con, fn);
 65   con->output_string(con, L"\r\n");
 66   ret = root->open(root, &fh, L".\\t.txt", EFI_FILE_MODE_READ, 0);
 67 
 68   if(ret == EFI_NOT_FOUND)
 69   {
 70     con->output_string(con, L"\r\nroot->open not found\r\n");
 71   }
 72   if(ret != EFI_SUCCESS)
 73   {
 74     con->output_string(con, L"\r\nroot->open fail\r\n");
 75     return ret;
 76   }
 77 
 78   ret = fh->read(fh, &BufSize, Buf);
 79   if(ret != EFI_SUCCESS)
 80   {
 81     con->output_string(con, L"\r\nroot->read fail\r\n");
 82   }
 83 
 84 
 85   for (int i=0 ; i < BufSize ; ++i)
 86   {
 87     u16_buf[i] = Buf[i];    
 88   }
 89 
 90     if(ret == EFI_SUCCESS)
 91     {
 92       //Buf[BufSize] = 0;
 93 
 94       con->output_string(con, L"\r\nprint t.txt\n\r");
 95       #if 1
 96       con->output_string(con, L"\r\n");
 97       con->output_string(con, u16_buf);
 98       con->output_string(con, L"\r\n");
 99       #endif
100     }
101     con->output_string(con, L"\r\nprint t.txt end\n\r");
102 
103     ret = fh->close(fh);
104 
105 
106 
107   return 0;
108 }

read_file.c 只用到 open, read, close。程式看起來在簡單不過了, 但我卻踩到一個問題。我是使用 usb 隨身碟來載入 efi shell, 也就是說, notebook 內建硬碟已經有了一個 fat EFI parartion, 隨身碟也有一個 fat EFI parartion, 而這個 locate_protocol 找到的檔案操作只能針對內建硬碟的 fat EFI parartion (它只能找到第一個 protocol, 第一個 protocol 對應到 "內建硬碟" 的 fat EFI parartion), 所以我在隨身碟的 fat EFI parartion 執行這個程式會找不到 t.txt, 花了好久時間我才知道有這問題, 所以這隻程式得在內建硬碟的 fat EFI parartion 才能執行成功。

印出來的部份不太正確, 不過不影響整個讀檔的正確性, 要印出來還得將字元轉成 uint16_t, 這是和 linux utf8 編碼的不同之處, 我也不太熟悉這樣的方式。

fig 1. 內建硬碟的 fat EFI parartion 執行成功的結果

補充要完全讀取一個檔案的方法 (不是分段讀取), list 2 是 xv6_uefi/bootloader 的程式碼, 首先要用 list 2 L44 取得 EFI_FILE_INFO*, FileInfo->FileSize 就可以得到這個檔案的大小, 再用 AllocatePages 配置記憶體, 傳給 File->Read, 就可以一次讀取所有檔案內容到記憶體。

但是在我的測試中, 得用 FileInfo->PhysicalSize 取得實體佔用的大小, list 1 是我遇到的問題。

list 1 FileInfo->FileSize 遇到的問題
用 file_info->file_size 配置記憶體, 在 elf64 大小是 44840, 會失敗,
fh->read 不會回錯誤, 但是看起來沒有完全讀取整個檔案內容。

不知道是不是這記憶體不夠 fh->read 用, 但是在 elf64 大小是 102520,
使用 file_info->file_size 配置記憶體又沒問題。

所以目前改用 file_info->physical_size 配置記憶體的大小。

list 2 xv6_uefi/bootloader/file_loader.c
 1 #include <Uefi.h>
 2 #include  <Uefi.h>
 3 #include  <Library/UefiLib.h>
 4 #include  <Library/UefiBootServicesTableLib.h>
 5 #include  <Protocol/BlockIo.h>
 6 #include  <Protocol/LoadedImage.h>
 7 #include  <Protocol/SimpleFileSystem.h>
 8 #include  <Library/DevicePathLib.h>
 9 #include  <Guid/FileInfo.h>
10 #include  <Library/MemoryAllocationLib.h>
11 #include  <Library/BaseMemoryLib.h>
12
42   UINTN FileInfoBufferSize = sizeof(EFI_FILE_INFO) + sizeof(CHAR16) * StrLen(FileName) + 2;
43   UINT8 FileInfoBuffer[FileInfoBufferSize];
44   Status = File->GetInfo(File, &gEfiFileInfoGuid, &FileInfoBufferSize, FileInfoBuffer);
45   if (EFI_ERROR(Status)) {
46     Print(L"Failed to Get FileInfo\n");
47     return Status;
48   }
49
50   EFI_FILE_INFO *FileInfo = (EFI_FILE_INFO*)FileInfoBuffer;
51   UINTN FileSize = FileInfo->FileSize;
52   Print(L"FileSize=%d\n",FileSize);
53   *FilePageSize = (FileSize + 4095) / 4096;
54   *FileAddr = 0;
55   Status = gBS->AllocatePages(
56   AllocateAnyPages,
57   EfiLoaderData,
58   *FilePageSize,
59   FileAddr);
60   if (EFI_ERROR(Status)) {
61     Print(L"Failed to Allocate Pages\n");
62     return Status;
63   }
65   Status = File->Read(
66     File,
67     &FileSize,
68     (VOID *)*FileAddr
69     );

2017年12月31日 星期日

uefi os loader (0) - write uefi program without edk2

今天是 20171231, 寫篇文章紀錄一下 2017 的最後一天, 很久沒寫電腦技術文章, 我找到了寫 uefi bootloader 的火苗, 先這麼開始吧!

2018 倒數計時
開發 uefi os loader 很困難 (或者應該說是陌生, 知道了其實並沒那麼難), 和 legacy bios os loader 相比之下, 有以下的問題要解決:
  1. 在 uefi 環境, x86 cpu 處於何種模式
  2. 取得 memory map, 如何解讀 memroy map 的資訊
  3. 如何載入 os kernel (在不自己寫 sata 的驅動程式之下)
  4. 如何離開 uefi 環境
  5. 離開 uefi 環境, x86 cpu 處於何種模式
  6. 離開 uefi 環境之後, 如何秀字
  7. 如何跳到 os kernel 執行
這些問題有些和 uefi 相關, 有些是 loader 本身的相關知識, 好像真的比開發 legacy bios os loader 難了不只一點。

https://github.com/naoki9911/xv6_uefi/ https://github.com/naoki9911/edk2/tree/f6392fdaa72cf3f54d43a00b26a74ce0e1184027/xv6_bootloader

提供了一個完整 uefi loader 的程式碼, 可以參考, 這個 loader 做了我上面提到的那些事情, 最後載入 xv6 kernel。程式碼還算容易, 應該難不倒你, xv6_bootloader 使用的是 edk2, edk2 用法中文資訊比較少。

由於一直找不到第一手資料 (中文世界通常指英文資料), 只好看第 0 手資料, 很希望我寫的這系列可以被當成第一手資料。

x86 cpu 開機處於真實模式, 你好奇在 uefi 環境時, cpu 處於什麼模式嗎?

根據 UEFI_Spec_2_7.pdf 2.3.4, uefi 開機之後在呼叫 ExitBootServices() 之前, x64 cpu 處於以下模式:
2.3.4 x64 Platforms
All functions are called with the C language calling convention. See Section 2.3.4.2 for more detail.
During boot services time the processor is in the following execution mode:
• UniprocessorIn long mode, in 64-bit modePaging enabled128 KiB, or more, of available stack space
• The stack must be 16-byte aligned. Stack may be marked as non-executable in identity mapped page tables.
• CR0.EM must be zero
• CR0.TS must be zero• Direction flag in EFLAGs clear
• 4 KiB, or more, of available stack space
• The stack must be 16-byte aligned
以上是簡短摘錄, 詳細請參閱 2.3.4, cpu 已經進入 long mode, 使用 page, 一核心, 所以 gdt 已經被設定了, 在 qemu 中, 是以下的內容:
gdt: 47, addr: 07ED6F18
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
uefi 挑 0x38 為 cs selector。

UEFI_Spec_2_7.pdf 2.1.3 有大略提到 UEFI OS loader, 可參考一下知道個大概 (當然只看這節的內容是寫不出來 UEFI OS loader 的)。uefi os loader 先提到這; 再來介紹不使用 edk2 來開發 uefi 程式的方法。

uefi 程式一般使用 edk2, 要完成一個 uefi hello 程式, 得照其規定來開發, 但是 edk2 實在太麻煩了, 有沒有 gcc + makefile 就可以搞定的方法呢?

很幸運的, 答案是肯定的。

也許你聽過 gnu efi, 做的也是類似的事情, 一年之後我才知道, 本篇提到的作法就是參考 gnu efi

README.gnuefi 這份文件也部份提到一些 efi 的 runtime 細節, 補上一些我從 source code 看不出來的細節。

GNU-EFI includes:
  1. crt0-efi-x86_64.o: A CRT0 (C runtime initialization code) that provides an entry point that UEFI firmware will call when launching the application, which will in turn call the "efi_main" function that the developer writes.
  2. libgnuefi.a: A library containing a single function (_relocate) that is used by the CRT0.
  3. elf_x86_64_efi.lds: A linker script used to link UEFI applications.
  4. efi.h and other headers: Convenience headers that provide structures, typedefs, and constants improve readability when accessing the System Table and other UEFI resources.
  5. libefi.a: A library containing convenience functions like CRC computation, string length calculation, and easy text printing.
  6. efilib.h: Header for libefi.a.
要打造這環境很麻煩, 需要自己編譯 toolchain (好像已經不用這麼做了), 因為 uefi 執行檔格式是 pe, 在 linux 預設的 gcc, 只能造出 elf 執行檔, 這太繁瑣了, 我不是用這個方式。很單純就是用系統的 toolchain 即可。

只要以下的檔案, 就可以開發 uefi 程式。這篇文章說明的是 x86_64 的用法, x86 32bit mode 就不介紹了。
  • efi_api.h efi.h types.h - efi 相關的 struct, function 定義, 看起來是從 edk2 參考而來。
  • efi_main.c - 使用 efi_main 的程式進入點。
  • crt0_x86_64_efi.S - efi 程式的初始化程式碼。
  • efi.ld - linker script
  • makefile - gcc/ld 編譯/連結相關的參數。
只要有了這幾個檔案, 就可以用我熟悉的 gcc + makefile 來開發 uefi 程式。

grub2 也是用類似的方法

grub-core/kern/efi/mm.c
grub_efi_get_memory_map()
{
  ...
  b = grub_efi_system_table->boot_services;
  ...
}

一個就是 system table, 一個就是 boot service, 咦! 好像是廢話。

linux 也有類似的程式:
boot/compressed/eboot.c
#define BOOT_SERVICES(bits)                                             \
static void setup_boot_services##bits(struct efi_config *c)             \
{                                                                       \
        efi_system_table_##bits##_t *table;                             \
        efi_boot_services_##bits##_t *bt;                               \
                                                                        \
        table = (typeof(table))sys_table;                               \
                                                                        \
        c->text_output = table->con_out; 

先來看看 efi_main.c, 很簡單, 呼叫 efi_main 來完成一個 uefi 程式, 裡頭用到 efi_system_table 裡頭的 efi_simple_text_output_protocol 的 output_string function, 可以用他來輸出字串到螢幕。

output_string 需要接受一個 efi_simple_text_output_protocol 和 u16 的 c-style 字串, 這對於我在 linux 開發的人來說很不習慣, 在 linux 下, 以 utf8 為主, 印出英文字串時, 只要使用 u8 c-sytle 字串就可以, 不過 uefi 只提供這個, 也只能照辦。所以一個普通字串就需要 prefix L, 這是 efi_main.c L8 為什麼需要 prefix L 的原因。

這個部份相當於經典程式 hello world 的寫法。

efi_main.c
 1 #include "efi.h"
 2 #include "efi_api.h"
 3 
 4 static efi_guid_t gEfiSimpleFileSystemProtocolGuid = EFI_GUID(0x964E5B22, 0x6459, 0x11D2,  0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B);
 5 
 6 efi_status_t efi_main(efi_handle_t image, struct efi_system_table *sys_table)
 7 {
 8   sys_table->con_out->output_string(sys_table->con_out, L"002 hello efi by gcc\n\r");
 9   return 0;
10 }

和經典程式 hello world 不同的是 (一般都被 c library 處理完畢, 程式員感覺不到他們的存在), 還需要初始化 uefi 的部份, crt0_x86_64_efi.S 就是在做這件事情。

這邊牽扯到 x64 的 2 種 function call convention, 參考《X86调用约定

注意到有 2 種不同的方式:
  1. 微软x64调用约定使用RCX, RDX, R8, R9这四个寄存器传递头四个整型或指针变量(从左到右)
  2. 此约定主要在Solaris,GNU/Linux,FreeBSD和其他非微软OS上使用。头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上。
很不幸這裡需要用到這兩種 function call convention, 所以雖然使用 gcc 在 linux 開發, 還是得搞懂 ms 的那套規則。因為 uefi 是使用 ms function call convention。雖然不太爽, 但也沒辦法。

_start 被 uefi bios 載入時, 使用 ms function call convention, crt0_x86_64_efi.S L17 的 %rcx 是 efi_handle_t (image), L18 是 efi_system_table (sys_table), 而 L25 call efi_main 時, 是使用 linux function call convention, 所以得把 %rdi 的值設定為 image, %rsi 設定為 sys_table。L17, 18, 22, 23 就是在做這些事情。之後進入了 efi_main 之後, 才可以正常使用 sys_table。

來看一下 efi api 的宣告, get_memory_map 是 uefi 提供 API, EFIAPI 是 #define EFIAPI __attribute__((ms_abi)), 代表要以 ms function call convention 來呼叫 uefi API。

efi_status_t (EFIAPI *get_memory_map)(efi_uintn_t *memory_map_size,
                                      struct efi_mem_desc *desc,
                                      efi_uintn_t *key,
                                      efi_uintn_t *desc_size,
                                      u32 *desc_version);
還沒完, 我們還需要 relocatable executable, L35 ~ L43 就是在做這件事情, 這邊很抱歉, 我不知道為什麼要這樣, 如果有人能告知我, 非常謝謝。

猜測是 pe 格式需要有 .reloc section, 這樣 uefi loader 才會去載入這個 uefi 執行檔。

ref: PE文件学习笔记 (四): 重定位表(Relocation Table)解析

[ 4] .reloc            PROGBITS         0000000000003000  00203000
       000000000000000a  0000000000000000   A       0     0     1

這時候最終的 elf 檔案就會有 .reloc section, 這樣 uefi 才會正常載入/執行這個 efi application。

而 elf 的 relocation 方式就用 elf 的 relocation 相關 section 來處理。最後還有一段 relocation 的程式碼, 這就是在做 elf relocation, 為了簡化, 我拿掉了, 這是在這個 uefi 程式被載入時, 會自己處理 .dynamic section 的內容, 標記為 DT_RELA, DT_RELASZ, DT_RELAENT 的 symbol, 不過我不知道怎麼造出這些 symbol, 所以就沒特別介紹這個, 這個範例程式沒處理 .dynamic section 的內容也不影響執行。list 1 為這個範例的 .dynamic section 內容。

我已經理解怎麼造出這些需要 relocation 的 symbol 了, 請參考: [code] 自己移動自己 - relocation

list 1. readelf -d my.elf
 1 descent@debian64:uefi_prog$ readelf -d my.elf
 2
 3 Dynamic section at offset 0x205000 contains 7 entries:
 4   Tag        Type                         Name/Value
 5  0x0000000000000004 (HASH)               0x0
 6  0x000000006ffffef5 (GNU_HASH)           0x8000
 7  0x0000000000000005 (STRTAB)             0x7000
 8  0x0000000000000006 (SYMTAB)             0x6000
 9  0x000000000000000a (STRSZ)              19 (bytes)
10  0x000000000000000b (SYMENT)             24 (bytes)
11  0x0000000000000000 (NULL)               0x0

crt0_x86_64_efi.S
 1 /*
 2  * crt0-efi-x86_64.S - x86_64 EFI startup code.
 3  * Copyright (C) 1999 Hewlett-Packard Co.
 4  * Contributed by David Mosberger <davidm@hpl.hp.com>.
 5  * Copyright (C) 2005 Intel Co.
 6  * Contributed by Fenghua Yu <fenghua.yu@intel.com>.
 7  *
 8  * All rights reserved.
 9  * SPDX-License-Identifier: BSD-3-Clause
10  */
11  .text
12  .align 4
13 
14  .globl _start
15 _start:
16  subq $8, %rsp
17  pushq %rcx
18  pushq %rdx
19 
20 0:
21 
22  popq %rsi
23  popq %rdi
24 
25  call efi_main
26  addq $8, %rsp
27 
28 .exit:
29  ret
30 
31  /*
32   * hand-craft a dummy .reloc section so EFI knows it's a relocatable
33   * executable:
34   */
35  .data
36 dummy: .long 0
37 
38 #define IMAGE_REL_ABSOLUTE 0
39  .section .reloc, "a"
40 label1:
41  .long dummy-label1    /* Page RVA */
42  .long 10     /* Block Size (2*4+2) */
43  .word (IMAGE_REL_ABSOLUTE << 12) +  0  /* reloc for dummy */

efi.ld 比較特別的事情算是 L7 的 .hash section, 這是給 dynamic section 用的, 而且一定要排在第一個, 詳請請參考 elf64 format 文件。神奇的是, 不需要指定 link 位址, 因為這個 uefi 程式要作到不管被載入到那個位址, 都可以正確執行, 這個 linker script 會從 0 開始計算位址。

README.gnuefi
.hash (and/or .gnu.hash)
        Collects the ELF .hash info (this section _must_ be the first
        section in order to build a shared object file; the section is
        not actually loaded or used at runtime).

efi.ld
 1 OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64")
 2 OUTPUT_ARCH(i386:x86-64)
 3 ENTRY(_start)
 4 SECTIONS
 5 {
 6  image_base = .;
 7  .hash : { *(.hash) }
 8  . = ALIGN(4096);
 9  .eh_frame : {
10   *(.eh_frame)
11  }
12  . = ALIGN(4096);
13  .text : {
14   *(.text)
15   *(.text.*)
16   *(.gnu.linkonce.t.*)
17  }
18  . = ALIGN(4096);
19  .reloc : {
20   *(.reloc)
21  }
22  . = ALIGN(4096);
23  .data : {
24   *(.rodata*)
25   *(.got.plt)
26   *(.got)
27   *(.data*)
28   *(.sdata)
29   *(.sbss)
30   *(.scommon)
31   *(.dynbss)
32   *(.bss)
33   *(COMMON)
34   *(.rel.local)
35   . = ALIGN(8);
36   *(SORT(.u_boot_list*));
37   . = ALIGN(8);
38   *(.dtb*);
39  }
40  . = ALIGN(4096);
41  .dynamic : { *(.dynamic) }
42  . = ALIGN(4096);
43  .rela : {
44   *(.rela.data*)
45   *(.rela.got)
46   *(.rela.stab)
47  }
48  . = ALIGN(4096);
49  .dynsym : { *(.dynsym) }
50  . = ALIGN(4096);
51  .dynstr : { *(.dynstr) }
52  . = ALIGN(4096);
53  .ignored.reloc : {
54   *(.rela.reloc)
55   *(.eh_frame)
56   *(.note.GNU-stack)
57  }
58  .comment 0 : { *(.comment) }
59 }

makefile 紀錄所需要的編譯/連結參數。關鍵是 L2 的 objcopy, 前面有提到, uefi 是 pe 格式, 但 linux 系統的 gcc 只會輸出 elf 格式, 怎麼辦呢? objcopy 會把 elf 轉成 pe 的格式, 仔細看 --target=efi-app-x86_64, 很神奇吧!

makefile L5 的 ld -shared 會插入 _DYNAMIC symbol, 和 crt0_x86_64_efi.S 以下程式碼有關:

list 2 lea + rip
lea image_base(%rip), %rdi
lea _DYNAMIC(%rip), %rsi

lea + rip + symbol 的這個用法很特別, 只存在 x86-64 模式下。
ref: 从机器码理解RIP 相对寻址

有點難懂, 直接用 objdump 來說明 lea + rip 指令。

list 3 lea + rip
1 0000000000002000 <_start>:
2     2000:       48 83 ec 08             sub    $0x8,%rsp
3     2004:       51                      push   %rcx
4     2005:       52                      push   %rdx
5     2006:       48 8d 3d f3 df ff ff    lea    -0x200d(%rip),%rdi       # 0 <image_base>
6     200d:       48 8d 35 ec 2f 00 00    lea    0x2fec(%rip),%rsi        # 5000 <_DYNAMIC>
7     2014:       59                      pop    %rcx

image_base 是 0, 而 list 3 L5 rip (L6 的位址) 是 200d + (-0x200d) 剛好是 0, 就是 image_base 的值。 list 3 L6 rip 是 0x2014 (L7 的位址) + (0x2fec) 剛好是 0x5000, 就是 _DYNAMIC 的值。 和一般 lea 的用法不太一樣。lea 在對 rip 暫存器和非 rip 暫存器似乎有 2 種不同的執行方式。
u.elf:     file format elf64-x86-64
Disassembly of section .hash:
0000000000000000 \<image_base\>:
   0:   03 00                   add    (%rax),%eax
   2:   00 00                   add    %al,(%rax)
   4:   07                      (bad)  
   5:   00 00                   add    %al,(%rax)
   7:   00 04 00                add    %al,(%rax,%rax,1)
   a:   00 00                   add    %al,(%rax)
   c:   02 00                   add    (%rax),%al
   e:   00 00                   add    %al,(%rax)
  10:   01 00                   add    %eax,(%rax)
  12:   00 00                   add    %al,(%rax)
  14:   00 00                   add    %al,(%rax)
  16:   00 00                   add    %al,(%rax)
  18:   05 00 00 00 00          add    $0x0,%eax

31 0x6999039       push   %rdx                                 
32 0x699903a       lea    -0x2041(%rip),%rcx        # 0x6997000 
33 0x6999041       lea    0x2fb8(%rip),%rdx        # 0x699c000 
34 0x6999048       callq  0x6999ced    
image_base 在被載入時才能得知其位址, 無法在 link timer 看出來, 所以我用了 gdb 來觀察真實的載入位址, L32 的 0x6997000 就是在計算 image_base 被載入的位址是 0x6997000, 透過 0x6999041 + (-0x2041) 計算得知。image_base 就是這個 uefi 程式的開頭位址, 為什麼要知道這個呢? 因為需要這個值來做 relocation, 而可以這麼做是因為這個 uefi 程式的 link 位址從 0 開始計算。

而在編譯成 shared object 時, lea 的這種語法只能搭配 rip, 是的, uefi 的編譯選項很類似 share object, 所以 uefi 程式其實是一個 share object, 然後 uefi shell 直接載入並執行這個 share object。

若 _start 被載入到 0x2000, list 3 L5 rdi 就是 0; 若 _start 被載入到 0x3000, list 3 L5 rdi 就是 0x1000 (0x300d + (-0x200d)), rdi 就是要紀錄這個 uefi (share object) 被載入到那一個位址, 後面的 _relocate function 就會用這個值來調整 relocateion 的相關修正。

而 _DYNAMIC 是 dynamic section 的位址, _relocate function 需要讀取這個 section 才知道需不需要做 relocation 的調整。

makefile
 1 efi_main.efi: efi_main.elf
 2  objcopy  -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel -j .rela -j .reloc --target=efi-app-x86_64 $< $@
 3 
 4 efi_main.elf: efi_main.o crt0_x86_64_efi.o
 5  ld -Bsymbolic -Bsymbolic-functions -shared --no-undefined -o $@ -T efi.ld $^
 6 
 7 efi_main.o: efi_main.c efi.h types.h efi_api.h
 8  gcc -nostdinc -I. -D__KERNEL__ -D__UBOOT__ -Wall -Wstrict-prototypes -Wno-format-security -fno-builtin -ffreestanding -fshort-wchar -Os -fno-stack-protector -fno-delete-null-pointer-checks -g -fstack-usage -Wno-format-nonliteral -Werror=date-time -fno-strict-aliasing -fomit-frame-pointer -fno-toplevel-reorder -fno-dwarf2-cfi-asm -D__I386__ -ffunction-sections -fvisibility=hidden -pipe -fpic -fshort-wchar -c $<
 9 
10 crt0_x86_64_efi.o: crt0_x86_64_efi.S
11  gcc -nostdinc  -D__UBOOT__ -D__ASSEMBLY__ -g -fno-strict-aliasing -fomit-frame-pointer -fno-toplevel-reorder -fno-dwarf2-cfi-asm -D__I386__ -ffunction-sections -fvisibility=hidden -pipe -fpic -fshort-wchar   -c crt0_x86_64_efi.S
12 
13 clean:
14  rm -rf *.o *.efi *.elf

makefile L5 -shared, L8/11 -fpic 是重點, 一定要這 2 個 option, 編譯出來的程式才能正常被 uefi 載入/執行。-fpic 表示我們要造出和位址無關的程式碼, 這觀念不難, 但平常我們不太容易練習到, 沾上邊的有 .so, 不過大部分程式員都是寫應用程式, 我認為很少人會去寫動態函式庫 (.so), 而且也不太會注意其和「位址無關」到底是什麼意思? 所以一個 uefi 執行檔其實是一個 share object, uefi loader 載入/執行的其實是 .so, 而不是一個真正的執行檔, 真是神奇。

字面上的解釋, 就是無論載入到 0x100, 0x200, 這個 efi application 都要正常執行。基本上平常的開發環境, 很難體會這點。

你需要寫一個 loader 和一個「與位址無關的程式」, 然後用這個 loader 去載入他才會有深刻的印象。

扯太多了, 總之, 這樣就可以靠 gcc + makefile 完成以 efi_main 為進入點的 uefi application。

在 qemu 上執行是沒有問題的
 1 UEFI Interactive Shell v2.1
 2 EDK II
 3 UEFI v2.60 (EDK II, 0x00010000)
 4 Mapping table
 5       FS0: Alias(s):FP0a:;BLK0:
 6           PciRoot(0x0)/Pci(0x1,0x0)/Floppy(0x0)
 7      BLK2: Alias(s):
 8           PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)
 9      BLK1: Alias(s):
10           PciRoot(0x0)/Pci(0x1,0x0)/Floppy(0x1)
11 Press ESC in 2 seconds to skip startup.nsh or any other key to continue.
12 Shell> fs0:
13 FS0:\> efi_main.efi
14 002 hello efi by gcc

當然在真實機器上也是可以正常執行的。

在真實機器上測試

接下來我便要用這樣的開發方式, 開發 uefi os loader。

ref: uefi document:
  • UEFI_Spec_2_7.pdf
  • PI_Spec_1_6.pdf
ref: linux-5.1.14/arch/x86/boot/compressed/eboot.c
efi_main()
{
        setup_graphics(boot_params);
        setup_efi_pci(boot_params);
        setup_quirks(boot_params);
}

2016年3月22日 星期二

使用 c++ 來開發 uefi/edk2 程式 (0)

進學致和,行方思遠。
20220622 補充:
在「write uefi program without edk2」之後, 我已經不用 edk2 來開發 uefi, 當然也不用在處理哪些可怕的 inf, dsc 檔案。

使用 c++ 也不需要像這篇這麼花功夫, 之後不會用本篇的方法來使用 c++, 這篇就留個紀錄。

我也知道為什麼 global object ctor 沒有被正常喚起, 因為沒做任何事情, 本來就不會被正常喚起, 手動呼叫 GLOBAL_XX function 算是正常用法。

以下原文:

edk2 並沒有支援用 c++ 來開發 uefi 程式, 我真是不敢相信還有這麼原始的開發環境, 竟然不支援 c++。我可是 c++ 愛好者, 不能用自己喜歡的語言開發程式, 感覺很不爽。

c++ 的好處我說過好幾次了 (疑! 我根本沒說過!!), c 我也是可以寫的, 為什麼堅持用 c++ 呢? 還真的說不上原因, 大概是網路上很多的討論都對 c++ 很不好, 什麼 XX 比不上 C, YY 比不上 JAVA, 這些言論更讓我對 c++ 抱不平, 決定要多多推廣 c++, 我又不善嘴炮言詞, 只好用程式碼來證明 c++ 的能耐。

UEFI原理与编程》第十章說明如何用 c++ (g++) 來開發 uefi 程式並提供了 GcppPkg 這個範例, 不過很可惜, 這部份書上寫的不夠詳細, 我花費了不少時間還是搞不定怎麼使用這個範例。

最後我做了和 GcppPkg 類似的事情, 再加上《UEFI原理与编程》第十章的說明, 搞出了自己的 c++ 作法。所以我雖然沒搞定書上的 GcppPkg 範例, 但是書中內容還是幫了我不少。

最困難的是編譯環境, 花了不少時間我才搞定 (沒有搞懂, 是搞定), 找了 edk2/AppPkg/Applications/Main/Main.c 來修改, 這是使用 c 語言 main 的開發方式。

我建立了 edk2/AppPkg/Applications/Main/m.cpp (從 edk2/AppPkg/Applications/Main/Main.c 複製而來), 也複製了一份 m.inf, m.inf 最重要的是 L27 ~ 42, 把 source code 的檔案填進去, build 就會去編譯這些檔案。

m.inf
 1 ## @file
 2 #   A simple, basic, application showing how the Hello application could be
 3 #   built using the "Standard C Libraries" from StdLib.
 4 #
 5 #  Copyright (c) 2010 - 2014, Intel Corporation. All rights reserved.<BR>
 6 #  This program and the accompanying materials
 7 #  are licensed and made available under the terms and conditions of the BSD License
 8 #  which accompanies this distribution. The full text of the license may be found at
 9 #  http://opensource.org/licenses/bsd-license.
10 #
11 #  THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
12 #  WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
13 ##
14 
15 [Defines]
16   INF_VERSION                    = 0x00010006
17   BASE_NAME                      = m
18   FILE_GUID                      = 5ea97c46-7491-4dfd-b442-747010f3ce5f
19   MODULE_TYPE                    = UEFI_APPLICATION
20   VERSION_STRING                 = 0.1
21   ENTRY_POINT                    = ShellCEntryLib
22 
23 #
24 #  VALID_ARCHITECTURES           = IA32 X64
25 #
26 
27 [Sources]
28   m.cpp
29   bst.cpp    
30   #crtbegin.cpp  
31   eh.cpp      
32   mylist.cpp  
33   mymap.cpp     
34   myvec.cpp
35   cstring.cpp   
36   gdeque.cpp  
37   k_stdio.cpp  
38   mem.cpp  
39   myiostream.cpp  
40   mystring.cpp  
41   #my_setjmp.c
42   my_setjmp.S
43 
46 [Packages]
47   StdLib/StdLib.dec
48   MdePkg/MdePkg.dec
49   ShellPkg/ShellPkg.dec
50 
51 [LibraryClasses]
52   LibC
53   LibStdio
54 
55 [BuildOptions]
56    #GCC:*_*_X64_CC_FLAGS = -fno-exceptions -fno-rtti -ffreestanding -nostdlib -nodefaultlibs -std=c++11
57    MSFT:*_*_*_CC_FLAGS =
58 


再來是把這個 m.inf 加到 AppPkg.dsc。

AppPkg.dsc.diff
 1  [LibraryClasses]
 3    #
 4    # Entry Point Libraries
 5    #
 6 @@ -105,6 +106,7 @@
 7  ########################################################################
 8  
 9  [Components]
10  AppPkg/Applications/Main/m.inf         


以下是測試的 c++ 檔案。cout 和 vector 不是 edk2 提供的, 是移植我那個玩具標準 c++ 程式庫得來的, namespace 是 DS 而不是 std, 書上的範例是移植 stl 過來, 我既然已經有自己的版本, 玩具歸玩具, 沒道理不用的。

還順便測試一下目前最潮的 lambda 語法。

還有 setjmp/longjmp x64 的版本。

m.cpp
 1 #include  "myvec.h"
 2 #include  "myiostream.h"
 3 #include "my_setjmp.h"
 4 using namespace DS;
 5 
 6 class Obj
 7 {
 8   public:
 9     Obj()
10     {
11       i=10;
12       ::printf("ctor\n");
13     }
14     ~Obj()
15     {
16       ::printf("dtor\n");
17     }
18   private:
19     int i;
20 };
21 
22 jmp_buf jbuf;
23 
24 int lambda_test(int girls = 3, int boys = 4)
25 {
26   auto totalChild = [](int x, int y) ->int{return x+y;};
27   return totalChild(girls, boys);
28 }
29 
30 int main (IN int Argc, IN char **Argv)
31 {
32   Obj obj;
33 
34   int total = lambda_test();
35   cout << "total: " << total << endl;
36 
37   vector<int> vec_i;
38 
39   vec_i.push_back(1);
40   vec_i.push_back(2);
41   vec_i.push_back(3);
42   for (int i=0 ; i < vec_i.size() ; ++i)
43     cout << vec_i[i] << endl;
44 
45   char *area = (char*)mymalloc(20);
46   char *a1 = new char [20];
47 
48   int j_ret = 9;
49   //printf("aaa\n");
50   //getchar();
51   j_ret = my_setjmp(jbuf);
52 
53   if (j_ret == 0)
54   {
55     printf("00\n");
56   }
57   else 
58   {
59     printf("11\n");
60     return 0;
61   }
62 
63   my_longjmp(jbuf, 5);
64   return 0;
65 }


build -p AppPkg/AppPkg.dsc 就可以編譯出 m.efi 了, 不過先別急著打這個指令, 還要寫個 script gcc (書上提供了一個, 照抄後再加上 g++ option, ref list 1), 判斷是 .c 檔時出動 gcc, .cpp 檔時出動 g++。當然也可能要修改 uefi/edk2/BaseTools/Scripts/GccBase.lds, 不過目前就不要搞太複雜, 這樣就可以了。

list 1. gcc script
 1 #!/bin/sh
 3 iscpp=0
 4 for i in "$@" ; do
 5   if [ "-" != "${i:0:1}" ] ; then
 6     if [ "cpp" = "${i##*.}" ]; then
 7       iscpp=1
 8     fi
 9   fi
10 done
11 
12 if [ $iscpp = 0 ]; then
13   echo gcc
14   /usr/bin/gcc -DUEFI $@
15 else
16   echo g++
18   /usr/bin/g++ -DUEFI -fpermissive -I. -DSTM32 -fno-exceptions -fno-rtti -ffreestanding -nostdlib -nodefaultlibs -std=c++11 $@
19 fi


好了, 現在可以痛快的敲下 build -p AppPkg/AppPkg.dsc

fig 2 為測試畫面, ctor/dtor 正常發動了, lambda 也正常, DS::vector, DS::cout 也正常, 感謝 bjarne stroustrup; 感謝 c++。

fig 2模擬器測試畫面


為了保險起見, 我在真實機器上同樣也做了測試, 真的是可以跑的。

真實機器的 uefi 執行畫面


global object ctor/dtor 沒有搞定, 比我想的還複雜些, 這不是什麼問題, 不要用就好了。 XD

我是用手動呼叫 GLOBAL_XX function 來搞定這件事。

uefi-shceme 就是這麼做的。

uefi function 手冊: http://www.bluestop.org/edk2/docs/trunk/getchar_8c.html