macOS交叉编译Raspberry Pi OS Linux内核
1 环境信息
- 开发板:Raspberry Pi 5 | Raspberry Pi OS Lite x64
- 宿主机:MacBook Air M3 | macOS 14.3
2 编译内核
2.1 准备交叉编译工具链
macos-cross-toolchains[1]这个开源项目提供了预编译好的工具链,可以通过homebrew直接下载使用:
$ brew tap messense/macos-cross-toolchains
$ brew install aarch64-unknown-linux-gnu
2.2 准备文件系统
Linux内核通常只能在大小写敏感的文件系统上编译,但macOS默认的文件系统是不区分大小写的,因此需要提前准备好一个大小写敏感的存储空间用于编译内核。
这里使用自带的命令行工具创建一个容量为40GB、大小写敏感的APFS文件系统、占用空间动态增长的磁盘映像:
$ hdiutil create -size 40g -fs "Case-sensitive APFS" -type SPARSE -volname build_raspberrypi_kernel build_raspberrypi_kernel
挂载刚刚创建的磁盘映像:
$ export KERNEL_BUILD_PARTITION_CS_APFS=/path/to/mountpoint/dir
$ hdiutil attach build_raspberrypi_kernel.sparseimage -mountpoint $KERNEL_BUILD_PARTITION_CS_APFS
与内核构建相关的操作都将在$KERNEL_BUILD_PARTITION_CS_APFS
目录下进行。
后续可使用如下命令卸载这个分区
$ hdiutil detach $KERNEL_BUILD_PARTITION_CS_APFS
2.3 下载内核源码
这里直接浅克隆代码仓,以减少等待时间:
$ cd $KERNEL_BUILD_PARTITION_CS_APFS
$ git clone --depth=1 https://github.com/raspberrypi/linux
2.4 构建内核配置
不做额外调整,直接构建默认配置。根据树莓派型号和系统架构的不同,make目标亦有区别,树莓派官方文档有详细说明[2]。
$ cd linux
$ KERNEL=kernel_2712
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bcm2712_defconfig
构建完成后,内核源码目录下将会产生.config
配置文件。修改.config
文件中的CONFIG_LOCALVERSION配置项,可以自定义内核版本号。这一步不是必须的,但可以避免覆盖既有的同版本号内核文件,方便区分新旧内核。
CONFIG_LOCALVERSION="-v8-16k-ik"
2.5 编译内核源码
开始编译内核:
$ make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules dtbs
上述命令需要执行较长时间,且可能会出现多种编译报错,具体的问题和解决方案见下节汇总。
2.6 编译问题汇总
2.6.1 make版本过低
错误信息
Makefile:15: *** GNU Make >= 3.82 is required. Your Make version is 3.81. Stop.
问题根因
macOS自带的make
版本通常不高,可能无法达到构建内核所要求的版本。
解决方案
升级make版本即可。直接通过homebrew安装:
$ brew install make
调整PATH环境变量,确保当前优先使用非系统自带的make:
$ export PATH="$(brew --prefix)/opt/make/libexec/gnubin:$PATH"
2.6.2 找不到elf.h
错误信息
HOSTCC scripts/sorttable
scripts/sorttable.c:27:10: fatal error: 'elf.h' file not found
#include <elf.h>
^~~~~~~
问题根因
macOS标准库没有elf.h
头文件,该头文件中定义有ELF相关的类型、结构体和宏。
解决方案
从Glibc代码仓下载elf.h
头文件[5],并将其置于/usr/local/include/
目录下:
$ wget https://github.com/bminor/glibc/raw/refs/heads/master/elf/elf.h -O /usr/local/include/elf.h
2.6.3 uuid_t重定义
错误信息
HOSTCC scripts/mod/file2alias.o
scripts/mod/file2alias.c:45:3: error: typedef redefinition with different types ('struct uuid_t' vs '__darwin_uuid_t' (aka 'unsigned char[16]'))
} uuid_t;
^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_uuid_t.h:31:25: note: previous definition is here
typedef __darwin_uuid_t uuid_t;
^
问题根因
内核中定义了一个uuid_t
结构体,但是macOS的SDK头文件中也定义了同名的类型,与内核的实现有细微的区别,导致出现重定义错误。
该结构体的定义位于scripts/mod/file2alias.c
。相关的改动在内核5.1版本引入[3],用于支持TEE设备的总线驱动框架,其使用uuid来识别设备和服务。
typedef struct {
__u8 b[16];
} uuid_t;
而在macOS的SDK头文件中也存在类似的定义:
typedef __darwin_uuid_t uuid_t;
typedef unsigned char __darwin_uuid_t[16];
解决方案
在内核代码定义uuid_t
结构体之前,使用宏替换掉冲突的名称即可[4]:
// 必须置于uuid结构体定义之前
#ifdef __APPLE__
#define uuid_t compat_uuid_t
#endif
typedef struct {
__u8 b[16];
} uuid_t;
2.6.4 找不到endian.h
错误信息
HOSTCC arch/arm64/kvm/hyp/nvhe/gen-hyprel
arch/arm64/kvm/hyp/nvhe/gen-hyprel.c:28:10: fatal error: 'endian.h' file not found
#include <endian.h>
^~~~~~~~~~
问题根因
macOS标准库没有可用的endian.h
头文件,该文件用于处理字节序转换。
解决方案
下载endian.h
头文件[6],并将其置于/usr/local/include/
目录下:
$ wget https://gist.github.com/panzi/6856583/raw/12f9f02f1298bb0bc054ba667bccc0cf032cdb03/portable_endian.h -O /usr/local/include/endian.h
2.6.5 sed调用异常
错误信息
GEN security/apparmor/capability_names.h
sed: 1: "include/uapi/linux/capa ...": command i expects \ followed by text
问题根因
macOS自带的sed工具在语法上和内核构建使用的GNU sed有些区别,可能会造成兼容性问题。
解决方案
直接通过homebrew安装GNU版本的sed:
$ brew install gsed
调整PATH环境变量,确保当前优先使用非系统自带的sed:
$ export PATH="$(brew --prefix)/opt/gnu-sed/libexec/gnubin:$PATH"
2.6.6 找不到linux/version.h
错误信息
HOSTCC drivers/accessibility/speakup/makemapdata.o
drivers/accessibility/speakup/makemapdata.c:13:10: fatal error: 'linux/version.h' file not found
#include <linux/version.h>
^~~~~~~~~~~~~~~~~
问题根因
内核构建配置默认会启用SPEAKUP模块,该模块提供无障碍辅助功能,可以帮助视障用户使用计算机(例如屏幕内容朗读)。
该模块的生成代码包含了linux/version.h
这个头文件,显然macOS上不会有这个头文件。
但实际上,对应的源码并没有使用到该头文件中的任何宏,所以在内核6.11之后的版本去除了冗余的包含代码[7]。
待后续树莓派内核代码与上游版本同步,应该就不会再出现这个问题了。
解决方案
可以直接在内核配置.config
中取消启用该模块:
#
# Speakup console speech
#
# 将下述内容全部注释
# CONFIG_SPEAKUP=m
# CONFIG_SPEAKUP_SYNTH_ACNTSA is not set
# CONFIG_SPEAKUP_SYNTH_APOLLO is not set
# CONFIG_SPEAKUP_SYNTH_AUDPTR is not set
# CONFIG_SPEAKUP_SYNTH_BNS is not set
# CONFIG_SPEAKUP_SYNTH_DECTLK is not set
# CONFIG_SPEAKUP_SYNTH_DECEXT is not set
# CONFIG_SPEAKUP_SYNTH_LTLK is not set
# CONFIG_SPEAKUP_SYNTH_SOFT=m
# CONFIG_SPEAKUP_SYNTH_SPKOUT is not set
# CONFIG_SPEAKUP_SYNTH_TXPRT is not set
# CONFIG_SPEAKUP_SYNTH_DUMMY is not set
# end of Speakup console speech
或者删除SPEAKUP模块代码中的#include <linux/version.h>
,涉及到的文件有:
drivers/accessibility/speakup/makemapdata.c
drivers/accessibility/speakup/genmap.c
3 安装内核
3.1 准备ext4挂载工具
macOS上无法直接挂载ext4文件系统,需要借助额外的工具,可以使用任意支持挂载ext4文件系统的工具(需支持读写)。
这里使用fuse-ext2,开启一个新的终端进行编译安装。首先安装依赖:
$ brew install --cask macfuse
$ brew install m4
$ brew install autoconf
$ brew install libtool
$ brew install e2fsprogs
下载源码并配置依赖环境变量:
$ git clone https://github.com/alperakcan/fuse-ext2.git
$ export PATH="$(brew --prefix)/opt/m4/bin:$PATH"
$ export PATH="$(brew --prefix)/opt/libtool/libexec/gnubin:$PATH"
$ export PKG_CONFIG_PATH="$(brew --prefix)/opt/e2fsprogs/lib/pkgconfig"
$ export LDFLAGS="-L$(brew --prefix)/opt/e2fsprogs/lib"
$ export CFLAGS="-I$(brew --prefix)/opt/e2fsprogs/include -Wno-incompatible-function-pointer-types"
如果只安装了Xcode命令工具,而没有安装完整的Xcode环境,则需要手动注释tools/macosx/Makefile.am
文件中的所有内容,以避免编译时出现类似这样的报错:
Making all in macosx
( cd ../../tools/macosx/prefpane; \
xcodebuild; \
)
xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance
编译安装:
$ cd fuse-ext2
$ ./autogen.sh
$ ./configure --prefix="/absolute/dir/for/fuse-ext2/installation/"
$ make -j$(nproc)
$ make install
可以建立软链接方便使用:
$ sudo ln -s /path/to/fuse-ext2/installation/bin/fuse-ext2 /usr/local/bin
3.2 挂载内核镜像
插入SD卡,执行下述命令找到SD卡对应的设备:
$ diskutil list
通常会有类似这样的输出:
/dev/disk8 (external, physical):
#: TYPE NAME SIZE IDENTIFIER
0: FDisk_partition_scheme *63.9 GB disk8
1: Windows_FAT_32 bootfs 536.9 MB disk8s1
2: Linux 63.3 GB disk8s2
其中,/dev/disk8s1
是内核启动引导分区(通常是FAT32格式),/dev/disk8s2
是内核根文件系统分区(通常是ext4格式)。 根据实际情况导出对应的环境变量:
export KERNEL_PART_DEV=/dev/disk8
export KERNEL_BOOT_DEV=/dev/disk8s1
export KERNEL_ROOT_DEV=/dev/disk8s2
macOS通常会自动挂载FAT32格式的分区到/Volumes/
目录下,因此需要先进行卸载。 执行如下命令进行检查:
$ mount
如果看到类似这样的输出:
/dev/disk8s1 on /Volumes/bootfs (msdos, local, nodev, nosuid, noowners, noatime)
则需要先进行卸载操作:
$ sudo diskutil unmount /Volumes/bootfs
接着就可以在内核源码根目录先创建好对应的挂载目录:
$ mkdir mnt
$ mkdir mnt/boot
$ mkdir mnt/root
执行挂载操作:
$ sudo mount -t msdos $KERNEL_BOOT_DEV mnt/boot
$ sudo fuse-ext2 $KERNEL_ROOT_DEV mnt/root -o rw+
3.3 执行安装
安装内核模块:
$ sudo env PATH=$PATH make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- INSTALL_MOD_PATH=mnt/root modules_install
安装内核和设备树:
$ sudo cp mnt/boot/$KERNEL.img mnt/boot/$KERNEL-backup.img
$ sudo cp arch/arm64/boot/Image mnt/boot/$KERNEL.img
$ sudo cp arch/arm64/boot/dts/broadcom/*.dtb mnt/boot/
$ sudo cp arch/arm64/boot/dts/overlays/*.dtb* mnt/boot/overlays/
$ sudo cp arch/arm64/boot/dts/overlays/README mnt/boot/overlays/
安装内核符号表,后续用于生成内核模块依赖关系和导出符号:
$ sudo mv mnt/root/boot/System.map mnt/root/boot/System.map.old
$ sudo cp System.map mnt/root/boot/System.map
当从macOS复制文件到外部卷时,会自动创建前缀为._的同名文件(Apple Double Format),这些文件用于记录macOS独有的附加信息,对于内核构建使用没有任何意义,反而可能导致生成内核模块依赖信息时出错,因此需要先清理干净:
$ sudo find mnt -type file -size 4k -name '._*' -delete
安装完成,卸载分区并弹出SD卡:
$ sudo diskutil unmount mnt/boot
$ sudo umount mnt/root
$ sudo diskutil eject $KERNEL_PART_DEV
此时,可以将SD卡重新装载到树莓派上并启动系统。
3.4 生成内核模块依赖文件
在前面安装内核时,可能会出现类似的警告信息:
DEPMOD mnt/root/lib/modules/6.6.51-v8-16k-ik+
Warning: 'make modules_install' requires depmod. Please install it.
This is probably in the kmod package.
这是因为macOS上没有depmod工具(属于kmod套件),无法分析内核模块的依赖信息和符号信息,可能会导致部分内核模块无法正常加载(例如无线网卡驱动)。
因此,在新的内核安装后,可以连接到树莓派,利用其自带的depmod重新处理内核模块的依赖关系:
$ sudo /sbin/depmod -ae -F /boot/System.map
重启系统使依赖文件生效:
$ sudo /sbin/reboot
重启后,可见新的内核版本已经生效:
$ uname -a
Linux raspberryki 6.6.51-v8-16k-ik+ #1 SMP PREEMPT Sat Sep 21 17:12:01 CST 2024 aarch64 GNU/Linux
至此,内核的编译、安装全部完成。
4 内核模块测试
既然已经成功编译了内核,不妨顺便用内核树跑个简单的内核模块hello world一下:
Makefile:
obj-m := hello_world.o
KERNEL_DIR ?= ~/dev/kernel/raspberrypi/build_raspberrypi_kernel/linux/
TARGET_ARCH = arm64
COMPILER_PREFIX = aarch64-linux-gnu-
PWD = $(shell pwd)
all:
make -C ${KERNEL_DIR} M=$(PWD) ARCH=${TARGET_ARCH} CROSS_COMPILE=${COMPILER_PREFIX} modules
clean:
make -C ${KERNEL_DIR} M=$(PWD) ARCH=${TARGET_ARCH} CROSS_COMPILE=${COMPILER_PREFIX} clean
hello_world.c:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int __init hello_init(void)
{
printk(KERN_INFO "hello world\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "goodbye world\n");
}
module_init(hello_init);
module_exit(hello_exit);
将编译后的内核模块复制到树莓派上,然后装载/卸载模块,即可在内核日志看到相关的打印信息:
$ sudo /sbin/insmod hello_world.ko
$ sudo /sbin/rmmod hello_world
$ dmesg | tail -3
[ 1773.361977] hello_world: loading out-of-tree module taints kernel.
[ 1773.362134] hello world
[ 1777.345510] goodbye world
参考
[1] macOS cross compiler toolchains
[2] Linux kernel cross compiled build configuration - Raspberry Pi Documentation
[3] tee: add bus driver framework for TEE based devices
[4] kernel: 5.4 fix build on darwin
[6] Endian conversion functions form endian.h
[7] treewide: remove unnecessary <linux/version.h> inclusion