☰
Windows下打造arm64的deb包:交叉编译、QEMU模拟与架构差异全解析
2026/9/28 18:59:07 网站建设 项目流程

事情是这样的:我手里一个开源项目一直发布 x64 的 deb 安装包,结果用户提了个 issue,说希望支持 ARM 设备。我人又在 Windows 上日常办公,云上的 ARM 构建机不想多掏钱,于是就想就地想办法——在 Windows 上把 arm64 的 deb 包给打出来。当时我以为这事就是装个虚拟机、改个架构名、打个压缩包,结果一路踩过去,发现至少有三个地方和我想的完全不一样。

这篇文章把当时想错的三个核心点完整复盘一下。整个过程涉及 Windows 环境下的 arm64 交叉构建、qemu 用户态模拟、deb 包内部结构,以及 arm64 和 x64 的真正架构差异。适合和我一样“手里只有一台 Windows 日常设备,但需要给 ARM Linux 设备出安装包”的人参考。读完你至少能避开我踩过的三个大坑,少折腾几个晚上。

1. 第一个想错的地方:必须准备一台 arm64 构建机

1.1 我最初的天真方案

拿到需求之后,我的第一反应是:要打 arm64 的包,就得先有一台能跑的 arm64 环境。于是我开始盘算几条路线:

  • 买一台 ARM 开发板,比如树莓派或者各种国产 ARM 板卡;
  • 申请云厂商的 ARM 云服务器;
  • 在本机用虚拟机软件跑一个 ARM 版 Linux,比如 QEMU 全系统模拟。

这些方案听上去都“可行”,但实际用起来各有各的难受。ARM 板卡到手要等快递,刷系统、配环境、接网络,搞完基本半天就过去了。云上 ARM 服务器按小时计费,为了打个包专门开一台不划算,而且很多云平台的 ARM 实例还不一定有现成的镜像。至于在 Windows 上开 QEMU 全系统模拟 ARM Linux,那速度更是惨不忍睹——我实际试过一次,装个最小 Ubuntu 系统都要大半个小时,进去之后命令行敲命令都有明显延迟感。

当时我的逻辑是:既然最终产物是 arm64 的 deb,那构建环境也必须是 arm64,不然怎么保证出来的包是对的?说实话,这句话现在看也不算全错,但问题在于对“环境”二字的理解过于狭窄了。

1.2 交叉编译和 qemu-user 才是正解

真正让我改变想法的是一个很朴素的思路:arm64 的 deb 包本质上是什么?是一个压缩包,里面装着为 arm64 架构编译的二进制文件,再加上一些控制信息。关键不在于“构建机器是 arm64”,而在于“包内的二进制是 arm64”。

那么怎么在 x64 上生成 arm64 的二进制?两条路:

  • 交叉编译:用aarch64-linux-gnu-gcc这种跑在 x64 上、生成 arm64 指令的编译器,直接编译出 arm64 的机器码;
  • 用户态模拟:用qemu-aarch64配合内核的binfmt_misc,让 x64 的 Linux 内核看到一个 arm64 ELF 文件时,自动调用 qemu 去翻译执行它。

我最初把“构建 arm64 包”和“构建 arm64 环境”画了等号,这是第一个想错的地方。实际需要的只是:一套能编译 arm64 代码的工具链,加上一个能运行 arm64 程序的执行环境。工具链解决“生成”,执行环境解决“验证”。

qemu-user 和 qemu 全系统模拟是两回事。全系统模拟是模拟一整台机器,包括 CPU、内存、外设,慢得离谱。而qemu-aarch64这种用户态模拟器,只负责把 arm64 的 ELF 可执行文件拿到 x64 上翻译执行,系统调用直接交给宿主机的 Linux 内核处理。也就是说,它不模拟硬件,性能损耗主要在指令翻译那一层。

配合binfmt_misc,Linux 内核会在执行 arm64 ELF 文件时自动调用qemu-aarch64,这样你就能在 x64 的 Linux 环境里直接跑 arm64 的程序——包括构建工具链、编译脚本、测试程序。

1.3 在 Windows 上的实际落地组合

我在 Windows 上选的组合是:WSL2 + 交叉工具链 + qemu-user + dpkg 工具链。

WSL2 本身就是 Windows 下的 Linux 子系统,提供一个完整的 Linux 内核环境。和纯虚拟机相比,它启动快、集成好,Windows 文件系统和 Linux 文件系统可以互相访问,我日常编辑代码在 Windows 侧用 IDE,构建跑到 WSL2 里执行,非常顺手。

安装步骤大致是:

# 在 WSL2 的 Ubuntu 环境里 sudo apt update # 安装 arm64 交叉编译工具链 sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu # 安装 qemu-user 和 binfmt 支持 sudo apt install qemu-user-binfmt binfmt-support # 安装打包工具 sudo apt install dpkg-dev debhelper lintian

装完之后验证一下模拟器是否生效:

# 检查 binfmt 注册情况 cat /proc/sys/fs/binfmt_misc/status # 或者直接看 qemu-aarch64 是否可执行 which qemu-aarch64

然后随便写个 arm64 的 hello world 试试能不能跑:

aarch64-linux-gnu-gcc -static -o hello hello.c ./hello

如果能正常打印输出,说明 binfmt 已经把 arm64 的 ELF 自动交给 qemu 执行了。这一步成功,整个方案的地基就打好了。

注意:hello这里加了-static,是因为动态链接的 arm64 程序在 qemu-user 环境下还需要找到 arm64 的动态库。后续真的构建复杂项目时,要么把 arm64 的依赖也装进 x64 环境的/usr/aarch64-linux-gnu路径,要么用更成熟的构建工具链去处理。

这里我第一个“想错了”的根源是:我把构建和运行等同于“必须原生一致”。其实只要处理好工具链和模拟执行两层,完全可以在 Windows 上搞出 arm64 的包。

2. 第二个想错的地方:以为 deb 就是把文件压成一个包

2.1 我当时自己鼓捣的“盗版 deb”

一开始我想得特别简单:deb 包嘛,不就是把程序文件放到对应的目录,然后打个 tar 包吗?我甚至自己写了个脚本:

tar czf myapp-arm64.deb ./usr ./etc

然后拿到一台 arm64 机器上,想着sudo dpkg -i一下就能装上。结果自然是:dpkg 直接报错,提示“不是合法的 Debian 格式”之类的信息。

那时候我才认真去看了 deb 包的真实格式,发现自己对它的理解太粗了。deb 不是一个简单的 tar 包,而是一个ar 归档文件,里面有固定结构。

2.2 deb 包内部到底长什么样

一个标准的 deb 包,用 ar 打包,里面至少有三个成员:

成员作用
debian-binary记录格式版本号,就一行,内容是2.0
control.tar.xz包含控制信息,比如包名、版本、架构、依赖、维护者等
data.tar.xz包含实际要安装到系统里的文件,按照根目录路径存放

控制信息里最关键的文件是control,它长这样:

Package: myapp Version: 1.0.0 Architecture: arm64 Maintainer: Your Name <you@example.com> Installed-Size: 2048 Depends: libc6 (>= 2.17), libssl3 Section: utils Priority: optional Description: My app for ARM64 Linux

字段看起来不多,但每一个都有讲究。Architecture必须写对,写arm64才表示这包给 ARM 64 位系统用;Depends如果不填或填错,安装时要么不检查依赖,要么在目标机器上一跑就缺库;Installed-Size如果不写,有些依赖计算工具会抱怨。

control.tar.xz里除了control文件,还经常有md5sums(记录每个文件的校验和)、postinst/prerm这类维护脚本,用来在安装后或卸载前执行一些操作。这些脚本也必须保证是可执行的,且脚本里的解释器路径要是目标架构能访问的。

data.tar.xz里的文件路径是相对于根目录的:比如usr/bin/myapp安装后就在/usr/bin/myapp。

2.3 用 dpkg-deb 正经打一次包

搞清楚结构之后,我改用标准工具来打包。准备目录的方式很关键:先把要安装的文件放到一个“临时根目录”里,再让 dpkg-deb 把整个目录打包。

# 假设编译产出的二进制是 ./build/myapp mkdir -p pkg/usr/bin cp build/myapp pkg/usr/bin/myapp # 写控制信息 mkdir -p pkg/DEBIAN cat > pkg/DEBIAN/control <<'EOF' Package: myapp Version: 1.0.0 Architecture: arm64 Maintainer: Your Name <you@example.com> Description: My ARM64 app Depends: libc6 (>= 2.17) EOF # 用 dpkg-deb 生成 deb 包 dpkg-deb --build --root-owner-group pkg myapp-arm64.deb

这里--root-owner-group一定要加。不加的话,包里的文件所有权默认是你当前用户的 UID/GID,安装到别人系统上就会出现文件属主混乱的问题,有些程序会因为权限不对直接拒绝启动。

生成的 deb 包可以用下面命令检查:

# 查看控制信息 dpkg-deb --info myapp-arm64.deb # 查看包含的文件 dpkg-deb --contents myapp-arm64.deb

--info会显示 control 文件的内容,--contents会列出 data 里的完整文件清单。这两步检查能帮你确认包里的架构字段、依赖声明、文件路径是否符合预期。

我第二个想错的点就在这里:deb 不是“把文件打个包”,它是一个有着严格内部结构、带控制信息和安装脚本的软件分发格式。不按格式来,dpkg 连认都不认识。

3. 第三个想错的地方:以为改个架构标签就算“arm64 版”

3.1 安装确实成功了,但一运行就崩

交叉编译这段,我用aarch64-linux-gnu-gcc编译了一个处理视频的小工具,然后把 Architecture 字段改成arm64,打了个包。在 WSL2 里用dpkg -i模拟安装,过程很顺利,没有报错。我当时心里还挺得意,觉得“arm64 版本也就这样嘛”。

结果我用 qemu-user 到/usr/bin/myapp运行时,发现完全起不来。一查日志,发现问题根本不是出在“没打对包”,而是出在二进制本身对架构的适配上。

3.2 arm64 和 x64 的真正差异在哪里

先弄清楚file命令的输出:

$ file myapp myapp: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked

这是 arm64 的 ELF 没错了。但动态链接的二进制文件,运行时还需要找到对应的动态链接器。x64 的动态链接器路径是/lib64/ld-linux-x86-64.so.2,arm64 的则是/lib/ld-linux-aarch64.so.1。

交叉编译出来的可执行文件在构建机器上链接时,默认会找 arm64 的环境路径,也就是/usr/aarch64-linux-gnu/lib/ld-linux-aarch64.so.1这类位置。如果你的 WSL2 环境里没有装 arm64 的动态库,只把myapp这一个文件放进包里,装到别人的 arm64 系统上看起来可能没问题,但在你的验证环境里跑不起来——因为 qemu-user 找不到 arm64 版的libc.so.6。

再往深一层说,deb 包里的依赖声明也要跟着架构走。在 x64 上你可能依赖libssl3 (>= 3.0),但在 arm64 的 Debian/Ubuntu 系统里,这个库的包名多数相同,但是 Multi-Arch 属性和库文件路径不同。arm64 系统的库文件在/usr/lib/aarch64-linux-gnu/下面,x64 的在/usr/lib/x86_64-linux-gnu/。如果你在 control 文件里没有正确声明 Depends,或者没有处理好 multiarch 相关的路径,包能装上,程序一跑就“缺 so 文件”。

还有一类隐蔽的坑是脚本。如果 deb 包里的postinst脚本是 shell 写的,第一行如果是#!/bin/bash那问题不大,因为 arm64 Linux 系统里通常也有 bash。但如果你在维护脚本里调用了某个 x64 的二进制,那在 arm64 设备上装包时就会直接报“找不到文件”或“无法执行”。

3.3 qemu-user 验证环境的“假象”问题

在 x64 的 WSL2 里用 qemu-user 验证 arm64 程序,有一个需要特别警惕的假象:qemu-user 能跑通,不等于真机上能跑通。

qemu-user 是用户态模拟,它不模拟硬件、不模拟内核,某些 ioctl 调用、设备节点访问、特殊系统调用在 qemu-user 里是直接转发给宿主机 x64 内核的。这对文件操作、网络操作这类常规任务基本够用,但如果你要验证的程序有特殊的硬件相关逻辑、需要访问某些/sys或/proc下的设备信息,qemu-user 的结果可能和真机不一致。

所以我的验证策略变成三层:

  • 个人开发阶段:qemu-user 跑通基本逻辑;
  • 打包阶段:用lintian静态检查 deb 包的依赖、脚本、权限问题;
  • 发布前:如果手头没有真机,至少要在云端申请一台临时的 arm64 实例做一次干净的dpkg -i安装验证。

我当时就漏了最后一步,以为 qemu-user 跑通就万事大吉,结果在真机上发现了一个和/dev/video0设备路径相关的 bug,只能重新发版本。真是血泪教训。

第三个想错的点,就是把“交叉编译成功”理解成了“架构适配完成”。二进制架构只是最浅的一层,动态库路径、依赖声明、维护脚本、multiarch 标记,这些才是决定 arm64 包能不能真正落地的关键。

4. 完整流程复盘与避坑速查

4.1 从零到出包的完整命令流

把三个误区捋清楚之后,整个流程就变得很清晰了。我最终在 Windows 上打出 arm64 deb 包的标准操作如下:

第一步,初始化 WSL2 构建环境(如果已有可跳过):

sudo apt update sudo apt install -y build-essential dpkg-dev debhelper lintian \ gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ qemu-user-binfmt binfmt-support

第二步,把项目源码放进 WSL2 文件系统(直接在 Windows 文件上交叉编译容易遇到符号链接和权限问题,建议复制到 WSL2 内部路径)。

第三步,交叉编译项目。以 CMake 项目为例:

cmake -B build-arm64 \ -DCMAKE_TOOLCHAIN_FILE=/path/to/arm64.cmake \ -DCMAKE_C_COMPILER=/usr/bin/aarch64-linux-gnu-gcc \ -DCMAKE_CXX_COMPILER=/usr/bin/aarch64-linux-gnu-g++ cmake --build build-arm64

第四步,组装包目录并写控制文件:

rm -rf pkg && mkdir -p pkg/usr/bin pkg/usr/share/doc/myapp cp build-arm64/myapp pkg/usr/bin/ cp LICENSE pkg/usr/share/doc/myapp/copyright mkdir -p pkg/DEBIAN cat > pkg/DEBIAN/control <<'EOF' Package: myapp Version: 1.0.0 Architecture: arm64 Maintainer: Your Name <you@example.com> Description: My ARM64 app Depends: libc6 (>= 2.17) EOF

第五步,打包并检查:

dpkg-deb --build --root-owner-group pkg myapp-arm64.deb # 检查控制信息 dpkg-deb --info myapp-arm64.deb # 检查文件清单 dpkg-deb --contents myapp-arm64.deb # 用 lintian 做静态检查 lintian myapp-arm64.deb

第六步,在 WSL2 里尝试安装到模拟环境,然后跑一次:

sudo dpkg -i myapp-arm64.deb myapp --version

第七步(强烈建议):有预算或条件的话,在云上开一台临时 ARM 实例,做一次干净安装验证。没有条件的话,至少要确保和维护者沟通好“这是在你设备上验证过的包还是未验证的包”。

4.2 常见问题速查表

现象常见原因解决方案
dpkg -i提示 “package architecture (arm64) does not match system (amd64)”在 x64 系统里不带--force-architecture尝试安装用 qemu-user + binfmt 模拟执行即可,不要强制装
dpkg -i提示 “is not a Debian format”打出来的根本不是标准 deb用dpkg-deb --build,别自己 tar
包能装上,但运行时提示No such file or directory动态链接器缺失,或依赖的 arm64 动态库未安装在验证环境里装好 arm64 的 libc、libssl 等;用file和readelf -l查看动态链接器路径
包能装上,但postinst脚本执行失败脚本里有 x64 二进制调用,或脚本没加可执行权限检查脚本里依赖,chmod +x,并确认解释器是目标架构可用到的
lintian报 “missing-depends-line” 等control 文件字段不完整补齐Depends、Installed-Size等必要字段
Windows 文件系统和 WSL2 之间拷贝文件后权限丢失NTFS 挂载权限映射问题在 WSL2 内部路径操作,避免直接在/mnt/c/...下构建

还有一些实操细节值得单独拎出来说:

第一,交叉编译时尽量使用静态库,尤其是一些小的命令行工具。-static编译出来的 arm64 可执行文件在 qemu-user 里跑起来最省心,因为它不需要去匹配 arm64 的动态库。但静态编译也不是银弹,如果程序依赖的某个库没有提供静态版本,那该动态还是要动态,这时候就必须在验证环境里把 arm64 版的依赖库装齐。

第二,Depends字段不要乱写。依赖是 deb 包和系统软件仓库之间的契约,建议先在目标版本的系统上查一下库的包名前缀带不带:arm64,确认Multi-Arch兼容性。随便写libssl3有时候不够,还要写libssl3 (>= 3.0)这样的最低版本。

第三,用dpkg-deb --root-owner-group处理文件所有权。这个参数不仅解决 UID 混乱,还会把包内的维护脚本和可执行文件权限保持正确。

4.3 给后来者的一句话

如果非要把这三次踩坑浓缩成一句话,我会说:架构只是一种“编译目标”,而 deb 包是一个“分发契约”。编译目标决定了你的二进制能不能在那台机器上跑,分发契约决定了你的包能不能被安装、依赖是否正确、文件是否放在预期位置。

这次在 Windows 上打出 arm64 的 deb 包,最终流程本身不复杂,复杂的是过程中不断纠正自己对“交叉编译、deb 结构、架构差异”这三个概念的浅层理解。如果一开始就明白这三点,我至少能省下一整天的折腾时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询