⭐️UEFI 中的 Protocol Handle 機制
一、ResetVector
Reset Vector(復位向量) 是 CPU(或其他處理器)在上電覆位(Power-on Reset)或手動復位(Reset信號觸發)後,無條件跳轉去執行的第一條指令的地址。
x86 實模式的典型值是 0xFFFFFFF0
CPU 收到 Reset 信號後的大致動作:
-
所有寄存器復位到默認值(包括 PC)。
-
PC 被強制加載 Reset Vector 地址處的內容(這個內容通常是一條跳轉指令)。
-
從該地址開始取指、譯碼、執行 → 進入Boot ROM / BIOS / Bootloader。
通常這個地址裏放的不是真正的第一條有用代碼,而是一條跳轉指令,比如:
; x86 實模式例子(物理地址 0xFFFFFFF0)
jmp far 0xF000:0xXXX ; 跳到BIOS入口
真正的啓動代碼(復位處理程序 reset handler)一般放在 Flash、ROM 或固化的 BootROM 裏。
以 x86 為例,主板一上電,CPU 的 PC 被設為 0xFFFFFFF0(物理地址,相當於 FFFF:FFF0 段:偏移)。這個地址屬於主板上的 SPI Flash 裏的 UEFI 固件鏡像(通過芯片組如 Intel PCH 映射到 4GB 地址空間頂部,通常 4-16MB 大小)。0xFFFFFFF0 處仍是一條 jmp 指令(通常是 jmp far 或短跳轉),跳到 UEFI 固件的真正入口(Reset Vector Code,位於固件鏡像末尾附近)。
二、OS loader 與 TSL
在之前的介紹中我們知道 TSL 階段的作用,即作為 BDS 和 RT 之間的過渡階段。此時系統的主要任務是從平台初始化轉向加載操作系統內核。那麼用於加載操作系統的 OS Loader 與 TSL 之間有什麼關係呢?OS Loader 是 TSL 階段的實際執行主體,負責加載並啓動操作系統。
在 TSL 階段 Boot Services 仍可用(直到 ExitBootServices() 被調用)。OS Loader 可以調用 UEFI 協議和服務來讀取文件系統、加載內核鏡像等。一旦 OS Loader 完成內核加載並準備跳轉到操作系統入口點,會調用 ExitBootServices(),標誌着 TSL 階段結束,進入 RT 階段。
OS Loader 是負責加載操作系統內核並將其控制權轉移給內核的程序。在 UEFI 環境下,它具有以下關鍵功能:
- 定位操作系統鏡像
- 通過 UEFI 文件系統協議(如 Simple File System Protocol)訪問 ESP(EFI System Partition)。
- 讀取配置文件(如
bootmgfw.efifor Windows,grubx64.efifor Linux)。
- 加載內核與初始化數據
- 將操作系統內核(如
vmlinuz、ntoskrnl.exe)和可能的 initramfs/initrd 加載到內存。 - 解析啓動參數(如來自 UEFI NVRAM 中的
Boot####變量)。
- 將操作系統內核(如
- 準備執行環境
- 設置必要的內存佈局、頁表(某些情況下)、傳遞啓動信息(如 ACPI 表、內存映射等)。
- 調用
ExitBootServices()終止 UEFI Boot Services,釋放對硬件的控制。
- 跳轉到操作系統入口點
- 將 CPU 控制權移交給操作系統內核,完成啓動過程。
示例:
- UEFI 固件在 BDS 階段找到 ESP 分區中的
\EFI\SYSTEMD\SYSTEMD-BOOTX64.EFI。 - 進入 TSL 階段,執行該 EFI 應用(即 OS Loader)。
- systemd-boot 顯示啓動菜單,用户選擇內核。
- 加載
vmlinuz和initrd到內存,設置 cmdline。 - 調用
ExitBootServices(),跳轉到內核入口。 - UEFI 進入 RT 階段,操作系統全面接管。
不通平台和系統使用的 OS Loader 示例:
| 平台類型 | 架構 | 操作系統 | 典型啓動流程 |
|---|---|---|---|
| 嵌入式 | ARM | Linux | BootROM → SPL → U-Boot → Kernel |
| PC | x86 | Linux | BIOS/UEFI → OS Loader (GRUB/systemd-boot) → Kernel |
| PC | x86 | Windows | UEFI → Windows Boot Manager → winload.efi → NTOSKRNL |
| 嵌入式/PC | ARM | Windows(少見) | ARM64 UEFI → bootmgfw.efi → Windows Kernel |
2.1 通用概念
- Bootloader
- Bootloader 是一段在操作系統內核運行前執行的代碼,負責初始化硬件、加載內核、傳遞啓動參數,在不同平台上名稱和層級不同。
- BIOS vs UEFI
- BIOS:傳統 x86 PC 固件,16 位實模式,功能有限。
- 現代標準固件,支持 32/64 位,模塊化、可擴展,有驅動模型和文件系統支持。
- ARM 平台一般不用 BIOS,直接使用 BootROM + 自定義 Bootloader 或 ARM Trusted Firmware (ATF) + UEFI。
由於 BIOS 的概念深入人心,如今我們一般稱傳統的固件叫做 Legacy BIOS,現代固件叫做 UEFI BIOS。
2.2 不通平台的啓動流程對比
-
嵌入式 Linux(ARM 架構,如 Raspberry Pi, i.MX6)
BootROM(芯片內置) → SPL(可選,如 U-Boot SPL) → U-Boot(主 Bootloader) → Linux Kernel(zImage/Image + dtb + initramfs) → 用户空間(init)嵌入式設備啓動的特點:
- 無標準固件:不像 PC 有 BIOS/UEFI,依賴 SoC 廠商提供的 BootROM。
- BootROM:固化在芯片中,上電後自動從預設介質(SD/eMMC/NAND/SPI Flash)加載第一段代碼(通常是 SPL 或直接 U-Boot)。
- U-Boot:最常用的嵌入式 Bootloader,支持命令行、腳本、設備樹(DTB)傳遞。
- 設備樹(Device Tree):ARM Linux 必須通過 Bootloader 傳遞 .dtb 文件描述硬件。
- 無 ExitBootServices():因為沒有 UEFI,直接跳轉到內核。
-
PC 上的 Linux(x86/x86_64 架構,UEFI 模式)
UEFI Firmware → OS Loader(如 GRUB2 / systemd-boot / shim.efi) → Linux Kernel(vmlinuz + initrd) → 用户空間桌面端 Linux 啓動特點
- 標準化固件:UEFI 提供統一接口(如 EFI System Partition, ESP)。
- ESP 分區:FAT32 格式,存放 .efi 可執行文件(如 grubx64.efi)。
- OS Loader:負責加載內核和 initrd,解析 /etc/default/grub 等配置。
- 無需設備樹:x86 硬件信息通過 ACPI 表傳遞。
- 調用 ExitBootServices():OS Loader 在跳轉內核前關閉 UEFI Boot Services。
BIOS 模式(Legacy)已逐漸淘汰,流程為:BIOS → MBR → GRUB Stage1/Stage2 → Kernel
-
PC 上的 Windows(x86/x86_64,UEFI 模式)
UEFI Firmware → \EFI\Microsoft\Boot\bootmgfw.efi(Windows Boot Manager) → winload.efi → ntoskrnl.exe(Windows 內核) → Session Manager (smss.exe) → 用户登錄- 完全依賴 UEFI(現代 Windows 不再支持純 Legacy BIOS 安裝)。
- Secure Boot:驗證 bootmgfw.efi 和 winload.efi 的數字簽名。
- BCD(Boot Configuration Data):替代舊的 boot.ini,存儲在 ESP 或系統分區。
- 同樣調用 ExitBootServices():由 winload.efi 完成。
注意:Windows 的 Boot Manager 本身就是一個 UEFI 應用(.efi 文件)。
-
ARM 架構的 Windows
目前 ARM 架構與 Windows 的組合在市場上還不常見,但有,可通過轉譯實現,部分應用能夠原生運行。目前較有前景的芯片例子:驍龍X Elite。
ARM64 UEFI Firmware(由 SoC 提供,如 Qualcomm Snapdragon) → bootmgfw.efi → winload.efi → ntoskrnl.exe- 與 x86 Windows 啓動流程幾乎一致,只是架構為 ARM64。
- 需要 ARM64 版本的 UEFI 固件(通常由 OEM 集成在 SoC 中)。
- 微軟要求 Secure Boot 和 UEFI 支持。
-
ARM64 服務器/開發板
啓動流程類似嵌入式 ARM Linux,但部分高端 ARM 服務器支持 UEFI + ACPI(而非設備樹)。
例如:EDK II UEFI → GRUB for ARM64 → vmlinuz(使用 ACPI 表)- 低端 ARM(嵌入式):U-Boot + Device Tree
- 高端 ARM(服務器/PC):UEFI + ACPI(更接近 x86 PC 模式)
總結:
| 維度 | 嵌入式 Linux (ARM) | PC Linux (x86 UEFI) | PC Windows (x86 UEFI) | ARM PC (Windows/Linux) |
|---|---|---|---|---|
| 固件 | BootROM(廠商定製) | 標準 UEFI | 標準 UEFI | ARM64 UEFI(OEM 提供) |
| Bootloader | U-Boot / Barebox | GRUB / systemd-boot | bootmgfw.efi | U-Boot(嵌入式)或 UEFI + GRUB/bootmgfw |
| 配置存儲 | 環境變量(U-Boot) | grub.cfg / kernel cmdline | BCD | BCD(Win)或 U-Boot env(Linux) |
| 硬件描述 | Device Tree (.dtb) | ACPI | ACPI | DTB(嵌入式)或 ACPI(高端 ARM) |
| 是否調用 ExitBootServices | ❌(無 UEFI) | ✅ | ✅ | ✅(若使用 UEFI) |
| Secure Boot | 通常無(除非自實現) | 可選(shim + signed kernel) | 強制(Win 11 要求) | 支持(Win on ARM 強制) |
| 典型介質 | eMMC / SPI Flash / SD | NVMe / SATA SSD(ESP 分區) | NVMe SSD(ESP) | eMMC / UFS / NVMe |
三、GRUB
GRUB 是 Linux 和類 Unix 系統中最著名、最常用的 Bootloader 之一。全稱 GRand Unified Bootloader,目前主流使用的是 GRUB 2(一般就讀 GRUB),早期版本 GRUB Legacy 已基本淘汰。
第二節中介紹了 TSL 階段執行的 OS Loader,而 GRUB 就是 Linux 系統啓動最常用的 Loader 之一。
GRUB 的工作流程:
- 顯示啓動菜單
- 從硬盤/SSD/U盤等設備讀取操作系統內核文件(如
vmlinuz) - 加載初始內存盤(initrd/initramfs)
- 傳遞啓動參數給內核(如 root=/dev/sda2 quiet splash)
- 將控制權交給操作系統內核,完成啓動交接
💡 在 UEFI 系統中,GRUB 本身是一個 .efi 可執行文件(如 grubx64.efi),由 UEFI 固件直接加載運行。
UEFI Firmware
↓
加載 ESP 分區中的 \EFI\ubuntu\grubx64.efi(即 GRUB)
↓
GRUB 讀取 /boot/grub/grub.cfg(配置文件)
↓
顯示啓動菜單(Ubuntu / Advanced options / Windows Boot Manager 等)
↓
用户選擇一項 → GRUB 加載對應的 vmlinuz + initrd
↓
GRUB 設置內核命令行參數(如 root=UUID=...)
↓
跳轉到 Linux 內核入口,移交控制權
GRUB 的應用場景包括:
| 場景 | 説明 |
|---|---|
| 多系統啓動 | 同一台電腦裝了 Windows + Linux,GRUB 菜單讓你選擇進哪個系統 |
| 多內核選擇 | Linux 更新後保留舊內核,GRUB 允許你選擇用哪個版本啓動 |
| 救援模式 | 可通過 GRUB 編輯啓動參數進入單用户模式或修復系統 |
| 網絡啓動(PXE) | 高級用法,GRUB 支持從網絡加載內核 |
在傳統 BIOS 模式下,GRUB 會分階段加載(Stage 1 → Stage 1.5 → Stage 2),但在 UEFI 模式下,這個過程被簡化了,因為 UEFI 本身提供了文件系統訪問能力。
GRUB 關鍵組件
| 組件 | 作用 |
|---|---|
| grub-install | 安裝 GRUB 到磁盤或 ESP 分區的工具(如 grub-install /dev/sda) |
| grub-mkconfig | 自動生成 /boot/grub/grub.cfg 的命令(通常通過 update-grub 調用) |
| grub.cfg | 主配置文件,定義菜單項、內核路徑、啓動參數等(不要手動編輯!) |
| /etc/default/grub | 用户可編輯的 GRUB 配置模板,運行 update-grub 後生效 |
| grub-efi | UEFI 版本的 GRUB 包(如 Debian/Ubuntu 中的 grub-efi-amd64) |
各種 Bootloader 總結
| Bootloader | 平台 | 特點 |
|---|---|---|
| GRUB 2 | x86/x86_64(PC/Linux) | 功能強大,支持腳本、主題、多系統 |
| systemd-boot | UEFI-only PC | 輕量、簡單,僅支持 EFI 分區內的內核 |
| U-Boot | 嵌入式 ARM/MIPS | 用於開發板,支持設備樹、網絡啓動 |
| rEFInd | UEFI 多系統 | 圖形化菜單,自動檢測所有 OS |
| Windows Boot Manager | Windows | 僅用於 Windows,通過 BCD 管理啓動項 |
四、Boot Services - GOP
GOP 即 Graphics Output Protocol,圖形輸出協議,是 UEFI 標準中用於提供基本圖形顯示能力的核心協議之一。GOP 屬於 Boot Services 階段可用的接口。它允許 UEFI 應用程序(如 OS Loader、UEFI Shell、圖形化啓動菜單)以像素級方式在屏幕上繪製圖像、文字、背景等。在調用 ExitBootServices() 後,GOP 協議失效,但屏幕內容通常保留。操作系統內核需自行接管顯卡驅動。
| 功能 | 説明 |
|---|---|
| 獲取屏幕分辨率 | 如 1920×1080、1280×720 等 |
| 獲取像素格式 | 如 PixelBlueGreenRedReserved8BitPerColor(BGR) |
| 訪問幀緩衝區(Frame Buffer) | 直接寫入顯存(物理地址)進行繪圖 |
| 設置顯示模式 | 切換分辨率(如果硬件支持) |
| 繪製簡單圖形 | 配合其他庫(如 uefi-graphics-lib)可畫線、矩形、位圖 |
💡 GOP 本身不提供“畫線”“寫字”等高級 API,它只暴露 Frame Buffer。高級圖形操作需上層軟件實現(如 GRUB 的 gfxterm、systemd-boot 的圖形菜單)。
在 EDK II 或 UEFI 開發中,GOP 主要通過以下結構體使用:
typedef struct _EFI_GRAPHICS_OUTPUT_PROTOCOL {
EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE QueryMode;
EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE SetMode;
EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT Blt; // Block Transfer(位塊傳輸)
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL;
Blt函數:用於將像素數據複製到屏幕(或從屏幕讀取),支持填充顏色、拷貝圖像等。Mode->Info:包含當前分辨率、像素格式、幀緩衝區物理地址(FrameBufferBase)。
各種 Loader 的使用
-
GRUB 圖形啓動菜單
-
GRUB 使用 GOP 進入圖形模式(
gfxmode=1920x1080) -
顯示背景圖、高亮菜單項
-
依賴 GOP 的
Blt操作刷新屏幕
-
-
systemd-boot
-
支持簡單的圖形啓動界面(需啓用)
-
使用 GOP 清屏並繪製文字菜單
-
-
Windows Boot Manager
- 在 UEFI 模式下使用 GOP 顯示 Windows 徽標和加載動畫
-
UEFI Shell 圖形擴展
- 可運行
.efi圖形程序(如診斷工具、Logo 顯示器)
- 可運行
需要注意的是,GOP 是 Boot Services ,OS 沒法調用。Frame Buffer 信息是由 UEFI 通過 ACPI 或 Device Tree 傳遞給 OS 的。雖然 OS 不能調用 GOP 協議,但 UEFI 會在 ExitBootServices() 之前,把關鍵圖形信息“固化”到標準位置,供操作系統讀取。
五、ESP 目錄
ESP(EFI System Partition,EFI 系統分區)是操作系統與 UEFI 固件之間進行啓動交互的“橋樑”。它本質上是一個格式化為 FAT32(有時也支持 FAT16/FAT12)的磁盤分區。用於存放 UEFI 可執行文件(.efi)、驅動、配置文件等,供 UEFI 固件直接讀取和運行。OS 的 Loader 放在 ESP 目錄,這樣 Loader 直接讀取當前路徑下的 xxx.efi 文件並解析運行即可。
| 特性 | 説明 |
|---|---|
| 文件系統 | FAT32 |
| 分區類型 GUID | C12A7328-F81F-11D2-BA4B-00A0C93EC93B(GPT 分區表) 或類型 ID 0xEF(MBR 分區表,較少見) |
| 大小建議 | Windows 要求 ≥100 MB,Linux 建議 ≥512 MB(便於多內核共存) |
| 是否可分配盤符 | 不應分配盤符(Windows 默認隱藏,Linux 通常掛載到 /boot/efi) |
| 內容可讀寫 | 是,但需謹慎操作(誤刪會導致無法啓動!) |
ESP 的典型目錄結構:
[ESP 分區根目錄]
├── EFI/
│ ├── BOOT/
│ │ └── BOOTX64.EFI ← 默認 fallback 啓動文件(x86_64 架構)
│ ├── Microsoft/
│ │ └── Boot/
│ │ ├── bootmgfw.efi ← Windows Boot Manager
│ │ └── BCD ← Windows 啓動配置數據庫
│ └── ubuntu/
│ ├── grubx64.efi ← Ubuntu 的 GRUB 啓動器
│ ├── shimx64.efi ← 支持 Secure Boot 的簽名代理
│ └── grub.cfg ← GRUB 的簡單配置(指向 /boot/grub/grub.cfg)
└── [其他可能文件]
├── drivers/ ← UEFI 驅動(少見)
└── tools/ ← UEFI 工具(如 fwupdate)
- BOOTX64.EFI:UEFI 的“備用啓動項”。如果 NVRAM 中的啓動項損壞,固件會自動嘗試從此路徑加載。
- *.efi 文件:都是 PE32+ 格式的可執行程序(類似 Windows 的 .exe,但專為 UEFI 環境編譯)。
- BCD:Windows 的啓動配置,二進制格式,用 bcdedit 管理。
- grub.cfg(在 ESP 中):通常只包含一行 configfile 指向真正的配置(位於 Linux 的 /boot/grub/grub.cfg)。
啓動流程示例(UEFI 模式):
- 電腦上電 → UEFI 固件初始化
- 固件讀取 NVRAM 中的啓動項(如 Boot0001: ubuntu)
- 根據啓動項路徑(如 \EFI\ubuntu\grubx64.efi),從 ESP 分區加載該 .efi 文件
- 執行 GRUB → GRUB 再從普通 ext4 分區加載 Linux 內核
- 完成啓動
在第一章講 EDKII 環境搭建的時候,命令
qemu-system-x86_64 \
-bios /home/ayuan/run-ovmf/bios.bin \
-drive format=raw,file=fat:rw:/home/ayuan/run-ovmf/hda-contents \
-m 1024M
中的 -drive 就給出了 ESP 的路徑。
在 Linux 系統中通常掛載在 /boot/efi(/boot/efi/EFI)。
六、用到的一些 Services
完成啓動的一些功能需要依賴的啓動或者運行時服務,非常明確的有以下幾個
6.1 Boot Services - Memory Allocation Services
Memory Allocation Services 為內存分配服務,用於在操作系統加載前動態申請和釋放內存,這些服務通過 EFI_BOOT_SERVICES 結構體中的函數指針提供。
Memory Allocation Services 在 EDKII 中的組織形式:
MdeModulePkg/
└── Core/
└── Dxe/
├── Mem/
│ ├── Pool.c ← Pool 內存分配(AllocatePool/FreePool)
│ ├── Page.c ← 頁面內存分配(AllocatePages/FreePages)
│ └── MemoryMap.c ← GetMemoryMap 實現
├── Core/
│ └── DxeMain.c ← 初始化內存服務,設置 gBS 表
└── Include/
└── CoreData.h ← 內存管理全局變量聲明
UEFI 內存分配服務主要包括以下 4 個核心函數:
| 函數名 | 作用 |
|---|---|
AllocatePages |
按 頁(Page) 為單位分配物理連續內存 |
FreePages |
釋放通過 AllocatePages 分配的內存 |
AllocatePool |
從 內存池(Pool) 中分配小塊內存(類似 malloc) |
FreePool |
釋放通過 AllocatePool 分配的內存 |
詳細接口説明
-
AllocatePages
// 為內核鏡像分配大塊連續內存 // 自動 4KB 對齊 typedef EFI_STATUS (EFIAPI *EFI_ALLOCATE_PAGES)( IN EFI_ALLOCATE_TYPE Type, // 分配策略(任意地址,指定地址*Memory等) IN EFI_MEMORY_TYPE MemoryType, // 內存用途類型 IN UINTN Pages, // 要分配的頁數 IN OUT EFI_PHYSICAL_ADDRESS *Memory // 輸入/輸出參數,返回分配的物理地址 ); -
FreePages
// 釋放之前通過 AllocatePages 分配的內存 typedef EFI_STATUS (EFIAPI *EFI_FREE_PAGES)( IN EFI_PHYSICAL_ADDRESS Memory, IN UINTN Pages ); -
AllocatePool
// 從內存池中分配指定字節數的小塊內存(內部由固件管理堆) // 內存不一定物理連續,但邏輯上連續 // 適合分配字符串、結構體、臨時緩衝區等 // 通常 8 字節對齊(具體由固件實現決定) typedef EFI_STATUS (EFIAPI *EFI_ALLOCATE_POOL)( IN EFI_MEMORY_TYPE PoolType, // 內存類型(通常用 EfiLoaderData) IN UINTN Size, // 字節數(不要求對齊到頁) OUT VOID **Buffer // 返回分配的虛擬地址(在 UEFI 應用地址空間中) ); -
FreePool
// 釋放 AllocatePool 分配的內存 typedef EFI_STATUS (EFIAPI *EFI_FREE_POOL)( IN VOID *Buffer ); -
GetMemoryMap
// 在退出啓動服務(ExitBootServices)之前,獲取系統當前的內存佈局信息。 // 核心作用是講物理內存的哪些地址範圍被什麼類型的數據佔用了(比如被UEFI固件、加載的EFI程序、或者系統保留內存等),以及這些內存區域的屬性(比如是否可寫、是否可執行)。 typedef EFI_STATUS (EFIAPI *EFI_GET_MEMORY_MAP) ( IN OUT UINTN *MemoryMapSize, // 輸入: 為 MemoryMap 緩衝區分配的大小; // 輸出: 實際需要的內存地圖大小 OUT VOID *MemoryMap, // 指向緩衝區的指針,用於存放內存描述符數組。每個描述符代表一個連續的內存區域。 OUT UINTN *MapKey, // 代表了當前內存地圖的“版本”。每次內存佈局發生變化(比如你又分配了一塊內存),這個 MapKey 就會改變。 OUT UINTN *DescriptorSize, // 返回單個內存描述符結構體(EFI_MEMORY_DESCRIPTOR)的大小。 OUT UINT32 *DescriptorVersion// 內存描述符結構體的版本號 ); // 內存描述符 typedef struct { UINT32 Type; // 內存類型 EFI_PHYSICAL_ADDRESS PhysicalStart; // 物理起始地址 EFI_VIRTUAL_ADDRESS VirtualStart; // 虛擬起始地址(Boot Services階段通常為0) UINT64 NumberOfPages; // 該區域有多少個4KB頁面 UINT64 Attribute; // 內存屬性 } EFI_MEMORY_DESCRIPTOR;- 在調用 ExitBootServices() 時,必須傳入參數 MapKey。UEFI會檢查你傳入的Key是否與當前最新的Key一致。如果不一致,説明在你獲取地圖和退出服務之間內存佈局變了,ExitBootServices() 會失敗,你需要重新獲取地圖並再次嘗試。
使用流程:
第一次調用(獲取所需大小):- 將 MemoryMap 指針設為 NULL。
- 將 MemoryMapSize 指向一個變量(比如設為0)。
- 調用 GetMemoryMap()。函數會失敗(返回 EFI_BUFFER_TOO_SMALL),但會在 MemoryMapSize 指向的變量中填入實際需要的緩衝區大小。
- 分配緩衝區:使用上一步獲取的大小,通過 AllocatePool() 或 AllocatePages() 分配足夠大的內存緩衝區。
第二次調用(獲取真實數據):
- 將新分配的緩衝區地址和大小傳入 GetMemoryMap()。
- 這次調用應該會成功返回 EFI_SUCCESS,並填充 MemoryMap、MapKey 等所有輸出參數。
- 立即調用 ExitBootServices():
拿到 MapKey 後,儘快調用 ExitBootServices(ImageHandle, MapKey),確保內存地圖沒有失效。
對於內存描述符,Type 字段尤為重要,它定義了內存的用途,常見的有:
- EfiConventionalMemory: 普通可用內存,這是操作系統可以自由分配和使用的。
- EfiBootServicesCode/Data: Boot Services的代碼和數據,在調用ExitBootServices後會變為EfiConventionalMemory。
- EfiRuntimeServicesCode/Data: 運行時服務,退出Boot Services後仍可使用。
- EfiMemoryMappedIO: 內存映射的I/O空間。
- EfiReservedMemoryType: 保留內存,不能使用。
接口的調用方式:
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/BaseMemoryLib.h>
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_BOOT_SERVICES *gBS = SystemTable->BootServices;
EFI_STATUS Status;
VOID *Buffer;
EFI_PHYSICAL_ADDRESS PhysAddr;
// 示例1:分配 2 頁(8KB)物理內存
PhysAddr = 0;
Status = gBS->AllocatePages(
AllocateAnyPages,
EfiLoaderData,
2, // 2 pages = 8192 bytes
&PhysAddr
);
if (!EFI_ERROR(Status)) {
Print(L"Allocated pages at 0x%llx\n", PhysAddr);
// 使用內存...
gBS->FreePages(PhysAddr, 2);
}
// 示例2:分配 256 字節池內存
Status = gBS->AllocatePool(
EfiLoaderData,
256,
&Buffer
);
if (!EFI_ERROR(Status)) {
ZeroMem(Buffer, 256);
Print(L"Pool allocated at %p\n", Buffer);
gBS->FreePool(Buffer);
}
return EFI_SUCCESS;
}
上面程序中涉及 EFI_SYSTEM_TABLE,這裏簡單介紹一下。通俗來説它就像是一個“目錄”或“索引”,它本身不包含所有功能,但它告訴你去哪裏找這些功能,包括重要的BootServices,BootServices指針,以及配置表。
當一個 UEFI 應用程序(如 Windows 的 bootmgfw.efi 或Linux的 grubx64.efi)被加載執行時,UEFI 固件會通過一個標準化的入口點(通常是EFI_IMAGE_ENTRY_POINT)傳遞兩個最重要的參數:
ImageHandle: 一個代表當前應用程序本身的句柄。SystemTable: 一個指向EFI_SYSTEM_TABLE結構體的指針。
配置表包括:
ACPI(RSDP): 指向ACPI根系統描述符表的指針,這是OS進行電源管理的基石。SMBIOS: 指向SMBIOS表的指針,包含硬件資產信息(型號、序列號、內存佈局等)。MPS: 舊的多處理器規範表(較少用)。- 顯卡GOP信息等。
6.2 Boot Services - Protocol Handler Services
Protocol Handler Services 即協議處理器服務,專門用於安裝、卸載、打開、關閉、查詢和管理 Protocol(協議)與 Handle(句柄)之間的關係。
其在 EDKII 中的組織:
MdeModulePkg/Core/Dxe/
├── Core/ ← DXE Core 主體
│ ├── Hand/ ← Handle & Protocol 管理(Protocol Handler Services)
│ ├── Sched/ ← 調度器(驅動加載等)
│ └── Event/ ← 事件與定時器服務
└── Library/ ← Boot Services 接口封裝(如 gBS 初始化)
與上一節中的內存分配服務子類中的接口調用方式一樣,gBS 指向一個靜態分配的 EFI_BOOT_SERVICES 結構體,其函數指針指向這些實現。
既然 Protocol Handler Services 提供協議和句柄管理,所以在介紹它之前,先了解一下 Protocol 和 Handle 是做什麼的。
6.2.1 Protocol 和 Handle 概述
Protocol 本質上是結構體加函數指針結合成的一套函數接口,Handle 則為一個資源對象的標識。Handle 代表一個資源實例,那麼這個實例提供哪些服務呢,由安裝在 Handle 上的 Protocol 決定。簡單來説,我們找到一個資源對象,要調用這個對象提供的服務,就要調用 Handle -> Protocol -> 具體函數。
如果你有 Linux 驅動開發經驗,一定對字符設備的註冊流程很熟悉,一個字符設備 device 會對應着一系列的文件操作函數集合 fileoperations,Handle 就類似於 device,Protocol 就類似於 Fileoperations。
UEFI 涉及這個 Protocol Handle 機制的動機在哪裏呢。這有類似於 Linux 的分層涉及思想了,在 UEFI 中,驅動、應用、Boot Manager 之間 不直接調用彼此的函數,不同層級之間程序的調用在 Linux 通過系統調用實現,在 UEFI 中即使用 Protocol Handle 機制。
下面舉個 UEFI 驅動的例子,看代碼更容易理解,示例代碼由 AI 生成。
1. 驅動程序
跟 Linux 類似,UEFI 也由驅動和應用程序,設計思想與 Linux 很類似。簡單來説 Driver 是服務提供者,裝到 Handle 上的 Protocol, App 是服務使用者,通過 Protocol 來做事。
| 對比項 | UEFI Driver | UEFI Application |
|---|---|---|
| 作用 | 向系統“提供服務” | 使用服務、執行一次性任務 |
| 提供內容 | Protocol(函數接口) | 使用協議、執行邏輯 |
| 加載時機 | Boot Services(加載得早) | 通常由 Shell/Boot Manager 運行 |
| 生命週期 | 可持續存在直到 ExitBootServices | 運行完就退出 |
| 被誰調用? | 由系統調用(Driver Binding) | 由用户或 Boot Manager 調用 |
| 能否處理硬件? | 是(PCI/USB/Block etc.) | 一般不處理硬件 |
| 是否參與設備枚舉? | 是(ConnectController) | 否 |
| 是否能被自動綁定? | 是(基於 GUID 和設備路徑) | 否 |
| 是否必須遵循 Driver Model? | 是 | 不需要 |
舉個例子:文件系統服務
驅動(Driver)實現文件系統協議 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL,安裝在某個 Handle 上。
BlockDeviceHandle
├── BlockIoProtocol
└── SimpleFileSystemProtocol
它提供讀目錄、讀文件的服務。假如你的 UEFI 程序需要讀文件:
LocateProtocol(&gEfiSimpleFileSystemProtocolGuid, ...);
總的來説驅動負責控制、初始化、封裝硬件,應用負責用這些已封裝好的服務。
#include <Uefi.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Protocol/DriverBinding.h>
// 1. 自定義一個協議
typedef struct {
EFI_STATUS (EFIAPI *Hello)(VOID);
} MY_PROTOCOL;
// MyHello 是個函數,把這個函數放到Protocol實例裏面
EFI_STATUS
EFIAPI
MyHello(VOID)
{
DEBUG((DEBUG_INFO, "MyProtocol: Hello called!\n"));
return EFI_SUCCESS;
}
MY_PROTOCOL mMyProtocol = {
MyHello
};
EFI_GUID gMyProtocolGuid = { 0xaabbccdd, 0x1234, 0x4321, {0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x11,0x22} };
// 2. Driver Binding:Supported()
// 判斷該驅動是否支持某個 Controller Handle
EFI_STATUS
EFIAPI
MyDriverSupported (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
) {
// 示例程序直接返回支持
return EFI_SUCCESS;
}
// 3. Driver Binding:Start()
// 當 ConnectController() 被調用時執行
EFI_STATUS
EFIAPI
MyDriverStart (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
) {
EFI_STATUS Status;
// 在 ControllerHandle 上安裝協議(綁定)
Status = gBS->InstallProtocolInterface(
&ControllerHandle,
&gMyProtocolGuid,
EFI_NATIVE_INTERFACE,
&mMyProtocol
);
DEBUG((DEBUG_INFO, "MyDriver: Installed MyProtocol. Status = %r\n", Status));
return Status;
}
// 4. Driver Binding:Stop()
// 用於卸載協議
EFI_STATUS
EFIAPI
MyDriverStop (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN UINTN NumberOfChildren,
IN EFI_HANDLE *ChildHandleBuffer
) {
EFI_STATUS Status;
Status = gBS->UninstallProtocolInterface(
ControllerHandle,
&gMyProtocolGuid,
&mMyProtocol
);
DEBUG((DEBUG_INFO, "MyDriver: Uninstalled MyProtocol. Status = %r\n", Status));
return Status;
}
// 5. DriverBinding Protocol 實例
EFI_DRIVER_BINDING_PROTOCOL gMyDriverBinding = {
MyDriverSupported,
MyDriverStart,
MyDriverStop,
0x10, // Version
NULL, // ImageHandle
NULL // DriverBindingHandle
};
// 6. 驅動入口
EFI_STATUS
EFIAPI
MyDriverEntryPoint (
IN EFI_HANDLE ImageHandle, // 這裏我們上文在講EFI_SYSTEM_TABLE時提到過
IN EFI_SYSTEM_TABLE *SystemTable
) {
// 安裝 Driver Binding Protocol
return EfiLibInstallDriverBindingComponentName2(
ImageHandle,
SystemTable,
&gMyDriverBinding,
ImageHandle,
NULL,
NULL
);
}
關於 UEFI 驅動框架的關鍵部分解釋:
UEFI 驅動採用動態綁定的方式,一個驅動可以支持多個設備,一個設備也可能被多個驅動嘗試支持(但最終只有一個成功綁定)。為了實現這種靈活的匹配和管理,UEFI 引入了:
-
Controller Handle(控制器句柄):代表一個“設備”或“服務”的抽象(比如一個 USB 控制器、一塊硬盤、一個網絡接口)。
-
Driver Binding Protocol:每個驅動都要提供這個協議,告訴系統:“我能支持哪些設備?怎麼啓動/停止?”
每個 UEFI 驅動必須實現一個
EFI_DRIVER_BINDING_PROTOCOL結構體,包含三個重要的函數指針。-
DriverSupported
EFI_STATUS MyDriverSupported ( IN EFI_DRIVER_BINDING_PROTOCOL *This, IN EFI_HANDLE ControllerHandle, IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath OPTIONAL ); // 舉例:驅動只支持有 UsbIo 協議的設備 Status = gBS->OpenProtocol( ControllerHandle, // 要查詢協議的對象句柄 &gEfiUsbIoProtocolGuid, // 協議GUID (VOID**)&UsbIo, // 返回協議接口指針 This->DriverBindingHandle, // 調用者(驅動)的句柄 ControllerHandle, // 控制器句柄 EFI_OPEN_PROTOCOL_TEST_PROTOCOL // 打開方式屬性,"測試模式" ); if (!EFI_ERROR(Status)) { return EFI_SUCCESS; // 支持! } return EFI_UNSUPPORTED;該函數用於判斷該驅動程序是否支持傳入的 ControllerHandle 代表的設備,如果檢查通過,系統會調用驅動的
Start()函數,在Start()中再使用BY_DRIVER正式打開協議。這種方式確保了驅動只會綁定到真正支持其協議的設備上。系統(通常是 Dispatcher 或 ConnectController)會遍歷所有已加載的驅動,對每一個 Controller Handle 調用此函數,問是否能驅動這個設備。-
Supported()調用觸發場景
graph TD A[驅動被加載] --> B[調用DriverEntry] B --> C[安裝DriverBinding協議] C --> D[UEFI掃描現有設備] D --> E[對每個設備調用Supported] E --> F{Supported返回成功?} F -->|是| G[將驅動標記為支持該設備] F -->|否| H[繼續掃描下一個設備] G --> I[等待ConnectController調用] I --> J[調用驅動的Start函數]舉個在 shell 中加載驅動的例子:
# 加載一個驅動 load MyUsbDriver.efi # 系統會立即: # 1. 調用 MyUsbDriver 的 Supported() 函數 # 2. 傳入系統中每個設備 Handle # 3. 對支持的設備,可能立即或稍後調用Start() # 插入一個USB設備 # 系統會: # 1. 創建新Handle # 2. 調用所有驅動的Supported()函數 # 3. 包括剛加載的MyUsbDriver -
OpenProtocol函數的作用
OpenProtocol 用於查看傳入的 Handle 上是否安裝了我們關心的協議(比如
EFI_USB_IO_PROTOCOL),如果符合你的驅動能力,就返回EFI_SUCCESS;否則返回EFI_UNSUPPORTED。 -
ControllerHandle 參數介紹
函數傳入的參數
ControllerHandle是要判斷是否支持的那個設備的句柄。比如:一個 SATA 控制器、一個 USB 鍵盤、一塊 NVMe 硬盤——每個在 UEFI 中都是一個 Handle。當平台固件新發現一個設備,會為這個設備創建一個 Handle,並且安裝對應的協議:
// 平台固件(或UEFI內核)發現一個PCI設備 // 創建一個新的 Handle 來代表這個設備 EFI_HANDLE PciDeviceHandle = AllocateHandle(); // 安裝 PCI I/O 協議到這個 Handle gBS->InstallProtocolInterface( &PciDeviceHandle, // Handle &gEfiPciIoProtocolGuid, // 協議GUID EFI_NATIVE_INTERFACE, PciIoProtocol // 協議實例 ); // 此時,PciDeviceHandle就是一個 ControllerHandle。當我們的驅動被調用時,會判斷驅動是否支持調用的設備,傳入的 ControllerHandle 即為調用驅動的設備句柄。
更具體的例子:場景,USB 鍵盤驅動。這個代碼的邏輯是:在固件啓動過程中會枚舉主板上連接的各種硬件設備,然後為每個硬件設備匹配對應的驅動,對應 USB 鍵盤設備,會將其 Handle 傳入多個驅動程序調用 DriverSupported,如果找到支持 USB 鍵盤設備的驅動,就會返回 EFI_SUCCESS,代碼如下:
// 當系統枚舉USB端口時... EFI_STATUS UsbKeyboardDriverSupported( IN EFI_DRIVER_BINDING_PROTOCOL *This, IN EFI_HANDLE ControllerHandle // 這個可能是USB鍵盤設備 ) { // 檢查:這個Handle是USB設備嗎? EFI_USB_IO_PROTOCOL *UsbIo; Status = gBS->OpenProtocol( ControllerHandle, &gEfiUsbIoProtocolGuid, (VOID**)&UsbIo, This->DriverBindingHandle, ControllerHandle, EFI_OPEN_PROTOCOL_TEST_PROTOCOL ); if (EFI_ERROR(Status)) { return EFI_UNSUPPORTED; // 不是USB設備 } // 檢查2:是鍵盤設備嗎? // 通過USB I/O協議獲取設備描述符 EFI_USB_DEVICE_DESCRIPTOR DevDesc; UsbIo->UsbGetDeviceDescriptor(UsbIo, &DevDesc); if (DevDesc.DeviceClass == 0 && // 由接口定義類別 DevDesc.DeviceSubClass == 0 && DevDesc.DeviceProtocol == 0) { // 獲取接口描述符 EFI_USB_INTERFACE_DESCRIPTOR IfDesc; UsbIo->UsbGetInterfaceDescriptor(UsbIo, &IfDesc); if (IfDesc.InterfaceClass == 3 && // HID類 IfDesc.InterfaceSubClass == 1 && // 啓動接口 IfDesc.InterfaceProtocol == 1) { // 鍵盤協議 return EFI_SUCCESS; // 支持USB鍵盤! } } return EFI_UNSUPPORTED; // 不是鍵盤 } -
-
DriverStart
EFI_STATUS MyDriverStart ( IN EFI_DRIVER_BINDING_PROTOCOL *This, IN EFI_HANDLE ControllerHandle, IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath OPTIONAL ); -
DriverStop
EFI_STATUS MyDriverStop ( IN EFI_DRIVER_BINDING_PROTOCOL *This, IN EFI_HANDLE ControllerHandle, IN UINTN NumberOfChildren, IN EFI_HANDLE *ChildHandleBuffer OPTIONAL );
-
對應的 INF 文件:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = MyDriver
FILE_GUID = aabbccdd-0000-1111-2222-123456789abc
MODULE_TYPE = UEFI_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = MyDriverEntryPoint
[Sources]
MyDriver.c
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiDriverEntryPoint
UefiBootServicesTableLib
DebugLib
[Protocols]
gEfiDriverBindingProtocolGuid
| 模塊 | 功能 |
|---|---|
| MyDriverEntryPoint | 驅動被加載時執行,安裝 Driver Binding |
| DriverBinding.Supported | 判斷能否驅動某個設備(簡化) |
| DriverBinding.Start | ConnectController 時被調用,並安裝你的協議 |
| DriverBinding.Stop | DisconnectController 時卸載協議 |
| MyProtocol | 自定義協議示例(你可以替換成自己的接口) |
驅動的運行過程(UEFI Driver Model 流程)
- 驅動被加載,入口安裝 Driver Binding
- Boot Manager 調用
ConnectController() - MyDriverSupported → 判斷是否支持設備
- MyDriverStart → 綁定驅動 + 安裝協議
- 其他驅動或應用可通過 LocateProtocol 打開你的協議
2. 應用程序
本示例説明了通過 LocateProtocol() 查找自定義的 MyProtocol;調用協議裏的函數(Hello());完整可編譯,可直接與前面的驅動示例配套使用。
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/PrintLib.h>
#include <Library/DebugLib.h>
// 與驅動中完全一致的協議聲明(只需複製)
typedef struct {
EFI_STATUS (EFIAPI *Hello)(VOID);
} MY_PROTOCOL;
EFI_GUID gMyProtocolGuid = {
0xaabbccdd, 0x1234, 0x4321,
{0xaa,0xbb,0xcc,0xdd,0xee,0xff,0x11,0x22}
};
// main 函數
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
) {
EFI_STATUS Status;
MY_PROTOCOL *MyProto = NULL;
Print(L"MyApp: Locating MyProtocol...\n");
// 1. 查找 MyProtocol
Status = gBS->LocateProtocol(
&gMyProtocolGuid,
NULL,
(VOID**)&MyProto
);
if (EFI_ERROR(Status)) {
Print(L"MyApp: Failed to locate MyProtocol: %r\n", Status);
return Status;
}
Print(L"MyApp: MyProtocol Found. Calling Hello()...\n");
// 2. 調用協議函數
Status = MyProto->Hello();
Print(L"MyApp: Hello() returned: %r\n", Status);
return Status;
}
看到上面的代碼,可能會有疑問,前面我們講過 Protocol 一半是安裝到某個 Handle 上的,但是在上面的應用程序中並沒有體現出來。首先我們要明確,UEFI 中 Protocol 必須依附於 Handle。代碼中沒有顯式使用 Handle,但 gEfiSimpleTextIn-ProtocolGuid 所對應的 Protocol 實例仍然是掛載在某個 Handle 上的。LocateProtocol() 內部會遍歷系統中所有 Handle,查找哪個 Handle 安裝了該 GUID 的 Protocol,然後返回其接口指針(即函數表)。你只是不需要自己操作 Handle,不代表 Handle 不存在或不重要。
注意:LocateProtocol() 只能用於全局唯一的 Protocol(比如控制枱輸入、實時時鐘等)。如果一個 Protocol 有多個實例(比如多個 USB 鍵盤都提供 SimpleTextIn),你就不能用 LocateProtocol(),而要用 LocateHandleBuffer() + HandleProtocol() 來枚舉所有 Handle 並選擇合適的那個。
反過來講 Handle 的存在就是為了:
- 支持多實例:多個設備可以提供相同類型的 Protocol(如多個網卡都有
SimpleNetworkProtocol),每個綁定到不同的 Handle。 - 資源管理:卸載驅動或設備時,可以通過 Handle 一次性移除其所有 Protocol。
- 協議組合:一個 Handle 可以掛多個 Protocol(如一個 USB 設備 Handle 同時有 DevicePath、UsbIo、BlockIo 等),形成完整的設備描述。
對應 INF 文件:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = MyApp
FILE_GUID = aabbccdd-1111-2222-3333-123456789abc
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
[Sources]
MyApp.c
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiLib
UefiBootServicesTableLib
PrintLib
5. 測試
-
先加載驅動(MyDriver.efi)
fs0:\> load MyDriver.efi -
然後運行測試 APP
fs0:\> MyApp.efi -
輸出示例:
MyApp: Locating MyProtocol... MyApp: MyProtocol Found. Calling Hello()... MyProtocol: Hello called! MyApp: Hello() returned: Success
6.2.2 Protocol Handler Services
上面我們對 Protocol 和 Handle 兩個重要的概念進行了説明,給出的代碼中其實已經用到了 Protocol Handler Services 中的各種管理功能。
協議管理服務中一些重要的 API:
-
註冊協議,即驅動將自己的服務以 Protocol 的方式暴露出來。
InstallProtocolInterface() ReinstallProtocolInterface() UninstallProtocolInterface() -
查找協議。
LocateProtocol() LocateHandle() LocateHandleBuffer() RegisterProtocolNotify() -
打開或關閉協議(權限控制),UEFI 不讓你隨便直接訪問別人的協議,它有權限模型。
OpenProtocol() CloseProtocol() // Open 時必須指定訪問類型 EFI_OPEN_PROTOCOL_GET_PROTOCOL EFI_OPEN_PROTOCOL_BY_DRIVER EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER EFI_OPEN_PROTOCOL_TEST_PROTOCOLOpenProtocol() 函數:
EFI_STATUS EFIAPI OpenProtocol( IN EFI_HANDLE Handle, // 要查詢協議的對象句柄 IN EFI_GUID *Protocol, // 協議GUID OUT VOID **Interface, OPTIONAL // 返回協議接口指針 IN EFI_HANDLE AgentHandle, // 調用者(驅動)的句柄 IN EFI_HANDLE ControllerHandle, // 控制器句柄 IN UINT32 Attributes // 打開方式屬性 );屬性 用途 是否需要Close 引用計數 TEST_PROTOCOL僅測試是否存在 否 不變 BY_DRIVER驅動使用協議 是 增加 GET_PROTOCOL獲取接口指針 是 增加 BY_CHILD_CONTROLLER子控制器使用 是 增加
關鍵 API 總結:
| 功能 | 關鍵 API | 作用 |
|---|---|---|
| 註冊協議 | InstallProtocolInterface | 將協議掛到 Handle 上 |
| 查找協議 | LocateProtocol / LocateHandle | 找協議實例或對應的設備句柄 |
| 打開協議 | OpenProtocol | 獲取協議接口(帶權限控制) |
| 關閉協議 | CloseProtocol | 釋放協議使用 |
| 監聽協議事件 | RegisterProtocolNotify | 當協議安裝後自動回調事件 |
| 驅動連接 | ConnectController | 綁定驅動與設備 |
| 驅動斷開 | DisconnectController | 解綁 |
還沒結束,持續補充中...
Steady Progress!