3. GNU Binutils 工具集:嵌入式开发的瑞士军刀
说实话,GNU Binutils 这套工具,我用了快二十年了。每次做 Rootfs 都离不开它们。有人觉得这些工具太底层、太枯燥,但在我看来,它们才是真正决定你系统能不能跑起来的关键。
Binutils 全称是 Binary Utilities,二进制工具集。它包含的每个小工具,都解决一个具体问题。今天我就挑最常用的六个,跟你聊聊我的实战经验。
核心知识点速览
- as:汇编器,把汇编代码变成机器码
- ld:链接器,把目标文件拼成可执行文件
- objdump:反汇编,看二进制里到底有什么
- readelf:查看 ELF 文件结构
- strip:给二进制文件瘦身
- ar:管理静态库
3.1 as 汇编器:从汇编到机器码
as 是 GNU 的汇编器。它的工作很简单:把你写的汇编代码,翻译成 CPU 能识别的机器码。嗯,听起来简单,但坑不少。
我记得刚做嵌入式那会儿,写了一段 ARM 汇编,用 as 编译死活报错。后来发现是语法版本问题。ARM 有 ARM 模式和 Thumb 模式,指令集不一样。as 默认用 AT&T 语法,跟 Intel 语法差别很大。
我的习惯:写汇编时,我会在文件开头明确指定架构。比如 ARM 平台,我会加一行 .arch armv7-a。这样 as 就知道该用哪套规则了。
# 一个简单的 ARM 汇编示例
.section .text
.global _start
_start:
mov r0, #1 @ 系统调用号:1 = exit
mov r7, #1 @ 系统调用号(ARM 用 r7)
svc #0 @ 触发系统调用
编译命令很简单:
arm-linux-gnueabihf-as -o test.o test.s
你会得到一个目标文件 test.o。它还不是可执行文件,需要链接器来处理。
3.2 ld 链接器:把碎片拼成整体
ld 是链接器。它的任务是把多个目标文件、库文件,按照你指定的规则,拼成一个完整的可执行文件。
说白了,你写的代码可能分散在十几个 .c 文件里。编译器把它们变成 .o 文件。ld 负责把这些 .o 文件粘在一起,还要处理函数调用、变量地址这些细节。
我个人觉得,ld 最核心的是链接脚本。它告诉 ld:代码放哪里,数据放哪里,堆栈怎么安排。在嵌入式开发中,这尤其重要。因为你的程序要跑在特定的内存地址上。
我曾经踩过的坑:有一次做 Bootloader,链接脚本里把 .text 段地址写错了。结果程序下载到板子上,一运行就死机。查了两天才发现,是链接脚本里少了个对齐指令。从那以后,我每次写链接脚本都会反复检查地址对齐。
# 一个简单的链接命令
arm-linux-gnueabihf-ld -T link.ld -o test test.o start.o
其中 link.ld 是链接脚本,内容大致如下:
SECTIONS
{
. = 0x8000; /* 起始地址 */
.text : { *(.text) } /* 代码段 */
.data : { *(.data) } /* 数据段 */
.bss : { *(.bss) } /* BSS 段 */
}
3.3 objdump 反汇编:看穿二进制
objdump 是我用得最频繁的工具之一。它能反汇编二进制文件,把机器码还原成汇编代码。你想想看,当你拿到一个编译好的程序,没有源码,怎么知道它干了什么?objdump 就是你的眼睛。
我经常用它来检查编译器生成的代码质量。比如,看看某个函数有没有被正确优化,或者检查有没有多余的指令。
# 反汇编整个文件
arm-linux-gnueabihf-objdump -d test
# 只看某个段的内容
arm-linux-gnueabihf-objdump -s -j .rodata test
输出示例:
00008000 <_start>:
8000: e3a00001 mov r0, #1
8004: e3a07001 mov r7, #1
8008: ef000000 svc 0
左边是地址,中间是机器码,右边是汇编指令。一目了然。
一个小技巧:如果你想看某个函数被编译成了什么,可以用 objdump -d test | grep -A 20 "<function_name>:"。这样只显示那个函数的反汇编结果,省得翻半天。
3.4 readelf:ELF 文件的解剖刀
readelf 用来查看 ELF 文件的内部结构。ELF 是 Linux 下可执行文件和目标文件的格式。它包含文件头、段表、符号表、重定位表等等。
为什么需要 readelf?因为有时候程序跑不起来,你不知道是哪个段出了问题。readelf 能帮你把 ELF 文件的结构看得清清楚楚。
# 查看 ELF 文件头
arm-linux-gnueabihf-readelf -h test
# 查看所有段
arm-linux-gnueabihf-readelf -S test
# 查看符号表
arm-linux-gnueabihf-readelf -s test
输出示例:
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Machine: ARM
Entry point address: 0x8000
看到 Entry point address 了吗?这就是程序入口地址。如果这个地址不对,程序肯定跑不起来。
实战经验:有一次我交叉编译一个程序,放到目标板上说"Exec format error"。用 readelf 一看,发现 Machine 字段显示的是 x86,不是 ARM。原来是交叉编译器没配好,编译出来的文件架构不对。readelf 帮我省了半天排查时间。
3.5 strip:给二进制文件瘦身
strip 的作用很简单:删除 ELF 文件中的符号表和调试信息。在嵌入式系统中,存储空间往往很宝贵。一个带调试信息的程序可能有几 MB,strip 之后能缩小到几百 KB。
你想想看,你的 Rootfs 总共就 32MB 的 Flash 空间。每个程序都带着调试信息,装不了几个就满了。strip 就是帮你省空间的利器。
# 瘦身一个可执行文件
arm-linux-gnueabihf-strip test
# 只删除调试信息,保留符号表
arm-linux-gnueabihf-strip --strip-debug test
# 查看瘦身前后的文件大小
ls -lh test
arm-linux-gnueabihf-strip test
ls -lh test
注意:strip 之后,你就没法用 gdb 调试这个程序了。所以我的习惯是:开发阶段保留调试信息,发布前再做 strip。另外,strip 不能恢复,操作前最好备份原始文件。
我曾经在一个项目里,用 strip 把整个 Rootfs 从 28MB 压缩到了 12MB。效果非常明显。但要注意,strip 只对 ELF 文件有效。如果你有脚本文件或者数据文件,strip 帮不了你。
3.6 ar:静态库的打包工
ar 是归档工具,用来创建和管理静态库(.a 文件)。静态库说白了,就是把一堆 .o 文件打包成一个文件。链接的时候,ld 会从 .a 文件里提取需要的 .o 文件。
我习惯把常用的函数封装成静态库。比如,驱动层的 GPIO 操作、I2C 通信、串口打印这些。这样多个项目可以复用,不用每次都重新编译。
# 创建静态库
arm-linux-gnueabihf-ar rcs libmylib.a gpio.o i2c.o uart.o
# 查看静态库里的内容
arm-linux-gnueabihf-ar t libmylib.a
# 从静态库中提取某个目标文件
arm-linux-gnueabihf-ar x libmylib.a gpio.o
ar 的参数说明:
| 参数 | 含义 |
|---|---|
| r | 替换或添加文件到库中 |
| c | 创建库(如果不存在) |
| s | 创建索引,加快链接速度 |
| t | 列出库中的文件列表 |
| x | 从库中提取文件 |
我的建议:创建静态库时,一定要加 's' 参数生成索引。否则链接的时候 ld 会报 "ar: creating an index" 的警告。虽然不影响功能,但链接速度会变慢。尤其是库文件很大的时候,差别很明显。
使用静态库链接程序:
arm-linux-gnueabihf-gcc -o test test.c -L. -lmylib
这里 -L. 表示在当前目录找库,-lmylib 表示链接 libmylib.a。
小结
这六个工具,每个都有自己的专长。as 负责汇编,ld 负责链接,objdump 和 readelf 帮你分析,strip 帮你瘦身,ar 帮你管理代码。在 Rootfs 制作中,它们缺一不可。
我个人觉得,掌握这些工具的关键是多用。别光看书,打开终端,拿一个实际的 ELF 文件,用 readelf 看看它的结构,用 objdump 反汇编看看代码,用 strip 试试瘦身效果。动手做一遍,比看十遍都管用。
一句话总结:Binutils 是嵌入式开发的基石。你越了解它们,就越能掌控你的系统。