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 在哪
|
|
接著在 ShellPkg\Application\Shell\Shell.c
的 UefiMain
來改寫一下吧。
|
|
在這邊,我們做了幾件事。
- 賦予變數
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
,R8
和R9
分別用來儲存呼叫參數的內容,若有更多的參數會以堆疊的形式傳遞。 所以這幾個欄位也可以幫助我們尋找可能出現問題的的 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
那麼我們來找看看造成 例外(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
並加上下面這一段並重新編譯一次
|
|
然後我們會發現在 Build\OvmfX64\NOOPT_VS2019\X64\ShellPkg\Application\Shell\Shell\Shell.cod
多了一隻檔案,裡面的內容夾雜著 C語言的程式碼與產生出來的組合語言。
|
|
從組合語言的內容可知:
- 行12237 將值
0
賦予變數a
所在的記憶體 - 行12241 將值
5
賦予變數b
所在的記憶體 - 行12245 將變數
b
所在的記憶體值存到eax(RAX)
- 行12246 將變數
a
所在的記憶體值存到ecx(RCX)
- 行12248 將
ecx(RCX)
除以eax(RAX)
==> 除以 0
這時候再往回看看 BIOS 丟出來的 CPU exception 訊息,是不是都對的上了呢。
上述完整的參考檔案請由此下載