- 操作系统
- 云原生
- 容器运行时
【免费下载链接】linuxkit
A toolkit for building secure, portable and lean operating systems for containers
导读
tools/mkimage-raw-efi/是 LinuxKit 项目中负责生成raw EFI 磁盘镜像(raw-efi输出格式)的核心工具:它接收一个包含内核、initrd 与命令行参数的 tarball,在容器内完成 EFI System Partition(ESP)的初始化、systemd-boot 引导器与 Linux Unified Kernel Image(UKI)的组装,最终在标准输出上吐出一份可直接写入磁盘的 GPT 格式原始镜像。读完本文,你将掌握该工具从 stdin 协议、UKI 构建、FAT32 最小化计算到 GPT 分区组装的完整工作原理,并能通过linuxkit build -format raw-efi在本地复现这一镜像构建流程。
一、工具定位:LinuxKit 的 EFI 启动盘输出格式
LinuxKit 的linuxkit build命令支持多种输出格式,其中raw-efi专门面向UEFI 固件启动场景。在 output.go 中可以看到它的注册逻辑:
"raw-efi": func(base string, image io.Reader, size int, arch string) error { kernel, initrd, cmdline, _, err := tarToInitrd(image) if err != nil { return fmt.Errorf("error converting to initrd: %v", err) } err = outputImg(outputImages["raw-efi"], base+"-efi.img", kernel, initrd, cmdline, arch) if err != nil { return fmt.Errorf("error writing raw-efi output: %v", err) } return nil },构建产物命名为base-efi.img,其镜像包由 images.yaml 中声明的linuxkit/mkimage-raw-efi:c00e49ece2f03e47f3a8e047a2eeea7bd4228a34提供——这正是本目录下 Dockerfile 构建出的工具镜像。
与 BIOS 时代的raw-bios(使用 syslinux,见 make-bios)不同,raw-efi走的是UEFI 原生启动路径:ESP 中只放 systemd-boot 引导器与一个 UKI 内核镜像,不依赖 MBR 引导扇区。
二、ESP 分区布局(官方 README 核心内容)
README.md 明确指出:脚本通过mkfs.vfat初始化 EFI System Partition,并向其中填充 systemd-boot 与 LinuxKit UKI 所需的目录和文件。最终 ESP 的目录结构如下:
ESP ├── EFI │ ├── BOOT # 根据架构放置以下引导器二进制之一 │ │ ├── BOOTX64.EFI # amd64 │ │ ├── BOOTAA64.EFI # arm64 │ │ └── BOOTRISCV64.EFI # riscv64 │ └── Linux │ └── linuxkit.efi # LinuxKit Unified Kernel Image (UKI) └── loader └── loader.conf # systemd-boot 配置文件布局中两个要点值得注意:
- 引导器文件名按 UEFI 规范命名:
EFI/BOOT/BOOTX64.EFI(amd64)、BOOTAA64.EFI(arm64)、BOOTRISCV64.EFI(riscv64)。这些是 UEFI 固件约定的"可移除介质默认引导路径"文件名,固件无需读取配置即可按架构自动发现。 - UKI 自动发现机制:放在
EFI/Linux目录下的 UKI 不需要在loader/entries中显式添加启动条目——systemd-boot 会自动扫描该目录并生成启动菜单项。这正是 loader 目录下只有loader.conf一个配置文件的原因。
三、工具链与运行环境
Dockerfile 揭示了该工具的构建与运行环境,它采用多阶段构建:
- 第一阶段从
linuxkit/systemd-boot镜像提取编译好的 systemd-boot EFI 二进制; - 第二阶段基于
linuxkit/alpine准备运行时,通过apk add安装:dosfstools:提供mkfs.vfat,用于创建 FAT32 文件系统;mtools:提供mmd/mcopy,用于向 FAT 镜像写入目录与文件;sgdisk/sfdisk:GPT 分区表工具;libarchive-tools:提供bsdtar,用于解压 stdin 输入的 tarball;py3-pefile:ukify 构建 UKI 时的 PE 文件解析依赖;binutils、busybox、xfsprogs等基础工具;
- 最终阶段从 scratch 组装:
COPY --from=mirror /out/ /带入 Alpine 根文件系统,COPY --from=systemd-boot . .带入引导器,COPY . .带入make-efi脚本本身,并以ENTRYPOINT [ "/make-efi" ]作为容器入口。
四、标准输入/输出协议与输入内容约定
make-efi脚本(tools/mkimage-raw-efi/make-efi)以 Unix 管道的方式工作,其协议在脚本注释中写明:
# input is a tarball on stdin with kernel and cmdline in /boot # output is an iso on stdout- 输入:标准输入上的压缩 tarball,其中必须包含
/boot下的内核与命令行参数文件; - 输出:标准输出上的 raw 磁盘镜像(脚本末尾的
cat $IMGFILE完成这一职责)。
解压与中间产物提取的关键代码:
[ -t 0 ] || bsdtar xzf - INITRD="$(find . -name '*.img')" KERNEL="./kernel" CMDLINE_FILE="$(find . -name cmdline)" CMDLINE="$(cat $CMDLINE_FILE )" UKI_FILE="linuxkit.efi"[ -t 0 ] || bsdtar xzf -:仅当标准输入不是 tty 时才从 stdin 解压。注释特别说明 BSD 版 tar 能自动识别压缩格式,而 GNU tar 不能,这也是选择libarchive-tools的原因;若 stdin 是 tty,则依赖挂载的卷提供文件(调试场景)。- 通过
find定位*.img(initrd)与cmdline文件,内核固定命名为kernel,命令行内容直接读出拼入启动参数。
五、UKI(Unified Kernel Image)的构建
脚本为 root 分区预生成一个随机 PARTUUID,并调用 systemd 的ukify工具将内核、initrd 与命令行打包为单个 EFI 可执行文件:
# PARTUUID for root PARTUUID=$(cat /proc/sys/kernel/random/uuid) # this is displayed as boot loader entry name OS_RELEASE="NAME=\"LinuxKit\"" cat >> loader.conf <<EOF timeout 0 EOF ukify build --linux=$KERNEL \ --initrd=$INITRD \ --cmdline="$CMDLINE text" \ --os-release=$OS_RELEASE \ --output=$UKI_FILEukify build将--linux(内核)、--initrd(initrd 镜像)、--cmdline(内核命令行)合并进一个linuxkit.efi文件。命令行为原有的$CMDLINE追加了text参数,强制内核使用文本模式控制台,便于在串口等无图形环境下排障。--os-release=NAME="LinuxKit"写入 UKI 的 os-release 段,systemd-boot 会用该名称显示启动菜单项。loader.conf仅写入timeout 0,即启动菜单不等待、直接引导默认项,符合 LinuxKit 追求"快速无交互启动"的定位。
值得一提的是,PARTUUID变量随后被用作 ESP 分区的--partition-guid,为分区表提供一个确定的、可被根文件系统root=PARTUUID=引用的标识(见下节 GPT 组装)。
六、FAT32 文件系统参数与最小化尺寸计算
这是脚本中最精细的部分:为了让 ESP 尽量小,它对 FAT32 的各个结构做了逐项手工核算,而不是直接交给mkfs.vfat按默认值分配。
6.1 固定参数
FAT_SIZE=32 SECTOR_SIZE=512 SECTORS_PER_CLUSTER=8 # these always include the Boot Sector and FS Information Sector (sector 0 and 1) among others for FAT32 RESERVED_SECTORS=32| 参数 | 值 | 含义 |
|---|---|---|
FAT_SIZE | 32 | FAT32 类型(每条目 4 字节) |
SECTOR_SIZE | 512 | 扇区大小(字节) |
SECTORS_PER_CLUSTER | 8 | 每簇 8 扇区,即 4KB 簇 |
RESERVED_SECTORS | 32 | 保留扇区,包含引导扇区(sector 0)与 FS 信息扇区(sector 1)等 |
6.2 数据量与 FAT 开销递推
UKI_FILE_SIZE=$(stat -c %s "$UKI_FILE") EFI_FILE_SIZE=$(stat -c %s "$BOOTFILE_DST") SYSTEMD_BOOT_FILE_SIZE=$(stat -c %s loader.conf) # this is the minimum size of our EFI System Partition ESP_DATA_SIZE=$(( $UKI_FILE_SIZE + $EFI_FILE_SIZE + $SYSTEMD_BOOT_FILE_SIZE )) ESP_DATA_SECTORS=$(( $ESP_DATA_SIZE / $SECTOR_SIZE )) # File Allocation Table Sectors = (clusters + 2) * bytes per cluster / sector size FILE_ALLOCATION_TABLE_SECTORS=$(( ( $ESP_DATA_SECTORS / $SECTORS_PER_CLUSTER + 2 ) * ( $FAT_SIZE / 8 ) / $SECTOR_SIZE )) # there are two file allocation tables hence 2 * $FILE_ALLOCATION_TABLE_SECTORS FAT_OVERHEAD_SECTORS=$(( $RESERVED_SECTORS + ( 2 * $FILE_ALLOCATION_TABLE_SECTORS ) )) FAT_OVERHEAD_SIZE=$(( $FAT_OVERHEAD_SECTORS * $SECTOR_SIZE ))计算链条清晰可循:
- 数据量:ESP 至少容纳三个文件——UKI、systemd-boot 二进制、
loader.conf,三者字节数之和即ESP_DATA_SIZE; - 簇数:
ESP_DATA_SECTORS / SECTORS_PER_CLUSTER得到占用的簇数,FAT32 的 FAT 表条目数 = 簇数 + 2(两个保留条目:0 与 1 号簇标记); - 单张 FAT 表扇区数:
(簇数 + 2) × 4 字节(FAT32 每条目)/ 512; - 总开销:保留扇区(32)+ 两张 FAT 表(FAT32 默认镜像两份 FAT),换算成字节得
FAT_OVERHEAD_SIZE。
6.3 对齐到 2048 扇区
# (x+1024)/1024*1024 rounds up to multiple of 1024KB, or 2048 sectors # some firmwares get confused if the partitions are not aligned on 2048 blocks # we will round up to the nearest multiple of 2048 blocks # since each block is 512 bytes, we want the size to be a multiple of # 2048 blocks * 512 bytes = 1048576 bytes = 1024KB ESP_FILE_SIZE_KB=$(( ( ( ($ESP_DATA_SIZE + $FAT_OVERHEAD_SIZE + 1024 - 1) / 1024 ) + 1024 - 1) / 1024 * 1024 )) ESP_FILE_SIZE_SECTORS=$(( $ESP_FILE_SIZE_KB * 1024 / $SECTOR_SIZE ))脚本注释点明了原因:部分固件在分区未按 2048 块对齐时会出现识别混乱。因此 ESP 文件总尺寸被向上取整到 1024KB(2048 块 × 512 字节)的整数倍。
七、ESP 镜像写入:mkfs.vfat + mtools
得到精确尺寸后,脚本用mkfs.vfat创建最小化 FAT32 镜像,再通过 mtools 系列命令填充目录结构:
mkfs.vfat -v -F $FAT_SIZE -S $SECTOR_SIZE -s $SECTORS_PER_CLUSTER -R $RESERVED_SECTORS -C $ESP_FILE $(( $ESP_FILE_SIZE_KB )) > /dev/null echo "mtools_skip_check=1" >> /etc/mtools.conf && \ mmd -i $ESP_FILE ::/EFI mmd -i $ESP_FILE ::/EFI/BOOT mmd -i $ESP_FILE ::/EFI/Linux mmd -i $ESP_FILE ::/loader mcopy -i $ESP_FILE $BOOTFILE_DST ::/EFI/BOOT/ mcopy -i $ESP_FILE $UKI_FILE ::/EFI/Linux/ mcopy -i $ESP_FILE loader.conf ::/loadermkfs.vfat的-F 32(FAT32)、-S 512(扇区)、-s 8(每簇扇区)、-R 32(保留扇区)与脚本手算参数一一对应,-C表示直接创建指定大小(KB)的文件镜像;mmd依次建立EFI/BOOT、EFI/Linux、loader目录,与 README 中的布局完全一致;mcopy将三个文件按规划路径拷入:引导器进EFI/BOOT/,UKI 进EFI/Linux/,配置进/loader;mtools_skip_check=1跳过 mtools 的磁盘一致性预检,因为该 FAT 镜像由脚本精确构造。
八、GPT 分区表与最终磁盘镜像组装
ESP 文件就绪后,脚本开始组装最终磁盘镜像:
ONEMB=$(( 1024 * 1024 )) SIZE_IN_BYTES=$(( $(stat -c %s "$ESP_FILE") + 4*$ONEMB )) BLKSIZE=512 MB_BLOCKS=$(( $SIZE_IN_BYTES / $ONEMB )) dd if=/dev/zero of=$IMGFILE bs=1M count=$MB_BLOCKS ESP_SECTOR_START=2048 ESP_SECTOR_END=$(( $ESP_SECTOR_START + $ESP_FILE_SIZE_SECTORS - 1 )) sgdisk --clear \ --new 1:$ESP_SECTOR_START:$ESP_SECTOR_END --typecode=1:ef00 --change-name=1:'EFI System' --partition-guid=1:$PARTUUID \ --attributes 1:set:2 \ $IMGFILE dd if=$ESP_FILE of=$IMGFILE bs=$BLKSIZE count=$ESP_FILE_SIZE_SECTORS conv=notrunc seek=$ESP_SECTOR_START- 总尺寸:ESP 文件大小 + 4MB 余量(脚本注释说明分别预留 BIOS boot、MBR 与 GPT 的 1MB 空间);先
dd从 /dev/zero 生成空白底图,保证分区表之外的区域为零填充; - 对齐起点:ESP 从扇区 2048 开始(1MB 边界,兼容传统 BIOS 引导区与 GPT 头部),结束扇区与 ESP 文件精确吻合;
- 分区属性:
sgdisk创建单个分区——类型码ef00(EFI System Partition)、名称EFI System、--partition-guid=1:$PARTUUID写入前文生成的随机 GUID;--attributes 1:set:2设置第 2 个属性位,即legacy BIOS bootable标志,使该 ESP 在 BIOS 兼容模式下同样可被识别为可引导分区; - 写入:最后用
dd conv=notrunc seek=2048将 ESP 镜像按扇区数精确拷入磁盘镜像的对应偏移。
九、架构选择与 systemd-boot 二进制映射
make-efi通过TARGETARCH环境变量(缺省回退到uname -m)选择对应架构的引导器:
ARCH=${TARGETARCH:-`uname -m`} case $ARCH in x86_64) BOOTFILE_SRC=/usr/lib/systemd/boot/efi/systemd-bootx64.efi BOOTFILE_DST=BOOTX64.EFI ;; aarch64) BOOTFILE_SRC=/usr/lib/systemd/boot/efi/systemd-bootaa64.efi BOOTFILE_DST=BOOTAA64.EFI ;; riscv64) BOOTFILE_SRC=/usr/lib/systemd/boot/efi/systemd-bootriscv64.efi BOOTFILE_DST=BOOTRISCV64.EFI ;; esac映射关系与 README 中的布局注释一一对应:x86_64 → BOOTX64.EFI、aarch64 → BOOTAA64.EFI、riscv64 → BOOTRISCV64.EFI,源文件来自容器内 systemd-boot 包的/usr/lib/systemd/boot/efi/目录(由 Dockerfile 第一阶段从linuxkit/systemd-boot镜像带入)。这解释了为何该工具可以跨 amd64 / arm64 / riscv64 三个架构产出各自的 EFI 启动盘。
十、在 LinuxKit 构建流程中使用 raw-efi 格式
将 raw-efi 接入标准构建流程的方式是使用linuxkit build的-format参数。在 build.go 中,buildFormats是一个可重复指定的formatList(支持逗号分隔或多次传参),合法取值来自mobybuild.OutputTypes()。典型用法:
linuxkit build -format raw-efi linuxkit.yml构建后会在当前目录生成<名字>-efi.img(命名规则见 output.go 的base+"-efi.img")。该原始镜像可直接dd到磁盘或 U 盘,在支持 UEFI 的机器上启动;也可以交给 QEMU 等虚拟机验证。
需要留意的是兼容性前提:官方文档 platform-qemu.md 明确说明qcow-efi与raw-efi格式"可能可用,但当前未测试"(The formats qcow-efi and raw-efi may also work, but are currently not tested)。因此在生产使用前,建议先在目标平台或 QEMU 上做一次实际启动验证。
十一、调试技巧
脚本为排障保留了钩子:
set -e # for debugging [ -n "$DEBUG" ] && set -x设置DEBUG环境变量即可开启 shell 的-x跟踪,打印每一步执行的命令与变量展开结果。此外,脚本把除最终镜像外的所有日志重定向到 stderr:
# we want everything except the final result to stderr ( exec 1>&2; ... ) cat $IMGFILE这意味着 stdout 上只出现纯粹的磁盘镜像字节流,任何警告、进度信息都走 stderr——既保证了管道下游(如linuxkit build或dd)收到的数据纯净,也便于在容器日志中观察构建过程。
结语
tools/mkimage-raw-efi/虽小,却完整演示了"现代 UEFI 启动盘的构造艺术":从 stdin 协议接收内核与 initrd,用 ukify 合成 UKI,手工推演 FAT32 的最小化布局,再以 sgdisk 组装 GPT 分区,最终通过 stdout 交付一份对齐、紧凑、可直接启动的 raw 磁盘镜像。理解它的每个计算步骤,不仅能帮你排查 LinuxKit EFI 启动问题,也能为自研嵌入式或云原生引导镜像提供一套可复用的参考实现。
- 操作系统
- 云原生
- 容器运行时
【免费下载链接】linuxkit
A toolkit for building secure, portable and lean operating systems for containers
相关推荐
鸣潮重复流程3步全托管:ok-ww自动战斗、声骸刷取与日常托管完整新手指南
鸣潮重复流程3步全托管:ok ww自动战斗、声骸刷取与日常托管完整新手指南 凌晨两点,你的电脑还在安静地跑着副本,窗口最小化在任务栏里。等醒来再打开游戏,日常已
GUI 自动化计算机视觉RPA人工智能3步完成Telegraf容器化:从零到生产级监控采集实战
3步完成Telegraf容器化:从零到生产级监控采集实战 还在为服务器监控配置繁琐而头疼吗?面对分布式系统的指标采集,你是否希望找到一种更高效、更灵活的部署方案
可观测性指标监控运维Spring Boot Admin 与 GraalVM 原生镜像:基于 sample-servlet-graalvm 的构建与运行实战指南
Spring Boot Admin 与 GraalVM 原生镜像:基于 sample servlet graalvm 的构建与运行实战指南 Spring Boo
后端可观测性指标监控监控大盘MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考