4 thứ chạy trước kernel: Boot Process ARM từ ROM đến U-Boot
4 stage chạy TRƯỚC kernel trên ARM: ROM → SPL → U-Boot → TF-A. Đào sâu Device Tree, Secure Boot, A/B partition. Hiểu cốt lõi embedded boot.
I. Mở bài - Cái ngày mình nhận ra kernel không phải thứ đầu tiên chạy
Mình từng tưởng rằng khi bật nút power, kernel Linux là thứ đầu tiên chạy.
Điều đó nghe có vẻ hợp lý mà - tên gọi "operating system kernel" mang cảm giác như nó là cái lõi, cái khởi nguồn, cái mà mọi thứ bắt đầu từ đó. Nhưng cái ngày mình ngồi debug một board ARM bị boot loop, đọc log UART và thấy những dòng chữ "U-Boot SPL 2023.04", "DRAM: 512 MiB", "Loading Kernel Image at 0x82000000" - mình mới ngộ ra: kernel chỉ là diễn viên chính xuất hiện ở hồi thứ năm của một vở kịch dài hơn nhiều.
Trước khi kernel Linux chạy, có ít nhất 4 thứ khác đã chạy rồi. Mỗi thứ có một nhiệm vụ riêng, và mỗi thứ tồn tại vì một lý do phần cứng cụ thể - không phải vì ai đó thích chia nhỏ quá trình cho vui.
Bài viết này là cái mình ước mình được đọc từ năm nhất đại học - một bản đồ hoàn chỉnh từ khi bạn bật nút power cho đến khi dòng prompt đầu tiên hiện ra. Không dùng analogies dở hơi, không dùng từ marketing - chỉ là cơ chế, dữ kiện, và những gì thực sự xảy ra bên trong con chip.
Mình viết từ góc nhìn của ngườii làm embedded Linux, nhưng nếu bạn làm kernel, firmware, hay đơn giản là tò mò về phần cứng - bài này cũng dành cho bạn. Và nếu bạn có gì muốn cãi lại, mình rất sẵn lòng - mình không tự tin 100% về mọi chi tiết, đặc biệt là những phần liên quan đến architecture-specific quirks mà mình chưa trực tiếp chạm vào.
II. Boot flow tổng quan - 5 giai đoạn từ power-on đến prompt
Hãy bắt đầu với bức tranh lớn. Khi bạn bật nút power trên một board ARM chạy Linux, dòng chảy thực thi diễn ra theo thứ tự này:
1. ROM Bootloader - code đầu tiên chạy, được mask-programmed vào trong SoC tại nhà máy. Nó không thể thay đổi. Nhiệm vụ duy nhất: tìm và load SPL từ các boot media như SD card, eMMC, SPI NOR, NAND, hoặc thậm chí qua UART/USB.
2. SPL (Secondary Program Loader) - một phiên bản U-Boot được cắt giảm nghiêm ngặt để vừa trong SRAM (thường chỉ 64-128KB). SPL khởi tạo DDR memory controller, rồi load U-Boot proper vào DRAM.
3. U-Boot Proper - bootloader đầy đủ với command-line interface, network stack, USB support, filesystem drivers. U-Boot load kernel image, device tree blob, và initramfs (nếu có).
4. Linux Kernel - decompress (nếu là zImage), setup initial page table, bật MMU, rồi gọi start_kernel() trong init/main.c.
5. init (PID 1) - process đầu tiên trong userspace, khởi chạy mọi service và application.
Nguồn: Embedded Interview Lab, U-Boot SPL Documentation
Câu trả lởi nằm ở một con số: SRAM chỉ có 64-256KB. Trên AM335x (BeagleBone Black), SRAM là 128KB - không đủ cho U-Boot full (thường 500KB-2MB). ROM không biết board dùng loại DDR nào (DDR3? LPDDR4? tần số bao nhiêu?) nên không thể khởi tạo DRAM. SPL tồn tại để làm cầu nối: nó nhỏ enough để chạy trong SRAM, nhưng đủ khả năng để khởi tạo DDR - điều kiện tiên quyết cho mọi thứ sau đó.
Đây là lý do mà ngay cả 2 board dùng cùng SoC (ví dụ cùng i.MX8 hoặc cùng AM335x) cũng có thể cần SPL khác nhau - DDR timing parameters phụ thuộc vào PCB trace length và DRAM vendor.
Source: Learning About Electronics
III. "Bạn nghĩ kernel boot đầu tiên? Sai. 4 thứ chạy trước nó"
Hãy đi sâu từng stage trước khi kernel chạy. Đây là phần quan trọng nhất của bài viết - hiểu được tại sao mỗi stage tồn tại sẽ giúp bạn debug boot issues mà không cần đoán mò.
Stage 1: ROM Bootloader - code bất di bất dịch
ROM Bootloader (còn gọi là Boot ROM, PPL - Primary Program Loader, hoặc IPL - Initial Program Loader) là code đầu tiên thực thi sau khi CPU reset. Nó nằm trong một vùng read-only memory được mask-programmed vào silicon - bạn không thể sửa, xóa, hay cập nhật nó.
ROM code có một nhiệm vụ duy nhất: tìm SPL từ một boot source hợp lệ và load nó vào SRAM. Thứ tự tìm kiếm thường là:
- SPI NOR Flash (CS0)
- NAND Flash
- eMMC / SD card
- UART / USB (serial boot - cho development)
- Ethernet (tùy SoC)
Trên BeagleBone Black, ROM code tìm một file tên MLO trên FAT partition của SD card hoặc eMMC. Trên RK3399, ROM code tìm image ở sector cụ thể trên storage. Mỗi SoC vendor có protocol riêng - đây là lý do tại sao bạn cần board-specific documentation để biết ROM code tìm ở đâu.
Một điều quan trọng: ROM code không khởi tạo DDR. Tại sao? Vì thông tin DDR là board-specific - cùng một SoC, board này dùng DDR3-1600, board kia dùng LPDDR4-2400, trace length khác nhau, DRAM vendor khác nhau. ROM code là SoC-level, không thể chứa thông tin của tất cả các board. Đây chính xác là lý do SPL tồn tại.
Stage 2: SPL - cầu nối giữa ROM và thế giới có RAM
SPL là phiên bản U-Boot được biên dịch với rất ít features. Nó chỉ chứa code khởi tạo cơ bản: clock, PLL, pinmux, và quan trọng nhất - DDR memory controller.
SPL chạy hoàn toàn trong SRAM. MMU ở trạng thái TẮT - vì chưa có DRAM, chưa có page table, và cũng không cần virtual memory khi chỉ làm một nhiệm vụ đơn giản. Stack pointer được đặt trên SRAM (thường ở cuối SRAM region).
Sau khi khởi tạo DDR, SPL thực hiện các bước:
- Load ATF (ARM Trusted Firmware) vào DRAM (nếu dùng secure boot)
- Load U-Boot proper vào DRAM (thường ở address cao trong DDR)
- Chuyển quyền điều khiển cho ATF hoặc trực tiếp cho U-Boot
Nguồn: Pine64 Wiki - RK3399 Boot Sequence, Element14 Community
Stage 3: U-Boot proper - khi bạn cuối cùng có một CLI
U-Boot proper chạy trong DRAM với đầy đủ chức năng. Bạn có command prompt, có thể chạy printenv để xem environment variables, dhcp để lấy IP, tftp để load kernel từ network. U-Boot cũng chịu trách nhiệm load 3 thứ:
- Kernel image -
zImage(32-bit ARM) hoặcImage(64-bit ARM) - Device Tree Blob (DTB) - mô tả phần cứng cho kernel
- initramfs (tùy chọn) - root filesystem tạm thờii
Stage 4: Kernel decompress và start_kernel()
Nếu kernel là zImage (compressed), code trong arch/arm/boot/compressed/head.S sẽ decompress trước. Entry point của uncompressed kernel là stext() trong arch/arm/kernel/head.S.
Tại đây, kernel thực hiện những việc sau trước khi chạy C code:
- Verify machine type và processor ID
- Tạo initial page table (
swapper_pg_dir) - Identity-mapping code region xung quanh
__turn_mmu_on - Bật MMU - đây là lúc hệ thống chuyển từ physical sang virtual memory
- Chuyển sang
start_kernel()tronginit/main.c
start_kernel() là hàm C đầu tiên của Linux. Nó gọi setup_arch() để parse Device Tree, khởi tạo memory management, scheduler, interrupts - rồi cuối cùng tạo init process (PID 1).
Nguồn: Linus Walleij - How the ARM32 kernel start (kernel có thể gặp bug nghiêm trọng — ví dụ CVE-2026-31431 trong Linux copy_*_user)s
IV. U-Boot - con quái vật đa năng mà embedded không thể sống thiếu
U-Boot không chỉ là một bootloader. Nó là một hệ sinh thái - một project open source (GPL-2.0) đã tồn tại hơn 20 năm, hỗ trợ hàng trăm boards và hàng chục architectures từ ARM, x86, RISC-V đến PowerPC, MIPS.
Điều làm U-Boot trở thành de-facto standard cho embedded không phải vì nó hoàn hảo - mà vì nó đủ nhỏ, đủ configurable, và đủ flexible cho thế giới nơi mỗi board là một cỗ máy khác nhau.
U-Boot Environment - bộ não của quá trình boot
U-Boot environment variables là tập hợp các key-value pairs được lưu trên persistent storage (thường là eMMC, SD card, hoặc SPI flash). Chúng điều khiển mọi khía cạnh của boot process:
# Xem tất cả environment variables
U-Boot> printenv
# Các biến quan trọng:
bootcmd=run distro_bootcmd # Command tự động chạy khi bootootdelay=2 # Số giây chờ trước khi auto-boot
bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait
loadaddr=0x82000000 # Địa chỉ load kernel image
fdt_addr=0x88000000 # Địa chỉ load Device Tree Blob
# Boot từ TFTP (development)
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.1
tftp ${loadaddr} zImage
tftp ${fdt_addr} board.dtb
bootz ${loadaddr} - ${fdt_addr}
U-Boot commands cơ bản - đây là những gì bạn sẽ gõ trên UART console khi bring-up một board mới.
Environment được lưu dưới dạng linearized list of strings với 4-byte CRC header. Từ Linux, bạn có thể đọc và sửa bằng fw_printenv và fw_setenv - hai tool biên dịch từ U-Boot source tree:
# /etc/fw_env.config - chỉ định vị trí environment trên storage
/dev/mmcblk1 0x400000 0x2000
# Đọc environment từ Linux
$ fw_printenv bootcmd
bootcmd=run distro_bootcmd
# Sửa environment (batch mode để giảm số lần ghi storage)
$ cat > uboot_vars << EOF
bootcmd load mmc 0:1 0x82000000 boot/zImage; load mmc 0:1 0x88000000 boot/board.dtb; bootz 0x82000000 - 0x88000000
bootdelay 0
EOF
$ fw_setenv -s uboot_vars
fw_setenv batch mode - giảm số lần ghi xuống flash, tăng tuổi thọ storage.
Nguồn: ACM Digital Library - U-Boot Environment Variables, NXP Community
boot.scr, distroboot, và extlinux.conf
U-Boot có một cơ chế chuẩn hóa gọi là Distro Boot để tìm kiếm và boot từ nhiều nguồn khác nhau. Nó quét các thiết bị theo thứ tự:
- External SD card
- Internal flash memory (eMMC)
- External USB storage
- Network (PXE/DHCP)
Tại mỗi thiết bị, U-Boot tìm file extlinux.conf (định dạng Syslinux-compatible) hoặc boot.scr (U-Boot script):
# /boot/extlinux/extlinux.conf - định dghĩa boot entry
DEFAULT primary
LABEL primary
MENU LABEL Linux Primary
KERNEL /boot/zImage
FDT /boot/board.dtb
APPEND console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait
# Tạo boot.scr từ boot.cmd
$ cat > boot.cmd << 'EOF'
load mmc 0:1 ${loadaddr} boot/zImage
load mmc 0:1 ${fdt_addr} boot/board.dtb
setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait
bootz ${loadaddr} - ${fdt_addr}
EOF
$ mkimage -A arm -O linux -T script -C none -a 0 -e 0 \\
-n "Distro Boot Script" -d boot.cmd boot.scr
extlinux.conf (khai báo) vs boot.scr (turing-complete script) - lựa chọn phụ thuộc vào mức độ kiểm soát bạn cần.
extlinux.conf là declarative - bạn khai báo kernel, DTB, và command line. boot.scr là imperative - bạn viết script Turing-complete với điều kiện, vòng lặp, và logic phức tạp. Mình thường dùng extlinux.conf cho production (đơn giản, dễ đọc) và boot.scr cho development (linh hoạt hơn).
Memory layout khi boot
Memory layout thay đổi qua từng phase của boot:
Pre-DRAM (SPL phase): Stack và global data nằm trong SRAM. MMU tẮT - mọi address là physical. SPL chạy position-independent code vì nó có thể được load ở bất kỳ đâu trong SRAM tùy thuộc vào SoC.
Post-DRAM (U-Boot phase): Sau khi spl_relocate_stack_gd() chuyển stack và global data sang DRAM, U-Boot proper tiến hành relocate_code() - copy bản thân code lên phần trên cùng của DRAM. Các vùng memory từ trên xuống: Exception Vectors → Free Space → Stack (growing downward) → Board Info + Global Data → Malloc Arena → U-Boot Code + BSS.
Kernel phase: Kernel được load vào physical address chia hết cho 16MB + TEXT_OFFSET (thường 0x8000). Initial page table (swapper_pg_dir) được đặt tại KERNEL_RAM_VADDR - PG_DIR_SIZE (thường ~0xC0004000 virtual, 0x10004000 physical). MMU được bật tại __turn_mmu_on - và ngay sau đó, kernel chạy trong virtual memory.
Nguồn: U-Boot Official Documentation - Memory Management, Linus Walleij
V. Device Tree đã giết Board Support Package như thế nào
Đây là một trong những chuyển đổi quan trọng nhất trong lịch sử embedded Linux - và có lẽ là điều khiến công việc của embedded developer dễ thở hơn rất nhiều.
Thợi kỳ đen tối: mỗi board cần một board file C riêng
Trước năm 2011, mỗi board ARM mới cần một board file - file C được hard-code trong arch/arm/mach-*/. Bạn phải viết code C để đăng ký từng platform_device, từng clock, từng GPIO - một cách procedural. Kernel source tree phình to với hàng trăm board files. Mỗi board mới = một patch + recompile kernel.
Vào năm 2011, cộng đồng ARM quyết định chuyển sang Device Tree - một cách tiếp cận declarative. Thay vì viết code C, bạn viết một file text (DTS - Device Tree Source) để mô tả phần cứng. Kernel tự động parse và đăng ký devices.
Cú pháp DTS - mô tả phần cứng bằng text
Device Tree có cấu trúc cây với nodes (đại diện cho thiết bị) và properties (định nghĩa đặc tính). Một ví dụ đơn giản:
// File: am335x-boneblack.dts
/dts-v1/;
#include "am33xx.dtsi" // SoC-level definitions
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB tại 0x80000000
};
ocp {
uart0: serial@44e09000 {
compatible = "ti,omap3-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "okay";
};
i2c0: i2c@44e0b000 {
compatible = "ti,omap4-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
clock-frequency = <400000>;
status = "okay";
eeprom@50 {
compatible = "atmel,24c32";
reg = <0x50>;
pagesize = <32>;
};
};
};
};
DTS snippet cho BeagleBone Black - mô tả memory, UART, I2C, và EEPROM bằng cú pháp declarative thay vì code C.
Key concepts trong Device Tree:
- compatible string - định nghĩa "programming model" của thiết bị, format
"vendor,model". Kernel dùng string này để match với driver thông quaof_match_table. Đây là cơ chế plug-and-play của embedded Linux. - phandle - định danh 32-bit duy nhất cho mỗi node. Các node khác tham chiếu qua phandle để thiết lập quan hệ (ví dụ: interrupt-parent, clock-source).
- reg - định nghĩa địa chỉ cơ sở và kích thước vùng thanh ghi.
- interrupts + interrupt-parent - mô tả interrupts và trỏ đến interrupt controller.
DTS được biên dịch thành DTB (Device Tree Blob) bằng công cụ dtc. U-Boot load DTB vào memory và truyền address cho kernel qua register (thường r2 trên ARM32, x0 trên ARM64). Kernel gọi unflatten_device_tree() để parse DTB thành runtime data structure.
Tại sao DT "giết" BSP?
Device Tree mang lại 5 lợi ích lớn so với board files:
- Một kernel image cho nhiều boards - chỉ cần thay đổi DTB, không cần recompile kernel
- Tách biệt phần cứng và kernel source - mô tả phần cứng không còn nằm trong C code
- Giảm duplicate code - file
.dtsichứa SoC definitions được include vào nhiều board.dts - Dễ bảo trì - sửa text file dễ hơn sửa code C và recompile
- OS-agnostic - U-Boot, Barebox, FreeBSD đều hỗ trợ Device Tree
Nhưng mình phải nói thật: DT không phải lúc nào cũng hoàn hảo. Bindings (định nghĩa các properties cho từng loại thiết bị) thay đổi giữa các kernel release, và không có một standard body formal nào quản lý chúng. Trên server/enterprise, ngườii ta chọn ACPI vì bindings phải ổn định trong 5-10 năm - điều mà DT community chưa đảm bảo được một cách formal.
Nguồn: Linux Kernel Documentation - Device Tree Usage Model, Thomas Petazzoni - Device Tree for Dummies
VI. ARM boot vs x86 UEFI - hai thế giới không giao nhau
Đây là câu hỏi mình nhận được nhiều nhất từ những bạn chuyển từ PC sang embedded: "Tại sao ARM không dùng BIOS/UEFI như PC?" Câu trả lởi ngắn gọn: vì ARM embedded và x86 PC là hai thế giới có nhu cầu hoàn toàn khác nhau.
x86: UEFI như một mini operating system
Trên x86, UEFI (Unified Extensible Firmware Interface) là một specification/API được motherboard vendors triển khai. UEFI có runtime services, có thể stay resident sau khi OS boot, hỗ trợ multi-OS, và quản lý nguồn điện thông qua ACPI.
UEFI "giống như một mini OS" - nó lớn hơn U-Boot rất nhiều, phức tạp hơn, và quan trọng nhất: nó được thiết kế cho một platform chuẩn hóa. Mọi x86 PC đều có cùng một cơ chế khởi động, cùng một định dạng executable (PE), và cùng một interface firmware. Bạn có thể cài Windows lên bất kỳ PC nào vì UEFI abstraction giấu đi sự khác biệt phần cứng.
ARM: mỗi SoC là một hành tinh riêng
ARM thì ngược lại. Mỗi SoC vendor (TI, NXP, Rockchip, Allwinner, MediaTek...) có Boot ROM riêng với protocol khác nhau. Không có "standard platform" như x86. Mỗi board cần một bootloader được build riêng, với DDR configuration riêng, device tree riêng.
U-Boot tồn tại vì nó phù hợp với thực tế này:
| Tiêu chí | ARM (U-Boot) | x86 (UEFI) |
|---|---|---|
| Platform | Mỗi SoC/board khác nhau | Standardized platform |
| Size | ~200KB-2MB (configurable) | ~1-10MB (UEFI firmware) |
| OS support | Thường chỉ 1 OS | Multi-OS |
| Runtime | "Get out of the way" sau boot | Stay resident |
| License | GPL-2.0 (open source) | Proprietary (vendor firmware) |
| Backward compat | Không cần | Cần (legacy BIOS support) |
| Config | Build-time configuration | Runtime configuration |
Nhưng mình phải thêm một điều: UEFI cũng có thể chạy trên ARM. Trên ARM64 server/desktop, các distribution như Fedora và Suse dùng U-Boot để load GRUB như một UEFI application, rồi GRUB load kernel. Linux kernel cũng có UEFI stub để có thể được start như một UEFI application. U-Boot thực ra cũng có partial UEFI implementation. Mọi thứ đang hội tụ - nhưng ở embedded edge, U-Boot vẫn là king vì nó nhẹ, nhanh, và đủ configurable.
Source: SuperUser
VII. Tại sao Raspberry Pi không dùng U-Boot mặc định, và đây là vấn đề
Raspberry Pi là một trường hợp đặc biệt - một "ngườii ngoài hành tinh" trong thế giới ARM boot. Và cách nó boot giải thích rất nhiều về những hạn chế mà Pi community phải đối mặt.
GPU boot trước ARM
Trên Raspberry Pi, GPU (VideoCore) được cấp nguồn trước ARM CPU. ARM CPU bị giữ ở trạng thái reset cho đến khi GPU hoàn tất mọi công việc chuẩn bị. Đây không phải là một lỗi thiết kế - đó là một lựa chọn có chủ đích, xuất phát từ việc chip Broadcom BCM2xxx ban đầu được thiết kế cho set-top boxes nơi cần khởi động nhanh giao diện đồ họa.
Chuỗi boot đầy đủ của Raspberry Pi:
- Power on - GPU được cấp nguồn, ARM CPU ở trạng thái reset
- VideoCore ROM - GPU chạy first-stage bootloader từ on-chip ROM
- bootcode.bin - GPU load từ SD, chạy trong L2 cache, khởi tạo SDRAM
- start.elf - GPU firmware, parse
config.txt, apply DT overlays, phân chia memory giữa GPU và ARM, loadkernel.imgvà DTB vào RAM - ARM release - GPU giải phóng ARM CPU từ reset, set entry point
- Kernel run - ARM CPU bắt đầu chạy kernel
Trust chain của Pi là: GPU ROM → GPU firmware → ARM kernel → Linux userspace. Điều này có nghĩa là ARM không thể verify GPU firmware. Secure boot trên Pi (nếu có) được anchor vào GPU, không phải ARM CPU - và đây là lý do tại sao Raspberry Pi Secure Boot không tương đương với PC Secure Boot.
Sau khi boot, GPU không bị unload - nó chạy một hệ điều hành gọi là VideoCore OS (VCOS). Khi Linux cần truy cập phần cứng mà ARM không truy cập trực tiếp được (HDMI, camera, audio), Linux giao tiếp với VCOS qua mailbox messaging system.
Nguồn: Patrick McCanna - The Boot Order of the Raspberry Pi Is Unusual
Trade-offs của thiết kế này
Ưu điểm: kernel replacement cực kỳ đơn giản - chỉ cần thay file kernel.img trên FAT partition của SD card. Không cần rebuild bootloader, không cần flash firmware. Đây là lý do Pi trở thành công cụ học tập phổ biến.
Nhược điểm: ARM side bị phụ thuộc hoàn toàn vào proprietary GPU firmware. Bạn không thể audit code khởi động, không thể tùy chỉnh boot flow, và secure boot bị hạn chế nghiêm trọng. config.txt được parse bởi GPU (proprietary), không phải Linux - nên những tùy chọn bạn đặt trong đó đôi khi hoạt động theo cách không tài liệu hóa.
Bạn vẫn có thể dùng U-Boot trên Pi - và mình thường làm vậy khi cần boot qua TFTP hoặc debug. Nhưng mặc định, Pi không cần U-Boot vì GPU đã đảm nhận toàn bộ chức năng bootloader.
VIII. Secure boot chain - TF-A, OP-TEE, và niềm tin bắt đầu từ silicon
Nếu bạn đã đọc đến đây, bạn biết rằng boot process là một chuỗi các stage nối tiếp nhau. Secure boot đặt ra một câu hỏi đơn giản: nếu bạn không tin stage trước, làm sao bạn tin stage sau?
Câu trả lởi: mỗi component phải verify component tiếp theo bằng digital signature, bắt đầu từ một hardware root of trust - code được burned vào silicon tại nhà máy, không thể thay đổi.
TF-A (Trusted Firmware-A) - 5 giai đoạn
TF-A (tiền thân là ARM Trusted Firmware) định nghĩa một secure boot chain chuẩn trên ARM AArch64 với 5 stage:
| Stage | Tên | Vai trò |
|---|---|---|
| BL1 | AP Trusted ROM | Code đầu tiên chạy - mask ROM, load BL2 |
| BL2 | Trusted Boot Firmware | Khởi tạo clocks, DDR, crypto peripheral; load và verify BL31, BL32, BL33 |
| BL31 | EL3 Runtime / Secure Monitor | Xử lý SMC (Secure Monitor Calls), quản lý chuyển đổi giữa Secure World và Normal World |
| BL32 | Trusted Runtime (OP-TEE) | Chạy trong Secure World, cung cấp TEE services (key management, crypto, biometric) |
| BL33 | Non-Trusted Firmware | U-Boot hoặc UEFI - bootloader thông thường |
Quy trình hoạt động: BL1 load BL2 từ storage → BL2 verify BL31, BL32, BL33 bằng digital signature → BL2 chuyển quyền cho BL31 qua SMC → BL31 setup secure monitor rồi gọi BL32 → BL32 khởi tạo TEE → BL31 chuyển quyền cho BL33 (U-Boot) → U-Boot verify FIT image chứa kernel → kernel chạy dm-verity để verify root filesystem.
OP-TEE và ARM TrustZone
OP-TEE (Open Portable Trusted Execution Environment) là một TEE open source chạy trong Secure World của ARM TrustZone. TrustZone chia CPU thành hai "thế giới":
- Normal World - Linux kernel và userspace chạy ở đây. Không thể truy cập Secure World memory/peripherals.
- Secure World - OP-TEE chạy ở đây. Có toàn quyền truy cập mọi tài nguyên.
Chuyển đổi giữa hai thế giới thông qua SMC (Secure Monitor Call) - một CPU instruction đặc biệt. OP-TEE thực thi hoàn toàn trong Secure World, cung cấp các dịch vụ như: key management, cryptographic operations, biometric authentication, và secure storage.
Thiết kế của OP-TEE tuân theo 3 nguyên tắc: Isolation (cô lập khỏi non-secure OS), Small footprint (đủ nhỏ để nằm trong on-chip memory), và Portability (dễ dàng tích hợp với các kiến trúc khác nhau).
Signed FIT Image - gói mọi thứ vào một file có chữ ký
FIT (Flattened Image Tree) là một binary đơn lẻ chứa kernel image, device tree, và initramfs - tất cả đều có metadata và digital signature. FIT được ký bằng private key, và public key được nhúng trong bootloader.
Một chuỗi secure boot đầy đủ trông như thế này:
- ROM Code verify BL2 qua OTP fuses (chứa hash của public key)
- BL2 verify BL33 (U-Boot) qua FIP (Firmware Image Package) chứa certificates
- U-Boot verify FIT image (kernel + DTB + initramfs) bằng embedded public key
- Kernel chạy dm-verity để verify root filesystem (Merkle tree)
Chain of Trust được thiết lập thông qua các certificate tự ký (X.509) trong FIP. ROTPK (Root of Trust Public Key) hash được burned vào OTP fuses - tạo thành hardware root of trust mà không thể thay đổi sau production.
Nguồn: STM32 Wiki - TF-A Overview, OP-TEE Documentation, Timesys - Secure Boot
IX. A/B partition và fallback boot - khi update không được phép phá hỏng thiết bị
Giả sử bạn đã có secure boot hoàn chỉnh - mỗi component đều verify component tiếp theo. Nhưng điều gì xảy ra khi firmware update bị lỗi? Thiết bị brick? Đó là lý do A/B partition tồn tại.
Cách A/B hoạt động
Android sử dụng A/B system updates với hai tập partition được gọi là "slots":
- Slot A - partition hiện tại đang chạy
- Slot B - partition không được truy cập trong normal operation, dùng cho update
Quy trình update:
- Hệ thống đang chạy từ Slot A
- OTA update tải firmware mới vào Slot B (trong khi thiết bị vẫn hoạt động bình thường)
- Reboot vào Slot B
- Nếu Slot B boot thành công → Slot B trở thành "active", Slot A trở thành backup
- Nếu Slot B boot thất bại → tự động fallback về Slot A
Điều này khiến updates "seamless" - ngườii dùng không bị gián đoạn. Và quan trọng hơn: nếu update làm thiết bị không boot được, thiết bị tự động quay về phiên bản cũ.
Rollback protection
A/B chỉ giải quyết vấn đề "update bị lỗi" - nó không ngăn chặn attacker downgrade về phiên bản cũ có lỗ hổng bảo mật. Đây là lý do rollback protection tồn tại.
Trong AVB 2.0 (Android Verified Boot), mỗi VBMeta struct chứa một rollback_index. Thiết bị lưu trữ rollback index cuối cùng trong tamper-evident storage (thường là RPMB partition trên eMMC). Thiết bị từ chối image nếu rollback_index < stored_rollback_index.
RPMB (Replay Protected Memory Block) được bảo vệ bằng HMAC key - OP-TEE có thể cung cấp RPMB secure storage implementation để bảo vệ private key dùng cho HMAC. Nếu attacker cố gắng downgrade firmware, signature verification sẽ pass (vì image là hợp lệ), nhưng rollback protection sẽ reject vì version quá cũ.
So sánh: Android AVB vs Linux Verified Boot
| Tiêu chí | Android AVB 2.0 | Linux UEFI Secure Boot |
|---|---|---|
| Root of Trust | SoC hardware fuses | UEFI firmware keys (PK, KEK, db) |
| Bootloader verification | SoC BootROM verify | UEFI firmware verify |
| Kernel verification | Bootloader qua AVB (vbmeta) | GRUB/Shim verify |
| Filesystem verification | dm-verity (Merkle tree) | Không mặc định |
| Rollback protection | Có (rollback index + tamper-evident storage) | Hạn chế |
| A/B updates | Có (seamless OTA) | Không mặc định |
| Recovery | A/B fallback, recovery mode | Không có fallback tự động |
Điểm khác biệt cốt lõi: Android AVB 2.0 bảo vệ toàn bộ chain từ bootloader đến filesystem, có rollback protection mạnh mẽ và A/B updates. Linux UEFI Secure Boot chỉ bảo vệ đến kernel, không có filesystem verification mặc định. Lý do: Android được thiết kế từ đầu với security in mind, còn Linux desktop là legacy system không có verified boot chain hoàn chỉnh.
X. Secure boot trên iPhone vs trên router Tenda - so sánh implementation
Đây là phần mình thích nhất - không phải vì nó kỹ thuật nhất, mà vì nó cho thấy cùng một khái niệm "secure boot" có thể triển khai ở hai đầu spectrum hoàn toàn khác nhau. iPhone là gold standard. Router Tenda consumer... thì không.
iPhone: BootROM → LLB → iBoot → Kernel - một chuỗi không thể phá vỡ
Secure boot chain của iPhone là:
- Boot ROM - code đầu tiên chạy, embedded trong hardware, immutable. Chứa Apple Root CA public key để verify LLB (trên A9 trở xuống) hoặc iBoot (A10+).
- LLB (Low-Level Bootloader) - xác thực và load iBoot. Đọc LocalPolicy (được ký bởi Secure Enclave) để xác định chế độ bảo mật.
- iBoot - xác thực tính toàn vẹn của iOS kernel. Kiểm tra digital signature, verify root hash của Signed System Volume (SSV).
- Kernel - chạy sau khi được iBoot verify.
Điều đặc biệt: Apple không chỉ verify signature - họ cá nhân hóa firmware cho từng thiết bị. System Software Authorization sử dụng ECID (Exclusive Chip ID) unique cho từng chip và nonce (anti-replay value) để ngăn downgrade. Apple server tạo signature cho (OS image + ECID + nonce), và thiết bị từ chối nếu không match.
Secure Enclave là một coprocessor riêng biệt (ARMv7a "Kingfisher" core) chạy sepOS (L4 microkernel tùy chỉnh). Nó có Memory Protection Engine với AES-256-XEX encryption và anti-replay protection. Ngay cả EL3 (highest privilege) trên Application Processor cũng không thể truy cập Secure Enclave.
Từ A11/S4 trở đi, Memory Protection Engine thêm replay protection với integrity tree rooted trong dedicated SRAM. Đây là mức bảo mật mà hầu hết embedded devices không có khả năng đạt được.
Router Tenda: thực trạng không có secure boot
Router Tenda (đại diện cho consumer embedded devices giá rẻ) nằm ở đầu kia của spectrum:
- Không có secure boot chain
- Firmware không được signed/verified
- Không có hardware root of trust
- Không có rollback protection
- Firmware updates qua web interface không qua process xác thực
Kết quả: hàng loạt lỗ hổng bảo mật nghiêm trọng. Tenda AC6 có OS command injection (CVE-2026-8264). Tenda AC7 có stack-based buffer overflow (CVE-2025-11528). Tenda W15Ev2 có nhiều OS command injection (CVE-2022-41395, CVE-2022-42053). Các lỗ hổng này cho phép remote attackers thực thi arbitrary commands với root privileges - và điều tệ hơn là attacker cũng có thể flash custom firmware vì không có signature verification.
Chênh lệch và tại sao
| Tiêu chí | iPhone | Router Tenda |
|---|---|---|
| Hardware RoT | Boot ROM (immutable, burned in factory) | Không có |
| Secure boot chain | Full chain: ROM → LLB → iBoot → Kernel | Không có chain |
| Signature verification | Mọi component được Apple sign và verify | Firmware không được sign |
| Rollback protection | System Software Authorization (ECID + nonce) | Không có |
| Secure coprocessor | Secure Enclave (dedicated core, L4 microkernel) | Không có |
| Memory protection | AES-XEX + anti-replay | Không có |
| Attack surface | Rất nhỏ (code signing bắt buộc) | Rất lớn (command injection, buffer overflow) |
| Cost of compromise | Cao (cần exploit chain) | Thấp (direct remote exploit) |
Chênh lệch này đến từ 4 lý do chính: cost pressure (router bán giá rẻ, không đủ margin đầu tư bảo mật phần cứng), use case khác (router không lưu sensitive data như fingerprint), market pressure khác (ngườii dùng không yêu cầu bảo mật cao cho router), và update model khác (firmware updates không qua process xác thực nghiêm ngặt).
Mình không nói điều này để chê Tenda - họ làm sản phẩm giá rẻ cho thị trường giá rẻ. Nhưng mình muốn chỉ ra: secure boot không phải là "có hoặc không" - nó là một spectrum. Và lựa chọn nằm ở chi phí, threat model, và những gì bạn đang bảo vệ.
Nguồn: Apple Security Guide, TU Graz - iOS Platform Security, CVE databases
XI. U-Boot SPL vs full U-Boot - tại sao phải tách hai stage
Nếu bạn từng tự hỏi "tại sao không gộp SPL và U-Boot thành một?" - câu trả lởi ngắn gọn là: phần cứng không cho phép. Và câu trả lởi đầy đủ hơn liên quan đến một bài toán chicken-and-egg mà mọi embedded engineer đều phải đối mặt.
Bài toán: U-Boot quá to cho SRAM
Internal SRAM (Static RAM) trên SoC thường chỉ có 64-256KB. Đây là bộ nhớ on-chip, không cần memory controller, có thể truy cập ngay sau reset. Nhưng U-Boot proper thường có kích thước 500KB-2MB - không thể vừa SRAM.
SPL (Secondary Program Loader) tồn tại như một "pre-loader" - chỉ giữ lại core initialization code, kích thước có thể nén xuống chỉ vài chục KB:
# Ví dụ kích thước trên một board ARM thực tế
u-boot-spl.bin ~28 KB (SPL - fit trong SRAM 128KB)
u-boot.bin ~512 KB (U-Boot proper - cần DRAM)
u-boot.itb ~1.2 MB (U-Boot + TF-A + DTB FIT image)
SPL làm gì?
SPL chạy trong SRAM với MMU TẮT, dùng physical addresses. Nhiệm vụ:
- Khởi tạo clock và PLL - đảm bảo CPU và peripherals chạy đúng tần số
- Khởi tạo pinmux - cấu hình GPIO pins cho chức năng cần thiết
- Khởi tạo DDR memory controller - đây là bước quan trọng nhất. Bao gồm: xác định memory type (DDR3/DDR3L/LPDDR4), data width (16/32-bit), clock frequency, timing parameters (tRCD, tRP, tRAS, CAS latency), VTT/VREF settings, rồi thực hiện memory training/calibration.
- Load ATF và U-Boot proper vào DRAM
- Chuyển quyền điều khiển
Một điểm quan trọng: SPL không thực hiện code relocation. Trong SPL, board_init_f() chỉ return về crt0 mà không gọi relocate_code(). Ngược lại, U-Boot proper có relocation từ flash/SRAM lên top of DRAM - để giải phóng lower memory cho kernel.
Trên một số nền tảng như RK3399, còn có TPL (Tertiary Program Loader) - stage nhỏ hơn cả SPL, chỉ để khởi tạo DRAM. Sequence đầy đủ: TPL → SPL → TF-A BL31 → U-Boot proper. Mainline U-Boot tạo ra 2 images: idbloader.img (TPL+SPL) và u-boot.itb (TF-A + U-Boot proper).
SRAM nhanh, đơn giản, không cần refresh - nhưng đắt và tốn diện tích silicon. Một chip SRAM 128KB chiếm diện tích silicon lớn hơn rất nhiều so với 128KB trong một chip DRAM 512MB. Vì vậy SoC chỉ có vài trăm KB SRAM "miễn phí" (on-chip), và cần external DRAM cho bộ nhớ chính. Đây là trade-off cơ bản của computer architecture: tốc độ vs chi phí vs dung lượng.
Source: Srinivas Chandupatla
XII. fastboot vs DFU - hai protocol để flash firmware
Khi làm việc với embedded devices, bạn cần một cách để flash firmware mà không cần JTAG programmer. Hai protocol phổ biến nhất là fastboot (Android) và DFU (USB standard). Chúng khác nhau ở nhiều khía cạnh quan trọng.
Fastboot - text-based, host-driven, nhanh
Fastboot là một protocol giao tiếp với bootloader qua USB, sử dụng 2 bulk endpoints (in, out). Nó là text-based, host-driven (host gửi command, device trả lời), và synchronous. Packet size: 64 bytes (full-speed USB) hoặc 512 bytes (high-speed USB).
# Các lệnh fastboot phổ biến
$ fastboot devices # Liệt kê thiết bị
$ fastboot flash boot boot.img # Flash boot partition
$ fastboot flash system system.img # Flash system partition
$ fastboot erase userdata # Xóa userdata partition
$ fastboot getvar all # Lấy tất cả bootloader variables
$ fastboot reboot # Reboot
$ fastboot flashing unlock # Unlock bootloader (xóa data!)
$ fastboot set_active a # Chọn slot A (A/B system)
# Flash kernel Image trực tiếp và boot
$ fastboot boot zImage-dtb
Fastboot commands - protocol đơn giản, text-based, được thiết kế cho Android development.
Fastboot cũng hỗ trợ UDP qua Ethernet, cho phép flash qua network - rất hữu ích khi làm việc với devices không có USB debugging.
DFU - USB standard class, vendor-agnostic
DFU (Device Firmware Upgrade) là một USB Device Class Specification (Rev 1.1, 2004), sử dụng USB class code 0xFE. Khác với fastboot (Android-specific), DFU là USB standard - được hỗ trợ trên nhiều loại MCU (STM32, TI Tiva, NXP, Silicon Labs).
DFU sử dụng control endpoint transfers với kích thước mặc định nhỏ (128 bytes), nên throughput thấp hơn fastboot đáng kể. DFU cũng không định nghĩa: address selection cho download/upload, khả năng erase blocks, kiểm tra memory đã erased, hay query target storage parameters. Các tính năng này thường được nhà sản xuất mở rộng qua vendor-specific commands.
Bảng so sánh
| Tiêu chí | Fastboot | DFU |
|---|---|---|
| Type | Android-specific protocol | USB Device Class standard |
| Transport | USB bulk endpoints (64-512 bytes) | USB control transfers (128 bytes) |
| Protocol style | Text-based | Binary |
| Speed | Nhanh (bulk endpoints) | Chậm hơn (control transfers) |
| Partition support | Native (flash:boot, erase:system) | Không - chỉ có memory address |
| A/B slots | Hỗ trợ (set_active) | Không |
| Network | Hỗ trợ UDP | Không |
| MCU support | Android devices + U-Boot | STM32, TI, NXP, Silicon Labs, ... |
Fastboot phù hợp khi: bạn làm Android/embedded Linux với A/B partition, cần flash partition-based, hoặc cần network flashing.
DFU phù hợp khi: bạn làm việc với microcontrollers (STM32, etc.), cần một standard USB class mà không phụ thuộc vào Android ecosystem, hoặc muốn một built-in bootloader trong ROM chip.
Mình cá nhân dùng fastboot cho hầu hết embedded Linux work (vì hầu hết boards đều chạy U-Boot với fastboot support), và DFU cho STM32 projects (vì nó built-in, không cần flash bootloader riêng).
Nguồn: U-Boot Fastboot Documentation, USB DFU 1.1 Specification
XIII. Kết bài - Những gì chạy trước kernel cũng quan trọng như chính kernel
Khi mình bắt đầu viết bài này, mình tưởng rằng mình đang viết về bootloaders. Nhưng nửa chừng mình nhận ra: đây thực ra là bài viết về niềm tin.
Mỗi stage trong boot process - từ ROM code cho đến init - đều dựa trên một giả định: stage trước đó đã làm đúng việc của nó. ROM tin rằng SPL hợp lệ. SPL tin rằng U-Boot không bị sửa đổi. U-Boot tin rằng kernel là đúng. Kernel tin rằng filesystem không bị hỏng. Secure boot đơn giản là cơ chế để verify những niềm tin đó - bằng digital signature thay vì hy vọng.
Nếu bạn làm embedded, mình nghĩ bạn nên hiểu boot process sâu hơn một chút so với "U-Boot load kernel rồi boot". Hiểu tại sao MMU phải TẮT ban đầu giúp bạn debug page table issues. Hiểu tại sao SPL phải tách ra giúp bạn optimize boot time. Hiểu Device Tree giúp bạn port kernel sang board mới mà không cần viết một dòng code C.
Và nếu có một điều mình muốn bạn nhớ từ bài viết này, đó là: chi phí của việc không hiểu boot process không phải lúc nào cũng hiện rõ ngay. Nó hiện ra khi board của bạn boot loop lúc 2 giờ sáng, và bạn không biết liệu vấn đề là ở DDR timing, DTB incompatible, hay kernel command line sai. Lúc đó, việc hiểu từng stage đang chạy gì sẽ tiết kiệm cho bạn hàng giờ đoán mò.
Mình thừa nhận là có những phần trong bài viết này mình chưa tự tin 100% - đặc biệt là những architecture-specific details liên quan đến ARM64 MMU setup (TCR_EL1, MAIR_EL1 configurations) và những quirks của các SoC mà mình chưa trực tiếp làm việc. Nếu bạn phát hiện điều gì không chính xác, mình rất muốn nghe - mình viết để học, không phải để thuyết giảng.
Cuối cùng, câu hỏi mình để lại: bạn đã bao giờ ngồi đọc U-Boot source code từ crt0.S đi qua board_init_f(), relocate_code(), đến board_init_r() chưa? Nếu chưa, có lẽ đây là lúc tốt để bắt đầu - vì đôi khi, con đường đến với understanding bắt đầu từ việc trace từng dòng assembly trong boot sequence.
Bình