在 HarmonyOS 上给 QEMU 搭一个最小 aarch64 Linux guest(内核 + busybox initramfs)
本文涉及到的全部工作以及本文的撰写全部由 AI 完成。
注:此前我们已经编译了 gcc-16 和 clang-22 以及 qemu ,相关记录在前面几篇文章里可以看到。事实证明事先准备好 gcc-16 和 clang-22 的确省了一些麻烦, ohos-sdk 提供的 clang-15 版本还是稍微低了点。
之前我们已经编译了 QEMU 。 QEMU 编出来之后,要在
qemu-system-aarch64的
-M virt上真正 boot 一个系统,需要一个 guest 内核 + 一个 initramfs。本文记录用本机工具链(gcc-16 / clang)从源码构建这两个东西,并最终引导到
busybox 的
sh提示符的全过程。绝大多数坑仍然是「OHOS musl 缺 glibc 遗留函数」和「gcc-16 的 specs 强注入 -fPIC」。
0. 结论先说
- guest 内核:
linux-6.12.12,用 brew clang 15 编(make CC=clang,见 §3) - initramfs:静态 busybox 1.36.1 ,用 gcc-16 编(
-static,见 §2) - 目标:
qemu-system-aarch64 -M virt -cpu cortex-a72 -nographic -kernel Image -initrd initramfs.cpio.gz - 三个关键坑:
- OHOS musl 缺一堆 glibc 遗留函数 → busybox 需要 compat 垫片(§2)
- gcc-16 的 specs 强注入
-fPIC,且排在-fno-pic之后 → 内核要用 clang 编(§3) - 内核 Makefile 里
CC = $(CROSS_COMPILE)gcc会覆盖环境变量 → 必须
make CC=clang(命令行传参)
1. 下载源码
bash
cd ~/ProjectSources
# busybox(约 2.5MB)
curl -o busybox-1.36.1.tar.bz2 https://busybox.net/downloads/busybox-1.36.1.tar.bz2
# Linux 内核(约 140MB)
curl -o linux-6.12.12.tar.xz https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.12.tar.xz
tar xjf busybox-1.36.1.tar.bz2
tar xf linux-6.12.12.tar.xz
2. 静态 busybox(gcc-16)+ OHOS musl 兼容垫片
busybox 用 gcc-16 编成静态二进制,但 OHOS musl 缺这些 glibc/utmpx 遗留函数:
gethostid / updwtmpx / setutxent / getutxent / getutxid / getutxline /
pututxline / endutxent / utmpxname / getusershell / endusershell /
ether_hostton / ether_ntohost / addmntent / sigisemptyset / sigandset /
sigorset / strverscmp
对策:写一个 busybox-compat.h(见附录),里面用 static inline 补齐这些函数
(utmp 系列直接置空、strverscmp 给一个自然版本比较实现),然后:
bash
cd ~/ProjectSources/busybox-1.36.1
export PATH="/storage/Users/currentUser/Installed/gcc-16/bin:/storage/Users/currentUser/.harmonybrew/opt/binutils/bin:$PATH"
make defconfig HOSTCC=clang # HOSTCC 必须命令行传,否则它硬编码 gcc
sed -i 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
sed -i 's/^CONFIG_HOSTID=y/# CONFIG_HOSTID is not set/' .config # hostid 用 gethostid,直接禁掉
make -j4 EXTRA_CFLAGS="-D__MUSL__ -include $PWD/../busybox-compat.h"
注意:
-include会作用到.S汇编文件,compat 头里必须用
#if !defined(__ASSEMBLER__) && !defined(__ASSEMBLY__)包住 C 代码。编出来的
busybox是静态 aarch64 ELF,但被 strip 掉了 HarmonyOS 签名(host上跑不了
Operation not permitted)------没关系 :它是要在 guest 里跑的,guest 是普通 Linux 内核,不校验 HarmonyOS 签名。
打包 initramfs
bash
cd ~/ProjectSources/guest
IR=initramfs; rm -rf "$IR"; mkdir -p "$IR"/{bin,sbin,etc,proc,sys,dev,tmp,root}
cp ../busybox-1.36.1/busybox "$IR/bin/busybox"
cd "$IR/bin"; for a in sh ls cat echo mount umount mkdir mknod ln sleep ps dmesg \
grep sed vi dd printf cp mv rm touch uname more clear; do ln -s busybox "$a"; done
cd ~/ProjectSources/guest
cat > "$IR/init" << 'EOF'
#!/bin/sh
mount -t proc proc /proc 2>/dev/null
mount -t sysfs sysfs /sys 2>/dev/null
mount -t devtmpfs devtmpfs /dev 2>/dev/null
mknod /dev/console c 5 1 2>/dev/null
echo "===== QEMU aarch64 guest booted! (busybox) ====="
echo "kernel: $(uname -srm)"
exec /bin/sh
EOF
chmod +x "$IR/init"
(cd "$IR" && find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz)
3. Linux 内核(brew clang 15)
坑 A:内核要 bc 生成 timeconst.h,OHOS 没装 bc
make 会跑 echo 250 | bc -q kernel/time/timeconst.bc。机器上没有 bc。
两个办法:brew install bc,或者(我这次用的)用 Python 等价重写 timeconst.bc
并做一个 bc 垫片放进 PATH(见附录的 gen_timeconst.py + bc 垫片)。
坑 B:gcc-16 的 specs 把 -fPIC 追加在 cc1_options 末尾
gcc-16 的 specs 里 *cc1_options: + ... -fPIC,这个 -fPIC 排在 %{f*} 之后,
所以任何命令行 -fno-pic 都会被它压过去。内核的 jump_label 用
asm goto( ... "i"(key) ...),"i" 约束要求链接期常量地址,PIC 下直接报
impossible constraint in 'asm'。结论:内核别用 gcc-16,换 brew clang 15。
坑 C:内核 Makefile 会覆盖环境里的 CC
Kbuild 顶层 CC = $(CROSS_COMPILE)gcc,export CC=clang 不生效。必须命令行传:
bash
cd ~/ProjectSources/linux-6.12.12
BUILD=/data/storage/el2/base/haps/entry/files/Homebrew/manual-builds/linux-build
mkdir -p "$BUILD"
export PATH="$HOME/ProjectSources/bc-shim:/storage/Users/currentUser/Installed/gcc-16/bin:/storage/Users/currentUser/.harmonybrew/opt/binutils/bin:$PATH"
make O="$BUILD" ARCH=arm64 defconfig
scripts/config --file "$BUILD/.config" --disable MODULES # 单体内核,不编模块
make O="$BUILD" ARCH=arm64 olddefconfig
make O="$BUILD" ARCH=arm64 CC=clang -j4 Image # CC 必须命令行传
产物:$BUILD/arch/arm64/boot/Image。
4. 引导
bash
cd ~/ProjectSources
BUILD=/data/storage/el2/base/haps/entry/files/Homebrew/manual-builds/linux-build
./qemu-11.1.1/build/qemu-system-aarch64 \
-M virt -cpu cortex-a72 -nographic \
-kernel "$BUILD/arch/arm64/boot/Image" \
-initrd guest/initramfs.cpio.gz \
-append "console=ttyAMA0"
预期:内核启动日志 → 走到 /init → 打印 banner → 进 busybox sh。
5. 踩坑速查
| 现象 | 原因 | 解法 |
|---|---|---|
busybox implicit declaration of gethostid/strverscmp/... |
OHOS musl 缺 glibc 遗留函数 | compat 垫片 -include busybox-compat.h |
-include 把 C 头喂进汇编报 unknown mnemonic |
-include 也作用于 .S |
compat 头加 __ASSEMBLER__ 保护 |
busybox 二进制 host 跑不了 Operation not permitted |
strip 掉了 HarmonyOS 签名 | 不用管(guest 里不校验签名) |
内核 bc: inaccessible or not found |
OHOS 没装 bc | brew install bc,或 Python 垫片 |
内核 impossible constraint in 'asm'(jump_label) |
gcc-16 specs 的 -fPIC 排在最后 |
内核改用 clang |
export CC=clang 不生效、还是 gcc 编 |
Kbuild CC = $(CROSS_COMPILE)gcc 覆盖环境 |
make ... CC=clang(命令行) |
附录:busybox-compat.h / gen_timeconst.py / bc 垫片
busybox-compat.h 内容:
c
/* busybox-on-OHOS-musl 兼容垫片:补齐 OHOS musl 缺失的 glibc/utmpx 遗留函数。
* 用法:EXTRA_CFLAGS="-D__MUSL__ -include /path/to/busybox-compat.h"
* 注意:-include 也会作用于 .S 汇编文件,所以这里必须做汇编保护。 */
#ifndef OHOS_BUSYBOX_COMPAT_H
#define OHOS_BUSYBOX_COMPAT_H
#if !defined(__ASSEMBLER__) && !defined(__ASSEMBLY__)
#include <ctype.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
struct utmpx;
struct ether_addr;
struct mntent;
/* ---- glibc 遗留函数 ---- */
static inline long gethostid(void)
{
return 0;
}
static inline void updwtmpx(const char *wtmp_file, const struct utmpx *ut)
{
(void)wtmp_file;
(void)ut;
}
/* ---- utmpx 全家(OHOS musl 只有 utmp 版本) ---- */
static inline void setutxent(void) {}
static inline void endutxent(void) {}
static inline void utmpxname(const char *file) { (void)file; }
static inline struct utmpx *getutxent(void) { return (struct utmpx *)0; }
static inline struct utmpx *getutxid(const struct utmpx *ut) { (void)ut; return (struct utmpx *)0; }
static inline struct utmpx *getutxline(const struct utmpx *ut) { (void)ut; return (struct utmpx *)0; }
static inline struct utmpx *pututxline(const struct utmpx *ut) { (void)ut; return (struct utmpx *)0; }
/* ---- getusershell/endusershell(su 用到) ---- */
static inline char *getusershell(void) { return (char *)0; }
static inline void endusershell(void) {}
/* ---- ether_hostton/ether_ntohost(ether-wake 用到) ---- */
static inline int ether_hostton(const char *hostname, struct ether_addr *addr)
{
(void)hostname;
(void)addr;
return -1;
}
static inline int ether_ntohost(char *hostname, const struct ether_addr *addr)
{
(void)hostname;
(void)addr;
return -1;
}
/* ---- addmntent(mount 写 mtab 用到) ---- */
static inline int addmntent(FILE *fp, const struct mntent *mnt)
{
(void)fp;
(void)mnt;
return 1;
}
/* ---- sigset 家族的 glibc 扩展(hush 等用到) ---- */
static inline int sigisemptyset(const sigset_t *set)
{
int sig;
for (sig = 1; sig <= NSIG; sig++) {
if (sigismember(set, sig))
return 0;
}
return 1;
}
static inline int sigandset(sigset_t *dest, const sigset_t *left, const sigset_t *right)
{
int sig;
sigemptyset(dest);
for (sig = 1; sig <= NSIG; sig++) {
if (sigismember(left, sig) && sigismember(right, sig))
sigaddset(dest, sig);
}
return 0;
}
static inline int sigorset(sigset_t *dest, const sigset_t *left, const sigset_t *right)
{
int sig;
sigemptyset(dest);
for (sig = 1; sig <= NSIG; sig++) {
if (sigismember(left, sig) || sigismember(right, sig))
sigaddset(dest, sig);
}
return 0;
}
/* ---- strverscmp(ls/sort 的 -V 版本排序用到) ---- */
static inline int strverscmp(const char *s1, const char *s2)
{
const char *p1, *p2;
size_t len1, len2;
int c;
while (*s1 && *s2) {
if (isdigit((unsigned char)*s1) && isdigit((unsigned char)*s2)) {
while (*s1 == '0') s1++;
while (*s2 == '0') s2++;
p1 = s1;
p2 = s2;
while (isdigit((unsigned char)*s1)) s1++;
while (isdigit((unsigned char)*s2)) s2++;
len1 = (size_t)(s1 - p1);
len2 = (size_t)(s2 - p2);
if (len1 != len2)
return len1 < len2 ? -1 : 1;
c = strncmp(p1, p2, len1);
if (c != 0)
return c;
continue;
}
if ((unsigned char)*s1 != (unsigned char)*s2)
return (unsigned char)*s1 - (unsigned char)*s2;
s1++;
s2++;
}
return (unsigned char)*s1 - (unsigned char)*s2;
}
#endif /* !__ASSEMBLER__ */
#endif /* OHOS_BUSYBOX_COMPAT_H */
bc-shim 下有 bc 和 gen_timeconst.py ,其中 bc 需要执行权限。
bc 内容:
sh
#!/bin/sh
# bc 垫片:内核构建只会在生成 timeconst.h 时调用 bc,直接走 Python 版。
exec python3 /storage/Users/currentUser/ProjectSources/bc-shim/gen_timeconst.py
gen_timeconst.py 内容:
python
#!/usr/bin/env python3
# 等价重写内核 kernel/time/timeconst.bc(用 Python 大整数),
# 供 bc 垫片调用,生成 include/generated/timeconst.h。
import sys, math
def gcd(a, b):
return math.gcd(a, b)
def fmul(b, n, d):
return (2**b * n + d - 1) // d
def fadj(b, n, d):
d = d // gcd(n, d)
return 2**b * (d - 1) // d
def fmuls(b, n, d):
s = 0
while True:
if fmul(s, n, d) >= 2**(b-1):
return s
s += 1
def hx(v):
return format(v, 'x')
def main():
data = sys.stdin.read().strip()
hz = int(data)
o = []
o.append("/* Automatically generated by kernel/time/timeconst.bc */")
o.append(f"/* Time conversion constants for HZ == {hz} */")
o.append("")
o.append("#ifndef KERNEL_TIMECONST_H")
o.append("#define KERNEL_TIMECONST_H")
o.append("")
o.append("#include <linux/param.h>")
o.append("#include <linux/types.h>")
o.append("")
o.append(f"#if HZ != {hz}")
o.append('#error "include/generated/timeconst.h has the wrong HZ value!"')
o.append("#endif")
o.append("")
if hz < 2:
o.append("#error Totally bogus HZ value!")
else:
s = fmuls(32, 1000, hz)
o.append(f"#define HZ_TO_MSEC_MUL32\tU64_C(0x{hx(fmul(s,1000,hz))})")
o.append(f"#define HZ_TO_MSEC_ADJ32\tU64_C(0x{hx(fadj(s,1000,hz))})")
o.append(f"#define HZ_TO_MSEC_SHR32\t{s}")
s = fmuls(32, hz, 1000)
o.append(f"#define MSEC_TO_HZ_MUL32\tU64_C(0x{hx(fmul(s,hz,1000))})")
o.append(f"#define MSEC_TO_HZ_ADJ32\tU64_C(0x{hx(fadj(s,hz,1000))})")
o.append(f"#define MSEC_TO_HZ_SHR32\t{s}")
cd = gcd(hz, 1000)
o.append(f"#define HZ_TO_MSEC_NUM\t\t{1000//cd}")
o.append(f"#define HZ_TO_MSEC_DEN\t\t{hz//cd}")
o.append(f"#define MSEC_TO_HZ_NUM\t\t{hz//cd}")
o.append(f"#define MSEC_TO_HZ_DEN\t\t{1000//cd}")
o.append("")
s = fmuls(32, 1000000, hz)
o.append(f"#define HZ_TO_USEC_MUL32\tU64_C(0x{hx(fmul(s,1000000,hz))})")
o.append(f"#define HZ_TO_USEC_ADJ32\tU64_C(0x{hx(fadj(s,1000000,hz))})")
o.append(f"#define HZ_TO_USEC_SHR32\t{s}")
s = fmuls(32, hz, 1000000)
o.append(f"#define USEC_TO_HZ_MUL32\tU64_C(0x{hx(fmul(s,hz,1000000))})")
o.append(f"#define USEC_TO_HZ_ADJ32\tU64_C(0x{hx(fadj(s,hz,1000000))})")
o.append(f"#define USEC_TO_HZ_SHR32\t{s}")
cd = gcd(hz, 1000000)
o.append(f"#define HZ_TO_USEC_NUM\t\t{1000000//cd}")
o.append(f"#define HZ_TO_USEC_DEN\t\t{hz//cd}")
o.append(f"#define USEC_TO_HZ_NUM\t\t{hz//cd}")
o.append(f"#define USEC_TO_HZ_DEN\t\t{1000000//cd}")
cd = gcd(hz, 1000000000)
o.append(f"#define HZ_TO_NSEC_NUM\t\t{1000000000//cd}")
o.append(f"#define HZ_TO_NSEC_DEN\t\t{hz//cd}")
o.append(f"#define NSEC_TO_HZ_NUM\t\t{hz//cd}")
o.append(f"#define NSEC_TO_HZ_DEN\t\t{1000000000//cd}")
o.append("")
o.append("#endif /* KERNEL_TIMECONST_H */")
sys.stdout.write("\n".join(o) + "\n")
main()