目錄

UEFI CPU exception 淺談

最近呢,因為一個 silicon code 的問題,觸發了 CPU 例外(exception),偏偏又死在不好找的地方,想說如果可以用 OvmfPkg 模擬的話就來寫一篇簡單的教學。

除以零的錯誤

既然是淺談,當然用最簡單的方式來介紹,所以就來介紹最常見也最容易複製的例外了。

寫過程式應該都知道的我們都知道,如果有程式碼除以零,該程式會立刻當掉,這是因為觸發了 Divide-by-zero Error CPU 例外(exception).

摘錄自 Exceptions

引用

Divide-by-zero Error

The Divide-by-zero Error occurs when dividing any number by 0 using the DIV or IDIV instruction, or when the division result is too large to be represented in the destination. Since a faulting DIV or IDIV instruction is very easy to insert anywhere in the code, many OS developers use this exception to test whether their exception handling code works. The saved instruction pointer points to the DIV or IDIV instruction which caused the exception.

從這邊我們可以得到一個線索,這個 exception 就是有人使用了 DIV 或是 IDIV 指令除以 0 造成的。

複製這個錯誤

要複製這個問題並不難,寫一小段 code 去除以 0 即可。

利用之前學到的 OvmfPkg 相關知識,反正 OvmfPkg 執行到最後會執行 Internal Shell,索性直接在 Shell 的 Entry 做個手腳吧。

首先開啟 ShellPkg\Application\Shell\Shell.inf 查看一下 EntryPoint 在哪

1
2
3
4
5
6
7
[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = Shell
  FILE_GUID                      = 7C04A583-9E3E-4f1c-AD65-E05268D0B4D1 # gUefiShellFileGuid
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = UefiMain

接著在 ShellPkg\Application\Shell\Shell.cUefiMain 來改寫一下吧。

324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS                      Status;
  CHAR16                          *TempString;
  UINTN                           Size;
  EFI_HANDLE                      ConInHandle;
  EFI_SIMPLE_TEXT_INPUT_PROTOCOL  *OldConIn;
  SPLIT_LIST                      *Split;
// Generate CPU exception >>>
  UINT8 a, b, c;

  a = 0;
  b = 5;
  c = b / a;
// Generate CPU exception <<<
  if (PcdGet8(PcdShellSupportLevel) > 3) {
    return (EFI_UNSUPPORTED);
  }

在這邊,我們做了幾件事。

  • 賦予變數 a 值為 0
  • 賦予變數 b 值為 5
  • b 除以 a 的結果存到變數c

所以我們可以預期會在 行342 產生 CPU 例外(exception)。 接著重新建置 OvmfPkg 產生新的 ROM。

如何閱讀 CPU 例外(exception) 的訊息

重新在 QEMU 內運行這個 Ovmf BIOS 後,會得到如下的 log。

[Bds]Booting EFI Internal Shell
[Bds] Expand Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(7C04A583-9E3E-4F1C-AD65-E05268D0B4D1) -> Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(7C04A583-9E3E-4F1C-AD65-E05268D0B4D1)
BdsDxe: loading Boot0002 "EFI Internal Shell" from Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(7C04A583-9E3E-4F1C-AD65-E05268D0B4D1)
InstallProtocolInterface: 5B1B31A1-9562-11D2-8E3F-00A0C969723B E7BB040
    PDB = c:\edk2\Build\OvmfX64\NOOPT_VS2019\X64\ShellPkg\Application\Shell\Shell\DEBUG\Shell.pdb
Loading driver at 0x0000DD7E000 EntryPoint=0x0000DD7E3D0 Shell.efi
InstallProtocolInterface: BC62157E-3E33-4FEC-9920-2D3B36D750DF EE68C18
ProtectUefiImageCommon - 0xE7BB040
  - 0x000000000DD7E000 - 0x0000000000129E40
BdsDxe: starting Boot0002 "EFI Internal Shell" from Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(7C04A583-9E3E-4F1C-AD65-E05268D0B4D1)
!!!! X64 Exception Type - 00(#DE - Divide Error)  CPU Apic ID - 00000000 !!!!
RIP  - 000000000DD84776, CS  - 0000000000000038, RFLAGS - 0000000000000202
RAX  - 0000000000000005, RCX - 0000000000000000, RDX - 0000000000000000
RBX  - 0000000000000000, RSP - 000000000FE398D0, RBP - 000000010B702000
RSI  - 00000000FFFCC374, RDI - 0000000000820000
R8   - 000000000E95C118, R9  - 000000000DE3F101, R10 - 000000000FE39350
R11  - 0000000000004F1C, R12 - 0000000000000000, R13 - 0000000000000000
R14  - 0000000000000000, R15 - 0000000000000000
DS   - 0000000000000030, ES  - 0000000000000030, FS  - 0000000000000030
GS   - 0000000000000030, SS  - 0000000000000030
CR0  - 0000000080010033, CR2 - 0000000000000000, CR3 - 000000000FC01000
CR4  - 0000000000000668, CR8 - 0000000000000000
DR0  - 0000000000000000, DR1 - 0000000000000000, DR2 - 0000000000000000
DR3  - 0000000000000000, DR6 - 00000000FFFF0FF0, DR7 - 0000000000000400
GDTR - 000000000F9EE598 0000000000000047, LDTR - 0000000000000000
IDTR - 000000000F2D3018 0000000000000FFF,   TR - 0000000000000000
FXSAVE_STATE - 000000000FE39530
!!!! Find image based on IP(0xDD84776) c:\edk2\Build\OvmfX64\NOOPT_VS2019\X64\ShellPkg\Application\Shell\Shell\DEBUG\Shell.pdb (ImageBase=000000000DD7E000, EntryPoint=000000000DD7E3D0) !!!!
???:Exception happened, ExceptionNum is 0, EIP = 0xDD84776.

下面我們先假裝我們不知道為什麼會有這個問題發生,先只從 log 觀察起。

而乍看之下還真不知道從何開始,不過其實耐心觀察還是可以看出一些東西的。

  • 首先,我們知道這個 例外(exception) 是因為 00(#DE - Divide Error) 除0 的問題造成的
  • 接著,這個 例外(exception) 發生在 EIP 為 0xDD84776 這個記憶體的指令。 EIP 會指在 CPU 目前的執行指令的記憶體上。另外也提供了 Shell 這個程式的 ImageBase 為 0xDD7E000, ImageSize 為 0x129E40, EntryPoint 為 0xDD7E3D0,而出問題的 EIP 就指在這個範圍裡面。
  • 而根據 x64 呼叫慣例 我們得知,CPU 暫存器中的 RCX, RDX, R8R9 分別用來儲存呼叫參數的內容,若有更多的參數會以堆疊的形式傳遞。 所以這幾個欄位也可以幫助我們尋找可能出現問題的的 code 在哪。 另外 RAX 可能會是傳回值。

根據以上資訊,我們接著觀察一下 Build\OvmfX64\NOOPT_VS2019\X64\ShellPkg\Application\Shell\Shell\DEBUG\Shell.map 這支 map 檔提供了什麼訊息可以協助我們尋找有問題的 code。

由於檔案太大,這邊只節錄部分出來。 先來驗證我們目前已知的資訊,從上面可以得知 ImageBase 為 0xDD7E000, EntryPoint 為 0xDD7E3D0, 所以 EntryPoint 的位移量為 0x3D0,所以我們應該可以在 map 檔中找到相對應的資訊

0001:00000110       _ModuleEntryPoint          00000000000003d0 f   UefiApplicationEntryPoint:ApplicationEntryPoint.obj

我們可以看到,名為 _ModuleEntryPoint 的 function 的確是 Shell 的 EntryPoint

問題
有人會問,上面 Shell.inf 不是明明寫著說 UefiMain 才是 EntryPoint 嗎,怎麼變成 _ModuleEntryPoint 了,這個有機會下次再解釋,這邊先這樣理解就好。

那麼我們來找看看造成 例外(exception) 的 EIP 指在哪裡。

我們取 EIP 0xDD84776 減去 ImageBase 0xDD7E000 會得到 0x6776,而這個位移量會落在下面這個範圍內

0001:00006490       UefiMain                   0000000000006750 f   Shell:Shell.obj
0001:000075b0       VerifySplit                0000000000007870 f   Shell:Shell.obj

所以,兇手應該就是 Shell.c 內的 UefiMain 造成的了。而這個結果也符合我們做的實驗內容了。

還能做什麼?

當然,在現實世界中,尋找 CPU 例外(exception) 沒有這麼簡單,還是要多多練習才能有更精確地找到問題點。 不過這邊還是可以再提供一個小方法可以多一點協助。

前面有提到,CPU 的暫存器也會提供一些線索,如果我們將程式碼產生出來的組合語言也一起產生出來閱讀是不是也能多提供一些資訊。 所以來試看看利用參數 /FAcs 吧。

再次開啟 ShellPkg\Application\Shell\Shell.inf 並加上下面這一段並重新編譯一次

1
2
[BuildOptions]
  MSFT:*_*_*_CC_FLAGS = /FAcs

然後我們會發現在 Build\OvmfX64\NOOPT_VS2019\X64\ShellPkg\Application\Shell\Shell\Shell.cod 多了一隻檔案,裡面的內容夾雜著 C語言的程式碼與產生出來的組合語言。

12218
12219
12220
12221
12222
12223
12224
12225
12226
12227
12228
12229
12230
12231
12232
12233
12234
12235
12236
12237
12238
12239
12240
12241
12242
12243
12244
12245
12246
12247
12248
12249
UefiMain PROC                       ; COMDAT

; 330  : {

$LN265:
  00000 48 89 54 24 10   mov     QWORD PTR [rsp+16], rdx
  00005 48 89 4c 24 08   mov     QWORD PTR [rsp+8], rcx
  0000a 48 81 ec a8 00
    00 00        sub     rsp, 168       ; 000000a8H

; 331  :   EFI_STATUS                      Status;
; 332  :   CHAR16                          *TempString;
; 333  :   UINTN                           Size;
; 334  :   EFI_HANDLE                      ConInHandle;
; 335  :   EFI_SIMPLE_TEXT_INPUT_PROTOCOL  *OldConIn;
; 336  :   SPLIT_LIST                      *Split;
; 337  :   UINT8 a, b, c;
; 338  :   a = 0;

  00011 c6 44 24 63 00   mov     BYTE PTR a$[rsp], 0

; 339  :   b = 5;

  00016 c6 44 24 62 05   mov     BYTE PTR b$[rsp], 5

; 340  :   c = b / a;

  0001b 0f b6 44 24 62   movzx   eax, BYTE PTR b$[rsp]
  00020 0f b6 4c 24 63   movzx   ecx, BYTE PTR a$[rsp]
  00025 99       cdq
  00026 f7 f9        idiv    ecx
  00028 88 44 24 78  mov     BYTE PTR c$[rsp], al

從組合語言的內容可知:

  • 行12237 將值 0 賦予變數 a 所在的記憶體
  • 行12241 將值 5 賦予變數 b 所在的記憶體
  • 行12245 將變數 b 所在的記憶體值存到 eax(RAX)
  • 行12246 將變數 a 所在的記憶體值存到 ecx(RCX)
  • 行12248ecx(RCX) 除以 eax(RAX) ==> 除以 0

這時候再往回看看 BIOS 丟出來的 CPU exception 訊息,是不是都對的上了呢。

範例

上述完整的參考檔案請由此下載

延伸閱讀