☰
Windows下用WSL交叉编译arm64 deb包的完整实践
2026/11/4 10:59:44 网站建设 项目流程

1. 事情是怎么开始的:想在 Windows 上弄出一个 arm64 的 deb 包

先交代一下背景。前阵子我拿到一块 arm64 的嵌入式板子,跑的是 Debian 系系统,平时习惯直接 SSH 上去敲命令。板子本身性能不算差,但要在上面编译软件还是不太舒服,CPU 资源有限、内存吃紧,交叉编译工具链也未必齐全。而我的主力开发机是 Windows,日常编辑代码、调试逻辑都在这台机器上,让我把整套开发环境搬进板子终端里,实在没那个心情。

于是问题就来了:怎么在 Windows 上直接打出 arm64 架构的 deb 包,然后传到板子上一条dpkg -i装完就用?这条路我前后试了好几种方案,包括在虚拟机里跑 Linux、直接装双系统,最后最顺的还是 WSL——也就是 Windows Subsystem for Linux。这篇文章把我完整跑通的流程、每一步背后的原因,以及我在中间踩过的几个有代表性意义的坑,全部写出来,照着做基本就能复现。

写之前先说清楚这文章适合谁:主要在 Windows 上做开发、但目标平台是 arm64 Linux 板子的朋友;需要给板子出 .deb 安装包、但又不想反复在板子上手动编译的朋友;以及刚开始接触交叉编译、想知道整个链路怎么串起来的朋友。下面所有步骤我都按自己实际操作的顺序来,不会只给结论不给原理。

2. 为什么绕这么大一圈:板子上直接编译的痛点和 WSL 交叉编译的优势

2.1 板子本地编译的常见尴尬

很多嵌入式板子不是不能本地编译,而是编译体验太差。我一开始也图省事,直接在板子上跑gcc,几分钟能编译完的小工具倒无所谓,一旦涉及稍微大一点的项目,比如 Qt、MySQL 客户端、带一堆依赖库的服务程序,板子就可能陷入长时间高负载。另一个让人头疼的问题是板子的软件源往往是定制过的,包不新、版本参差,为了装编译依赖还得先折腾源列表,这过程本身就很容易把人劝退。

也有朋友会反问:那我在 Windows 上装个通用 Linux 虚拟机不就行了?虚拟机我确实试过,性能损耗倒还能忍,真正烦的是共享目录、网络配置、命令行体验这些细节。WSL 在这里面有个明显优势——它跟 Windows 文件系统的交互做得非常自然,你可以在 Windows 里用熟悉的编辑器写代码,然后在 WSL 里直接调用 Linux 工具链跑编译,二者共享磁盘的思路让整个流程少了很多来回搬运文件的动作。

2.2 交叉编译到底在解决什么问题

所谓交叉编译,简单说就是"在一个架构的机器上,编译出另一个架构机器能运行的程序"。放在我这个场景里,就是我在 x86_64 的 Windows 上,借助 WSL 里安装的 aarch64 工具链,生成 arm64 Linux 上能直接运行的可执行文件和库。

我习惯用一个生活化的类比来解释这件事:你请了一位只会用某种语言的翻译,让他把一份中文文件翻译成英文,他不需要跑到英国去才能干活,只要他同时懂中文和英文,在哪儿都能完成翻译。交叉编译器也一样,它运行在 x86_64 的机器上,但生成的机器码指令集是 ARM 的,这决定了最终产物没法在本地直接运行,但放到目标板子上就完全没问题。

2.3 什么时候其实不用交叉编译

当然我也不建议所有情况都无脑交叉编译。如果只是偶尔改个小脚本,或者项目纯 Python、纯 Shell,根本没有 C/C++ 编译环节,那直接在板子上跑都行,没必要引入工具链复杂性。又或者你的板子性能非常强劲——比如开发板套件配了不错的 ARM 核心,内存也够大,那本地编译反而是更简单的选择,因为依赖解析天然没问题。

我的经验是,适合交叉编译的场景通常具备这些特征:目标架构明确且固定、项目包含较重度的 C/C++ 代码、编译环境需要大量依赖、以及打包流程需要重复执行多次。当这几个条件同时满足时,像本文这样的方案就非常划算。

3. 环境准备:WSL 安装的细节、工具链选型和目标架构确认

3.1 WSL 安装要留心的几个点

WSL 的安装其实已经很简单,管理员身份的 PowerShell 里跑一句就能搞定:

wsl --install -d Ubuntu-22.04

如果安装很慢,可以加上--web-download参数,避免从 Microsoft Store 走。安装完进入 WSL,我强烈建议顺手确认一下用的是不是 WSL2,因为 WSL1 在系统调用兼容性上不如 WSL2,交叉编译有些动态链接或权限操作可能在 WSL1 里出幺蛾子。

wsl -l -v

如果看到版本是 1,可以用下面命令升级:

wsl --set-version Ubuntu-22.04 2

这里有个真正的经验点:不要把编译项目放在 /mnt/c/ 或 /mnt/d/ 这种 Windows 挂载目录下。WSL2 访问 Windows 文件系统的性能非常差,交叉编译涉及的 IO 操作频繁,放那里速度会让你怀疑人生。我在 WSL 的 Linux 文件系统里建了一个~/workspace/arm64目录,专门放交叉编译相关的内容,实测速度比放在 /mnt/c 下快好几倍。

3.2 交叉编译工具链到底选哪套

工具链的本质是一整套交叉工具的组合,至少包含编译器、汇编器、链接器、库和相关头文件。Debian/Ubuntu 生态里最方便的是从官方源直接装:

sudo apt update sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

装完以后,你会得到aarch64-linux-gnu-gcc、aarch64-linux-gnu-g++、aarch64-linux-gnu-objdump、aarch64-linux-gnu-readelf等一系列命令。这套官方源里的工具链以 glibc 为运行库,适合 Debian/Ubuntu 系的目标系统,我做 deb 包恰好就是这类系统,所以最合适。

如果你要面向特定的厂商板卡,人家往往也提供定制工具链,比如某些嵌入式厂商会直接把整套 SDK 打包好,里面的工具链针对特定内核版本做了优化。但那种情况下你通常得按厂商给的脚本配置环境变量,灵活性反而低一些。我个人的建议是:只要目标系统是 Debian/Ubuntu 系,优先用官方源工具链;只有涉及厂商私有库、特殊内核模块时,才去用厂商工具链。

3.3 用 multiarch 补齐交叉编译期间缺少的目标库

这是整个准备阶段最容易踩的坑。交叉编译不是只有编译器就够了,程序如果链接了第三方库,比如 openssl、zlib、libsqlite3,那宿主机的 x86_64 系统里装的这些库是不能被 aarch64 程序直接链接使用的——架构都不同,链接器不可能拿 x86_64 的 .so 给你 arm64 的目标用。

解决办法叫 multiarch。Debian/Ubuntu 支持在同一个系统里为多个架构安装软件包,打开 arm64 架构支持后,就可以安装 arm64 版本的库:

sudo dpkg --add-architecture arm64 sudo apt update sudo apt install libssl-dev:arm64 zlib1g-dev:arm64 libsqlite3-dev:arm64

装完之后,交叉编译时链接器能找到 /usr/aarch64-linux-gnu 路径下的目标库。这个目录是 Debian 多架构的标准布局,ls /usr/aarch64-linux-gnu会看到大量 arm64 专属的库和头文件。很多人的交叉编译卡在ld: cannot find -lssl这类错误上,十有八九就是没做这一步。

3.4 第一次编译前先确认目标架构和 glibc 版本

工具链装好了,别急着写项目。先检查两个最基本的指标:目标架构字符串和 glibc 版本。

aarch64-linux-gnu-gcc -dumpmachine # 输出类似 aarch64-linux-gnu aarch64-linux-gnu-gcc -print-file-name=libc.so.6 # 可以定位交叉运行库位置

同时也可以在板子上跑一下uname -m和ldd --version,确认板子的真实架构以及 glibc 版本。为什么 glibc 版本这么重要?因为交叉编译链接的 glibc 版本不能比板子上的更高,否则程序拷过去运行时会直接报version GLIBC_2.34 not found。官方源里工具链的 glibc 一般和 Ubuntu 的版本保持一致,如果板子上的系统比较旧或比较特殊,就要额外小心。

这一整套确认过程,我建议记录成一张小表:

项目检查命令预期结果
目标架构字符串aarch64-linux-gnu-gcc -dumpmachineaarch64-linux-gnu
板子实际架构uname -maarch64
目标库目录ls /usr/aarch64-linux-gnu/lib若干 .so 文件
glibc 兼容性对比工具链与板子的ldd --version工具链版本不高于板子版本

4. 真正动手交叉编译:从一个最小例子到 autotools 和 CMake 项目

4.1 最朴素的验证:三行 C 代码走通全链路

为了快速验证工具链和环境是否正常,我先写了一个最简单的hello-arm64.c:

#include <stdio.h> int main(void) { printf("hello from arm64 deb\n"); return 0; }

编译指令非常直接:

aarch64-linux-gnu-gcc -o hello-arm64 hello-arm64.c

要想确认产物是 arm64 的,用file命令看是最直观的:

file hello-arm64 # 输出类似:ELF 64-bit LSB executable, ARM aarch64...

很多新手会在这时候尝试在 WSL 里直接运行它,结果得到cannot execute binary file: Exec format error。这个报错就是最典型的"你交叉编译成功了,可它没法在本地跑"的信号。看到它不要慌张,反而应该确认:ELF 头里的 CPU 架构确实已经变成了 ARM aarch64。

4.2 autotools 项目的标准交叉编译姿势

真实项目里当然不会只有三行 C 代码。最常见的老牌 C/C++ 项目用的是 autotools 那套构建系统,也就是我们熟悉的./configure && make && make install。交叉编译时关键在 configure 这一步,要给足参数和环境变量:

./configure \ --host=aarch64-linux-gnu \ --build=x86_64-linux-gnu \ --prefix=/usr \ CC=aarch64-linux-gnu-gcc \ CXX=aarch64-linux-gnu-g++ \ PKG_CONFIG_PATH=/usr/aarch64-linux-gnu/lib/pkgconfig

逐个参数解释一下。--host告诉构建系统最终程序运行的平台是 aarch64-linux-gnu,这是交叉编译的开关;--build表示当前构建环境,如果没有编译器自己探测也可能出错,显式指定更稳;--prefix=/usr直接指定安装路径,这样将来 deb 包的目录结构比较干净;CC 和 CXX 指定交叉编译器,比在 PATH 里去找更明确;PKG_CONFIG_PATH 指向 arm64 pkg-config 文件的目录,否则 pkg-config 会找到宿主机 x86_64 的 .pc 文件,导致链接错误的库架构。

如果编译过程中发现某些库找不到,可以先查这个库是否存在 arm64 版本:

dpkg -l | grep libxxx | grep arm64 pkg-config --list-all | grep xxx

4.3 CMake 项目的交叉编译:toolchain 文件怎么写得顺手

现代项目里 CMake 的比重越来越高。CMake 交叉编译通常要写一个 toolchain 文件,这文件说白了就是告诉 CMake:编译器是谁、目标架构是什么、从哪儿找库和头文件。我常用的最小 toolchain 长这样:

set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++) set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

几个关键点值得展开说。CMAKE_FIND_ROOT_PATH指向目标系统的根目录,CMake 会优先在这个路径下查找库和头文件。MODE_PROGRAM NEVER表示查找可执行程序时不要用目标根目录,因为编译过程中偶尔会调用宿主机的小工具,这个设置能避免 CMake 在 aarch64 目录里找本地程序。而MODE_LIBRARY、MODE_INCLUDE、MODE_PACKAGE设为ONLY,意味着库、头文件、包配置全部只在目标根目录找,这样就杜绝了误用宿主机 x86_64 文件的可能。

使用的时候通过命令行传入:

cmake -B build-arm64 \ -DCMAKE_TOOLCHAIN_FILE=toolchain-arm64.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr

然后常规执行cmake --build build-arm64 -j$(nproc)和cmake --install build-arm64 --prefix ./staging就行了。我把编译产物安装到临时 staging 目录,后续打包 deb 时直接从这个目录取材。

4.4 编译库依赖时的顺序问题

如果你的项目需要自己编译某些依赖库,会构成一个依赖链条。这种情况下顺序就很重要,必须从最底层的库开始,逐级往上。比如你要编译一个依赖 libfoo 的程序,那得先交叉编译 libfoo,再把它的头文件和库安装到目标系统根目录里,然后程序项目才能找到它。

我通常的做法是给每个依赖库都设置同样的--prefix,也就是/usr/aarch64-linux-gnu,这样它们会自动安装到标准交叉搜索路径下,后一层的项目在链接时不需要额外指定路径。如果某个库不支持直接安装到这个目录,也可以手动把它的 include 和 lib 拷过去,只要路径对得上,链接器就能找到。这条经验在处理 OpenSSL、libffi、libevent 这类经典依赖时非常实用。

5. 把编译好的文件封装成标准 deb 包:目录结构、control 文件和打包命令

5.1 deb 包到底是什么

很多做嵌入式的人编译完程序,习惯直接把二进制拷贝到板子某个目录下,再把运行脚本配好,这当然能跑。但一旦涉及升级、卸载、依赖管理,手动方式就开始乱了。deb 包存在的意义就是把程序文件、元信息、安装/卸载脚本打成一个"有规矩"的文件夹,让系统级的包管理工具 dpkg/apt 能接管这一切。

deb 包的结构可以简单拆成两部分:一部分是常规的文件目录树,对应安装后要放到系统根目录下的内容;另一部分是名为DEBIAN的元信息目录,里面放着control文件、可选的维护脚本和校验文件。DEBIAN这个名字是固定的,而且不会被打进系统目录中去。

5.2 搭建打包目录树

这里我用一个假想的命令行工具mything做例子,它的编译产物是一个二进制文件/usr/bin/mything,另有一个配置文件/etc/mything/config.ini。我手动构建打包目录:

mything_0.1.0_arm64/ ├── DEBIAN/ │ └── control ├── etc/ │ └── mything/ │ └── config.ini └── usr/ └── bin/ └── mything

别小看这件事。很多第一次打 deb 的人会漏掉目录层级,直接根目录下放usr/bin,打出来的包装进系统后文件位置全错。搭建目录树的本质就是模拟安装后的根目录视图,usr/bin会被展开到/usr/bin,etc/mything会展开到/etc/mything。

5.3 control 文件每个字段的含义

DEBIAN/control是整个包的灵魂。我参考 Debian 官方的 Packaging 规范,结合自己的实际操作经验,经常用的最小集合是这样的:

Package: mything Version: 0.1.0 Section: utils Priority: optional Architecture: arm64 Depends: libc6 (>= 2.31) Maintainer: Your Name <you@example.com> Description: A small utility for arm64 boards This is a sample package built via cross compilation.

逐行解释。Package必须全小写且符合 Debian 包命名规则;Version格式建议符合 Debian 对版本排序的预期,形如0.1.0、1.2.3-1都可以;Architecture必须写arm64,这是整篇文章标题的核心所在——如果这里写错成 amd64,包倒是能装,但 dpkg 会直接报警说架构不匹配;Depends用来声明运行时依赖,比如依赖 glibc 的话写libc6,最稳妥的做法是包含某个最低版本;Description第一行是短描述,后面可以跟多行长描述,行首建议加一个空格,这是 Debian 格式的老规矩。

关于这几个字段的重要性,我给过一个段位排序:

字段作用写错什么样
Architecture决定能否在目标板安装写错架构直接拒绝安装
Package决定卸载和升级的目标名包名冲突时容易被覆盖
Version决定版本比较和升级逻辑版本写错可能无法正常升级
Depends决定依赖自动检测逻辑缺依赖时安装报错最集中的源头

5.4 用 dpkg-deb 把目录打成包

目录准备好了,打包命令其实特别简单:

dpkg-deb --build --root-owner-group mything_0.1.0_arm64

执行完就会在同级目录得到mything_0.1.0_arm64.deb。其中--root-owner-group很重要,它强制让包内所有文件的属主显示为 root:root,而不是当前 WSL 用户的 UID。否则装到板子上后,包里的文件可能带着一个不存在的 UID,虽然多数时候不影响运行,但遇到必须按权限判断的程序时会非常难受,比如某些 setuid 工具。

如果你希望包内包含文件校验信息,可以手动生成DEBIAN/md5sums。不过从我的经验看,现代 dpkg 已经不怎么强依赖于它,缺少 md5sums 并不会影响安装。为了控制篇幅,日常自用或内部发布的包我通常不额外生成。

5.5 发出之前先在本机检查这个 deb

在还没传到板子之前,别急着scp。先用几个命令自检一下:

dpkg-deb -I mything_0.1.0_arm64.deb # 查看元信息,确认 Architecture 和 Depends dpkg-deb -c mything_0.1.0_arm64.deb # 列出包内文件,确认路径正确 dpkg-deb -x mything_0.1.0_arm64.deb /tmp/verify # 解包到临时目录验证文件结构

如果发现文件名、架构或者路径不对,这时候重新打包成本是很低的,别等到板子上安装失败再回头查。

6. 上板安装:dpkg -i一行命令能顺滑运行的前提和常见拦路虎

6.1 把 deb 包传到板子上的几种方式

打包完成后,接下来就是把包搞到板子上。最简单的是 scp:

scp mything_0.1.0_arm64.deb user@192.168.x.x:/tmp/

如果板子和 Windows 之间不方便走通 SSH,也可以用 U 盘拷,或者搭个临时的 HTTP 服务在板子上wget下载。文件传过去后,在板子上进入/tmp目录,执行:

sudo dpkg -i mything_0.1.0_arm64.deb

这是一行命令没错,但它的前置条件是:包内的依赖在板子上已经满足,或者板子上的 apt 源可以自动补装依赖。基于这一点,我强烈建议在交叉编译时就确认好Depends字段里写的库,尽量选板子系统本来就会带的基础库。比如大部分 Debian 板子都会装 libc6,甚至系统里下意识就会依赖它,这样 dpkg -i 就能非常顺滑。

6.2 那些常见的 dpkg -i 报错与含义

实战里最常遇到几类报错,我列个表记录一下,都是我自己实际碰到过的:

报错表现根本原因解决动作
package architecture (arm64) does not match system (amd64)把 arm64 包装到 x86 机器上了去板子上装,别在 WSL 里装
dpkg: error processing package ... dependency problemsDepends 声明的库缺失或版本不满足apt -f install补依赖,或检查 Depends 是否写得过严格
cannot open ... No such file or directory包里文件路径不对,或依赖的动态库找不到用dpkg -L查看安装位置;用ldd检查动态库
version of ... is ... but ... is required依赖的库版本比声明低降低 Depends 里的版本要求,或在板子上升级对应库

这里有一条非常重要的实战原则:先在板子本地执行uname -m和ldd --version,再决定 Depends 里的版本下限。很多人在 x86 的工具链上照着 Ubuntu 最新版写 Depends,结果板子上的 glibc 版本旧一点,就卡在依赖不满足那一步。实际上你在交叉编译时所链接的 glibc 版本已经设了一个底线,只要 Depends 不小于这个底线就行,没必要写一个板子给不了的过高版本。

6.3 依赖缺失时如何补救

dpkg -i 如果因为依赖问题中断,有一个很粗暴但有效的补救:板子上执行

sudo apt -f install

这个命令会尝试修复系统里处于"半安装"状态的包,把缺失的依赖自动补上。但前提是板子的 apt 源能访问到对应包。如果板子在隔离网络里无法联网,那就要提前准备离线 deb 包,比如在宿主机上预先下载好依赖的 .deb,然后一起传过去。这种离线依赖处理起来确实烦琐,所以省事的方法还是在打包阶段就把 Depends 限制到板子系统自带的库范围内。

6.4 安装了怎么确认真的可用

安装完成后不要只看"装好了"三个字,我习惯做一轮简单验证,确认三个层面都正常。

查看包管理状态:

dpkg -s mything # 应该能看到 Status: install ok installed

确认文件落在预期位置:

dpkg -L mything # 列出所有安装的文件路径

直接运行并查看动态库依赖:

which mything mything ldd $(which mything)

ldd的输出会列出程序依赖的共享库,如果里面出现 "not found",说明包内声明的依赖和实际动态库检查不一致,这是最容易被忽略的问题。比如程序链接了 libfoo.so.2,但 Depends 里没写对应包,dpkg -i 可能照样装成功,运行起来才报错。所以我的习惯是:在打包前用readelf -d或ldd先列出动态依赖,再把这些依赖映射成 Debian 包名,最后写进 Depends。这个过程虽然花几分钟时间,但能省掉上板后反复试错的精力。

7. 几个让这个问题复杂化的特殊情况:Qt 项目、私有工具链和 Dropbear 类工具

7.1 Qt 项目的交叉编译思路

热词里出现了 Qt 交叉编译,这确实是 arm64 打包里很有代表性的一类场景。Qt 的问题在于它自己就是一个庞大的依赖树,而且 qmake/CMake 的交叉配置五花八门。我建议的思路是:先在 WSL 里用目标板系统的 Qt 交叉套件编译 Qt 自身,或者直接用厂商提供的 Qt SDK;然后让项目通过 toolchain 文件找到这些库。如果你发现自己被迫同时管理 Qt 的 mkspec、xcb 依赖和一堆平台插件,那就需要足够耐心分层解决。我个人的经验是,这类项目最好先打一个小 demo 跑通整个交叉编译加打包流程,再扩展到实际业务代码,否则一次引入太多变量会很难排查。

7.2 这类"服务型"工具怎么处理

像 Dropbear 这种 SSH 服务工具在板子上需求很大,很多人也会交叉编译它。我拿它做例子,因为它同时涉及 autotools 体系、可执行文件、systemd 服务配置。打包时你不仅要把二进制放进/usr/sbin/,往往还要管/etc/dropbear的配置目录和运行用户。这种情况下 control 文件里的Depends可能还要加 shadow 用户管理相关的包,打包目录树里也要加入DEBIAN/postinst脚本来创建用户或目录。这里没法展开太多,但可以记住一个原则:凡是一个安装过程中需要"额外动作"的事,都写到 postinst 脚本里。dpkg 会在安装时自动执行它。

7.3 厂商私有工具链怎么融入这套流程

如果你用的是厂商定制工具链,那前面说的官方源多架构方案就未必适用了。厂商工具链往往自带整套 sysroot,路径可能在某个 SDK 的sysroots/aarch64-linux-gnu/下。这种场景下只要把 CMake toolchain 的CMAKE_FIND_ROOT_PATH指向厂商 sysroot,把 autotools 的--prefix也指向那个 sysroot 即可。其余 deb 打包环节完全一致。我自己在实际工程里用过这种方式,只要目标系统的 glibc 和库版本与厂商 sysroot 兼容,整套流程跑起来和官方工具链没有本质差别。

8. 一句话总结我没写在前面的话

做到这里,整个链路就闭环了。从 Windows 上的 WSL 环境起步,装上aarch64-linux-gnu-gcc和 multiarch 库,交叉编译得到 arm64 产物,用dpkg-deb --build打成标准 deb 包,再传到板子上dpkg -i完成安装,整个过程没离开 Windows 开发机,板子只在最后执行安装命令时闪现了一下。

最后再分享一点个人体会。这套方案里最容易让新手原地打转的其实不是交叉编译本身,而是环境变量、路径和多架构库这三个基础环节。它们单独看都不难,但混在一起时非常容易出错。我的建议是第一次做的时候,严格按最简示例走一遍全流程,确认每个阶段都能顺利通过,再拿到真实项目上扩展。这样出了问题也容易定位,不至于把所有环节搅在一起排查。如果你后续想把这套流程再往前推一步,可以把整个打包动作写进一个 Makefile 或脚本,以后每次编译完自动出 deb,基本就实现了一键构建的效果。

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

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

立即咨询