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     );

沒有留言:

張貼留言

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

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