Post

Understanding Embedded Linux Boot Flow

A technical walkthrough of the embedded Linux boot flow — Boot ROM, SPL, U-Boot, kernel, and rootfs.

Understanding Embedded Linux Boot Flow

Quy trình khởi động (boot flow) của SoC nhúng “thuần” — tức những chip sinh ra dành riêng cho mục đích công nghiệp/IoT/automation, không lai với dòng chip mobile/consumer. Dùng BeagleBone Black (TI Sitara AM335x) làm reference case study vì đây là board có boot flow tương đối đơn giản, tuyến tính, dễ theo dõi từng bước.

Lưu ý: đây là boot flow cụ thể của BBB/AM335x, không phải một “chuẩn” áp dụng cứng cho mọi SoC. Nhiều dòng SoC nhúng khác — TI Sitara, NXP i.MX, STM32MP1, Allwinner, Rockchip (bản non-Android)… có kiến trúc boot tương tự về mặt vai trò và trình tự các tầng, nhưng chi tiết triển khai (tên thành phần, cách nạp firmware, giới hạn phần cứng…) có thể khác nhau đáng kể giữa các hãng. BBB được chọn làm reference vì đủ đơn giản để nhìn rõ từng khái niệm cốt lõi trước khi mở rộng sang SoC khác.

Stage Overview

graph LR
    A[Power-on / Reset] --> B[Boot ROM]
    B --> C
    subgraph BL["Bootloader"]
        C[SPL / MLO] --> D[U-Boot]
    end
    D --> E[Linux Kernel]
    E --> F[RootFS + init]

Nguyên lý xuyên suốt: mỗi tầng chỉ có nhiệm vụ chuẩn bị và chuyển quyền thực thi cho tầng tiếp theo — không tầng nào xác thực tầng kế tiếp trước khi trao quyền.


1. Power-on / Reset

Khi cấp nguồn, PMIC (Power Management IC) cấp điện theo trình tự (power sequencing) cho từng khối trong SoC, rồi phát tín hiệu reset cho CPU. CPU bắt đầu thực thi tại một địa chỉ cố định (reset vector) — địa chỉ này luôn trỏ vào vùng Boot ROM nội bộ của chip, không phải RAM ngoài (vì lúc này RAM ngoài chưa được cấu hình).

BBB: PMIC là TPS65217 (hoặc TPS65910 tùy revision). Reset vector của AM335x trỏ vào Boot ROM tại địa chỉ 0x00000000.


2. Boot ROM (Immutable, Fixed in Silicon)

Đây là code duy nhất trong toàn chuỗi không thể thay đổi được vì nó được khắc trong quá trình sản xuất chip.

Nhiệm vụ:

  • Đọc các chân cấu hình phần cứng (SYSBOOT / bootstrap pins) để biết nên đọc SPL từ nguồn nào (SD card, eMMC, SPI NOR, UART, USB…) theo thứ tự ưu tiên nào.
  • Khởi tạo tối thiểu — chỉ đủ để đọc được đúng thiết bị lưu trữ vừa chọn ở trên (ví dụ bật clock cho MMC controller nếu chọn boot từ SD card/eMMC).
  • Đọc và nạp SPL vào SRAM nội bộ (không phải DDR, vì DDR chưa init) — vùng SRAM này rất nhỏ (thường 64–128KB), nên SPL phải cực kỳ tối giản.
  • Nhảy vào entry point của SPL — chuyển quyền thực thi cho SPL.

Lưu ý khi so sánh giữa các hãng: cách encode SYSBOOT pins, thứ tự thiết bị mặc định, và giới hạn kích thước SRAM sẽ khác nhau theo từng SoC — nhưng vai trò của Boot ROM luôn giống nhau.


3. SPL / FSBL (First Stage Boot Loader)

Tên gọi khác nhau tùy hãng:

HãngTên gọi
TIMLO (thực chất là U-Boot SPL)
XilinxFSBL
Nhiều hãng khácSPL

Nhiệm vụ:

  • Khởi tạo DDR RAM — quan trọng nhất, vì Boot ROM không biết cấu hình RAM ngoài (mỗi board có loại RAM, timing khác nhau nên phải config runtime).
  • Khởi tạo pinmux và clock tree đầy đủ hơn Boot ROM.
  • Đọc bootloader chính (u-boot.img) từ đúng thiết bị lưu trữ mà Boot ROM đã chọn ở bước trước (SD card, eMMC, hoặc SPI NOR — tuỳ SYSBOOT pins), nạp vào DDR vừa khởi tạo.
  • Chuyển quyền thực thi cho U-Boot — nhảy tới entry point của u-boot.img vừa nạp trong DDR.

BBB: File MLO phải đặt đúng tên (viết hoa) vì Boot ROM tìm theo tên cố định. Nằm trong FAT boot partition khi boot từ SD card.


4. Main Bootloader (U-Boot / Barebox…)

Đây là bootloader “đầy đủ tính năng” nhất trong chuỗi — có shell tương tác, hỗ trợ nhiều loại filesystem, và cơ chế cấu hình dựa trên biến môi trường (environment variables). Đây cũng là tầng duy nhất người dùng trực tiếp thao tác khi debug boot fail hoặc tùy chỉnh cấu hình khởi động, nên phần dưới đi sâu vào cơ chế nội bộ của nó hơn các tầng khác.

Khác với Boot ROM (đóng, thuộc sở hữu silicon vendor) hay SPL (build từ chính source U-Boot nhưng cấu hình tối giản riêng cho từng board), U-Boot bản thân là một dự án open source đã có sẵn (u-boot.org / git.denx.de), dùng chung cho hàng trăm dòng SoC khác nhau. TI không tự viết U-Boot — họ đóng góp board support (defconfig, board files, cấu hình DDR/pinmux riêng cho AM335x) vào cùng một codebase chung. Đây là lý do các cơ chế autoboot, environment, uEnv.txt ở phần dưới hoạt động giống hệt nhau dù board/SoC là gì — tất cả đều chạy trên cùng một core U-Boot, chỉ khác phần board-specific.

BBB: File u-boot.img nằm trong FAT boot partition cùng MLO, zImage, dtb.

4.1. Autoboot — Wait-and-Boot Mechanism

Khi U-Boot khởi động xong phần init phần cứng, nó không nhảy thẳng vào kernel ngay. Nó in ra một dòng đếm ngược kiểu:

1
Hit any key to stop autoboot: 3

Đây là khoảng thời gian được định nghĩa bởi biến môi trường bootdelay (đơn vị giây). Nếu không có phím nào được nhấn trong khoảng thời gian này, U-Boot tự động chạy lệnh được lưu trong biến bootcmd. Nếu có phím được nhấn, nó rơi vào U-Boot shell — nơi có thể gõ lệnh tay để debug (printenv, mmc list, fatls mmc 0:1…).

flowchart TD
    A[U-Boot finishes hardware init] --> B{Key pressed within<br/>bootdelay seconds?}
    B -- Yes --> C[Enter U-Boot shell]
    B -- No --> D["Run bootcmd"]
    C --> E["Manual commands: printenv, mmc list, fatls..."]
    D --> F["fatload kernel + dtb into RAM"]
    F --> G["bootz / booti → jump to kernel"]

Đây chính là cơ chế mình dùng khi debug boot fail: ngắt autoboot, kiểm tra tay từng bước xem load kernel/dtb có lỗi ở đâu.

4.2. Environment Variables — U-Boot’s Configuration Core

U-Boot lưu toàn bộ cấu hình boot dưới dạng key-value trong một vùng nhớ riêng gọi là environment (thường lưu trong một phần MMC/SD hoặc SPI flash riêng, tách khỏi filesystem chứa kernel).

Các biến quan trọng nhất:

BiếnÝ nghĩa
bootcmdLệnh (hoặc chuỗi lệnh) được chạy khi autoboot kết thúc
bootargsChuỗi tham số truyền cho kernel (console, root=…)
bootdelaySố giây chờ trước khi autoboot
loadaddrĐịa chỉ RAM để load kernel image vào
fdtaddrĐịa chỉ RAM để load device tree blob vào

bootcmd thường là một chuỗi lệnh gộp, ví dụ dạng đơn giản hoá:

1
bootcmd=fatload mmc 0:1 ${loadaddr} zImage; fatload mmc 0:1 ${fdtaddr} board.dtb; bootz ${loadaddr} - ${fdtaddr}

Tức là: đọc zImage từ FAT partition vào địa chỉ loadaddr, đọc dtb vào fdtaddr, rồi gọi bootz để nhảy vào kernel.

4.3. uEnv.txt and boot.scr — Overriding Config Without Rebuilding U-Boot

Thay vì hardcode bootcmd lúc build, U-Boot hỗ trợ đọc một file text nằm ngay trong boot partition để override biến môi trường tại runtime:

  • uEnv.txt: file text đơn giản, mỗi dòng là một cặp biến=giá_trị. U-Boot tự tìm và load file này (nếu bootcmd mặc định có gọi lệnh run loadbootenv; run importbootenv), rồi ghi đè lên environment gốc.
  • boot.scr: là một script U-Boot đã được biên dịch bằng công cụ mkimage (chính công cụ dùng để tạo FIT Image mà mình đã biết từ project Secure Boot), cho phép chạy logic phức tạp hơn (if/else, loop) so với uEnv.txt vốn chỉ là gán biến đơn giản.

Đây là lý do khi đổi kernel hay chỉnh root partition trên BBB, chỉ cần sửa uEnv.txt trên thẻ SD chứ không cần build lại U-Boot.

4.4. Jumping to the Kernel

Khi mọi thứ đã load xong (kernel image + dtb đã nằm trong RAM), U-Boot dùng lệnh bootz (cho zImage) hoặc booti (cho Image trên ARM64) để chuyển quyền thực thi sang kernel, kèm theo địa chỉ RAM của dtb đã load — đây là cách kernel biết cấu hình phần cứng của board mà không cần tham số nào khác. Sau bước này, U-Boot không còn tồn tại trong bộ nhớ nữa — vùng RAM nó dùng sẽ bị kernel ghi đè khi cần.

4.5. Files Read During the U-Boot Stage

Trên BBB, toàn bộ các file dưới đây đều nằm trong cùng một FAT boot partition (partition đầu tiên trên SD/eMMC):

Thứ tự đọcFileMục đích
1u-boot.imgChính U-Boot — được MLO load ở tầng trước, không phải U-Boot tự đọc
2Environment (tách riêng, không phải file trên FAT)Nạp bootcmd, bootargs, bootdelay… mặc định đã build sẵn trong U-Boot
3uEnv.txt (nếu có) hoặc boot.scr (nếu có)Override lại environment mặc định — đây là bước đọc file thật sự đầu tiên mà U-Boot tự thực hiện sau khi chạy
4zImage (hoặc Image trên ARM64)Linux Kernel image
5am335x-boneblack.dtb (tên có thể khác tùy board)Device Tree Blob — mô tả phần cứng cho kernel
6 (tuỳ chọn)initrd.img/uInitrdInitial ramdisk, nếu hệ thống có dùng

Thứ tự này khớp với logic trong bootcmd: đọc override config trước, rồi mới đọc kernel + dtb + (initrd nếu có), cuối cùng mới nhảy vào kernel. Cơ chế thật sự bên dưới việc “đọc file” này — partition table, FAT filesystem — được nói kỹ hơn ở phần Đào sâu cuối bài, để không làm loãng mạch chính.


5. Linux Kernel

  • Giải nén kernel (nếu cần) — file zImage mà U-Boot vừa nhảy vào không phải kernel thật, mà là một stub giải nén nhỏ đứng trước phần kernel đã được nén (gzip/lz4/xz tùy config build). Stub này chạy trước tiên, giải nén kernel thật ra một vùng RAM khác rồi mới nhảy tiếp vào entry point thật. Trên ARM64, U-Boot dùng Image (không nén) qua lệnh booti, nên bước này không xảy ra — đó là lý do có chữ “(nếu cần)”.
  • Khởi tạo memory management, scheduler, driver core — kernel tự dựng lại toàn bộ hạ tầng runtime của chính nó từ đầu.
  • Parse Device Tree để biết cấu hình phần cứng của board cụ thể (thay vì hardcode).
  • Mount root filesystem theo tham số root= trong bootargs — ví dụ root=/dev/mmcblk0p2 nghĩa là mount partition thứ 2 của SD card làm /. Đến bước này kernel mới có nơi để tìm /bin, /etc, /lib… — trước đó toàn bộ quá trình vẫn nằm hoàn toàn trong kernel-space, chưa có chương trình userspace nào tồn tại.

    Một số hệ thống có thêm bước trung gian: mount initramfs (rootfs tạm trong RAM, đóng gói cùng kernel hoặc nạp riêng qua initrd.img/uInitrd) trước, dùng nó để tìm driver/module cần thiết rồi mới switch_root sang rootfs thật. Trên BBB với setup đơn giản (driver MMC/ext4 đã build sẵn vào kernel), bước này thường được bỏ qua — kernel mount thẳng root= chỉ định.

  • Chạy tiến trình init (/sbin/init mặc định, hoặc theo init= nếu có chỉ định khác) — tiến trình userspace đầu tiên, điểm chuyển giao từ kernel-space sang user-space.

6. RootFS + init

  • init (systemd, busybox init, runit…) — tiến trình PID 1, đọc cấu hình riêng của nó để khởi động các service theo đúng thứ tự phụ thuộc.
  • Mount các filesystem còn lại chưa có ở bước rootfs ban đầu (/proc, /sys, /dev, các partition data khác), setup network, chạy ứng dụng.
  • Kết thúc tại màn hình login hoặc thẳng vào ứng dụng — đây là điểm boot flow “chính thức” xem như hoàn tất.

Quick Summary Table

Giai đoạnThành phầnVị trí lưu (BBB)Vai trò chínhChuyển quyền cho
0Power-on/Reset—PMIC cấp nguồn, CPU nhảy vào reset vectorBoot ROM
1Boot ROMTrong chip (immutable)Đọc SYSBOOT pins, nạp SPLMLO (SPL)
2MLO (SPL)FAT boot partition (SD/eMMC)Init DDR, nạp u-boot.imgu-boot.img
3u-boot.imgFAT boot partition (SD/eMMC)Load kernel + dtb, truyền bootargsKernel (zImage)
4Kernel (zImage)FAT boot partition (SD/eMMC)Init OS, mount rootfsinit
5RootFS + initRoot partitionChạy userspace—

Beyond BBB: TF-A on Modern ARMv8 SoCs

Toàn bộ chuỗi ở trên không có TF-A — nói trước để tránh hiểu nhầm. Nhưng nếu đọc tài liệu boot của các SoC mới hơn, sẽ gặp cái tên này liên tục, nên cũng nên biết nó nằm ở đâu.

TF-A (ARM Trusted Firmware-A) là framework mã nguồn mở do ARM phát hành, đảm nhiệm phần firmware bảo mật chạy ở mức đặc quyền cao hơn cả kernel. Mục đích: để mỗi hãng chip khỏi phải tự viết một lớp firmware bảo mật riêng — cứ port TF-A về là xong. TI, NXP, Rockchip, Xilinx… đều làm vậy.

A. Secure World and Exception Levels

Muốn hiểu TF-A thì phải nắm hai khái niệm nền của ARMv8, vì cả kiến trúc TF-A dựng trên đó.

Thứ nhất — hai “thế giới”. Phần cứng chia hệ thống làm hai vùng tách biệt: Normal World (nơi Linux và ứng dụng chạy) và Secure World (nơi giữ những thứ nhạy cảm). Đây không phải ngăn cách bằng phần mềm, mà do chính chip cưỡng chế — code bên Normal World không thể đọc/ghi vào bộ nhớ hay thiết bị đã cấp cho Secure World.

Thứ hai — Exception Level (EL). ARMv8 chia quyền thực thi thành 4 mức, số càng lớn quyền càng cao:

MứcAi chạy ở đó
EL3Firmware thường trú, canh cửa giữa hai thế giới — chỗ của TF-A
Secure EL1Trusted OS (ví dụ OP-TEE), bên Secure World
EL2Hypervisor, nếu hệ thống có dùng
EL1Linux kernel
EL0Ứng dụng userspace

Đây chính là lý do AM335x không có TF-A: Cortex-A8 thuộc ARMv7, không có mô hình EL này, nên cũng không có mức EL3 để đặt firmware thường trú vào. Cùng là TI, nhưng dòng K3 (AM62x, AM64x — ARMv8) thì có.

B. TF-A Boot Stages (BL1–BL33)

TF-A đặt tên các giai đoạn theo kiểu BL (Boot Loader) — gặp thường xuyên trong tài liệu:

graph TD
    subgraph SW["Secure World"]
        BL1["BL1 — Boot ROM<br/>(EL3)"]
        BL2["BL2 — Trusted Boot Firmware<br/>loads remaining images"]
        BL31["BL31 — Runtime Firmware<br/>(EL3, RESIDENT)"]
        BL32["BL32 — Trusted OS<br/>(Secure EL1, optional)"]
    end
    subgraph NW["Normal World"]
        BL33["BL33 — U-Boot / EDK2<br/>(EL2 or EL1)"]
        KRN["Linux Kernel"]
    end
    BL1 --> BL2 --> BL31
    BL31 --> BL32
    BL31 --> BL33 --> KRN
    KRN -. "SMC call" .-> BL31

Đối chiếu với chuỗi BBB ở các mục trên thì phần lớn là đồ đã quen:

TF-ATương ứng bên BBB
BL1Boot ROM
BL2SPL (MLO)
BL31(không có)
BL32(không có)
BL33U-Boot

Tức là TF-A không phát minh lại chuỗi boot — nó chèn thêm hai khối vào giữa:

  • BL31 (Runtime Firmware) — khác mọi thứ trong bài này ở một điểm căn bản: nó không kết thúc sau khi bàn giao. BL1, BL2, U-Boot đều biến mất sau khi trao quyền, còn BL31 ở lại thường trú suốt thời gian máy chạy. Nó là chốt canh giữa hai thế giới: mỗi lần Linux cần nhờ việc bên Secure World, lời gọi đều đi qua đây. Nó cũng là nơi implement PSCI — cơ chế chuẩn để Linux bật/tắt CPU core, suspend, reboot.
  • BL32 (Trusted OS) — thường là OP-TEE (Open Portable Trusted Execution Environment), một hệ điều hành nhỏ mã nguồn mở chạy bên Secure World. Nó nhận những việc không muốn giao cho Linux: giữ và thao tác với key, verify chữ ký, DRM… Linux không chạm trực tiếp được vào vùng của nó, muốn nhờ việc thì gọi qua SMC (Secure Monitor Call) — cơ chế chuyển sang thế giới bên kia, giống gọi syscall nhưng vượt qua ranh giới phần cứng. Khối này tuỳ chọn: nhiều hệ thống có TF-A nhưng không bật OP-TEE.

Điểm đáng chú ý cuối: U-Boot không bị thay thế, nó chỉ được đổi tên thành BL33 và lùi xuống chạy trong Normal World. Mọi thứ ở mục 4 vẫn đúng nguyên — vẫn autoboot, vẫn bootcmd, vẫn uEnv.txt. Khác biệt với người làm BSP là output build có thêm binary của TF-A (và OP-TEE nếu bật), cùng với việc từ nay U-Boot không còn chạy ở mức đặc quyền cao nhất nữa.


Deep Dive: File Read Mechanism & Userspace View

Phần này giải thích cơ chế bên dưới việc “đọc file” đã nhắc ở các mục trên, và cách nhìn lại các file boot đó từ userspace sau khi Linux đã chạy.

A. How U-Boot (and SPL) Locate Files on the SD Card

Các mục trên nói về việc “đọc file X” nhưng chưa giải thích cơ chế đọc thật sự diễn ra thế nào. Trên BBB, các file này nằm trong một filesystem thật (FAT32), nên việc “tìm được đúng file” đi qua 2 lớp:

Lớp 1 — Partition table (MBR)

SD card được chia thành nhiều partition, mô tả trong Master Boot Record (MBR) — 512 byte đầu tiên của thẻ. MBR ghi rõ: partition nào bắt đầu ở sector nào, kích thước bao nhiêu, kiểu filesystem gì (FAT32 = type 0x0C/0x0B, ext4 = type 0x83…). Trên BBB thường có 2 partition:

graph TD
    subgraph SD["SD Card"]
        subgraph FAT["Partition 1 — FAT (boot)"]
            MLO[MLO]
            UBOOT[u-boot.img]
            UENV["uEnv.txt / boot.scr"]
            ZIMAGE[zImage]
            DTB[am335x-boneblack.dtb]
        end
        subgraph ROOTFS["Partition 2 — ext4 (rootfs)"]
            BIN["/bin /sbin /etc /lib /usr ..."]
        end
    end

Cả Boot ROM lẫn SPL đều có driver đọc MBR tối giản tích hợp sẵn — đủ để tìm ra partition đầu tiên có kiểu FAT.

Lớp 2 — FAT filesystem (bên trong partition đó)

Sau khi xác định đúng partition, Boot ROM/SPL/U-Boot dùng driver FAT tối giản (read-only) để:

  1. Đọc boot sector của FAT — biết được cluster size, vị trí FAT table, vị trí root directory.
  2. Duyệt root directory entries — mỗi entry là một cặp tên file → cluster bắt đầu. Đây là bước Boot ROM tìm ra file tên chính xác MLO.
  3. Đi theo cluster chain (FAT table cho biết cluster tiếp theo của file, cho đến khi gặp end-of-chain) để đọc toàn bộ nội dung file, ghép lại thành 1 khối liên tục trong RAM/SRAM.

Đây chính là lý do mọi file boot trên BBB đều là file thật trong một filesystem chuẩn. Nhưng cơ chế “MBR + FAT” này không phải chuẩn chung cho mọi SoC — nó là cách TI thiết kế cho cả dòng Sitara (AM335x, AM437x, AM57xx… đều theo mô hình X-Loader/MLO tương tự). Nhiều vendor khác làm hoàn toàn khác: NXP i.MX, Rockchip, Allwinner đọc SPL/bootloader tại một raw offset cố định trên thẻ (không qua filesystem nào cả); STM32MP1 lại dùng GPT với partition được tìm theo tên (fsbl1, fsbl2…) chứ không phải theo filename trong FAT. Cách đọc khác nhau này ảnh hưởng trực tiếp đến việc debug/flash firmware trên từng dòng SoC — phần này mình sẽ đào sâu ở một bài riêng.

B. Looking Back from Userspace

Vì toàn bộ file boot (MLO, u-boot.img, uEnv.txt, zImage, *.dtb) nằm trên một FAT partition thật — không phải vùng nhớ ẩn hay reserved area — nên sau khi Linux đã boot xong, hoàn toàn có thể quay lại thấy đúng những file đó từ userspace.

Trên BBB (Debian image chuẩn): partition FAT boot thường được tự động mount, ví dụ tại /boot/uboot hoặc /boot/firmware tùy distro:

1
ls -la /boot/uboot/

Sẽ thấy chính xác MLO, u-boot.img, zImage, am335x-boneblack.dtb, uEnv.txt… — cùng những file mà Boot ROM/SPL/U-Boot đã đọc lúc khởi động.

Vài lưu ý:

  • Nếu image tối giản (Buildroot, một số config Yocto) không tự mount boot partition, có thể tự mount tay: mount /dev/mmcblk0p1 /mnt && ls /mnt.
  • Điều này chỉ đúng khi boot process dùng filesystem thật (FAT/ext4…). Nếu SoC boot theo raw offset (đọc thẳng byte tại một vị trí cố định trên block device, không qua filesystem), thì không có “file” nào để userspace nhìn thấy — chỉ có thể trích ra bằng cách đọc raw đúng offset/size (dd if=/dev/mmcblk0 bs=512 skip=<offset> count=<size> ...), vì hệ thống không biết gì về ranh giới file đó.

Nguyên tắc chung: nếu boot process đọc file qua filesystem, file đó luôn thấy lại được từ userspace miễn là mount đúng partition. Nếu đọc theo raw offset, khái niệm “file” chỉ tồn tại trong đầu người thiết kế layout — hệ thống không hề biết.

This post is licensed under CC BY 4.0 by the author.