Ray's Blog

计算机学习与实践笔记:Linux、网络与深度学习,从原理出发。Ray's notes on computing — Linux, networking, and deep learning, from first principles.

计算机如何启动?如何部署Arch Linux Arm?

想装Arch Linux

最近我给自己的Nanopi-R2s上安装了Arch Linux,这里记录一下心得。本文适合那些想了解计算机启动过程的朋友,当然,对于想给自己的ARM设备刷ArchLinuxARM的朋友也会有帮助。

在没有官方支持的硬件上部署Arch Arm对新手来说有难度,但同时也是深入理解Linux底层架构的机会——这正是Arch的价值,它强制用户直面底层配置。

网上虽然有一些教程可供参考,但是大多数都只提供操作步骤,如果不理解每个步骤的意义,出了问题就会很难处理。我不打算也写一篇那样的教程,而是会对比PC架构,聊一下ARM的启动过程,然后详细解释部署流程。

ARM 设备的启动流程

要成功部署系统,首先要理解设备从通电到系统就绪的全过程。我们将其简化为三个阶段:

  1. 固件初始化阶段:
    • 传统PC:BIOS(Basic Input Output System)完成硬件初始化、POST自检、启动设备选择

    • ARM架构:SoC(System on chip)芯片内置BootROM,执行初始化后从存储设备固定位置加载引导程序

  2. 引导加载阶段。加载系统的工具叫做Bootloader(引导程序)。它为硬件提供了一个抽象层,支持对复杂文件系统的操作,并且传递参数给内核。常见引导程序:
    • x86:GRUB
    • ARM:U-Boot(嵌入式设备主流选择)
    • Windows:bootmgfw.efi(UEFI环境)
  3. 内核加载阶段。 这里存在一个”先有鸡还是先有蛋”的哲学问题:操作系统需要管理存储设备,但自身又存放在存储设备中。解决方案是:
    • Bootloader直接加载包含基础驱动和文件系统模块的内核
    • 通过initramfs(初始内存文件系统)建立临时根文件系统,最终挂载真正的根文件系统(rootfs)

基于上述原理,我们需要准备以下组件:

组件 作用 获取方式
U-Boot 二级引导程序 交叉编译或预编译二进制
Linux内核 系统核心 官方仓库或自定义编译
设备树(Device Tree) 硬件描述文件 厂商SDK提供或自行编译
initramfs 临时根文件系统 Arch Linux ARM官方镜像
rootfs 根文件系统 Arch Linux ARM官方镜像

实战部署流程

部署一共四件事:编译 U-Boot → 把它刷到磁盘的固定位置 → 分区并解压根文件系统 → 用 boot.scr 打通 U-Boot 和内核。对照上一节的三个阶段:前两件事解决”引导加载”(阶段2),后两件事解决”内核加载”(阶段3);至于阶段1的 BootROM,它烧死在芯片里,我们管不着也不用管。

参考FriendlyElec Wiki配置交叉编译环境:

    # 安装友善提供的交叉编译器
    # 注意:这是 HTTP 明文 + 裸 IP 链接,建议先 curl 下载脚本审查后再用 sudo 执行
    sudo bash -c \
  "$(curl -fsSL http://112.124.9.243:3000/friendlyelec/build-env-on-ubuntu-bionic/raw/branch/cn/install.sh)"
    #配置环境
    export PATH=/opt/FriendlyARM/toolchain/11.3-aarch64/bin:$PATH
    export GCC_COLORS=auto

首先编译UBoot。我推荐从上游U-boot项目编译,而不是wiki上面uboot-rockchip,更简单直接,项目开发比较活跃。

    # 编译 ARM64 Rockchip SoC镜像
    git clone --depth 1 https://github.com/TrustedFirmware-A/trusted-firmware-a.git
    cd trusted-firmware-a
    make realclean
    make CROSS_COMPILE=aarch64-linux-gnu- PLAT=rk3328
    cd ..
    # 编译Uboot
    git clone --depth 1 https://source.denx.de/u-boot/u-boot.git
    cd u-boot
    export BL31=../trusted-firmware-a/build/rk3328/release/bl31/bl31.elf
    make evb-rk3328_defconfig
    make CROSS_COMPILE=aarch64-linux-gnu-

编译好之后把它刷入SD卡。因为SoC ROM不像BIOS有充足的空间,所以它的连接方式比较简单粗暴,就是从存储设备的固定位置读取。

瑞芯微芯片遵循它自己的分区标准

阶段 名称 程序 文件 磁盘位置
1 主引导 ROM code BootRom  
2 二级引导 U-Boot u-boot-rockchip.bin 0x40
    TPL/SPL    
3 boot分区 Linux内核 boot.img 0x8000
    Initrd镜像 initramfs-linux.img  
    设备树二进制文件 rk3328-nanopi-r2s.dtb  
      boot.scr  
4 root文件系统     0x40000

请注意就像Bootloader不太够直接拉起系统,因此使用Initrd建立临时根文件系统一样,SoC ROM只能读取比较简单的程序,而U-boot相对于SoC显得像庞然大物,所以中间也有很多过渡阶段。 这个过程被称为两步加载,甚至有三步加载(先拉起TPL/SPL,再由他们拉起更大的启动程序)。好消息是我们不用管这些,U-boot二进制程序里面包括第二步和第三步加载,直接把整个二进制文件刷到从64个扇区起的位置就可以了。

dd if=u-boot-rockchip.bin of=/dev/sdX seek=64 conv=notrunc

为什么偏偏是第 64 个扇区?回看上面那张分区标准表:二级引导的磁盘位置写的是 0x40,换算成十进制正好是 64——seek=64 就是从那张表里抄来的。BootROM 不认识分区表和文件系统,它只会到这个约定死的位置去读,所以这个数字一个都不能错。

如果不行的话,就需要分别制作TPL/SPL,Uboot和Trust的镜像,然后分别刷到第64,16384,24576扇区了,参考Uboot文档

然后分区、刷文件系统,然后根据ArchlinuxARM的指引下载和解压Rootfs文件。

    # 为了简单,只分一个区
    parted /dev/sdX mkpart '' ext4 32768s -1s
    # 选择ext4文件系统
    mkfs.ext4 /dev/sdX1
    # 挂载分区
    mount /dev/sdX1 /mnt
    # 下载和解压 Arch Rootfs
    wget http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz
    bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C /mnt

接下来,从Archwiki上下载一个boot.scr脚本,放在/boot文件夹下面。这个文件是连接U-boot 和Linux操作系统的关键步骤。 我们编译的U-boot里面有个配置参数CONFIG_DISTRO_DEFAULTS,如果允许它,Uboot会扫描可启动的磁盘里面的boot.scr或者extlinux.conf文件,然后执行这些文件。 一个extlinux.conf 文件看起来是这样:

label Arch with uart devicetree overlay
    kernel /arch/Image.gz
    initrd /arch/initramfs-linux.img
    fdt /dtbs/arch/board.dtb
    fdtoverlays /dtbs/arch/overlay/uart0-gpio0-1.dtbo
    append console=ttyS0,115200 console=tty1 rw root=UUID=fc0d0284-ca84-4194-bf8a-4b9da8d66908

这个配置文件告诉U-boot内核的位置,Initrd镜像的位置,和FDT文件的位置。FDT是关于设备的信息数据的二进制文件。然后是需要传递给内核的参数。

再看一下boot.scr脚本。需要注意的是boot.scr是一个二进制文件,我们不能直接打开,我们应该看对应的cmd文件。但是因为他们传递数据是基于文本,直接打开也能看出里面在干什么。 脚本里面内容比较多,我们不展示全文,只展示关键的部分。

    setenv bootargs console=ttyS2,1500000 root=PARTUUID=${uuid}
    load ${devtype} ${devnum}:${bootpart} ${kernel_addr_r} /boot/Image
    load ${devtype} ${devnum}:${bootpart} ${fdt_addr_r} /boot/dtbs/${fdtfile};
    load ${devtype} ${devnum}:${bootpart} ${ramdisk_addr_r} /boot/initramfs-linux.img;
    booti ${kernel_addr_r} ${ramdisk_addr_r}:${filesize} ${fdt_addr_r};

可见这个文件里面就是一堆U-boot命令,而且作用和上面的extlinux.conf文件是一样的,即传递一些内核参数,加载内核,设备树二进制文件,以及Initrd镜像到内存的指定位置,最后启动内核和Initrd。

这些内存位置的变量则在源文件主板的Header文件中,比如rk3328_common.h里面的:

    #define ENV_MEM_LAYOUT_SETTINGS		\
	"scriptaddr=0x00500000\0"	\
	"script_offset_f=0xffe000\0"	\
	"script_size_f=0x2000\0"	\
	"pxefile_addr_r=0x00600000\0"	\
	"fdt_addr_r=0x01e00000\0"	\
	"fdtoverlay_addr_r=0x01f00000\0"	\
	"kernel_addr_r=0x02080000\0"	\

我们下载的这个boot.scr脚本没办法直接使用,反正我这里行不通,至少存在以下几个问题:

  1. Arch提供的initramfs是裸镜像。裸 initrd 不是不能加载,而是要在 booti 行显式给出大小(写成 ${ramdisk_addr_r}:${filesize},就像上面 Armbian 脚本那样);如果不想在 booti 行带 :size,就得用 mkimage 套个头,让 U-Boot 自己从头部读出大小。这里选择后者。
  2. dtb 文件也得换成能用的。NanoPi R2 有多个修订版(rev00、rev01 等),硬件略有差异,对应的 dtb 也不同;不同来源的内核文件名还不一样–主线 Linux 用 rk3328-nanopi-r2s.dtb,友善 BSP 树用 rk3328-nanopi-r2-rev00.dtb 这类名字。Arch 预编译的 dtb 在我这里没一个能用,最后是自己编译内核和 dtbs 才解决。所以下面 boot.cmd 里的文件名只是我那次能跑通的那个,照抄前请先确认你手上的 dtb 实际叫什么。

不用担心,只要知道这个文件的作用,我们完全可以自己写这个脚本。 我的boot.cmd脚本差不多是这样:

load ${devtype} ${devnum}:${distro_bootpart} ${ramdisk_addr_r} ${prefix}uInitrd
load ${devtype} ${devnum}:${distro_bootpart} ${kernel_addr_r} ${prefix}Image

# 注意:dtb 文件名随内核来源/设备修订版而变,此处用的是友善 BSP 的 rev00(见上文说明)
load ${devtype} ${devnum}:${distro_bootpart} ${fdt_addr_r} ${prefix}dtbs/rk3328-nanopi-r2-rev00.dtb
fdt addr ${fdt_addr_r}
fdt resize 65536
booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r}

写完之后,安装Uboot-tools包,然后用mkimage命令制作ramdisk和boot.scr:

    # 输入路径跟脚本中的地址对应
    mkimage -A arm64 -O linux -T ramdisk -C none -n "Initrd Image" -d /mnt/boot/initramfs-linux.img /mnt/boot/uInitrd;
    mkimage -A arm64 -O linux -T script -C none -n "Boot Script" -d boot.cmd /mnt/boot/boot.scr

mkimage 做的事情很简单:给文件包上一个 U-Boot 能识别的头部,里面记录着类型、架构、大小和校验和。这样 U-Boot 加载 uInitrd 和 boot.scr 的时候,才知道自己拿到的是什么东西、多大、有没有损坏。

这也回答了前面遗留的问题:Arch 提供的 initramfs 之所以“不能直接用”,并不是裸镜像天生不能加载,而是我的 boot.cmd 里 booti 行没带 :size(对比上面 Armbian 脚本的 ${ramdisk_addr_r}:${filesize})。两条 booti 写法的差别就在这里–要么在命令行给大小,要么让 mkimage 头把大小嵌进镜像,二选一。我选了后者,所以 boot.cmd 可以写 booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r} 而不带 :size,代价是 initramfs 得先用 mkimage 包一层。 到了这一步我们有了U-boot作为启动加载程序,就基本完成了,然后插电看看是不是已经搞定了。

避坑指南

通过USB-TTL模块查看启动日志。我没有这个模块,只能看指示灯,出了问题也不知道出在哪个环节,浪费了很多时间。

dtb 是整条流程里最容易卡的一步:NanoPi R2 有多个修订版,dtb 名字又随内核来源而变,Arch 预编译的基本不能用。这一步起不来又没串口的话几乎只能盲调,建议直接从 Armbian 镜像里抠一个能用的 dtb 先把启动链路跑通,再换 Arch 的 rootfs。

现在想来最好从Armbian这个项目下载一个能用的系统, 测试Uboot和boot.scr或者extlinux.conf能不能工作,最后再刷入Arch的文件系统了。

不建议使用友善官方镜像,因为它们分区太细了,需要debug的环节就太多了。

最好能准备两个SD卡,一个用于测试,一个用于正式部署。

参考链接:

https://wiki.friendlyelec.com/wiki/index.php/NanoPi_R2S/zh
https://opensource.rock-chips.com/wiki_Boot_option
https://docs.u-boot.org/en/latest/board/rockchip/rockchip.html#rockchip-boards
https://archlinuxarm.org/platforms/armv8/rockchip/rock64
https://gist.github.com/larsch/a8f13faa2163984bb945d02efb897e6d