Linux 嵌入式开发学习笔记(一):编译 → 权限 → ADB → 驱动加载
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 交叉编译
| 本机编译 | 交叉编译 | |
|---|---|---|
| 工具 | gcc | aarch64-linux-gnu-gcc(RK3588 是 ARM64) |
| 产物运行平台 | PC (x86) | 开发板 (ARM) |
| 典型命令 | gcc hello.c -o hello | aarch64-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 (读/写/执行)
| 权限 | 符号 | 数字 |
|---|---|---|
| 读 | r | 4 |
| 写 | w | 2 |
| 执行 | x | 1 |
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
| insmod | modprobe | |
|---|---|---|
| 依赖处理 | 不处理,缺啥报错 | 自动加载依赖的模块 |
| 模块路径 | 要写明 .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.ko | adb 后面直接跟文件名,漏了 push | adb 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 |
| 驱动 Makefile | make -C $(KDIR) M=$(PWD) modules | 借内核的构建系统编我的模块 |
| 权限 | chmod +x / chmod 755 | 没 x 就 Permission denied |
| 传文件 | adb push/pull | PC 和板子之间的搬运工 |
| 进板子 | adb shell | 穷人版 SSH |
| 加载驱动 | insmod xxx.ko | 触发 module_init |
| 看结果 | dmesg | printk 的唯一出口 |
| 卸载 | rmmod xxx | 不带 .ko,触发 module_exit |
| 查模块 | lsmod | 谁在内核里,一目了然 |
更多推荐


所有评论(0)