Linux_01:交叉编译与开发环境实战——从单片机跨到 Linux 的第一道坎 📅 2026/8/22 9:48:46 适用人群已经能熟练点亮 STM32、调过几个传感器、看得懂寄存器但一打开嵌入式 Linux教程就懵的同学。教程上来就是交叉编译“根文件系统”“内核镜像”“设备树”每一个字你都认识连起来完全不知道在说什么。没看过本专栏其他篇也能读这是嵌入式Linux基础的第一篇从零讲交叉编译和目标板连接前置只需要你会用 Linux 命令行cd/ls/gcc/make。读完你能得到① 搞懂为什么从单片机到 Linux 像换了个世界——多出来的工具链、根文件系统、内核到底各自管什么② 交叉编译是什么、为什么必须交叉能看懂arm-linux-gnueabihf-gcc这种外星文三元组③ 在 Ubuntu 上装好 ARM 工具链把一个 hello.c 编出 ARM ELF用file/readelf看清楚它和主机 gcc 编出来的有什么不一样再用 QEMU 跑一下验证④ 怎么用串口/ssh/scp 连上开发板把代码传过去运行⑤ 从main 里直接写寄存器到用户态调 sysfs/i2c-dev/spidev的思维转换搞清用户态/内核态分层⑥ 我踩过的 N 个坑和几个故意试错的实验。一、为什么从单片机到 Linux 像换了个世界先说我的真实翻车现场。大三下我买了块树莓派 4B想着我都会 STM32 了Linux 不也是个嵌入式系统嘛应该差不多。结果开机进桌面后我打开终端搜怎么点亮 LED——教程说echo 1 /sys/class/gpio/gpioxx/value。我愣了寄存器呢时钟使能呢模式配置呢我写了一年的GPIOA-BSRR 15;到这里变成往一个文件里写个 1更崩溃的在后面。我想把之前在 STM32 上写的 SHT30 驱动搬过去编译时报错cannot execute binary file: Exec format error。我以为代码错了改了一晚上最后才发现——我用主机的 gcc 编了 ARM 板上要跑的程序根本不是同一套指令集。这还不是最难受的。最难受的是教程里那些词每一个我都得现查单片机世界 嵌入式 Linux 世界 ───────── ───────────────── 一份 .hex 烧进去 ① 内核镜像 zImage ② 设备树 dtb ③ 根文件系统 rootfs一堆文件 ④ bootloaderU-Boot main() 直接跑 init → shell → 你的程序 直接写寄存器 通过文件 / 设备节点访问 sysfs / /dev/xxx 程序在 Flash 里 程序在 rootfs 的 /usr/bin 之类 断电即停 有日志、有服务、有开机自启1.1 多出来的三层中间商┌─────────────────────────────────────┐ │ 你的应用hello.c │ ← 用户态 ├─────────────────────────────────────┤ │ C 库glibc/musl 系统调用层 │ ├─────────────────────────────────────┤ │ Linux 内核zImage │ ← 内核态 │ 驱动、调度、TCP/IP、VFS… │ ├─────────────────────────────────────┤ │ 设备树dtb描述硬件拓扑 │ ├─────────────────────────────────────┤ │ U-Bootbootloader引导内核 │ ├─────────────────────────────────────┤ │ ARM 硬件SoC │ └─────────────────────────────────────┘在单片机里你写的main几乎是裸跑在 CPU 上中间顶多隔一层 HAL 库。在 Linux 里你的main上面是 glibc下面隔了内核 驱动 设备树 bootloader四层。每一层都可能让你的程序跑不起来所以一上来懵是正常的——不是你菜是世界变复杂了。1.2 一句话定位本篇这篇不教你配内核、不教你写驱动那是后面几篇的事。这篇只解决最底层的一步怎么把在 PC 上写的 C 代码变成能在 ARM 板上跑的可执行文件然后把它送到板子上跑起来。把这个最基础的闭环跑通了后面的根文件系统、设备树、驱动才有一个能落脚的地基。二、交叉编译是什么 为什么必须交叉2.1 先回顾原生编译单片机时代你一直在干的事你在 STM32 上用 Keil 编译Keil 调的是 ARMCC 编译器。你的 PC 是 x86 的编译器跑在 x86 上但它产出的 .hex 是 ARM 指令——这其实就已经是交叉编译了只是 Keil 把这件事包装得让你没察觉。原生编译native x86 PC 上跑 gcc → 产出 x86 可执行文件 → 在 x86 PC 上跑 交叉编译cross x86 PC 上跑 arm-gcc → 产出 ARM 可执行文件 → 拷到 ARM 板上跑所以交叉编译对你不是新东西新的是在 Linux 下这件事你得自己显式做——自己装工具链、自己选编译器、自己处理 libc。Keil 帮你屏蔽掉的Linux 全要你自己面对。2.2 为什么 Linux 下必须显式交叉编译原因很简单PC 的性能、内存、磁盘远大于目标板。在树莓派上编一个大点的库比如 OpenSSL可能要半小时PC 上两分钟搞定。而且很多板子根文件系统就一两百 MB连编译器都装不下。所以工程上的标准做法是PC 上交叉编译 → 把产物拷到板上跑。2.3 host / build / target 三元组GCC 官方文档里描述一个工具链用三个角色角色含义本篇例子build编译器自己在哪跑x86_64 PCUbuntuhost编译产出的程序在哪跑ARM 板树莓派target这个工具链产出的代码给谁跑和 host 一致普通应用编译时 hosttarget对于编应用这种最常见的场景hosttarget所以工具链名里只体现一个目标。三元组的完整描述在你自己编 GCC 工具链时才严格区分平时apt install gcc-arm-linux-gnueabihf装的工具链buildx86_64、hosttargetarm-linux-gnueabihf。2.4 拆开arm-linux-gnueabihf-gcc这个外星文这个文件名不是乱起的是 GNU triple三元组的约定arch-vendor-os-abi其中 vendor 经常省略成unknown或直接不写。arm - linux - gnueabihf │ │ │ │ │ └─ ABIgnueabihf glibc EABI Hard Float硬浮点 │ └─ 操作系统linux跑 Linux 内核有系统调用 └─ 架构arm32 位 ARMAArch32几个常被搞混的兄弟照着 ABI 后缀对一下工具链前缀ABI浮点给谁用arm-none-eabi-bare-metal无 OS软浮点单片机Cortex-M跑裸机/RTOSarm-linux-gnueabi-Linux glibc软浮点旧 ARM Linux没硬浮点单元arm-linux-gnueabihf-Linux glibc硬浮点树莓派Cortex-A7/A53 等有 VFPaarch64-linux-gnu-Linux glibc64 位硬浮点树莓派 3B 64 位系统、RK3399⚠️血泪经验arm-linux-gnueabi没 hf和arm-linux-gnueabihf带 hf不能混用。如果目标板根文件系统里的 glibc 是 hf 版的你用没 hf 的工具链编出来的程序板上一跑就报Illegal instruction或者No such file or directory其实是动态链接器路径不对。树莓派 4B 默认系统就是 hf 版新手认准arm-linux-gnueabihf准没错。三、搭建交叉编译环境Ubuntu 上手把手3.1 装 ARM 工具链Ubuntu 官方源里有现成的交叉工具链一行命令搞定不需要去网上下 Linaro 之类sudoaptupdatesudoaptinstall-ygcc-arm-linux-gnueabihf# 顺带装 g编 C 用和 gdb调试用sudoaptinstall-yg-arm-linux-gnueabihf gdb-multiarch装完验证一下版本arm-linux-gnueabihf-gcc--version# arm-linux-gnueabihf-gcc (Ubuntu 11.x-...) 11.x.0# Copyright (C) 2021 Free Software Foundation, Inc.Ubuntu 这个包还会自动带上对应的 glibc 头文件和库放在/usr/arm-linux-gnueabihf/下这就是默认 sysroot后面会用到。3.2 写一个 hello.c// hello.c —— 就是个普通 hello world没什么花头#includestdio.hintmain(void){printf(Hello from ARM! sizeof(int)%zu\n,sizeof(int));return0;}3.3 用交叉编译器编它# 用主机 gcc 编一份对照用gcc hello.c-ohello_x86# 用交叉编译器编一份arm-linux-gnueabihf-gcc hello.c-ohello_arm两条命令都能成功没报错。这时候你已经踩到第一个隐形的坑了——如果你不知道有两个 gcc很可能你跑的是gcc编出来的是 x86 程序拷到板子上必报Exec format error。我们用file命令对比一下两个产物filehello_x86 hello_armhello_x86: ELF 64-bit LSB shared object, x86-64, ..., dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ... hello_arm: ELF 32-bit LSB pie executable, ARM, EABI5, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, ...看file输出里的两个关键信息架构x86-64 vs ARM、32-bit vs 64-bit。这告诉你这份 ELF 能在哪种 CPU 上跑。interpreter动态链接器/lib/ld-linux-armhf.so.3。这是程序启动时第一个被加载的加载器它负责把libc.so.6这些动态库加载进来。目标板必须有这个文件否则报No such file or directory——尽管你ls明明能看到 hello_arm 在那儿新手会怀疑自己疯了。3.4 用 readelf 进一步看依赖readelf比file更细能看到这个 ARM ELF 依赖哪些动态库arm-linux-gnueabihf-readelf-dhello_arm|grepNEEDED0x00000001 (NEEDED) Shared library: [libc.so.6]只依赖libc.so.6。再看看解释器动态链接器路径arm-linux-gnueabihf-readelf-lhello_arm|grepinterpreter# [Requesting program interpreter: /lib/ld-linux-armhf.so.3]这条信息很关键板上的/lib/ld-linux-armhf.so.3必须存在且必须和你编时的 ABI 一致hf 版。新手 80% 的程序跑不起来都卡在这两个东西之一上。3.5 在 PC 上用 QEMU 验证没有板子也能跑没板子也想验证编得对不对用 QEMU 的用户模式user-mode它能在 x86 上直接跑 ARM 的 ELF不需要整个系统镜像sudoaptinstall-yqemu-user-static# -L 指定 sysroot让 QEMU 知道去哪找 ld-linux 和 libcqemu-arm-static-L/usr/arm-linux-gnueabihf ./hello_arm# 输出Hello from ARM! sizeof(int)4跑通了说明交叉编译这一步没问题。QEMU 在这里干的事是截获 ARM 指令翻译成 x86 跑同时把 ARM 的系统调用翻译成 x86 的系统调用。它不是在模拟整个开发板只模拟用户态程序——对验证交叉编译结果已经够了。3.6 静态链接把依赖打包进去应急方案有时候你只是想把一个小工具扔到板上跑不想管 libc 版本对不对——那就静态链接把 glibc 也编进可执行文件里arm-linux-gnueabihf-gcc hello.c-ohello_arm_static-staticfilehello_arm_static# ELF 32-bit LSB executable, ARM, ..., statically linked, ...注意statically linked——这种程序不依赖板上的 libc体积大几百 KB 到 1MB但几乎能在任何 ARM Linux 上跑。新手调通环境前先用-static排除是不是 libc 不匹配这个问题是个很实用的技巧。四、开发板连接与登录代码编出来了怎么送到板子上跑这一节解决PC ↔ 板子的通路问题。我以树莓派 4B 为例其他 ARM 板流程几乎一样。4.1 三种连接方式速览方式 需要的硬件 优点 缺点 ───────── ────────────── ───── ───── 串口 console USB-TTL 杜邦线 不依赖网络/IP 需要接线、波特率受限 SSH 一根网线 / 同一 Wi-Fi 速度快、操作方便 需要先知道板子 IP scp/nfs 在 SSH 基础上 传文件方便 NFS 配置稍麻烦实战顺序我建议先用串口把板子点亮拿到 IP再切 SSH 干活传大文件用 scp。4.2 串口 console救命的最后一根线树莓派 4B 的 GPIO 14/15TXD/RXD默认是串口。把 USB-TTL 模块接上USB-TTL 树莓派 ───── ────── RXD ────────── TXD (GPIO14) TXD ────────── RXD (GPIO15) GND ────────── GND ⚠️ 不要接 VCC板子自己供电⚠️ 树莓派 4B 默认蓝牙占用了硬件串口ttyAMA0给串口 console 的是个软件 mini-UARTttyS0波特率会受 CPU 频率影响抖动。如果出现乱码用sudo raspi-config→ Interfacing Options → Serial → 关闭 shell、打开硬件再在/boot/config.txt加enable_uart1把硬件串口还给 console。这个坑踩过一次就记住了。PC 上用minicom或picocom连sudoaptinstall-ypicocomsudopicocom-b115200/dev/ttyUSB0# 退出按 Ctrl-A Ctrl-X上电后就能看到 U-Boot → 内核启动日志 → 最后出现登录提示符。串口 console 是你板子进不去系统时唯一的救命手段SSH 进不去的时候它永远在除非硬件坏了。4.3 SSH日常干活的主力板子能进系统后先在串口里查 IP# 板上执行ipaddr# 找 wlan0 或 eth0 的 inet 地址比如 192.168.1.108PC 上sshpi192.168.1.108# 默认密码raspberry树莓派 OS/ 或你自己设的第一次连会问Are you sure you want to continue connecting?输yes。之后用 SSH 密钥免密更省事# PC 上把公钥推到板子ssh-copy-id pi192.168.1.108# 之后再 ssh 就不用输密码了4.4 把编译产物传到板上scp / rsync最常用的就是scp一行命令把文件拷过去# PC 上执行把 hello_arm 传到板子的 ~/scphello_arm pi192.168.1.108:~/板上执行sshpi192.168.1.108chmodx hello_arm ./hello_arm# Hello from ARM! sizeof(int)4传一整个目录或者要增量同步用rsyncrsync-avz--exclude*.o./build/ pi192.168.1.108:~/build/# -a 归档模式保留权限/时间 -v 详细 -z 压缩大项目反复传每次 scp 全量传太慢。rsync只传变化的文件是开发循环里提速最明显的一个工具。4.5 NFS 共享让板子直接挂载PC 目录如果你改一版代码就 scp 一次几十次下来会疯。NFS 挂载让板子把 PC 上某个目录当本地目录用改完代码板上直接跑省掉传输环节。PC 端Ubuntusudoaptinstall-ynfs-kernel-server# 编辑 /etc/exports加一行# /home/you/share 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)sudoexportfs-rasudosystemctl restart nfs-kernel-server板上sudomount-tnfs192.168.1.100:/home/you/share /mnt/nfscd/mnt/nfs./hello_armNFS 配置有点繁琐新手第一周可以先用 scp等开发循环跑顺了再上 NFS不要一上来就折腾它。五、从裸金属到 Linux 应用的思维转换这一节不讲新命令讲一个思维方式的转弯——这个弯没转过来你写 Linux 应用永远别扭。5.1 单片机时代main 里直接写寄存器// STM32 上点灯你熟悉的写法#includestm32f4xx.hintmain(void){RCC-AHB1ENR|RCC_AHB1ENR_GPIOAEN;// 使能 GPIOA 时钟GPIOA-MODER|(110);// PA5 输出模式GPIOA-BSRR(15);// PA5 置高灯亮while(1){}}这套写法的前提是你的程序独占整个 CPU可以直接访问任何寄存器地址。这在单片机里成立因为只有一个程序在跑。5.2 Linux 时代用户态不能直接碰寄存器Linux 是个多任务操作系统用户程序不能直接读写物理寄存器——否则你一写 GPIO把别人的 GPIO 也改了系统就乱套了。所有硬件访问必须经过内核内核帮你检查权限、做调度。Linux 提供了几条用户态访问硬件的通道通道谁提供的适合什么场景怎么用sysfs内核 GPIO/PWM 框架简单 GPIO 控制、调试echo/cat读写/sys/class/.../dev/mem mmap内核临时调试、需要直接读写物理地址open(/dev/mem)mmap需要 root/dev/i2c-Ni2c-dev内核 I2C 子系统用户态驱动 I2C 传感器openioctl(I2C_SLAVE)read/write/dev/spidevN.Mspidev内核 SPI 子系统用户态驱动 SPI 设备openioctl(SPI_IOC_MESSAGE)内核驱动你自己写量产、性能要求高把驱动编进内核/模块用户态只调文件接口5.3 同样点个灯Linux 用户态怎么写最简单不用写 C——直接 shell 操作 sysfs# 假设 GPIO5 已被内核 export 出来echo5/sys/class/gpio/export# 申请这个 GPIOechoout/sys/class/gpio/gpio5/direction# 设为输出echo1/sys/class/gpio/gpio5/value# 输出高灯亮echo0/sys/class/gpio/gpio5/value# 输出低灯灭echo5/sys/class/gpio/unexport# 释放用 C 写也是同样的思路——打开文件、写值// led_on.c —— Linux 用户态点灯所有操作都是读写文件#includestdio.h#includestdlib.h#includefcntl.h#includeunistd.h#includestring.hintmain(void){intfdopen(/sys/class/gpio/gpio5/value,O_WRONLY);if(fd0){perror(open);return1;}write(fd,1,1);// 写 1 等价于 echo 1 valueclose(fd);return0;}对比一下两种点灯的路径深度单片机 Linux 用户态 main main │ │ write() 系统调用 ▼ ▼ 寄存器 内核 vfs_write() │ ▼ 内核 GPIO 驱动 gpiod │ ▼ 寄存器内核态才能访问核心转变从直接操作硬件变成和内核商量着来。你写的write()是一次系统调用CPU 会从用户态切到内核态内核检查完权限再去碰寄存器。慢一点但安全、可调度、可多进程共存。5.4 用户态 / 内核态分层为什么必须理解维度用户态你的应用内核态内核 驱动能访问的内存只有自己的进程空间整个物理内存能访问的寄存器❌ 不能✅ 能出错的影响进程崩溃整个系统崩溃panic切换代价一次系统调用 上下文切换这个分层决定了一件事在 Linux 下做嵌入式你大部分时间是在用户态写应用硬件访问通过内核提供的接口只有需要更高性能、或者要支持内核没现成驱动的外设才写内核驱动。新手最常见的两个误区以为 Linux 点个灯还要写驱动——不用sysfs 就够用调试阶段完全够用。以为不用学驱动了——量产或者对性能/中断有要求时sysfs 的延迟几十微秒级就不够了必须写驱动。本专栏后面会专门讲。六、新手必踩的 N 个坑#坑现象原因正确做法1用主机 gcc 编了 ARM 板要跑的程序板上执行报cannot execute binary file: Exec format error产出的是 x86 ELFARM 板看不懂用arm-linux-gnueabihf-gcc编完用file确认架构是 ARM2工具链 hf 和板子 libc 不匹配板上运行报Illegal instruction硬浮点指令在软浮点 libc 上跑不起来板子上是 gnueabi软浮点libc你却用 gnueabihf硬浮点编或反过来板上ls /lib/ld-linux-armhf.so.3——存在就是 hf 版工具链也必须用 hf3动态链接器路径在板上不存在ls明明看到文件执行却报No such file or directory缺/lib/ld-linux-armhf.so.3不是缺你那个程序板上ls /lib/ld-linux*检查调通前用-static先排除4scp 上去没执行权限./hello_arm报Permission deniedscp 不一定保留 x板上chmod x hello_arm再跑5板子时间和主机差好几年make反复全量重编SSL/TLS 全失败板子没装电池、NTP 没配开机回到 1970板上sudo timedatectl set-ntp true或sudo date -s 2026-08-05 12:00:006主机ldd看 ARM 程序依赖报not a dynamic executable或一堆unknown主机 ldd 只认识 x86 ELF用arm-linux-gnueabihf-readelf -d xxx | grep NEEDED看依赖7复制别人的工具链路径不对找不到libc.so、crt1.osysroot 没配arm-linux-gnueabihf-gcc -print-sysroot看默认路径或加--sysroot/path8ssh 改 hosts 文件 IP 变了连不上ssh: connect to host xxx port 22: No route to hostDHCP 重新分配了 IP板上设静态 IP或 PC 的~/.ssh/config写别名9QEMU 跑 ARM 程序报找不到 libcqemu-arm-static ./hello_arm报Failed to load /lib/ld-linux...没-L指定 sysrootqemu-arm-static -L /usr/arm-linux-gnueabihf ./hello_arm1064 位板上跑 32 位程序cannot execute binary file树莓派 3B 装了 64 位系统但用 32 位工具链编64 位系统用aarch64-linux-gnu-gcc或板上装 32 位兼容库七、动手练一练光看不练假把式。下面这几步我都标了预期现象和翻车了怎么救跟着敲一遍。练习 1故意用主机 gcc 编扔到板上看 Exec format error# PC 上gcc hello.c-ohello_wrongscphello_wrong pi板子IP:~/# 板上sshpi板子IPchmodx hello_wrong ./hello_wrong预期-bash: ./hello_wrong: cannot execute binary file: Exec format error为什么主机 gcc 编出来是 x86-64 ELFARM 板的 CPU 看不懂这套指令集。验证回到 PC 上file hello_wrong会看到x86-64。这就是第 6 节坑 1 的现场。练习 2交叉编译 QEMU 验证 传到板子真跑# PC 上arm-linux-gnueabihf-gcc hello.c-ohello_armfilehello_arm# 确认是 ARMqemu-arm-static-L/usr/arm-linux-gnueabihf ./hello_arm# 先 QEMU 验scphello_arm pi板子IP:~/sshpi板子IPchmod x hello_arm ./hello_arm预期三处都能打印Hello from ARM! sizeof(int)4。翻车点如果板上报No such file or directory去看/lib/ld-linux-armhf.so.3在不在坑 3。练习 3静态链接排除 libc 干扰arm-linux-gnueabihf-gcc hello.c-ohello_static-staticfilehello_static# 看到 statically linkedscphello_static pi板子IP:~/sshpi板子IP./hello_static预期跑通。如果静态版能跑、动态版不能跑那一定是板上 libc 版本和工具链不匹配坑 2。练习 4用 readelf 看依赖用 ldd 看为什么不行# 主机上注意主机 ldd 不认识 ARM 程序ldd hello_arm# not a dynamic executable ← 这不是 bug是 ldd 只认本机架构# 改用 readelf 看arm-linux-gnueabihf-readelf-dhello_arm|grepNEEDED# 0x00000001 (NEEDED) Shared library: [libc.so.6]# 看动态链接器路径arm-linux-gnueabihf-readelf-lhello_arm|grepinterpreter# [Requesting program interpreter: /lib/ld-linux-armhf.so.3]预期理解动态链接器 NEEDED 库这两个东西就是程序运行前必须就位的依赖清单。练习 5板上点灯sysfs# 板上执行GPIO 编号按你板子的原理图改。树莓派 4B 板载 LED 是电源指示灯不可编程控制# 这里用 GPIO17是树莓派 40-pin 排针上的物理第 11 脚外接 LED 限流电阻到 GNDecho17/sys/class/gpio/export# 申请 GPIO17echoout/sys/class/gpio/gpio17/direction# 设输出echo1/sys/class/gpio/gpio17/value# 亮sleep1echo0/sys/class/gpio/gpio17/value# 灭echo17/sys/class/gpio/unexport这个练习是第 5 节思维转换的现场版——没有一行 C没有一次寄存器操作灯就亮了。如果你还想用 C 写一遍参考 5.3 的led_on.c。小结从单片机跨到嵌入式 Linux第一道坎不是某个具体知识点而是整个开发模型的转变。这篇把最基础的闭环跑通了工具链PC 是 x86、板子是 ARM必须用arm-linux-gnueabihf-gcc交叉编译。三元组arch-os-abi不是装饰是 ABI 匹配的硬要求。产物验证file看架构、readelf -d看依赖、readelf -l看动态链接器没板子时用qemu-arm-static -L sysroot先验。连接通路串口 console 救命、SSH 干活、scp/rsync 传文件、NFS 共享省去传输。思维转变从main 直接写寄存器变成用户态通过 sysfs/i2c-dev/spidev 跟内核商量着来用户态/内核态分层是 Linux 嵌入式和单片机最根本的差异。静态链接是调通环境前的应急手段不是常态——但它能帮你把libc 不匹配这个变量先排除掉。一句话总结嵌入式 Linux 的开发环境 交叉工具链 目标板连接 用户态/内核态分层模型。这三样齐了你才有资格往根文件系统、设备树、驱动那一步走。写这篇时翻了 GCC、BusyBox / 内核文档和板子官方资料具体版本以你手上的为准。下一篇Linux_02_根文件系统与BusyBox从init到shell