打印
[其它产品/技术]

内存区域和链接脚本的奥秘

[复制链接]
52|10
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
用_sdata这样的变量来标记各个段在Flash或RAM中的位置。
ResetHandler的地址被精准地放在0x00000004,方便MCU找到。
这些操作看似天衣无缝,但实际上,它们要么依赖约定俗成的规则,要么是无数次复制粘贴的祖传代码。不同MCU的内存映射可能千差万别,启动脚本的变量命名可能五花八门,甚至程序段的划分也可能各不相同。既然没有统一标准,这些信息就得在项目中明确指定。对于用类Unix-ld工具链接的项目来说,这个指定工作就落在了链接脚本身上。

今天,我们继续以一个最简单的程序为例,拆解一下链接脚本的门道。

链接过程

链接是编译的最后一步,它把一堆编译好的目标文件合并成一个完整的程序,同时填上正确的地址,让所有内容各就各位。

在链接之前,编译器已经把你的源代码逐一编译成了机器码。在这个过程中,编译器会为地址留下占位符,因为它既不知道代码在整个程序中的最终位置,也对当前文件外的符号一无所知。

链接器的工作就是把这些目标文件,连同C标准库等外部依赖,合并成一个完整的程序。为了搞清楚每个部分该放哪儿,链接器需要一份地图——这就是链接脚本的用武之地。最后,链接器会把所有占位符替换成实际地址。

接下来就以我们的代码为例,看看main函数到底发生了什么。编译器先把minimal.c编译成目标文件:

arm-none-eabi-gcc -c -o build/objs/a/b/c/minimal.o minimal.c <CFLAGS>
用nm工具查看minimal.o的符号表:

arm-none-eabi-nm build/objs/a/b/c/minimal.o
...
00000000 T main
...

可以看到,main函数的地址还是个0,说明还没分配具体位置。接下来,链接器登场:

arm-none-eabi-gcc <LDFLAGS> build/objs/a/b/c/minimal.o <其他目标文件> -o build/minimal.elf

再次查看符号表:

arm-none-eabi-nm build/minimal.elf
...
00000294 T main
...

这回main函数有了个具体的地址,链接器完成了它的使命。除了地址分配,链接器还能顺手生成调试信息、清理无用代码,或者做全程序优化(LTO)。不过今天我们先聚焦链接脚本的本质,别的功能下回再说。

使用特权

评论回复
沙发
星辰大海不退缩|  楼主 | 2025-5-26 19:10 | 只看该作者
解剖链接脚本

链接脚本就像一份程序的施工图纸,主要包含四部分:

内存布局:告诉链接器有哪些内存区域可用,地址和大小是多少。
段定义:规定程序的各个部分该放哪儿。
选项:指定架构、入口点等额外配置。
符号:在链接时注入到程序中的变量。

程序的地基:内存布局

链接器要想分配空间,必须先知道有哪些内存可用,地址从哪儿开始,大小是多少。这部分信息由MEMORY定义提供。数据手册会写清楚内存映射如下:

内部Flash:起始地址0x00000000,大小256KB。
内部SRAM:起始地址0x20000000,大小32KB。
翻译成链接脚本的MEMORY定义,就是:

MEMORY
{
  rom (rx)  : ORIGIN = 0x00000000, LENGTH = 0x00040000
  ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}

这里的rom和ram只是名字,随你叫什么都行,flash和sram也常见。括号里的rx表示只读可执行,rwx表示可读写可执行。这些属性只是描述内存的性质,不是真的能改变硬件保护状态。

使用特权

评论回复
板凳
星辰大海不退缩|  楼主 | 2025-5-26 19:11 | 只看该作者
地段划分:段定义

程序的代码和数据会被分到不同的段,段是内存中连续的区域。段的划分没有硬性规定,但通常会根据以下原则:

放进同一个内存区域的符号归一个段。
需要一起初始化的符号归一个段。
回顾上篇,我们提到了两类需要批量初始化的符号:

已初始化的静态变量,需要从Flash复制到RAM。
未初始化的静态变量,需要清零。
此外,链接脚本还要关心:

代码和常量数据,通常放Flash
保留的RAM区域,比如堆栈
按照惯例,段的命名如下:

.text:代码和常量
.bss:未初始化数据
.stack:堆栈
.data:已初始化数据

如果我们不定义任何段,只写内存布局:

MEMORY
{
  rom (rx)  : ORIGIN = 0x00000000, LENGTH = 0x00040000
  ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}

SECTIONS
{
    /* 空荡荡! */
}

链接器照样能工作,但用objdump检查生成的elf文件,会发现符号表空空如也。链接器虽然能靠默认假设链接程序,但至少需要知道入口点或.text段的内容。

使用特权

评论回复
地板
星辰大海不退缩|  楼主 | 2025-5-26 19:12 | 只看该作者
.text段

我们先给.text段安排上,它得住进ROM:

SECTIONS
{
    .text :
    {
    } > rom
}

接下来要告诉链接器,哪些东西该放进.text段。用objdump检查目标文件:

arm-none-eabi-objdump -h build/objs/a/b/c/minimal.o
会看到一堆.text开头的段,这是因为我们用了-ffunction-sections和-fdata-sections编译选项,每个函数和数据都有自己的段。如果没用这些选项,编译器可能会把多个函数合并到一个.text.<某标识>段。

要把所有.text开头的段放进.text,我们用通配符*:

.text :
{
    KEEP(*(.vector*))
    *(.text*)
} > rom

这里的.vector*是向量表,包含ResetHandler等关键函数,必须放在.text段开头,确保MCU能找到。向量表的故事我们下回细聊。

检查elf文件,代码相关符号都出现了,数据符号还没影。

使用特权

评论回复
5
星辰大海不退缩|  楼主 | 2025-5-26 19:12 | 只看该作者
.bss段-未初始化数据
.bss段存放未初始化的静态变量,分配在RAM中,但不需要加载内容(因为都是0)。定义如下:

.bss (NOLOAD) :
{
    *(.bss*)
    *(COMMON)
} > ram

*(COMMON)是全局未初始化变量的特殊段,比如int foo;会放在这,而static int foo;不会。NOLOAD表示这个段不需要加载内容。

.stack段-栈

.stack段也在RAM,不需要加载内容,但得明确指定大小,还要按ARM调用规范(AAPCS)对齐到8字节边界。

这里要用到链接器的“位置计数器” . ,它记录当前内存区域的偏移。随着段的添加,计数器会自动前进。你可以用ALIGN函数强制对齐,或者通过赋值调整偏移,但不能往回退,否则链接器会报错。

.stack段的定义如下:

STACK_SIZE = 0x2000; /* 8KB */

.stack (NOLOAD) :
{
    . = ALIGN(8);
    . = . + STACK_SIZE;
    . = ALIGN(8);
} > ram

使用特权

评论回复
6
星辰大海不退缩|  楼主 | 2025-5-26 19:14 | 只看该作者
.data段
.data段存放有初始值的静态变量。设备断电时RAM清空,所以这些数据得从Flash复制到RAM。ResetHandler会在启动时完成这个搬家工作。

为此,每个段需要两个地址:

LMA(加载地址):加载器把数据写到这(一般是Flash)。
VMA(虚拟地址):程序运行时数据在这(一般是RAM)。
定义.data段时,指定VMA和LMA:

.data :
{
    *(.data*);
} > ram AT > rom
也可以显式指定地址:

.data ORIGIN(ram) : AT(ORIGIN(rom))
{
    . = ALIGN(4);
    _sdata = .;
    *(.data*);
    . = ALIGN(4);
    _edata = .;
}



链接器符号

上篇提到的_ebss、_sdata等变量,其实是链接器生成的符号,用于告诉代码各段的地址。定义方式类似C语言赋值:

.text :
{
    KEEP(*(.vectors .vectors.*))
    *(.text.*)
    *(.rodata.*)
    _etext = .;
} > rom

.bss (NOLOAD) :
{
    _sbss = . ;
    *(.bss .bss.*)
    *(COMMON)
    _ebss = . ;
} > ram

.data :
{
    _sdata = .;
    *(.data*);
    _edata = .;
} > ram AT >rom

注意,引用这些符号时要用取地址操作,比如:

uint8_t *data_byte = &_sdata;
直接用_sdata会出问题。

链接脚本就像一张地图,告诉链接器如何把代码和数据安顿好。但其实理解了内存布局、段定义和符号的套路,你也能轻松理解。

使用特权

评论回复
7
daichaodai| | 2025-5-26 19:30 | 只看该作者
这个和汇编链接什么关系

使用特权

评论回复
8
suncat0504| | 2025-5-27 09:24 | 只看该作者
在C中使用这种连接,会不会影响程序运行?毕竟声明变量、分配内存等操作会动态使用内存的。

使用特权

评论回复
9
suncat0504| | 2025-5-27 09:25 | 只看该作者
老早以前,在流程式编程的时候,有GOTO语句,后来不提倡,因为不利于程序解读、维护。固定内存指定区域的操作,会不会有其它负面影响呢。

使用特权

评论回复
10
小夏天的大西瓜| | 2025-5-27 09:54 | 只看该作者
动态解析还是需要资源的

使用特权

评论回复
11
地瓜patch| | 2025-5-27 17:31 | 只看该作者
这个有深度,看一遍理解不了

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

281

主题

2431

帖子

5

粉丝