Skip to content

EDK2 (UEFI)

EDK2 (EFI Development Kit II, thường viết EDK II) là bản cài đặt mã nguồn mở tham chiếu của hai bộ đặc tả UEFI và PI, do cộng đồng TianoCore duy trì. Đây là nền của phần lớn firmware UEFI trên PC, server x86 và server ARM64, đồng thời là firmware mặc định của máy ảo QEMU/KVM (OVMF, ArmVirtQemu). Trang này trình bày theo thứ tự: quan hệ giữa đặc tả và EDK2, các pha boot, cấu trúc mã nguồn, hệ thống build, mô hình driver, build và chạy OVMF trên QEMU, EDK2 trên ARM64, biến UEFI và debug. Firmware legacy BIOS mà UEFI thay thế, và các nghĩa của từ "BIOS", xem tại BIOS (Legacy). Vị trí của firmware trong chuỗi boot nói chung xem tại Bootloader trong hệ nhúng Linux.

UEFI, PI và EDK2

UEFI Forum phát hành hai nhóm đặc tả độc lập với nhau:

  • UEFI Specification quy định giao diện giữa firmware và hệ điều hành: system table, boot services, runtime services, định dạng ảnh PE/COFF cho ứng dụng EFI, biến NVRAM, GPT, Secure Boot. Hệ điều hành và bootloader (GRUB, kernel có EFI stub) chỉ cần biết đặc tả này.
  • PI Specification (Platform Initialization) quy định cách bên trong firmware được tổ chức: các pha SEC, PEI, DXE, định dạng firmware volume (FV), cơ chế HOB (Hand-Off Block, cấu trúc dữ liệu truyền thông tin từ pha trước sang pha sau), System Management Mode (SMM) và Management Mode (MM).

EDK2 cài đặt cả hai. Phần lõi trong repo edk2 độc lập với phần cứng; code riêng cho từng board nằm trong edk2-platforms, còn binary không công khai mã nguồn (ví dụ FSP của Intel, blob của SoC) nằm trong edk2-non-osi. Một firmware thương mại (AMI Aptio, Insyde H2O, Phoenix) thường là EDK2 cộng với code nền tảng riêng của hãng.

Thành phần Nội dung Repo
Lõi MdePkg, MdeModulePkg, UefiCpuPkg, CryptoPkg, NetworkPkg, SecurityPkg, ShellPkg, BaseTools tianocore/edk2
Nền tảng ảo OvmfPkg (x86, RISC-V, LoongArch), ArmVirtPkg (ARM64) tianocore/edk2
Board thật Raspberry Pi, Intel MinPlatform, các board ARM64 server tianocore/edk2-platforms
Binary FSP, firmware blob của SoC tianocore/edk2-non-osi

EDK2 dùng giấy phép BSD-2-Clause-Patent. Mã nguồn được phát hành theo các tag edk2-stable<YYYYMM>, mỗi năm bốn bản vào tháng 2, 5, 8 và 11; tag mới nhất tại thời điểm viết là edk2-stable202608. Các tag .01 (ví dụ edk2-stable202508.01) là bản vá sau phát hành.

Các pha boot

Các pha boot theo PI và UEFI trong EDK2

Firmware EDK2 đi qua bảy pha. Ba pha đầu thuộc PI và chỉ có ý nghĩa với người viết firmware; từ BDS trở đi, hành vi được quy định bởi UEFI Specification và là phần hệ điều hành nhìn thấy.

Pha Tên đầy đủ Việc chính
SEC Security Nhận reset vector, chuyển CPU sang chế độ 32/64-bit, dựng RAM tạm (trên x86 là cache-as-RAM, dùng cache của CPU làm RAM khi DRAM chưa sẵn sàng), tìm và nạp PEI core. Đây là gốc tin cậy đầu tiên của firmware.
PEI Pre-EFI Initialization PEI core dispatch các module nhỏ gọi là PEIM. Việc quan trọng nhất là khởi tạo DRAM (memory training). Kết quả được ghi vào danh sách HOB, sau đó DXE IPL nạp DXE core vào DRAM.
DXE Driver Execution Environment DXE core cung cấp boot services và runtime services, dispatch toàn bộ driver theo thứ tự phụ thuộc (khai báo bằng depex). Các driver PCI, USB, storage, mạng, đồ họa, bảng ACPI và SMBIOS được dựng ở pha này.
BDS Boot Device Selection Kết nối console và thiết bị boot, đọc các biến BootOrder và Boot####, hiển thị setup menu nếu được yêu cầu, rồi chạy boot option được chọn.
TSL Transient System Load Ứng dụng EFI đang chạy: OS loader (GRUB, systemd-boot, Windows Boot Manager), kernel Linux có EFI stub hoặc UEFI Shell. Boot services vẫn dùng được.
RT Runtime Sau khi OS loader gọi ExitBootServices(), firmware giải phóng boot services. Hệ điều hành chỉ còn gọi được runtime services như GetVariable, SetVariable, GetTime, ResetSystem.
AL After Life Tắt máy, reset, hoặc xử lý capsule update khi hệ điều hành yêu cầu.

Trên Linux, kernel gọi runtime services qua driver efi. Biến UEFI được xuất ra /sys/firmware/efi/efivars/, và efibootmgr dùng giao diện này để đọc và sửa danh sách boot option.

Cấu trúc mã nguồn

Mã nguồn EDK2 tổ chức theo package: mỗi thư mục *Pkg là một package, có một file .dec khai báo những gì package đó công bố ra ngoài. Đơn vị nhỏ hơn là module: một driver, một ứng dụng hoặc một thư viện, mô tả bởi một file .inf.

Package Nội dung
MdePkg Header theo đặc tả UEFI/PI (Uefi.h, PiPei.h, các protocol), thư viện cơ bản không phụ thuộc phần cứng (BaseLib, BaseMemoryLib, DebugLib)
MdeModulePkg Module dùng chung: DXE core, PEI core, BDS, driver bus (PCI, USB, NVMe, SATA), console, biến UEFI
UefiCpuPkg Khởi tạo CPU, đa nhân (MP services), SMM trên x86
CryptoPkg Thư viện mật mã (bọc OpenSSL hoặc MbedTLS), dùng cho Secure Boot và TLS
SecurityPkg Secure Boot, đo lường vào TPM (measured boot), xác thực ảnh
NetworkPkg TCP/IP, PXE, HTTP boot, iSCSI
ShellPkg UEFI Shell và các lệnh shell
OvmfPkg, ArmVirtPkg Firmware cho máy ảo QEMU, KVM, Xen, Cloud Hypervisor
BaseTools Công cụ build: script build (Python), GenFv, GenFw, GenFfs (C)

Thư mục ArmPkg, ArmPlatformPkg, EmbeddedPkg chứa code dùng chung cho ARM. EmulatorPkg chạy firmware như một tiến trình trên host, dùng để thử code mà không cần máy ảo. UefiPayloadPkg đóng gói EDK2 thành payload cho một bootloader khác (coreboot, Slim Bootloader) đảm nhận phần khởi tạo phần cứng.

Một số thư viện bên ngoài được kéo về dưới dạng git submodule, trong đó có OpenSSL, MbedTLS, Brotli, libfdt và libspdm. Clone thiếu submodule là lỗi thường gặp khi build lần đầu.

Hệ thống build

Các loại file mô tả

EDK2 không dùng Makefile viết tay. Script build đọc bốn loại file mô tả, sinh Makefile vào thư mục Build/, rồi gọi make hoặc nmake.

File Viết tắt Vai trò
.dec Declaration Khai báo của package: đường dẫn include, GUID, protocol, library class, PCD mà package công bố
.inf Module Information Mô tả một module: loại module, entry point, file nguồn, package và library class phụ thuộc
.dsc Platform Description Mô tả một nền tảng: danh sách module cần build, chọn bản cài đặt cụ thể cho từng library class, giá trị PCD, cờ build
.fdf Flash Description Bố cục image flash: các firmware device (FD), firmware volume (FV) và module nằm trong từng FV

Library class là cơ chế tách giao diện khỏi bản cài đặt. Module chỉ khai báo cần DebugLib; file .dsc quyết định DebugLib đó là bản in ra serial, bản in ra cổng I/O debug của QEMU, hay bản rỗng cho build RELEASE. Nhờ vậy cùng một module dùng được trên nhiều nền tảng mà không sửa code.

PCD (Platform Configuration Database) là các tham số cấu hình có kiểu, khai báo trong .dec và gán giá trị trong .dsc. PCD có thể là hằng số lúc build (FixedAtBuild), cờ bật tắt tính năng (FeatureFlag), hoặc giá trị đọc ghi được lúc chạy (Dynamic). Ví dụ gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel chọn mức log được in.

Môi trường và lệnh build

git clone https://github.com/tianocore/edk2.git
cd edk2
git checkout edk2-stable202608
git submodule update --init

make -C BaseTools          # build công cụ C (GenFv, GenFw...)
source edksetup.sh         # đặt WORKSPACE, PATH, tạo Conf/ từ template

edksetup.sh tạo ba file trong Conf/ từ template ở BaseTools/Conf/: target.txt (nền tảng, kiến trúc, toolchain mặc định), tools_def.txt (định nghĩa các toolchain) và build_rule.txt. Có thể sửa target.txt hoặc truyền tham số trực tiếp cho build:

build -p OvmfPkg/OvmfPkgX64.dsc -a X64 -t GCC -b DEBUG
Tham số Ý nghĩa
-p File .dsc của nền tảng (ACTIVE_PLATFORM)
-a Kiến trúc đích: IA32, X64, AARCH64, RISCV64, LOONGARCH64. Có thể lặp lại, ví dụ -a IA32 -a X64 cho firmware có PEI 32-bit và DXE 64-bit
-t Toolchain tag: GCC, CLANGDWARF, CLANGPDB, VS2022
-b DEBUG, RELEASE hoặc NOOPT (tắt tối ưu, dễ debug bằng gdb)
-D Định nghĩa macro cho .dsc/.fdf, ví dụ -D SECURE_BOOT_ENABLE=TRUE
-n Số luồng build

Toolchain tag GCC5 đã bị bỏ

Nhiều hướng dẫn cũ dùng -t GCC5. Theo lịch sử thay đổi trong BaseTools/Conf/tools_def.template, các tag GCC48, GCC49 và GCC5 đã được thay bằng GCC (và GCCNOLTO), sau đó bị xóa hẳn. Với tag stable hiện tại cần dùng -t GCC.

Kết quả nằm ở Build/<PlatformName>/<TARGET>_<TOOLCHAIN>/. File .efi của từng module nằm trong thư mục kiến trúc (X64/), còn image flash hoàn chỉnh nằm trong FV/.

Mô hình lập trình

Handle, protocol và GUID

Mọi dịch vụ trong DXE và UEFI được trao đổi qua protocol: một struct chứa con trỏ hàm và dữ liệu, định danh bằng một GUID 128-bit. Protocol được gắn lên handle, một đối tượng trừu tượng đại diện cho thiết bị, driver hoặc ảnh đã nạp. Ví dụ, một handle đại diện cho ổ NVMe có thể mang EFI_DEVICE_PATH_PROTOCOL (vị trí của thiết bị), EFI_BLOCK_IO_PROTOCOL (đọc ghi block) và sau khi driver FAT kết nối thì có thêm EFI_SIMPLE_FILE_SYSTEM_PROTOCOL.

Code tìm dịch vụ bằng boot services: LocateProtocol lấy một instance bất kỳ của protocol, LocateHandleBuffer liệt kê mọi handle có protocol đó, OpenProtocol mở protocol trên một handle cụ thể. Đây là cơ chế thay thế cho việc gọi hàm trực tiếp giữa các module, vì mỗi driver là một ảnh PE/COFF độc lập được nạp lúc chạy.

Loại module

Trường MODULE_TYPE trong file .inf quyết định module chạy ở pha nào và được nạp ra sao.

MODULE_TYPE Chạy ở Ví dụ
SEC SEC Reset vector của nền tảng
PEIM PEI Khởi tạo DRAM, khởi tạo chipset sớm
DXE_DRIVER DXE Driver nền tảng, nạp theo depex
DXE_RUNTIME_DRIVER DXE và RT Dịch vụ biến UEFI, đồng hồ thời gian thực
DXE_SMM_DRIVER, MM_STANDALONE SMM/MM Ghi flash an toàn cho biến Secure Boot
UEFI_DRIVER DXE, BDS Driver theo UEFI Driver Model (PCI, USB, NVMe)
UEFI_APPLICATION BDS, TSL UEFI Shell, công cụ chẩn đoán, OS loader

UEFI Driver Model

Driver loại UEFI_DRIVER không tự dò phần cứng khi được nạp. Hàm entry point chỉ cài EFI_DRIVER_BINDING_PROTOCOL, gồm ba hàm:

  • Supported(): kiểm tra driver có điều khiển được một controller handle hay không, thường bằng cách mở protocol của bus cha (ví dụ EFI_PCI_IO_PROTOCOL) và đọc vendor ID, class code.
  • Start(): nhận quyền điều khiển controller, khởi tạo phần cứng và cài protocol mới lên handle (ví dụ EFI_BLOCK_IO_PROTOCOL). Driver bus còn tạo handle con cho từng thiết bị phát hiện được.
  • Stop(): gỡ protocol và trả lại controller.

Việc gọi các hàm này do ConnectController() đảm nhận, thường được BDS kích hoạt. Cách tách như vậy cho phép BDS chỉ kết nối những thiết bị cần cho boot, rút ngắn thời gian khởi động, và cho phép option ROM trên card PCIe cung cấp driver của riêng nó.

Ứng dụng UEFI tối giản

Module mẫu MdeModulePkg/Application/HelloWorld minh họa cấu trúc của một ứng dụng. Phiên bản rút gọn:

Hello.inf
[Defines]
  INF_VERSION    = 0x00010005
  BASE_NAME      = Hello
  FILE_GUID      = 3c1f6a3e-6f0a-4f43-9b1e-6a2d1f0c7b21
  MODULE_TYPE    = UEFI_APPLICATION
  VERSION_STRING = 1.0
  ENTRY_POINT    = UefiMain

[Sources]
  Hello.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiApplicationEntryPoint
  UefiLib
Hello.c
#include <Uefi.h>
#include <Library/UefiLib.h>

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  Print (L"Hello from UEFI, firmware vendor: %s\n", SystemTable->FirmwareVendor);
  return EFI_SUCCESS;
}

FILE_GUID phải là GUID riêng cho mỗi module (sinh bằng uuidgen). Chuỗi trong UEFI là UTF-16, nên dùng tiền tố L"...". Để build, thêm đường dẫn file .inf vào mục [Components] của một file .dsc (ví dụ MdeModulePkg/MdeModulePkg.dsc) rồi chạy build. File Hello.efi sinh ra chạy được từ UEFI Shell.

Build và chạy OVMF trên QEMU

OVMF (Open Virtual Machine Firmware) là nền tảng OvmfPkg của EDK2 dành cho máy ảo x86. Đây là cách nhanh nhất để thử EDK2 vì không cần phần cứng.

build -p OvmfPkg/OvmfPkgX64.dsc -a X64 -t GCC -b DEBUG
ls Build/OvmfX64/DEBUG_GCC/FV/
# OVMF.fd  OVMF_CODE.fd  OVMF_VARS.fd ...

Ngoài toolchain C, OVMF cần thêm NASM (assembler cho phần SEC) và trình biên dịch ASL iasl (gói acpica-tools) để build bảng ACPI. File .fdf của OVMF định nghĩa ba firmware device:

File Nội dung
OVMF_CODE.fd Code firmware, nên gắn ở chế độ chỉ đọc
OVMF_VARS.fd Vùng lưu biến UEFI (NVRAM), mỗi máy ảo cần một bản sao riêng
OVMF.fd Gộp hai file trên thành một image
cp Build/OvmfX64/DEBUG_GCC/FV/OVMF_VARS.fd my_vars.fd

qemu-system-x86_64 -machine q35 -m 2G -nographic \
    -drive if=pflash,format=raw,unit=0,readonly=on,file=Build/OvmfX64/DEBUG_GCC/FV/OVMF_CODE.fd \
    -drive if=pflash,format=raw,unit=1,file=my_vars.fd \
    -debugcon file:debug.log -global isa-debugcon.iobase=0x402 \
    -drive format=raw,file=fat:rw:./esp

Các điểm cần lưu ý:

  • Gắn firmware bằng -pflash/if=pflash thay vì -bios thì biến UEFI mới được lưu lại giữa các lần chạy. Theo OvmfPkg/README, build có Secure Boot bắt buộc dùng cách này.
  • Build DEBUG của OVMF ghi log ra cổng I/O 0x402, không ra serial. Hai tham số -debugcon và -global isa-debugcon.iobase=0x402 lưu log đó vào debug.log. Muốn log ra serial thì build với -D DEBUG_ON_SERIAL_PORT.
  • fat:rw:./esp để QEMU giả lập một ổ FAT từ thư mục ./esp trên host. Đặt Hello.efi hoặc EFI/BOOT/BOOTX64.EFI vào đó để chạy thử mà không cần tạo image disk.
  • Script OvmfPkg/build.sh gói cả bước build và chạy QEMU: OvmfPkg/build.sh -a X64 rồi OvmfPkg/build.sh -a X64 qemu.

Các bản phân phối Linux đóng gói sẵn OVMF (gói ovmf trên Debian/Ubuntu, edk2-ovmf trên Fedora) để libvirt và virt-manager dùng làm firmware UEFI cho máy ảo. Cách QEMU/KVM sử dụng firmware xem tại KVM và QEMU.

EDK2 trên ARM64

Trên ARM64, EDK2 thường không phải phần mềm đầu tiên chạy sau reset. Chuỗi boot điển hình là Boot ROM, sau đó Trusted Firmware-A (TF-A) với các stage BL1/BL2 khởi tạo DRAM và BL31 chạy thường trú ở EL3, rồi mới tới EDK2 ở vai trò BL33 chạy ở EL2. Như vậy phần việc của pha SEC và PEI trên x86 được TF-A đảm nhận một phần, còn EDK2 chủ yếu cung cấp DXE, BDS và giao diện UEFI. Khái niệm về TF-A và các exception level xem tại Secure boot và TrustZone.

Nền tảng Package Ghi chú
QEMU virt ArmVirtPkg/ArmVirtQemu.dsc Firmware AAVMF (gói qemu-efi-aarch64 trên Debian), đọc device tree do QEMU cung cấp
Raspberry Pi 4 edk2-platforms/Platform/RaspberryPi/RPi4 Cho phép boot các bản phân phối generic bằng UEFI và ACPI
Server ARM64 edk2-platforms và mã nguồn của hãng Tuân theo chương trình Arm SystemReady: UEFI cộng ACPI

Điểm khác lớn so với U-Boot là cách mô tả phần cứng cho kernel. Server ARM64 theo SystemReady dùng ACPI, còn các board nhúng thường dùng device tree. EDK2 hỗ trợ cả hai: có thể truyền device tree qua configuration table của UEFI, hoặc dựng bảng ACPI bằng DynamicTablesPkg. Kernel Linux nhận thông tin này qua EFI stub, không cần bootloader truyền địa chỉ DTB trong thanh ghi như khi boot từ U-Boot.

U-Boot EDK2
Đối tượng chính SoC nhúng, board ARM/RISC-V PC, server x86 và ARM64, máy ảo
Khởi tạo DRAM SPL PEI (x86), TF-A BL2 (ARM64)
Cấu hình Kconfig, environment, script .dsc, .fdf, PCD, biến UEFI
Giao diện với OS bootm/booti, tùy chọn lớp UEFI (bootefi) UEFI đầy đủ, runtime services
Mô tả phần cứng Device tree ACPI hoặc device tree
Giấy phép GPL-2.0+ BSD-2-Clause-Patent

U-Boot cũng cài đặt một tập con của UEFI đủ để chạy GRUB và kernel EFI stub, xem GRUB. Với board nhúng chỉ cần boot bản phân phối generic, lớp UEFI của U-Boot thường đủ dùng; EDK2 phù hợp hơn khi cần tuân thủ SystemReady, cần runtime services đầy đủ hoặc cần hệ sinh thái driver option ROM của PCIe.

Biến UEFI, Secure Boot và cập nhật firmware

Biến UEFI được lưu trong một firmware volume riêng trên flash (trên OVMF là OVMF_VARS.fd). Mỗi biến có tên, GUID của nhà cung cấp và thuộc tính: NON_VOLATILE (giữ sau khi tắt nguồn), BOOTSERVICE_ACCESS, RUNTIME_ACCESS (hệ điều hành đọc được) và TIME_BASED_AUTHENTICATED_WRITE_ACCESS (chỉ ghi được khi kèm chữ ký hợp lệ). Các biến quan trọng:

Biến Vai trò
BootOrder, Boot#### Danh sách và thứ tự boot option, BDS đọc ở mỗi lần khởi động
PK, KEK, db, dbx Khóa và danh sách chữ ký của Secure Boot
SecureBoot, SetupMode Trạng thái Secure Boot hiện tại (chỉ đọc)
OsIndications Hệ điều hành yêu cầu firmware vào setup menu hoặc xử lý capsule ở lần boot sau

Trong EDK2, Secure Boot được bật khi build bằng -D SECURE_BOOT_ENABLE=TRUE. Code kiểm tra chữ ký ảnh nằm trong SecurityPkg, dùng CryptoPkg để xác minh chữ ký Authenticode. Để chống việc hệ điều hành bị chiếm quyền ghi đè trực tiếp các biến khóa, các nền tảng thật đặt driver ghi biến trong SMM (x86) hoặc Standalone MM trong secure world (ARM64). Khái niệm chain of trust xem tại Secure boot và TrustZone; chuỗi shim, GRUB và kernel được ký xem tại GRUB.

Cập nhật firmware theo chuẩn UEFI dùng capsule: hệ điều hành gửi một gói firmware đã ký qua runtime service UpdateCapsule(), firmware kiểm tra chữ ký và ghi flash ở lần khởi động sau. EDK2 cung cấp khung Firmware Management Protocol (FMP) trong FmpDevicePkg. Trên Linux, fwupd dùng cơ chế này cùng với cơ sở dữ liệu LVFS để phân phối bản cập nhật firmware.

Debug

Cách Mô tả
DEBUG ((DEBUG_INFO, "...")) Macro in log, bị loại bỏ hoàn toàn trong build RELEASE. Mức log được lọc theo bitmask PcdDebugPrintErrorLevel, các bit định nghĩa trong MdePkg/Include/Library/DebugLib.h (DEBUG_ERROR là 0x80000000, DEBUG_INFO là 0x40, DEBUG_VERBOSE là 0x00400000)
ASSERT () Dừng tại chỗ khi điều kiện sai, hành vi điều khiển bởi PcdDebugPropertyMask
Serial và debugcon Đầu ra của DebugLib do .dsc chọn: UART trên board thật, cổng 0x402 trên OVMF
gdb qua QEMU Chạy QEMU với -s -S, build NOOPT để giữ thông tin debug. Vì mỗi driver nạp ở địa chỉ khác nhau, cần nạp symbol theo địa chỉ in trong debug log (dòng Loading driver at 0x...)
UEFI Shell Lệnh drivers, devtree, dh (liệt kê handle và protocol), dmpstore (in biến UEFI), memmap, bcfg (sửa boot option)
SourceLevelDebugPkg Debug agent cho phép debug mức mã nguồn trên phần cứng thật qua serial hoặc USB

Chưa kiểm chứng trên máy

Các lệnh build và chạy OVMF trên trang này lấy từ OvmfPkg/README và tài liệu TianoCore tại tag edk2-stable202608, chưa được chạy thử trong quá trình viết trang.

Liên quan

Tài liệu tham khảo

  1. tianocore/edk2 - github.com (mã nguồn, ReadMe.rst, License.txt, danh sách tag edk2-stable*)
  2. OvmfPkg/README - github.com (build OVMF, chạy trên QEMU, debug log)
  3. BaseTools/Conf/tools_def.template - github.com (lịch sử toolchain tag)
  4. EDK II Build Instructions - tianocore.org
  5. How to Build OVMF - tianocore.org
  6. UEFI Specifications - uefi.org (UEFI Specification, PI Specification)
  7. tianocore/edk2-platforms - github.com
  8. The UEFI Driver Model, UEFI Driver Writer's Guide - tianocore-docs.github.io
  9. Linux kernel: The EFI Boot Stub - docs.kernel.org