Linux开发——链接脚本与 Makefile 实践指南

在嵌入式 Linux 或裸机开发中,除了写 C/汇编代码,还必须处理 编译、链接和内存布局。
这其中,链接脚本 (Linker Script) 和 Makefile 是两个非常关键的工具。
-
链接脚本:决定程序中代码和数据在内存中的布局。
-
Makefile:让整个编译、链接过程自动化。
本文结合 i.MX6ULL 的实际开发案例,带大家从零理解链接脚本和 Makefile 的用法。
一、为什么需要链接脚本?
在 PC 程序开发中,编译器和操作系统会自动安排内存布局,开发者几乎不用关心。
但在嵌入式开发(尤其是裸机环境)下,没有操作系统帮忙,我们必须明确告诉编译器:
-
代码放到哪里?
-
已初始化的全局变量放哪里?
-
未初始化的变量放哪里?
-
常量数据又该放哪?
这就需要 链接脚本 来描述程序在内存中的分布。
它的作用就像一张 内存地图,指导链接器 ld 把各段内容放到合适的位置。
二、常见的内存段与作用
在 C/汇编编译结果中,目标文件通常包含以下几个段:
| 段名 | 内容 | 示例 |
|---|---|---|
| .text | 程序代码(函数、指令) | main(), 各种函数体 |
| .data | 已初始化的全局变量 / 静态变量 | int a = 5; |
| .bss | 未初始化的全局变量 / 静态变量 | int b; 或 static int c=0; |
| COMMON | 未初始化的非静态全局变量(类似 .bss) | int g; |
| .rodata | 只读数据(常量、字符串、跳转表) | "hello", const int x=10 |
其中 .bss 段必须在启动代码中清零,否则变量会带随机值。
三、示例链接脚本(imx6ull.lds)
下面是一份适用于 i.MX6ULL 的链接脚本:
SECTIONS
{
. = 0x87800000; /* 程序入口地址 (SDRAM 起始位置) */
.text : {
obj/start.o /* 启动代码必须放在最前面 */
*(.text) /* 其他目标文件的代码 */
}
.rodata ALIGN(4) : {
*(.rodata*) /* 只读常量、字符串、跳转表 */
}
.data ALIGN(4) : {
*(.data) /* 已初始化的全局变量、静态变量 */
}
/* 定义 bss 段起止符号,启动代码会用来清零 */
__bss_start = .;
.bss ALIGN(4) : {
*(.bss) /* 未初始化的变量 */
*(.COMMON) /* COMMON 段 */
}
__bss_end = .;
}
📌 要点:
-
程序入口地址指定为
0x87800000(常见的 DDR 入口)。 -
start.o必须放在最前面,因为它包含复位向量和启动代码。 -
__bss_start与__bss_end是符号,在start.S里会用循环清零。
四、编译过程所需工具
整个编译 → 链接 → 转换 → 调试的过程涉及多个工具:
| 工具 | 作用 |
|---|---|
| gcc | 将 .c 源文件编译为目标文件 .o |
| as | 将 .S 汇编文件编译为 .o |
| ld | 链接器,使用 .lds 把多个 .o 拼成 .elf |
| objcopy | 格式转换:ELF → 纯二进制 .bin |
| objdump | 反汇编 ELF,生成 .dis 调试文件 |
最终我们得到 .bin 文件,烧录到开发板即可运行。
五、创建的 Makefile
在实际项目中,我们使用 Makefile 来自动化管理编译过程。
下面是一份带详细注释的示例:
# ================== 基本配置 ==================
target = led # 目标文件名,不带扩展名
cross_compiler = arm-linux-gnueabihf- # 交叉编译器前缀
cc = $(cross_compiler)gcc # C 编译器
ld = $(cross_compiler)ld # 链接器
objcopy = $(cross_compiler)objcopy # ELF → BIN
objdump = $(cross_compiler)objdump # 反汇编工具
# 头文件目录
incdirs = bsp imx6ull stdio/include
# 源码目录
srcdirs = bsp project stdio/lib
# 库路径与库配置
pathlib = -lgcc -L/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4/
# 将头文件目录转成 -I 选项
include = $(patsubst %, -I%, $(incdirs))
# ================== 文件收集 ==================
# 搜索 .c 和 .S 文件
cfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.c))
sfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.S))
# 去掉路径,只保留文件名
cfilenodir = $(notdir $(cfiles))
sfilenodir = $(notdir $(sfiles))
# 对应生成 obj/xxx.o
cobjs = $(patsubst %, obj/%, $(cfilenodir:.c=.o))
sobjs = $(patsubst %, obj/%, $(sfilenodir:.S=.o))
objs = $(cobjs) $(sobjs) # 所有目标文件
VPATH = $(srcdirs) # 告诉 make 在这些目录下找源文件
# ================== 规则部分 ==================
# 生成 bin 文件
$(target).bin : $(objs)
$(ld) -Timx6ull.lds -o$(target).elf $^ $(pathlib) # 链接成 ELF
$(objcopy) -O binary -S -g $(target).elf $@ # 转换为 BIN
$(objdump) -D $(target).elf > $(target).dis # 生成反汇编
# 编译汇编文件
obj/%.o : %.S
@mkdir -p obj
$(cc) -Wall -Wa,-mimplicit-it=thumb -fno-builtin -nostdlib -c $(include) -o $@ $<
# 编译 C 文件
obj/%.o : %.c
@mkdir -p obj
$(cc) -Wall -Wa,-mimplicit-it=thumb -fno-builtin -nostdlib -c $(include) -o $@ $<
# ================== 辅助规则 ==================
.PHONY : clean
clean:
rm -rf $(objs) $(target).elf $(target).bin $(target).dis
# 下载规则(根据自己工具修改)
load:
./../imxdownload ./$(target).bin /dev/sdb
📌 核心流程:
-
.c/.S → .o:编译生成目标文件。 -
ld:使用imx6ull.lds链接成led.elf。 -
objcopy:转换成led.bin。 -
objdump:生成led.dis(反汇编文件)。 -
make clean:一键清理。 -
make load:下载到开发板。
六、整体编译流程图
源码 (C / ASM)
│
▼
gcc/as
│
▼
目标文件 .o
│
▼
ld (用 .lds)
│
▼
ELF 文件 .elf
│
┌───┴─────┐
│ │
objcopy objdump
│ │
▼ ▼
BIN 文件 反汇编 .dis
(烧录) (调试)
七、总结
-
链接脚本 就是一张 “内存地图”,控制各段放在哪里。
-
Makefile 让整个编译流程自动化,更加高效。
-
嵌入式开发中,理解
.text / .data / .bss / .rodata的区别非常重要。 -
熟悉
gcc/ld/objcopy/objdump的使用,可以帮助我们更好地调试与优化程序。
只要理解了这些工具链与脚本的工作原理,嵌入式开发过程就会更加清晰、可控。
更多推荐



所有评论(0)