☰
Windows下用WSL交叉编译arm64开发板deb包实战指南
2026/11/4 1:02:17 网站建设 项目流程

手里只有一台 Windows 笔记本,目标却是一块 arm64 的开发板,最终还要交出一个“像样点的安装包”——这个场景对做嵌入式、做边缘设备交付的人应该不陌生。我这次的操作路径是:在 WSL 里交叉编译出 arm64 的二进制,手动打成 deb 包,传到板子上,一行sudo dpkg -i完成安装。整条链路不依赖虚拟机图形界面,不需要在板子上装编译环境,做完之后想清理,把对应的 WSL 发行版删掉就结束了。

这篇东西适合两类人。第一类是经常和 RK3588、树莓派这类 arm64 开发板打交道的开发者,板子只是运行载体,真正的开发必须在 x86 主机上完成;第二类是主力机是 Windows,但要给 Linux 设备做安装包的程序员。我会把交叉编译工具链怎么选、deb 包的目录结构长什么样、control 字段到底要写哪些、板子上 df -i 常见失败怎么处理,一步步说清楚。你不需要先把 Debian 打包规范啃一遍,跟着这条链路走一遍,大概率就能复现。

1. 为什么选 WSL:把 Windows 笔记本变成 Linux 打包车间

1.1 为什么不直接用 Windows 工具或虚拟机

deb 包本质上是一个 ar 归档,里面塞了 control.tar 和 data.tar 两部分:前者放元数据,后者放实际文件。Windows 上当然有能解压 ar 的工具,但完整的打包、校验、安装语义都在 dpkg-deb 和 dpkg 这套 Linux 生态里。你手动拼一个 ar 文件很容易,可一旦涉及依赖声明、postinst 脚本、数据库状态记录这些真实使用环节,手搓就不现实了。所以多数人的第一条路都是先搞一个 Linux 环境。

虚拟机的问题是启动慢、占内存。哪怕你用 8GB 内存的笔记本,跑一个带桌面的 Ubuntu 虚拟机做编译也挺难受。WSL2 是微软自家实现的轻量虚拟机,没有完整桌面,更像一个“进程级”的 Linux 环境,启动只要几百毫秒,内存按需分配。对打 deb 包这件事来说,WSL2 里是完整的 dpkg 生态,够用且干净。

我见过有人为了打一个内部工具包,先去装了双系统,这属于杀鸡用牛刀。WSL 占用小、可复制、出问题直接重建发行版,比双系统和虚拟机都轻一个量级。

1.2 WSL1 和 WSL2 怎么选

如果你的系统支持 WSL2(Win10 1903+ 或 Win11),我建议直接用 WSL2。WSL2 是一个真正的 Linux 内核,对 dpkg、apt 这类依赖内核特性的工具兼容性更好。WSL1 是系统调用翻译层,很多场景能跑,但遇到文件权限、inotify、sysfs 这类底层调用,容易出一些说不清的怪毛病。单看打 deb 包这个操作,WSL1 也许也能干,但没必要留坑。

确定当前发行版是几代版本,一条命令:

wsl -l -v

如果 VERSION 列是 2 就没问题。只有一个发行版且是 1 的话,可以转换:

wsl --set-version <发行版名> 2

转换过程会花一点时间,转换完最好wsl --shutdown重启一下,让配置彻底生效。

1.3 装一个干净的 Ubuntu 编译环境:在线一行,离线走 import

新系统直接一行:

wsl --install

默认装的就是 WSL2。如果下载不稳定,可以加参数改成网页下载方式:

wsl --install --web-download wsl --update --web-download

这两个参数让安装器走 web 通道而不是商店通道,对网络环境不太稳定的场景更友好。碰到公司内网完全没法在线装的,离线思路是从官方或镜像站下载一份 rootfs tar.gz,然后导入:

wsl --import Ubuntu2204 C:\WSL\Ubuntu2204 D:\downloads\rootfs.tar.gz

注意wsl --import导入的发行版默认没有普通用户,第一次进入是 root,日常编译最好手动创建一个用户再继续。还有一个细节:别图省事把发行版装到 C 盘,wsl --import的存储位置尽量选容量大的盘,后面装交叉工具链、构建缓存都会占空间。

装完 Ubuntu 发行版后,我习惯先跑一轮更新:

sudo apt update sudo apt upgrade -y

网络慢的话就换软件源,这一步做完,后面装交叉编译器、dpkg-deb 这类工具就不会卡在依赖上。

2. 交叉编译工具链:装好、验证、查架构三件事一次讲完

2.1 C/C++:apt 一条龙装 arm64 交叉 GCC

deb 包里最常见的是 C/C++ 编译产物。在 Ubuntu 上装交叉工具链非常省事:

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

装完以后,可执行文件名都带aarch64-linux-gnu-前缀,例如:

  • aarch64-linux-gnu-gcc
  • aarch64-linux-gnu-g++
  • aarch64-linux-gnu-readelf
  • aarch64-linux-gnu-objdump

这里要先说清楚一个概念:交叉编译就是用 x86 的机器,生成 arm64 目标板子能“看懂”的机器码。就像你请了一位懂北京话发音规则的老师,让他在广州本地帮你把普通话稿子校对、录音成北京口音的版本,最后把录音文件带到北京直接放。工具链正常运行在 Windows 侧的 WSL 里,但产出的二进制只能在 arm64 设备上执行。

写个最小验证程序:

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

编译并检查文件类型:

aarch64-linux-gnu-gcc -o hello-arm hello-arm.c file hello-arm

看到类似ELF 64-bit LSB pie executable, ARM aarch64的输出,就说明交叉编译链路通了。这个文件在你的 Windows 上是跑不起来的,别去双击它,这是正常现象。

如果你用的是厂商 SDK,比如 RK、全志、NXP 系列,它们一般会在 SDK 的 prebuilts/gcc 目录下自带一套交叉工具链,命名通常是aarch64-none-linux-gnu-前缀。这类工具链和 apt 装的区别主要是 glibc 版本,使用时需要把 bin 目录加进 PATH:

export PATH=/opt/rk3588-sdk/prebuilts/gcc/bin:$PATH

厂商自带工具链的好处是和 SDK 里的内核头文件、库版本完全对齐,打 deb 时如果涉及内核相关模块,这时用自带链更稳妥。

2.2 Go:三个环境变量搞定,别忘了 GOARM

如果你的项目是 Go 写的,交叉编译最简单,因为 Go 工具链默认静态编译,不需要额外装交叉 GCC(除非你开了 CGO)。打包流程照旧,二进制提前编好:

GOOS=linux GOARCH=arm64 go build -trimpath -o mytool .

如果目标板是树莓派 3B+ 这类 ARMv7 的老设备,而且板子系统是 32 位用户态,那要这样编:

GOOS=linux GOARCH=arm GOARM=7 go build -o mytool .

注意区分:aarch64 对应 64 位 arm64,arm 对应 32 位 armhf。热词里那些“arm64 和 arm 交叉编译”的困惑大多来自这里。我见过同事明明板子是 32 位系统,编了个 arm64 的二进制传上去,执行时直接提示文件格式无法识别。板子系统是 32 位还是 64 位,先在板子上执行uname -m看清楚了再决定编译参数。

2.3 Rust:target 加 linker 两步走

Rust 项目交叉编译需要额外配一下 linker,它不像 Go 那样自己能完成整个链接过程。先加目标:

rustup target add aarch64-unknown-linux-gnu

然后在项目根目录的.cargo/config.toml里指定链接器:

[target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc"

之后构建:

cargo build --release --target aarch64-unknown-linux-gnu

产物在target/aarch64-unknown-linux-gnu/release/下。Rust 的很多 crate 底层会链接 C 库,所以前面那个gcc-aarch64-linux-gnu包也得装,它在这里就是给 Rust 提供交叉链接器。

2.4 用 dpkg-architecture 核实目标架构名

deb 包里的Architecture字段、编译器前缀、multiarch 库路径,经常是三个叫法,特别容易混。我一般会在打包前跑一次:

dpkg-architecture -a arm64

看到关键变量:

DEB_HOST_ARCH=arm64 DEB_HOST_ARCH_CPU=arm64 DEB_HOST_MULTIARCH=aarch64-linux-gnu

这一组变量可以看作一个对照表:deb 包里的 Architecture 写arm64,库文件走usr/lib/aarch64-linux-gnu/,编译器前缀是aarch64-linux-gnu-。以后你看到 aarch64,知道那是工具链的命名习惯;看到 arm64,知道这是 deb 和 dpkg 体系的架构字段;两者本质是同一个东西。amd64 同理,对应 x86_64 工具链。

如果你要打包给 32 位 ARM 板子,架构字段要写armhf,而不是armel。两个都是 32 位,但硬浮点和软浮点 ABI 不同。现在常见的开发板基本都是 armhf 或 arm64,armel 基本只在老设备上见了。

3. dpkg-deb 手工打包:目录结构、control 字段与校验文件

3.1 deb 包先拆开看,再照着建

很多人在用dpkg-deb -b之前,没见过 deb 包内部长什么样。我第一次打包时也有点懵,对着教程建目录,结果把 DEBIAN 目录放到了包内容的外面。理解 deb 包结构其实不难,它就两部分:

staging/ ├── DEBIAN/ │ ├── control │ ├── md5sums │ └── postinst └── usr/ ├── bin/ │ └── hello-arm └── lib/ └── myapp/ └── data.bin

DEBIAN目录放元数据和安装脚本,它本身不会安装到系统里。usr/、etc/、var/这些普通目录里的内容,安装时会按照相对路径覆盖到根文件系统的对应位置。比如usr/bin/hello-arm会安装到/usr/bin/hello-arm。

打包命令:

dpkg-deb --build --root-owner-group staging hello-arm_1.0.0_arm64.deb

--root-owner-group这个参数很关键。不加它,如果当前用户不是 root,打出来的包文件 owner 会是你当前用户 uid,安装到板子上时 dpkg 会警告“user root does not own files in archive”。虽然一般也能装,但会造成奇怪现象,尤其后续有程序要读文件权限时。加了它,归档内所有文件的 owner 强制成 root,这是 hand-build deb 包时最推荐的做法。

包名规范是包名_版本号_架构.deb。dpkg 不强求文件名,但按规范命名,scp 到板子上少看错,下次升级也方便按文件名区分。

3.2 control 字段逐条说明

staging/DEBIAN/control是最重要的文件。一个最小可用的例子:

Package: hello-arm Version: 1.0.0 Architecture: arm64 Maintainer: Your Name <you@example.com> Depends: libc6 (>= 2.17) Section: utils Priority: optional Description: A tiny hello command for arm64 boards

字段用途:

  • Package:包名,安装后dpkg -l里显示的名字,也是/var/lib/dpkg/status数据库里的索引键。
  • Version:版本号,dpkg 靠它比较升级和降级。
  • Architecture:最关键的一个字段,写错了dpkg -i会直接拒绝安装。
  • Maintainer:维护者信息。
  • Depends:运行依赖。如果你用了动态链接,必须把依赖的动态库对应包名写进来。查看二进制依赖最直接的办法是用交叉 readelf:
aarch64-linux-gnu-readelf -d hello-arm

看到 NEEDED 条目,去板子对应的发行版软件源里查这些库属于哪个包,再写进 Depends。

  • Section 和 Priority 是分类字段,写不写都不影响安装,但写了更规范。
  • Description 第一行是概要,后面可以缩进写长描述。

还有一个Installed-Size字段,手工dpkg-deb时不写也没关系,dpkg 安装时自己会计算。如果你用 dpkg-gencontrol 那套正规工具链,它会自动生成。

3.3 md5sums 与 postinst:什么时候必须手动管

md5sums 文件不是必须的,但很多仓库规范要求带上。生成方式很简单:

cd staging find usr -type f -exec md5sum {} \; > DEBIAN/md5sums

如果目录不止 usr,比如还有 opt、etc,就一并覆盖:

find usr opt etc -type f -exec md5sum {} \; > DEBIAN/md5sums

DEBIAN 目录还有一个易踩点:postinst 等控制脚本的权限必须是 755,control 文件保持 644。如果 postinst 写成 644,安装时 dpkg 执行配置脚本会提示权限不足,它不会默认帮你加执行位。

什么时候需要写 postinst?当你的包安装完还需要做“环境登记”动作时。比如要注册 update-alternatives、生成配置文件、把二进制软链到 /usr/bin。一个最简 postinst:

#!/bin/sh set -e ln -sf /usr/local/lib/myapp/runme /usr/bin/myapp exit 0

注意 postinst 同样要保证 Unix 换行、不带 BOM,否则语法可能在极端情况下出错。控制脚本里引用路径尽量用绝对路径,避免 PATH 环境差异导致的意外。

3.4 链接库检查:为什么静态链接是省心选项

嵌入式板子的运行环境千差万别,板子上的 glibc 版本可能比你编译机上的老很多,动态链接的二进制在 exec 阶段就可能挂掉。交叉编译产物在 x86 上没法直接跑,不能用常规的 ldd 分析,得用交叉 readelf:

aarch64-linux-gnu-readelf -d hello-arm

如果看到一串 NEEDED,比如libssl.so.3、libcrypto.so.3,而目标板系统偏老,板子上没有对应 so 文件,启动时就会报 “cannot open shared object file”。应对这种板子环境不确定的场景,我通常直接静态链接:

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

静态编译出来的可执行文件只依赖系统内核,基本不会因为库版本缺库翻车。Go 默认就是静态编译,Rust 如果 target 选aarch64-unknown-linux-gnu默认动态链接 glibc,想完全静态需要追加target-feature=+crt-static,或者改用 musl 目标。在交叉编译打 deb 这件事上,能静态就静态,省得在板子上补一堆运行库。

4. 板子上的一行 dpkg -i:成功安装与常见失败现场

4.1 从 Windows 传到板子的三种方式

包打好了,怎么传到板子上?按现场条件选:

  • 有网,同一局域网:scp hello-arm_1.0.0_arm64.deb user@board_ip:/tmp/
  • 板子上有 web 服务:把包放到 web 目录,板子侧 wget 拉取
  • 完全离线:U 盘拷包。插上 U 盘后看设备节点,挂载 exFAT 格式先确认内核支持,不行就换成 FAT32 的 U 盘,兼容性最好

Windows 侧想传文件也不用另开终端,直接在 WSL 里跑 scp 就行。WSL 能访问/mnt/c/下的 Windows 文件,把这边的文件路径指过去,和 Linux 环境操作没有差别。

4.2 dpkg -i 正常时到底做了什么

板子上执行安装:

sudo dpkg -i hello-arm_1.0.0_arm64.deb

正常成功输出大致是这样:

Selecting previously unselected package hello-arm. (Reading database ... 31425 files and directories currently installed.) Preparing to unpack hello-arm_1.0.0_arm64.deb ... Unpacking hello-arm (1.0.0) ... Setting up hello-arm (1.0.0) ...

dpkg 实际干的事分几步:解析归档里的 control.tar 和 data.tar,检查依赖与版本冲突;如果有 preinst,先运行 preinst;解压 data.tar 到根文件系统;运行 postinst;最后把包信息写进/var/lib/dpkg/status。这也是为什么同一台板子反复安装同一个 deb,依赖没有变化时,靠的就是这套数据库记录状态。

安装完成后,板上执行dpkg -l就能看到 hello-arm 的记录。卸载用:

sudo dpkg -r hello-arm

dpkg 卸载不会自动处理它引发的依赖,如果别的包依赖 hello-arm,卸载时还会提示。

4.3 安装失败第一现场:架构或依赖不匹配

最常见的两个报错,一个是架构写错:

dpkg: error processing package hello-arm (--install): package architecture (amd64) does not match system (arm64)

说明打包时 Architecture 写错了,或者拿错包。先确认板子架构:

dpkg --print-architecture

arm64 的板子大概率输出 arm64。再看 deb 包头:

dpkg-deb -I hello-arm_1.0.0_arm64.deb

对照一下 Architecture 字段,错误很明显。

另一个高频报错是依赖缺失:

dpkg: dependency problems prevent configuration of hello-arm: hello-arm depends on libssl3; however: Package libssl3 is not installed.

出现这报错不急着重新打包。先看板子上能不能直接补依赖:

sudo apt update sudo apt --fix-broken install

--fix-broken install(简称-f install)会读取/var/lib/dpkg/status,把当前因依赖缺失卡住的包一起解决,再把缺失依赖补齐,然后继续配置之前没配置完的包。很多时候一条命令就把安装流程走完了。

4.4 装完跑不起来的两个隐蔽原因

安装成功不等于程序能跑。我在实际交付中见过两次“板子上 dpkg -i 成功,但执行就报错”的情况。

第一次是报No such file or directory。这个报错极具迷惑性——文件明明在。其实不是文件缺失,而是动态链接解释器缺失。二进制是动态链接且解释器路径指向/lib/ld-linux-aarch64.so.1,板子上没有这个 so,内核在 exec 阶段就会报这条错误。解决办法就是前面说的静态链接,或者把板子上缺失的 ld 和对应 so 补齐。

第二次是文件权限。打包时 staging 里的usr/bin/hello如果权限是 644,打包后安装下去自然不可执行。检查一下:

ls -l /usr/bin/hello

如果是 644,回到 WSL 重新chmod +x,再重新打包上传。权限问题在 Windows 侧尤其容易踩,因为 Windows 文件系统没有 Unix 可执行位的概念,从 Windows 目录拷贝文件到 WSL 时,默认权限可能不理想。

5. 反复踩过的坑:换行符、/mnt/c、架构名和 qemu 那条岔路

5.1 在 Windows 里编辑 control 文件:CRLF 会导致什么

很多人习惯用 Windows 的记事本或 VS Code 直接改 staging 目录下的 control 文件,一保存就把换行符写成 CRLF。dpkg 解析 control 时对 CRLF 相当敏感,报错时会出现类似line 2: expected a colon的信息,或者某些字段根本识别不了。

检查一下:

file DEBIAN/control

输出里看到 CRLF 字样,就用 dos2unix 转掉:

sudo apt install dos2unix -y dos2unix DEBIAN/control DEBIAN/postinst

更稳妥的做法:项目目录从一开始就放在 WSL 内部,比如~/work/,不要放到/mnt/c/下编辑。Windows 侧编辑器确实方便,但文件一旦跨文件系统,换行符、权限、inode 三个坑会一起冒出来,到板子上再定位就费劲了。

5.2 /mnt/c 路径下的性能与权限问题

WSL2 访问 Windows 目录走的是 9P 协议,对小文件大量读写非常慢。有一次我在/mnt/c/workspace下交叉编译一个带 Qt 依赖的项目,编译时间比放到~/work慢了接近一倍。这不是心理作用,是文件系统 I/O 的差异。

权限问题更隐蔽。/mnt/c下创建的目录默认 owner 是 drvfs 映射的用户,dpkg-deb --build如果不加--root-owner-group,出来的包 owner 就是 1000 而不是 root。装上板子后 dpkg 会警告,虽然一般能装,但我不建议留着这种隐患。所以打包命令别忘了--root-owner-group,或者干脆全程在 WSL 原生文件系统里操作。

5.3 arm64、armhf、amd64:名字相近的架构

这个命名对照表在开头提过,实际操作中还是有人反复搞混,这里直接列出来:

术语含义
arm64deb/Architecture 字段里的 64 位 ARM,对应编译器前缀 aarch64
aarch64编译器与 ELF 工具链里的 64 位 ARM 叫法,就是 arm64
armhf32 位 ARM 硬浮点,对应编译器 arm-linux-gnueabihf
armel32 位软浮点,老设备才用,现在基本见不到
amd64deb 字段里的 64 位 x86,对应编译器 x86_64-linux-gnu

在 Windows 侧做开发,很多人会把 amd64 和 arm64 两个词搞混。其实记住:你的 Windows 是 amd64,目标板是 arm64,交叉编译器前缀是 aarch64-linux-gnu-,deb Architecture 写 arm64。三个叫法各归各的,对齐了就没有歧义。

5.4 别把 qemu 模拟和交叉编译混为一谈

网上不少教程会推荐 qemu-user + binfmt_misc,在 x86 上模拟 arm64 环境跑程序,甚至用 qemu 直接安装 arm64 的 deb 包。这确实能用于冒烟测试,但和本文的目标完全不同。我们要交付的是 arm64 原生 deb 包,不是让 x86 机器去运行它,也不是在 x86 上模拟出一个 arm64 的完整系统给用户。

qemu 模拟的典型用途是给交叉编译做 sysroot,比如通过qemu-aarch64-static配合 debootstrap 构建一个 arm64 的根文件系统,用来安装依赖库。那是构建 sysroot 或做兼容性验证的路线,不是打 deb 安装包的路线。交叉编译产出的东西本身就是目标板子能直接运行的二进制,在 x86 上跑不动不代表打包失败,反而说明架构方向对了。

按这套流程走完之后,我个人的习惯是再做两个轻量校验:在 Windows 侧用dpkg-deb -c看文件清单,确认没有把 staging 目录里的无用文件打进去;上传前再用dpkg-deb -I看一眼 Architecture 和 Depends。这两条命令加起来十秒钟,能拦掉大多数返工。交叉编译打 deb 这件事本身不复杂,复杂的是每一步的隐性前提——架构名、换行符、文件权限、动态链接器,它们单独拿出来都不难,但串在一起时,任何一个错位都会让板子上那一行 dpkg -i 多一个报错。希望这条链路能帮你在 Windows 上把 arm64 的 deb 包一次打顺。

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

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

立即咨询