适用人群:已经能熟练点亮 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 = 1<<5;,到这里变成"往一个文件里写个 1"?
更崩溃的在后面。我想把之前在 STM32 上写的 SHT30 驱动搬过去,编译时报错 cannot execute binary file: Exec format error。我以为代码错了,改了一晚上,最后才发现------我用主机的 gcc 编了 ARM 板上要跑的程序,根本不是同一套指令集。
这还不是最难受的。最难受的是教程里那些词,每一个我都得现查:
text
单片机世界 嵌入式 Linux 世界
───────── ─────────────────
一份 .hex 烧进去 ① 内核镜像 zImage
② 设备树 dtb
③ 根文件系统 rootfs(一堆文件)
④ bootloader(U-Boot)
main() 直接跑 init → shell → 你的程序
直接写寄存器 通过文件 / 设备节点访问
(sysfs / /dev/xxx)
程序在 Flash 里 程序在 rootfs 的 /usr/bin 之类
断电即停 有日志、有服务、有开机自启
1.1 多出来的三层"中间商"
text
┌─────────────────────────────────────┐
│ 你的应用(hello.c) │ ← 用户态
├─────────────────────────────────────┤
│ C 库(glibc/musl)+ 系统调用层 │
├─────────────────────────────────────┤
│ Linux 内核(zImage) │ ← 内核态
│ (驱动、调度、TCP/IP、VFS...) │
├─────────────────────────────────────┤
│ 设备树(dtb)描述硬件拓扑 │
├─────────────────────────────────────┤
│ U-Boot(bootloader,引导内核) │
├─────────────────────────────────────┤
│ 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 把这件事包装得让你没察觉。
text
原生编译(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 PC(Ubuntu) |
| host | 编译产出的程序在哪跑 | ARM 板(树莓派) |
| target | 这个工具链产出的代码给谁跑 | 和 host 一致(普通应用编译时 host=target) |
对于编应用这种最常见的场景,host==target,所以工具链名里只体现一个目标。三元组的完整描述在你自己编 GCC 工具链 时才严格区分,平时
apt install gcc-arm-linux-gnueabihf装的工具链,build=x86_64、host=target=arm-linux-gnueabihf。
2.4 拆开 arm-linux-gnueabihf-gcc 这个外星文
这个文件名不是乱起的,是 GNU triple(三元组)的约定:<arch>-<vendor>-<os>-<abi>,其中 vendor 经常省略成 unknown 或直接不写。
text
arm - linux - gnueabihf
│ │ │
│ │ └─ ABI:gnueabihf = glibc + EABI + Hard Float(硬浮点)
│ └─ 操作系统:linux(跑 Linux 内核,有系统调用)
└─ 架构:arm(32 位 ARM,AArch32)
几个常被搞混的兄弟,照着 ABI 后缀对一下:
| 工具链前缀 | ABI | 浮点 | 给谁用 |
|---|---|---|---|
arm-none-eabi- |
bare-metal,无 OS | 软浮点 | 单片机(Cortex-M,跑裸机/RTOS) |
arm-linux-gnueabi- |
Linux + glibc | 软浮点 | 旧 ARM Linux,没硬浮点单元 |
arm-linux-gnueabihf- |
Linux + glibc | 硬浮点 | 树莓派(Cortex-A7/A53 等,有 VFP) |
aarch64-linux-gnu- |
Linux + glibc | 64 位硬浮点 | 树莓派 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 之类):
bash
sudo apt update
sudo apt install -y gcc-arm-linux-gnueabihf
# 顺带装 g++(编 C++ 用)和 gdb(调试用)
sudo apt install -y g++-arm-linux-gnueabihf gdb-multiarch
装完验证一下版本:
bash
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
c
// hello.c ------ 就是个普通 hello world,没什么花头
#include <stdio.h>
int main(void)
{
printf("Hello from ARM! sizeof(int)=%zu\n", sizeof(int));
return 0;
}
3.3 用交叉编译器编它
bash
# 用主机 gcc 编一份(对照用)
gcc hello.c -o hello_x86
# 用交叉编译器编一份
arm-linux-gnueabihf-gcc hello.c -o hello_arm
两条命令都能成功,没报错。这时候你已经踩到第一个隐形的坑了 ------如果你不知道有两个 gcc,很可能你跑的是 gcc,编出来的是 x86 程序,拷到板子上必报 Exec format error。我们用 file 命令对比一下两个产物:
bash
file hello_x86 hello_arm
text
hello_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 依赖哪些动态库:
bash
arm-linux-gnueabihf-readelf -d hello_arm | grep NEEDED
text
0x00000001 (NEEDED) Shared library: [libc.so.6]
只依赖 libc.so.6。再看看解释器(动态链接器)路径:
bash
arm-linux-gnueabihf-readelf -l hello_arm | grep interpreter
# [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,不需要整个系统镜像:
bash
sudo apt install -y qemu-user-static
# -L 指定 sysroot(让 QEMU 知道去哪找 ld-linux 和 libc)
qemu-arm-static -L /usr/arm-linux-gnueabihf ./hello_arm
# 输出:Hello from ARM! sizeof(int)=4
跑通了说明交叉编译这一步没问题。QEMU 在这里干的事是:截获 ARM 指令,翻译成 x86 跑;同时把 ARM 的系统调用翻译成 x86 的系统调用。它不是在模拟整个开发板,只模拟"用户态程序"------对验证交叉编译结果已经够了。
3.6 静态链接:把依赖打包进去(应急方案)
有时候你只是想把一个小工具扔到板上跑,不想管 libc 版本对不对------那就静态链接,把 glibc 也编进可执行文件里:
bash
arm-linux-gnueabihf-gcc hello.c -o hello_arm_static -static
file hello_arm_static
# ELF 32-bit LSB executable, ARM, ..., statically linked, ...
注意 statically linked------这种程序不依赖板上的 libc,体积大(几百 KB 到 1MB),但几乎能在任何 ARM Linux 上跑 。新手调通环境前,先用 -static 排除"是不是 libc 不匹配"这个问题,是个很实用的技巧。
四、开发板连接与登录
代码编出来了,怎么送到板子上跑?这一节解决"PC ↔ 板子"的通路问题。我以树莓派 4B 为例(其他 ARM 板流程几乎一样)。
4.1 三种连接方式速览
text
方式 需要的硬件 优点 缺点
───────── ────────────── ───── ─────
串口 console USB-TTL + 杜邦线 不依赖网络/IP 需要接线、波特率受限
SSH 一根网线 / 同一 Wi-Fi 速度快、操作方便 需要先知道板子 IP
scp/nfs 在 SSH 基础上 传文件方便 NFS 配置稍麻烦
实战顺序我建议:先用串口把板子点亮拿到 IP,再切 SSH 干活,传大文件用 scp。
4.2 串口 console:救命的最后一根线
树莓派 4B 的 GPIO 14/15(TXD/RXD)默认是串口。把 USB-TTL 模块接上:
text
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_uart=1,把硬件串口还给 console。这个坑踩过一次就记住了。
PC 上用 minicom 或 picocom 连:
bash
sudo apt install -y picocom
sudo picocom -b 115200 /dev/ttyUSB0
# 退出按 Ctrl-A Ctrl-X
上电后就能看到 U-Boot → 内核启动日志 → 最后出现登录提示符。串口 console 是你板子进不去系统时唯一的救命手段,SSH 进不去的时候它永远在(除非硬件坏了)。
4.3 SSH:日常干活的主力
板子能进系统后,先在串口里查 IP:
bash
# 板上执行
ip addr # 找 wlan0 或 eth0 的 inet 地址,比如 192.168.1.108
PC 上:
bash
ssh pi@192.168.1.108
# 默认密码:raspberry(树莓派 OS)/ 或你自己设的
第一次连会问 Are you sure you want to continue connecting?,输 yes。之后用 SSH 密钥免密更省事:
bash
# PC 上把公钥推到板子
ssh-copy-id pi@192.168.1.108
# 之后再 ssh 就不用输密码了
4.4 把编译产物传到板上:scp / rsync
最常用的就是 scp,一行命令把文件拷过去:
bash
# PC 上执行,把 hello_arm 传到板子的 ~/
scp hello_arm pi@192.168.1.108:~/
板上执行:
bash
ssh pi@192.168.1.108
chmod +x hello_arm
./hello_arm
# Hello from ARM! sizeof(int)=4
传一整个目录或者要增量同步,用 rsync:
bash
rsync -avz --exclude='*.o' ./build/ pi@192.168.1.108:~/build/
# -a 归档模式(保留权限/时间) -v 详细 -z 压缩
大项目反复传,每次 scp 全量传太慢。
rsync只传变化的文件,是开发循环里提速最明显的一个工具。
4.5 NFS 共享:让板子直接"挂载"PC 目录
如果你改一版代码就 scp 一次,几十次下来会疯。NFS 挂载让板子把 PC 上某个目录当本地目录用,改完代码板上直接跑,省掉传输环节。
PC 端(Ubuntu):
bash
sudo apt install -y nfs-kernel-server
# 编辑 /etc/exports,加一行
# /home/you/share 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
sudo exportfs -ra
sudo systemctl restart nfs-kernel-server
板上:
bash
sudo mount -t nfs 192.168.1.100:/home/you/share /mnt/nfs
cd /mnt/nfs && ./hello_arm
NFS 配置有点繁琐,新手第一周可以先用 scp,等开发循环跑顺了再上 NFS,不要一上来就折腾它。
五、从裸金属到 Linux 应用的思维转换
这一节不讲新命令,讲一个思维方式的转弯------这个弯没转过来,你写 Linux 应用永远别扭。
5.1 单片机时代:main 里直接写寄存器
c
// STM32 上点灯,你熟悉的写法
#include "stm32f4xx.h"
int main(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // 使能 GPIOA 时钟
GPIOA->MODER |= (1 << 10); // PA5 输出模式
GPIOA->BSRR = (1 << 5); // 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-N(i2c-dev) | 内核 I2C 子系统 | 用户态驱动 I2C 传感器 | open + ioctl(I2C_SLAVE) + read/write |
| /dev/spidevN.M(spidev) | 内核 SPI 子系统 | 用户态驱动 SPI 设备 | open + ioctl(SPI_IOC_MESSAGE) |
| 内核驱动 | 你自己写 | 量产、性能要求高 | 把驱动编进内核/模块,用户态只调文件接口 |
5.3 同样点个灯:Linux 用户态怎么写
最简单(不用写 C)------直接 shell 操作 sysfs:
bash
# 假设 GPIO5 已被内核 export 出来
echo 5 > /sys/class/gpio/export # 申请这个 GPIO
echo out > /sys/class/gpio/gpio5/direction # 设为输出
echo 1 > /sys/class/gpio/gpio5/value # 输出高,灯亮
echo 0 > /sys/class/gpio/gpio5/value # 输出低,灯灭
echo 5 > /sys/class/gpio/unexport # 释放
用 C 写也是同样的思路------打开文件、写值:
c
// led_on.c ------ Linux 用户态点灯,所有操作都是"读写文件"
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd = open("/sys/class/gpio/gpio5/value", O_WRONLY);
if (fd < 0) { perror("open"); return 1; }
write(fd, "1", 1); // 写 "1" 等价于 echo 1 > value
close(fd);
return 0;
}
对比一下两种点灯的"路径深度":
text
单片机: 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 ELF,ARM 板看不懂 | 用 arm-linux-gnueabihf-gcc,编完用 file 确认架构是 ARM |
| 2 | 工具链 hf 和板子 libc 不匹配 | 板上运行报 Illegal instruction(硬浮点指令在软浮点 libc 上跑不起来) |
板子上是 gnueabi(软浮点)libc,你却用 gnueabihf(硬浮点)编,或反过来 | 板上 ls /lib/ld-linux-armhf.so.3------存在就是 hf 版,工具链也必须用 hf |
| 3 | 动态链接器路径在板上不存在 | ls 明明看到文件,执行却报 No such file or directory |
缺 /lib/ld-linux-armhf.so.3,不是缺你那个程序 |
板上 ls /lib/ld-linux* 检查;调通前用 -static 先排除 |
| 4 | scp 上去没执行权限 | ./hello_arm 报 Permission denied |
scp 不一定保留 +x | 板上 chmod +x hello_arm 再跑 |
| 5 | 板子时间和主机差好几年 | make 反复全量重编;SSL/TLS 全失败 |
板子没装电池、NTP 没配,开机回到 1970 | 板上 sudo timedatectl set-ntp true,或 sudo date -s "2026-08-05 12:00:00" |
| 6 | 主机 ldd 看 ARM 程序依赖 |
报 not a dynamic executable 或一堆 unknown |
主机 ldd 只认识 x86 ELF | 用 `arm-linux-gnueabihf-readelf -d xxx |
| 7 | 复制别人的工具链路径不对 | 找不到 libc.so、crt1.o |
sysroot 没配 | arm-linux-gnueabihf-gcc -print-sysroot 看默认路径,或加 --sysroot=/path |
| 8 | ssh 改 hosts 文件 IP 变了连不上 | ssh: connect to host xxx port 22: No route to host |
DHCP 重新分配了 IP | 板上设静态 IP,或 PC 的 ~/.ssh/config 写别名 |
| 9 | QEMU 跑 ARM 程序报找不到 libc | qemu-arm-static ./hello_arm 报 Failed to load /lib/ld-linux... |
没 -L 指定 sysroot |
qemu-arm-static -L /usr/arm-linux-gnueabihf ./hello_arm |
| 10 | 64 位板上跑 32 位程序 | cannot execute binary file |
树莓派 3B+ 装了 64 位系统,但用 32 位工具链编 | 64 位系统用 aarch64-linux-gnu-gcc,或板上装 32 位兼容库 |
七、动手练一练
光看不练假把式。下面这几步我都标了"预期现象"和"翻车了怎么救",跟着敲一遍。
练习 1:故意用主机 gcc 编,扔到板上看 Exec format error
bash
# PC 上
gcc hello.c -o hello_wrong
scp hello_wrong pi@板子IP:~/
# 板上
ssh pi@板子IP
chmod +x hello_wrong
./hello_wrong
预期 :-bash: ./hello_wrong: cannot execute binary file: Exec format error
为什么:主机 gcc 编出来是 x86-64 ELF,ARM 板的 CPU 看不懂这套指令集。
验证 :回到 PC 上 file hello_wrong,会看到 x86-64。这就是第 6 节坑 1 的现场。
练习 2:交叉编译 + QEMU 验证 + 传到板子真跑
bash
# PC 上
arm-linux-gnueabihf-gcc hello.c -o hello_arm
file hello_arm # 确认是 ARM
qemu-arm-static -L /usr/arm-linux-gnueabihf ./hello_arm # 先 QEMU 验
scp hello_arm pi@板子IP:~/
ssh pi@板子IP "chmod +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 干扰
bash
arm-linux-gnueabihf-gcc hello.c -o hello_static -static
file hello_static # 看到 statically linked
scp hello_static pi@板子IP:~/
ssh pi@板子IP "./hello_static"
预期:跑通。如果静态版能跑、动态版不能跑,那一定是板上 libc 版本和工具链不匹配(坑 2)。
练习 4:用 readelf 看依赖,用 ldd 看为什么不行
bash
# 主机上(注意:主机 ldd 不认识 ARM 程序)
ldd hello_arm
# not a dynamic executable ← 这不是 bug,是 ldd 只认本机架构
# 改用 readelf 看
arm-linux-gnueabihf-readelf -d hello_arm | grep NEEDED
# 0x00000001 (NEEDED) Shared library: [libc.so.6]
# 看动态链接器路径
arm-linux-gnueabihf-readelf -l hello_arm | grep interpreter
# [Requesting program interpreter: /lib/ld-linux-armhf.so.3]
预期:理解"动态链接器 + NEEDED 库"这两个东西,就是程序运行前必须就位的依赖清单。
练习 5:板上点灯(sysfs)
bash
# 板上执行(GPIO 编号按你板子的原理图改。树莓派 4B 板载 LED 是电源指示灯,不可编程控制;
# 这里用 GPIO17,是树莓派 40-pin 排针上的物理第 11 脚,外接 LED + 限流电阻到 GND)
echo 17 > /sys/class/gpio/export # 申请 GPIO17
echo out > /sys/class/gpio/gpio17/direction # 设输出
echo 1 > /sys/class/gpio/gpio17/value # 亮
sleep 1
echo 0 > /sys/class/gpio/gpio17/value # 灭
echo 17 > /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