macOS交叉编译RaspberryPi Kernel

前言

本文主要介绍如何使用macos编译RaspberryPi内核,并使用新内核进行启动。笔者编译出来的内核可以正常启动,但存在协议栈问题(具体问题未分析)。
另外需要说明的是,由于笔者只有一块树莓派3代B板,因此使用本文编译方法仅在3B板上进行过验证,其它类型的树莓派未作验证。
本文默认读者已经了解以下内容:

  1. macos上brew工具的使用
  2. git工具及GitHub的使用

代码下载

确认文件系统是否大小写敏感

在下载代码之前,我们需要先做一件事:确认准备存放代码的目录是否是大小写敏感的,因为linux kernel中存在大小写敏感的文件。笔者已经验证过,该流程无法跳过。确认方式如下:

  1. 打开Disk Utility工具,检查存放目录所在卷的格式,如下:

  1. 如果如上图一样显示为大小写敏感则说明该卷是大小写敏感的,后续操作也无需执行,否则需要创建大小写敏感的新卷(或者也可以使用移动硬盘)。点击右上角的+创建新卷:

  1. 在创建新卷的对话框中选择格式为APFS(区分大小写),如下图所示。卷名及大小请读者自行定义。

  1. 创建完成后可以在/Volumes目录下看到新的卷。如笔者是创建了一个名为source_code的新卷,因此会有一个/Volumes/source_code目录。
  2. 创建新卷后可能会碰到终端操作时提示文件磁盘读写无权限。碰到这种情况时,我们需要变更系统偏好设置->安全性与隐私->完全磁盘访问权限,在界面上把终端给勾上,如下:

使用git下载代码

当确认准备存放代码的目录是大小写敏感的之后,我们可以使用git工具下载树莓派代码,命令如下:

git clone https://github.com/raspberrypi/linux.git -b rpi-6.1.y --depth 1

需要注意的是,此处我选择的是分支rpi-6.1.y,读者可以自行指定所需分支。若碰到git clone无法下载代码的问题,可以考虑挂代理或从网页端直接下载压缩包进行解压。

环境准备

在准备编译之前,需要安装一些基本工具,如makeopensslgsedllvm等。由于笔者先前环境中已经安装过一些库,因此此处依赖的工具及库可能有遗漏。

安装make

make是一个常用的构建工具,它通过解析makefile中定义的目标及规则进行构建。构建kernel需要使用GNU Make,最版本最好是4.4以上。笔者当前OS为Monterey 12.6.5,自带的make为3.18版本,使用该版本的make构建会有异常,如下:

/Volumes/source_code/raspberry_linux/Makefile:1252: *** multiple target patterns.  Stop.
make: *** [__sub-make] Error 2

根据错误提示,我们可以看到根目录下Makefile的1252行前后代码如下:

1248 # _LDFLAGS_vmlinux is a workaround for the 'private export' bug:
1249 #   https://savannah.gnu.org/bugs/?61463
1250 # For Make > 4.4, the following simple code will work:
1251 #  vmlinux: private export LDFLAGS_vmlinux := $(LDFLAGS_vmlinux)
1252 vmlinux: private _LDFLAGS_vmlinux := $(LDFLAGS_vmlinux)
1253 vmlinux: export LDFLAGS_vmlinux = $(_LDFLAGS_vmlinux)
1254 vmlinux: vmlinux.o $(KBUILD_LDS) modpost
1255     $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.vmlinux

即4.4以前版本的make在解析private export时会出现异常,因此需要将make升级到4.4及以后的版本。
make的安装比较简单,我们使用brew进行安装:

brew install make

安装完后,我们可能会发现make的版本并未更新,这可能是因为搜索路径导致的问题,可以修改PATH环境变量,将新make的地址添加到PATH中,如下:

# 安装后make版本不变
eric@Yubins-MacBook-Air raspberry_linux % make --version
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

This program built for i386-apple-darwin11.3.0

# 检查make的路径
eric@Yubins-MacBook-Air raspberry_linux % which make
/usr/bin/make

# 将新安装的make的路径添加到PATH环境变量中
eric@Yubins-MacBook-Air raspberry_linux % export PATH=/usr/local/opt/make/libexec/gnubin:$PATH

# 检查make的路径
eric@Yubins-MacBook-Air raspberry_linux % which make
/usr/local/opt/make/libexec/gnubin/make

# 检查make的版本
eric@Yubins-MacBook-Air raspberry_linux % make --version
GNU Make 4.4.1
安装llvm

macOS上现主要使用clang进行C/C++代码的语法分析和词法分析,生成LLVM IR(Intermediate Representation)。不同于gcc的是,gcc既是一个编译器,也是一堆编译工具的集合;而clang只是作为编译器,其它类似gcc的代码优化、链接、从汇编代码到机器代码的编译则是由LLVM框架实现的。
因此,我们如果要编译kernel需要安装clang及llvm。由于不太确定macos是否自带有llvm(我也忘记之前是否自己有安装过),因此此处说明llvm的安装方法,命令比较简单:

brew install llvm

安装完llvm之后,我们需要关注llvm的安装位置(后续编译会使用),可用以下命令查看:

brew info llvm

在笔者的电脑上,llvm是安装在/usr/local/opt/llvm目录。

安装openssl

如果前面有安装了llvm,默认是安装openssl的,若openssl未安装,可使用以下命令进行安装:

brew install openssl

同样的,我们可以使用以下命令获取openssl的安装地址,该地址在后续构建内核时会使用到:

brew info openssl

在笔者的电脑上,llvm是安装在/usr/local/opt/openssl目录

安装gsed

macos自带有sed,但语法上与kernel构建脚本中sed的使用有差异。因此若使用系统自带的sed,则构建过程会出现错误,如下:

因此,需要使用gnu sed替换掉macos自带的sed,安装命令如下:

brew install gsed

安装完gsed,我们就可以使用gsed,如下:

eric@MacBook-Air raspberry_linux % gsed --version
gsed (GNU sed) 4.9
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

但是需要注意的是,sed命令仍然是使用系统自带的sed工具。我们可以使用全局替换将代码脚本中所有的字符串sed都替换成gsed。笔者尝试过使用alias命令将sed命令指向gsed,但可惜的是alias无法传递给子交互shell,因此只能使用字符串全局替换。

安装macFuse

该程序是为了fuse-ext2(下文会介绍)使用,我们可以简单地使用brew工具进行安装,命令如下:

brew install macfuse
安装fuse-ext2

fuse-ext2程序可以让我们在macos上挂载(mount)ext4的文件系统。其安装方式比较简单,可以参考github链接中的介绍进行安装。期间会要求输入用户密码。

代码适配

下载elf.h

kernel的编译中依赖elf.h文件,但macos中并不带该文件,因此在构建过程中会出现错误:

要解决这个问题,只需要从网络上下载合适的elf.h即可,但笔者找了几个elf.h都存在问题。最后找到的可用的elf.h可参考glibc/elf/elf.h
下载的elf.h文件拷贝到/usr/local/include目录下即可。需要注意的是,如果该目录下已经有elf.h,可考虑将原elf.h进行重命名进行备份。

适配uuid_t

kernel中定义了一个struct uuid_t结构体,macos自身也定义了一个同名的结构体,而且两个结构体的定义存在差异,因此在编译过程中会出现如下错误:

scripts/mod/file2alias.c:47:3: error: typedef redefinition with different types ('struct uuid_t' vs '__darwin_uuid_t' (aka 'unsigned char[16]'))
} uuid_t;
  ^
/Library/Developer/CommandLineTools/SDKs/MacOSX12.sdk/usr/include/sys/_types/_uuid_t.h:31:25: note: previous definition is here
typedef __darwin_uuid_t uuid_t;
                        ^

若要解决这个问题,需要将kernel中struct uuid_t的名称更换掉,我们可以在uuid_t的定义(文件scripts/mod/file2alias.c,Line 47)之前,加上如下代码进行struct uuid_t的命名替换:

#ifdef __APPLE__
#define uuid_t compat_uuid_t
#endif

该段宏定义可以将uuid_t的命名全部替换为compat_uuid_t。该段适配代码来自参考资料macOS交叉编译Linux避坑指南
需要注意的是,适配代码中的__APPLE__宏是macOS的预定义宏,详细可参考Porting UNIX/Linux Applications to OS X

下载endian.h

kernel编译过程需要endian.h头文件,但在/usr/local/include目录下并未包含该文件,因此在构建过程会出现以下报错:

arch/arm64/kvm/hyp/nvhe/gen-hyprel.c:28:10: fatal error: 'endian.h' file not found
#include <endian.h>
         ^~~~~~~~~~

需要从网络上下载一份endian.h/usr/local/include目录下,详细可参考endian.h not found on mac osx

内核编译

基本命令

前面提到,我们将会使用make + clang + llvm在macos上进行交叉编译,由于目标板(树莓派3B)支持64位架构,因此决定编译64位内核。在macos上进行交叉编译的基本指令为:

make -j4 ARCH=arm64 CC=clang CROSS_COMPILE=aarch64-linux-gnu- LLVM=/usr/local/opt/llvm/bin/ HOSTCFLAGS="-I/usr/local/include -I/usr/local/opt/openssl/include" HOSTLDFLAGS="-L/usr/local/opt/openssl/lib/" <target>

上面的指令中:

  1. -j4指同时启动4条线程进行编译构建。若不指定-j则整个编译过程是串行的,耗时较长。但-jN中的N并不是越大越好,既与构建系统中的TARGET之间的依赖有关,也与内核数量有关。
  2. ARCH=arm64指待构建的是arm架构64位的软件,CROSS_COMPILE=aarch64-linux-gnu-指的是构建过程中可执行程序的前缀。具体可参考根目录下Makefile的说明:
    # Cross compiling and selecting different set of gcc/bin-utils
    # ---------------------------------------------------------------------------
    #
    # When performing cross compilation for other architectures ARCH shall be set
    # to the target architecture. (See arch/* for the possibilities).
    # ARCH can be set during invocation of make:
    # make ARCH=ia64
    # Another way is to have ARCH set in the environment.
    # The default ARCH is the host where make is executed.
    # CROSS_COMPILE specify the prefix used for all executables used
    # during compilation. Only gcc and related bin-utils executables
    # are prefixed with $(CROSS_COMPILE).
    # CROSS_COMPILE can be set on the command line
    # make CROSS_COMPILE=ia64-linux-
    # Alternatively CROSS_COMPILE can be set in the environment.
    # Default value for CROSS_COMPILE is not to prefix executables
    # Note: Some architectures assign CROSS_COMPILE in their arch/*/Makefile
    ARCH        ?= $(SUBARCH)
  3. CC=clang指编译C代码使用的是clang程序
  4. LLVM=...指的是系统上LLVM的路径,该路径在前面安装LLVM的章节已有说明
  5. HOSTCFLAGS=-I...HOSTLDFLAGS=...则分别指编译过程可搜索头文件的路径、编译过程中可搜索链接Library的路径
生成配置文件

根据树莓派官网指导,64位的3B板可以使用默认配置bcm2711_defconfig,即使用以下命令生成配置文件 :

KERNEL=kernel8
make bcm2711_defconfig ARCH=arm64 CC=clang CROSS_COMPILE=aarch64-linux-gnu- LLVM=/usr/local/opt/llvm/bin/

其它板子或其它位数的配置文件可参考官方指导说明Apply the Default Configuration

当前也可以使用原有树莓派系统上的配置,不过本文并不打算介绍该方法,有兴趣的可参考其他博主的文章梳理树莓派4B内核交叉编译过程(Debian11自带编译器)

去除speakup模块

kernel默认配置中有一个SPEAKUP模块,如下:

% cat .config | grep SPEAKUP
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

从代码说明上看,speakup是一种无障碍(accessibility)功能,用于合成语音。但在macos上speakup模块编译过程会出现如下报错:

drivers/accessibility/speakup/makemapdata.c:13:10: fatal error: 'linux/version.h' file not found
#include <linux/version.h>
         ^~~~~~~~~~~~~~~~~
1 error generated.

出错的代码可参考Linux Kernel上的提交speakup: Generate speakupmap.h automatically,从Log说明上看,makemapdata.c这个文件是用于生成speakupmap.h头文件的。推测编译过程应该会先生成一个makemapdata程序,然后再自动生成speakupmap.h文件。但鉴于笔者的树莓派并不需要speakup模块,因此选择在源码根目录下.config文件中,将speakup模块的功能配置取消掉。即不编译speakup模块以规避该问题。若读者需要该功能,原则上可以在/usr/local/include/linux目录下放一个version.h文件。取消后的.config配置如下:

% cat .config | grep SPEAKUP
# CONFIG_SPEAKUP=m  # 修改点1
# 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  # 修改点2
# CONFIG_SPEAKUP_SYNTH_SPKOUT is not set
# CONFIG_SPEAKUP_SYNTH_TXPRT is not set
# CONFIG_SPEAKUP_SYNTH_DUMMY is not set
自定义版本号(可选)

该过程只是为了方便我们确定树莓派启动用的内核是我们自行编译的内核。修改方法比较简单,在源码根目录下生成的.config中将CONFIG_LOCALVERSION的值改成自定义的值,如笔者在其后面增加了yangpaopao的后缀,如下:

CONFIG_LOCALVERSION="-v8-yangpaopao"
开始编译

使用以下指令可以开始内核的构建:

make -j4 ARCH=arm64 CC=clang CROSS_COMPILE=aarch64-linux-gnu- LLVM=/usr/local/opt/llvm/bin/ HOSTCFLAGS="-I/usr/local/include -I/usr/local/opt/openssl/include" HOSTLDFLAGS="-L/usr/local/opt/openssl/lib/" Image modules dtbs

在上面的指令中,构建的目标是Image modules dtbs共三个。三个Target的作用分别如下:

  1. Image: 经过压缩的,在启动(boot)过程中使用的内核镜像。
  2. modules: 内核驱动模块
  3. dtbs: 设备树相关blob文件,由dts编译而来。

其它版本的可参考官方指导Build with Configs

使用新内核启动树莓派

上面编译完成之后,我们便可以获取到我们所需的产物。接下来就是将编译好的内核模块和镜像放到SD卡上。步骤如下:

安装modules

该步骤相当麻烦。我们使用的SD卡其实分为两个区:一为boot区,其格式为FAT32;一为root区,其格式为ext4。而我们将SD卡插到mac上时,只可以看到boot区,而看不到root区。为了解决该问题,我们需要安装macFusefuse-ext2程序。安装步骤已经在前面介绍过。
我们先将SD卡插入PC,插入后使用df -h命令我们可以看到boot区已经挂载好了:

% df -h
Filesystem       Size   Used  Avail Capacity iused      ifree %iused  Mounted on
/dev/disk2s1    255Mi   78Mi  177Mi    31%       0          0  100%   /Volumes/bootfs

我们可以看到上面设备名为disk2s1,该设备对应boot区。类似的,disk2s2设备名应该对应root区。我们使用fuse-ext2程序将root区进行挂载,如下:

sudo fuse-ext2 /dev/disk2s2 mnt/ext4 -o rw+

需要注意的是,不同的PC及设备环境,设备名可能会有差异,不能直接照搬上面的指令。
上面的指令将root区挂载到了mnt/ext4目录,如果需要取消挂载,则使用sudo umount mnt/ext4即可。
挂载完后,我们可以使用make命令安装modules:

sudo make ARCH=arm64 CC=clang CROSS_COMPILE=aarch64-linux-gnu- LLVM=/usr/local/opt/llvm/bin/ INSTALL_MOD_PATH=mnt/ext4 modules_install

此处需要注意的有三点 :

  1. 需要使用sudo命令,否则可能出现无权限写入的问题。
  2. 不可使用-j4等并发方案,否则会出现IO异常
  3. INSTALL_MOD_PATH为modules需要安装的位置,此处我们选择root区挂载的位置即可。
安装image及dtb

针对64位的版本,分别需要如下文件:

arch/arm64/boot/dts/broadcom/*.dtb
arch/arm64/boot/dts/overlays/*.dtb* 
arch/arm64/boot/Image

32位的版本可参考官方文档Building the Kernel

我们将所生成的产物拷贝到树莓派SD卡上,覆盖原有文件,即:

  1. 拷贝arch/arm64/boot/Image到SD卡根目录下,重命名为kernel8.img,原有系统img建议重命名为kernel8_bk.img以防止新内核无法启动。
  2. 拷贝arch/arm64/boot/dts/broadcom/*.dtb到SD卡根目录下
  3. 拷贝arch/arm64/boot/dts/overlays/*.dtb* 到SD卡overlays目录下

以上过程需要注意的是:

  1. 也可参考使用scp等方式进行传输,直接在树莓派上修改/boot目录下的文件。但官方版本的sshd默认不打开root账号的登录权限,需要修改sshd配置。
  2. 官方文档中提到可以在SD卡根目录下config.txt指定启动使用的kernel image,见For 64-bit。但笔者尝试后发现树莓派无法正常启动。

将文件拷贝到SD卡相应位置后,我们启动树莓派,使用uname -a命令检查内核版本,可以发现树莓派已使用编译的内核进行启动,如下:

eric@yangpaopao:~$ uname -a
Linux yangpaopao 6.1.25-v8-yangpaopao+ #2 SMP PREEMPT Fri Apr 28 22:12:11 CST 2023 aarch64 GNU/Linux

使用源码默认配置的config文件,貌似协议栈有问题,不过不在本文的讨论范围:

eric@yangpaopao:~$ ping www.baidu.com
ping: socket: Address family not supported by protocol
ping: www.baidu.com: Temporary failure in name resolution

参考资料

留言

您的邮箱地址不会被公开。 必填项已用 * 标注