Linux_01:交叉编译与开发环境实战——从单片机跨到 Linux 的第一道坎

适用人群:已经能熟练点亮 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 输出里的两个关键信息:

  1. 架构:x86-64 vs ARM、32-bit vs 64-bit。这告诉你这份 ELF 能在哪种 CPU 上跑。
  2. interpreter(动态链接器)/lib/ld-linux-armhf.so.3。这是程序启动时第一个被加载的"加载器",它负责把 libc.so.6 这些动态库加载进来。目标板必须有这个文件,否则报 No such file or directory ------尽管你 ls 明明能看到 hello_arm 在那儿,新手会怀疑自己疯了。

3.4 用 readelf 进一步看依赖

readelffile 更细,能看到这个 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-UART ttyS0,波特率会受 CPU 频率影响抖动。如果出现乱码,用 sudo raspi-config → Interfacing Options → Serial → 关闭 shell、打开硬件,再在 /boot/config.txtenable_uart=1,把硬件串口还给 console。这个坑踩过一次就记住了。

PC 上用 minicompicocom 连:

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 下做嵌入式,你大部分时间是在用户态 写应用,硬件访问通过内核提供的接口;只有需要更高性能、或者要支持内核没现成驱动的外设,才写内核驱动

新手最常见的两个误区:

  1. 以为 Linux 点个灯还要写驱动------不用,sysfs 就够用,调试阶段完全够用。
  2. 以为不用学驱动了------量产或者对性能/中断有要求时,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_armPermission 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.socrt1.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_armFailed 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,第一道坎不是某个具体知识点,而是整个开发模型的转变。这篇把最基础的闭环跑通了:

  1. 工具链 :PC 是 x86、板子是 ARM,必须用 arm-linux-gnueabihf-gcc 交叉编译。三元组 <arch>-<os>-<abi> 不是装饰,是 ABI 匹配的硬要求。
  2. 产物验证file 看架构、readelf -d 看依赖、readelf -l 看动态链接器;没板子时用 qemu-arm-static -L sysroot 先验。
  3. 连接通路:串口 console 救命、SSH 干活、scp/rsync 传文件、NFS 共享省去传输。
  4. 思维转变:从"main 直接写寄存器"变成"用户态通过 sysfs/i2c-dev/spidev 跟内核商量着来",用户态/内核态分层是 Linux 嵌入式和单片机最根本的差异。
  5. 静态链接是调通环境前的应急手段,不是常态------但它能帮你把"libc 不匹配"这个变量先排除掉。

一句话总结:嵌入式 Linux 的"开发环境"= 交叉工具链 + 目标板连接 + 用户态/内核态分层模型。这三样齐了,你才有资格往根文件系统、设备树、驱动那一步走。

写这篇时翻了 GCC、BusyBox / 内核文档和板子官方资料,具体版本以你手上的为准。


下一篇:Linux_02_根文件系统与BusyBox从init到shell

相关推荐
Pocker_Spades_A1 小时前
终端里也能斗地主:Linux部署Ratel,从本地对战到公网联机
linux·运维·服务器
佳児素花痴╮2 小时前
C语言链接库
linux·运维·服务器
雷电小将军2 小时前
诡秘之主手游占卜家序列加点 诡秘之主手游占卜家怎么加点
服务器·网络·游戏·电脑·玩游戏
云飞云共享云桌面2 小时前
苏州昆山精密机械加工厂研发部门10个SolidWorks可以部署云飞云共享云桌面吗?
大数据·运维·服务器·自动化·汽车·制造
陈皮波比茶2 小时前
Python项目实战-网络机器人(爬虫)
开发语言·网络·爬虫·python·机器人
NYFEA2 小时前
徕飞 FCA系列全面上新,一站式I/O扩展国产替代方案
单片机·嵌入式硬件
白猫不黑2 小时前
网络安全/信息安全专业学习路线与入行指南:从大学到岗位的完整规划
网络·学习·安全·web安全·网络安全·黑客技术
wWYy.3 小时前
Linux发送数据包的过程
linux·网络
小嵌同学3 小时前
Linux 内核中的 Per-CPU 机制详解
linux·运维·arm开发·嵌入式