Linux 嵌入式开发学习笔记(一):编译 → 权限 → ADB → 驱动加载

知识主线:在主机上写代码、用 Makefile 管理编译 → 交叉编译生成 ARM 可执行文件/驱动模块 → chmod 赋予执行权限 → 通过 ADB 推送到开发板 → insmod 加载驱动 → dmesg 查看结果。
这条链路就是嵌入式 Linux 开发的日常最小闭环。


〇、知识脉络总览

┌────────────────── 宿主机 (PC / Ubuntu) ──────────────────┐
│                                                          │
│  驱动源码 (.c) ──► Makefile ──► 交叉编译 (make)           │
│                     │                  │                 │
│                     ▼                  ▼                 │
│              编译规则/依赖       hello (可执行文件)       │
│              自动化构建          xxx.ko (驱动模块)        │
│                                      │                   │
│                              adb push ──────┐            │
└──────────────────────────────────────────────┼───────────┘
                                               ▼
┌────────────────── 目标机 (RK3588 开发板) ────────────────┐
│  chmod +x hello   →  ./hello        (应用程序跑起来)   │
│  insmod xxx.ko    →  lsmod / dmesg  (驱动进内核)       │
│  rmmod xxx        →  卸载驱动                            │
└──────────────────────────────────────────────────────────┘

应用层玩"文件"(open/read/write),内核层玩"模块"(insmod/rmmod),两者通过设备文件 /dev/xxx 握手。


一、编译命令(gcc 与交叉编译)

1.1 本机编译 vs 交叉编译


本机编译交叉编译
工具gccaarch64-linux-gnu-gcc(RK3588 是 ARM64)
产物运行平台PC (x86)开发板 (ARM)
典型命令gcc hello.c -o helloaarch64-linux-gnu-gcc hello.c -o hello

关键认知:在 x86 的 Ubuntu 上编译出的程序默认不能在 ARM 板子上跑,必须换交叉编译器。这也是 Makefile 里要写 CC = aarch64-linux-gnu-gcc 的原因。

1.2 gcc 常用参数

gcc -c hello.c          # 只编译不链接,生成 hello.o
gcc hello.o -o hello    # 链接生成可执行文件
gcc -Wall hello.c       # 打开所有警告(好习惯)
gcc -g hello.c          # 带调试信息,可用 gdb 调试
gcc -I./include         # 指定头文件搜索路径

编译四步:预处理 → 编译 → 汇编 → 链接。-c 就是停在前三步。


二、Makefile 语法

2.1 为什么需要 Makefile

源文件一多,手敲 gcc 又慢又容易漏。Makefile 把"谁依赖谁、怎么编"写成规则,make 自动判断哪些文件变了、只重编变过的部分。

2.2 核心语法:一条规则

目标: 依赖
<Tab>命令        # 注意:命令前必须是 Tab,不是空格!

2.3 应用程序 Makefile 模板

CC      = aarch64-linux-gnu-gcc   # 变量定义,$(CC) 引用
TARGET  = hello
OBJS    = hello.o

$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

%.o: %.c                            # 模式规则:所有 .c 编成 .o
	$(CC) -c $< -o $@               # $< 第一个依赖,$@ 目标

.PHONY: clean
clean:
	rm -f $(OBJS) $(TARGET)

三个常用自动变量:

  • $@ —— 目标文件名
  • $< —— 第一个依赖文件
  • $^ —— 所有依赖文件

2.4 驱动模块 Makefile

驱动编译依赖内核源码树,写法是固定的:

obj-m += mydrv.o                    # 编成模块(.ko),obj-y 则是编进内核

KDIR := /path/to/kernel/source      # 内核源码目录
PWD  := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules   # 跳去内核目录、回头编当前模块

clean:
	make -C $(KDIR) M=$(PWD) clean

-C 表示"进入该目录再执行 make",M=$(PWD) 告诉内核构建系统"我的模块源码在这儿"。

2.5 其他要点

  • .PHONY:声明伪目标(如 clean),避免与同名文件冲突。
  • make -j8:8 线程并行编译,大工程明显提速。
  • 赋值::= 立即展开,= 延迟展开。

三、可执行权限(chmod +x)

3.1 问题现象

$ ./hello
bash: ./hello: Permission denied   # 权限不足

原因:adb push 过去的文件,或新编译的文件,默认没有执行权限。

3.2 解决

chmod +x hello      # 给所有用户加执行权限
./hello             # 正常运行

3.3 权限的本质

$ ls -l hello
-rwxr-xr-x 1 root root 8344 Jul 23 10:00 hello
 │└┬┘└┬┘└┬┘
 │ │  └─ 其他人: r-x
 │ └───── 同组:   r-x
 └────────属主:   rwx (读/写/执行)
权限符号数字
读r4
写w2
执行x1
  • chmod 755 hello = rwxr-xr-x(最常用)
  • chmod 777 file = 全部放开(调试可用,生产环境不能这么干)
  • chmod u+x hello = 只给属主加执行权限

记忆点:目录的 x 权限表示"能否进入该目录",和文件的"能否执行"含义不同。


四、ADB 命令

ADB(Android Debug Bridge)本是安卓调试桥,在 Linux 开发板(尤其安卓/Linux 双系统或 Buildroot 带 adbd 的板子)上常被用来传文件 + 开 shell,走 USB 或网络,比串口传文件快得多。

4.1 连接与确认

adb devices                 # 列出已连接设备(确认板子在不在)
adb connect 192.168.1.100   # 网络方式连接板子
adb shell                   # 直接进入板子的 shell(就像 SSH)

4.2 文件传输(开发中最常用)

adb push hello /data/            # PC → 板子
adb pull /data/log.txt ./        # 板子 → PC

4.3 板子上执行命令

adb shell chmod +x /data/hello   # 远程加权限
adb shell /data/hello            # 远程运行
adb shell ls /data               # 远程查看

4.4 常用补充

命令作用
adb root以 root 权限重启 adbd(需要板子支持)
adb remount重新挂载系统分区为可读写(改系统文件前用)
adb reboot重启板子
adb install app.apk安装安卓应用(安卓板子用)

典型工作流:make 编完 → adb push → adb shell chmod +x → adb shell 运行,四连。


五、内核驱动框架

5.1 为什么驱动是"模块"

Linux 内核驱动一般以 .ko(kernel object)模块形式存在:不用重新编译整个内核,编好一个小文件插进去就能用,开发调试都靠它。

5.2 驱动的最小骨架(字符设备)

#include <linux/module.h>
#include <linux/init.h>

static int __init mydrv_init(void)      // insmod 时执行
{
    printk(KERN_INFO "mydrv loaded\n");
    return 0;
}

static void __exit mydrv_exit(void)     // rmmod 时执行
{
    printk(KERN_INFO "mydrv unloaded\n");
}

module_init(mydrv_init);                // 注册入口
module_exit(mydrv_exit);                // 注册出口

MODULE_LICENSE("GPL");                  // 许可证,不写会警告
MODULE_AUTHOR("fzc");
MODULE_DESCRIPTION("my first driver");

对比认知:

  • 应用程序从 main() 开始 → 驱动从 module_init 注册的函数开始
  • 应用 return 结束 → 驱动常驻内核,直到 rmmod 触发 module_exit
  • 应用用 printf → 驱动用 printk,输出要用 dmesg 看

5.3 拓一小步:字符设备的完整脉络

用户态 app:  open("/dev/mydrv") → read() → write()
                    │ (系统调用)
                    ▼
内核态驱动:  file_operations 结构体里对应的 .open/.read/.write

注册一个字符设备的关键三步:

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open  = my_open,
    .read  = my_read,
    .write = my_write,
};

// init 里:alloc_chrdev_region() 申请设备号 → cdev_add() 注册 → class_create()/device_create() 自动生成 /dev 节点
// exit 里:逆序注销

对应这几天学的 GPIO:app 通过 /dev/xxx 或 sysfs 文件 → 走到驱动里的 file_operations → 驱动再操作寄存器。DTS(设备树)负责告诉内核"硬件长什么样",驱动负责"怎么操作它",两者是描述与实现的关系。


六、驱动加载命令

6.1 三件套

insmod mydrv.ko     # 加载模块(会触发 mydrv_init)
lsmod               # 查看当前已加载的所有模块
rmmod mydrv         # 卸载模块(触发 mydrv_exit,注意不带 .ko)

6.2 验证与排错

dmesg               # 看内核打印,printk 的输出都在这
dmesg | tail        # 只看最后几条(刚加载完就看它)
dmesg -c            # 清空后再操作,干扰少
cat /proc/devices   # 查看已注册的字符/块设备及主设备号
ls /dev/mydrv       # 设备节点有没有生成

6.3 拓展:insmod vs modprobe


insmodmodprobe
依赖处理不处理,缺啥报错自动加载依赖的模块
模块路径要写明 .ko 路径从 /lib/modules 下按名字找
场景开发调试常用系统部署常用

6.4 常见报错速查

报错原因
Invalid module format编译用的内核源码版本和板子上跑的内核不一致(最坑,重编或换对源码树)
unknown symbol依赖的别的模块没加载,或内核没导出该符号
Device or resource busy申请的资源(GPIO/设备号)被别人占了
Operation not permitted不是 root,sudo insmod

七、一次完整的实战闭环

# ==== 宿主机 ====
cd mydrv/
make                                          # ① 用 Makefile 交叉编译
# 得到 mydrv.ko 和测试程序 test

adb push mydrv.ko /data/                      # ② ADB 推送
adb push test /data/

# ==== 目标机 ====
adb shell                                     # ③ 进板子
chmod +x /data/test                           # ④ 测试程序加执行权限
insmod /data/mydrv.ko                         # ⑤ 加载驱动
dmesg | tail                                  #    看 printk: "mydrv loaded"
lsmod | grep mydrv                            #    确认模块在
/data/test                                    # ⑥ 跑应用测试驱动
rmmod mydrv                                   # ⑦ 测完卸载
dmesg | tail                                  #    看 "mydrv unloaded"

八、实操记录(上午真实命令复盘)

本章全部来自真实操作截图与终端记录,对应环境:Ubuntu 虚拟机(RK3588 SDK)→ /mnt/hgfs 共享目录 → Windows PowerShell → ADB → 开发板。

8.1 驱动模块编译实录(helloworld / helloworld4)

cd ~/T7-2-linux/smart-rk3588/wzg_test/helloworld
make clean        # 清掉旧产物(.ko/.o/Module.symvers 全删,只剩 .c 和 Makefile)
make              # 交叉编译,生成 helloworld.ko

make 实际执行的是这条:

make -C /home/sailing/T7-2-linux/smart-rk3588/kernel \
     ARCH=arm64 \
     CROSS_COMPILE=/home/sailing/T7-2-linux/smart-rk3588/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu- \
     M=$(pwd) modules

四个参数各管一件事:

参数作用
-C <内核目录>进内核源码树,借它的构建系统
ARCH=arm64目标架构是 ARM64(RK3588)
CROSS_COMPILE=...gnu-交叉工具链前缀(注意结尾带 -,编译时自动拼出 gcc/ld 等)
M=$(pwd) modules我的模块源码在当前目录,按模块方式编

编译成功的标志是看到这几行,最终 LD [M] 出 .ko:

  CC [M]  .../helloworld.o
  MODPOST .../Module.symvers
  CC [M]  .../helloworld.mod.o
  LD [M]  .../helloworld.ko

8.2 应用程序:gcc vs 交叉编译对比实验

同一个 helloworld.c,用两种编译器编出两个世界的程序:

gcc helloworld.c -o helloworld2                        # x86 版:本机能跑
./helloworld2
# ----hello world------                                # ✅ 正常输出

aarch64-none-linux-gnu-gcc helloworld.c -o helloworld3 # ARM 版:给板子跑
./helloworld3
# aarch64-binfmt-P: Could not open '/lib/ld-linux-aarch64.so.1'  # ❌ x86 跑不了 ARM 程序

教训:推板子的必须是交叉编译的 helloworld3;gcc 编的 helloworld2 拷到板子上一样跑不了。 架构不对,chmod +x 也救不回来。

验证产物架构的方法:

aarch64-none-linux-gnu-readelf -h helloworld3 | grep Machine
# Machine: AArch64   ← 这才是对的

8.3 虚拟机 → Windows:共享目录搬运

cp helloworld.ko /mnt/hgfs/share2204/        # 拷单个文件到共享目录
cp wzg_test/ /mnt/hgfs/share2204/ -rf        # 整个目录递归拷贝

/mnt/hgfs/share2204/ 是 VMware 的共享文件夹,拷过去 Windows 侧 D:\share2204 就能拿到,然后接 adb push。

8.4 Windows 侧 ADB 实录(PowerShell)

adb push .\helloworld.ko /      # 推送驱动模块到板子根目录
adb push .\helloworld3 /        # 推送 ARM 版测试程序
adb pull b.txt ./               # 从板子拉文件回 Windows 当前目录

8.5 工具链 bin/ 目录里常用的几个

RK3588 SDK 自带工具链在 prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/,真正高频的就这几个:

aarch64-none-linux-gnu-gcc      # 交叉编译器(最核心,Makefile 的 CROSS_COMPILE 前缀配的就是它)
aarch64-none-linux-gnu-g++      # C++ 版
aarch64-none-linux-gnu-objdump  # 反汇编,看编译产物
aarch64-none-linux-gnu-readelf  # 看 ELF 结构(确认编出的是 ARM64 不是 x86)
aarch64-none-linux-gnu-strip    # 去调试符号,瘦身可执行文件
aarch64-none-linux-gnu-gdb      # 交叉调试

目录里还有一套 aarch64-rockchip1031-linux-gnu- 前缀的工具,功能一样;Makefile 里用哪套就全套一致,不能混用。

加到 PATH 就不用每次敲全路径:

export PATH=$PATH:/home/sailing/T7-2-linux/smart-rk3588/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin

8.6 上午翻车记录

报错原文原因正确姿势
adb.exe: unknown command .\helloworld.koadb 后面直接跟文件名,漏了 pushadb push .\helloworld.ko /
cp: 在 '/mnt/hgfs/share2204/' 后缺少要操作的目标文件只写了目标目录,缺源文件cp 源文件 目标目录,顺序别反
.../aarch64-none-linux-gnu-: 没有那个文件或目录只敲了工具链前缀没写 gcc补全成 aarch64-none-linux-gnu-gcc
./helloworld3 报 Could not open '/lib/ld-linux-aarch64.so.1'在 x86 上跑 ARM 程序ARM 程序推板子上跑,本机验证用 readelf

另一个无用操作:vim helloworld3.ko——.ko 是二进制,vim 打开全是乱码。看模块信息要用:

modinfo helloworld.ko      # 看作者、描述、许可证、依赖

8.7 实操版完整链路(本次真实环境)

# ==== Ubuntu 虚拟机(编译) ====
vim helloworld.c && vim Makefile          # 写代码、写规则
make clean && make                        # 编出 helloworld.ko
aarch64-none-linux-gnu-gcc test.c -o test # 测试程序交叉编译
cp helloworld.ko test /mnt/hgfs/share2204/  # 拷到共享目录

# ==== Windows PowerShell(搬运) ====
adb push .\helloworld.ko /
adb push .\test /

# ==== 开发板(运行) ====
adb shell
chmod +x /test          # 应用程序加执行权限(.ko 不需要!)
insmod /helloworld.ko
dmesg | tail            # 看模块打印
/test                   # 跑测试程序
rmmod helloworld

细节:chmod +x 只针对应用程序,.ko 驱动模块不需要执行权限,它不是被执行的,是被 insmod 加载进内核的。


九、知识点速记卡片

环节命令/文件一句话记忆
编译make规则在 Makefile 里,只重编变过的
Makefile目标: 依赖 + Tab 命令Tab 是灵魂,错了直接报 missing separator
驱动 Makefilemake -C $(KDIR) M=$(PWD) modules借内核的构建系统编我的模块
权限chmod +x / chmod 755没 x 就 Permission denied
传文件adb push/pullPC 和板子之间的搬运工
进板子adb shell穷人版 SSH
加载驱动insmod xxx.ko触发 module_init
看结果dmesgprintk 的唯一出口
卸载rmmod xxx不带 .ko,触发 module_exit
查模块lsmod谁在内核里,一目了然

更多推荐