内容提要
引言1. 基于C语言的嵌入式MCU应用工程编译结果的代码和数据段(segment)概述
1.1 .text/.code段
1.2 .bss段
1.3 .data段
1.4 堆(.heap)
1.5 栈(.stack)
2. .text、.bss、.data、.heap和.stack段在嵌入式MCU内存中的空间分布
3. 链接器对全局符号的处理--强符号(Strong Symbols)与弱符号(Weak Symbols)
3.1 什么是强符号(Strong Symbols)和弱符号(Weak Symbols)
3.2 链接器对全局符号的处理规则和注意事项
4. 什么是.COMMON段及其与.bss段的关系
4.1 什么是.common段
4.2 通过一个实例来分析.bss段--未初始化的全局变量与初始为0的全局变量是否等同?
4.3 全局变量定义的注意事项和建议
5. S32DS IDE中print size信息解析
5.1 配置使能S32DS IDE应用工程编译结果的print size输出功能
5.2 S32DS IDE应用工程编译结果print size输出信息解析
5.3 S32DS IDE应用工程编译结果print size输出bss大小构成分析总结
引言
在嵌入式MCU软件开发中,链接文件(Linker File)和内存映射文件(memory map,简称map文件)是学习和掌握一个CPU架构(比如ARM Cortex A/R/M和Power e200z0/2/3/4/6/7)系列MCU的基础和关键所在。前者作为输入告诉工具链目标MCU的存储器分布和大小消息以及应用程序的代码和数据分配规则/信息,后者为工具链中链接器输出结果的总结概括,是当前应用工程,是否满足给定目标MCU链接文件规定内存分配结果的验证。
虽然链接文件和map文件因工具链不同而形式和语法有所不同,比如开发 S08/S12/MagniV S12Z系列MCU的CodeWarrior IDE应用工程的链接文件为.prm文件,而开发KEA/S32K/Qorivva MPC57xx/S32x系列MCU的S32DS IDE(实质为GNUGCC工具链)的为.ld文件,但其中所包含的信息却是大同小异的。
因此,如果能够了解和掌握链接文件和map文件的本质---信息构成和功能,即可一通百通,举一反三。
1. 基于C语言的嵌入式MCU应用工程编译结果的代码和数据段(segment)概述
通常,基于C语言的嵌入式MCU应用工程的编译结果(elf文件)中都会包含以下代码和数据段:
1.1 .text/.code .text/.code代码段(text segment/code segment)是用于存储程序执行代码的一块内存区域。
这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序,将SRAM属性定义为只读存储器(read only),也可以用作.text段的存储器)。
Tips:代码段中,除了用户程序代码(.code/.text)外, 还包含以下只读的常数数据:
.const/.rodata 段:存放const定义的只读数据.string段:存放字符串常量;.copy/.init段: 存放有初始化值全局变量(.data段)的初始化值;
1.2 .bss段 .bss符号起始块段(bss segment)是用于存储程序中初始化值为0或未初始化的全局变量的一块内存区域。
bss是英文Block Started by Symbol的简称。
bss段属于静态内存分配。
1.3 .data段 .data数据段(data segment)是用于存放程序中已初始化的全局变量的一块内存区域。
数据段属于静态内存分配。其初始化值存储在Flash中,掉电不丢失,但运行时(runtime)地址在SRAM中。 所以.data段占用相同大小的Flash和SRAM存储器地址空间。
.data段在每次MCU复位启动(startup)过程中, 由启动代码完成初始化,从而保证,在进入main()函数后,第一次使用.data段中的全局变量时,初始化值是编程时设置的。
1.4 堆(.heap) 堆(.heap堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);
当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
1.5 栈(.stack) 栈(.stack)又称堆栈,由C语言运行时自动分配和管理(通过嵌入式MCU CPU内核专用堆栈指针寄存器SP--Stack Pointor实现),是C语言运行访问最为频繁的内存空间。
在函数中定义的局部变量(但不包括static声明的变量,static意味着在数据段中存放变量)通常会被分配带栈中。
除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中返回给调用函数。
栈的压栈(push)和出栈(pop)具有后进先出(LIFO--Last In First Out)的特点,其保证了C语言程序函数调用和中断处理时的现场保护/恢复,从而使得嵌入式MCU中的应用程序可以正常工作。
从这个意义上讲,可将堆栈看成一个数据临时寄存和交换的内存区。
2. .text、.bss、.data、.heap和.stack段在嵌入式MCU内存中的空间分布
对于嵌入式MCU来说,其内存(Memory, 也叫存储器)主要包括以下几大类:
SRAM:静态随机访问存储器,可按字节读写,写数据先无需擦除,读写速度最快,掉电数据丢失;
Flash:闪速存储器(简称闪存)。包括P-Flash(Program Flash,存储程序)/C-Flash(Code Flash,存储代码)和D-Flash(Data Flash,存储数据),数据掉电不丢失,必须先擦除再编程,按sector/block擦除,sector/block尺寸大(典型为512B/1KB/2KB/4KB,16KB/128KB甚至256KB),读写速度慢;
EEPROM:电可擦除只读存储器,数据掉电不丢失,必须先擦除再编程,按sector/block擦除,sector尺寸小(典型为2/4字节),读写速度较快;
以上介绍的.text、.bss、.data、.heap和.stack段在嵌入式MCU中的存储器中的典型分配关系如下表:
| SRAM | Flash | EEPROM |
.text段 | √(remap重映射) | √ | √ |
.bss段 | √ | × | × |
.data段 | √ | √(存储初始化值) | √(存储初始化值) |
.heap段 | √ | × | × |
.stack段 | √ | × | × |
3. 链接器对全局符号的处理--强符号(Strong Symbols)与弱符号(Weak Symbols)
3.1 什么是强符号(Strong Symbols)和弱符号(Weak Symbols)
对于链接器来说,所有的全局符号(全局变量和全局函数)可分为两种:强符号(Strong Symbols)和弱符号(Weak Symbols)。
gcc的attribute中有个__attribute__((weak)),就是用来声明这个符号是弱符号的。gcc手册中这样写道:
The weak attribute causes the declaration to be emitted as a weak symbol rather than a global. This is primarily useful in defining library functions which can be overridden in user code, though it can also be used with non-function declarations. Weak symbols are supported for ELF targets, and also for a.out targets when using the GNU assembler and linker.
GNU GCC手册
一般来说,函数和已初始化的变量是强符号,而未初始化的变量是弱符号。
3.2 链接器对全局符号的处理规则和注意事项
链接时,全局符号需要满足下列三条规则:
1. 同名的强符号只能有一个。2. 有一个强符号和多个同名的弱符号是可以的,但定义会选择强符号的。3. 有多个弱符号时,链接器可以选择其中任意一个/选择占用内存空间最大的那个(不同的工具链处理不同)。
这三条规则看起来很好理解,其实不然,尤其是当这些弱符号类型和强符号不同时!表面上看起来正确的程序会导致严重的错误! 比如下面两个C文件:a.c:
- #include "stdio.h"int x=7; /*this a strong symbol with initial value*/
- void Func1(void)
- {
- if(x==7) printf("global variable x = %d\r\n", x) ;
- else printf("global variable x = %f\r\n", x) ;
- }
- b.c:
- double x;/*this a weak symbol without initial value*/
- void Func2(void)
- {
- x = 0.5f;
- }
- main.c:
- extern void Func1(void);
- extern void Func1(void);
- void main(){
- Func1();
- Func2();
- }
将它们一起编译运行。并且在Func
2()函数中给x赋值,你会发现,y也改变了!虽然x被看作是double,但其定义会取a.c中的int x,也就是说,在b.c中会把a.c中的int x当double来用!这当然是错误!之所以会这样,就是因为上面的规则2。避免这种错误的一个方法是,给gcc加上-fno-common选项。当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小此时是未知的,因为有可能其他编译单元中同符号名称的弱符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在BSS段分配空间,因为所需要的空间大小此时是未知的。但是链接器在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终的输出文件的BSS段为其分配空间。所以总体来看,未初始化的全局变量还是被放在BSS段。《程序员的自我修养》
4. 什么是.COMMON段及其与.bss段的关系
在嵌入式MCU的编译结果map文件中,我们经常能够看到.COMMON段,它属于我们前面介绍的哪一段呢?
4.1 什么是.common段
前文讲到,.bss段中存放的是未初始化或者初始化值为0的全局变量。其实在很多工具链中,默认都为未初始化(定义时没有赋初始化值,比如如定义结构体类型的全局变量)的全局变量单独分配一个段,那就是.COMMON段了。在链接的最后阶段将其与初始化值为0的全局变量.bss段一起合并放在真正的.bss段中。
因此,在链接文件中,通常.bss段的定义如下(e.g. KEA/S32K的S32DS 应用工程链接文件):- /* Uninitialized data section */
- .bss : { /* This is used by the startup in order to initialize the .bss section */
- . = ALIGN(4);
- __START_BSS = .;
- __bss_start__ = .;
- *(.bss) *(.bss*) *(COMMON)
- . = ALIGN(4);
- __bss_end__ = .;
- __END_BSS = .;
- } > SRAM
即编译器对未初始化或初始化值为0全局变量的处理方法如下:
- 如果初始化的值为0,那么直接将其保存在.bss段;
- 如果没有初始化值,则将其保存在.common段;
- 待到链接时再将以上.bss段和.common段合并,统一放入BSS段;
4.2 通过一个实例来分析.bss段--未初始化的全局变量与初始为0的全局变量是否等同?
尽管未初始化的全局变量有可能在编译阶段被保存在common段,但是最终还是会放到BSS段。那么我们是否可以将未初始化的全局变量与初始为0的全局变量等同起来呢?
请看下面的测试代码:
文件test1.c:- #include
- int init = 0;
- void init1()
- {
- if (0 == init)
- {
- init = 1;
- printf("init1\n");
- }
- }
文件test2.c:- #include
- int init = 1;
- void init2()
- {
- if (init) {
- init = 0;
- printf("init2\n");
- }
- }
在main函数中,调用test1.c和test2.c中的两个初始化函数:文件main.c
- void init1();
- void init2();
- int main(){
- init1();
- init2();
- return 0;
- }
使用gcc编译链接以上3个文件:gcc -g -c test1.cgcc -g -c test2.cgcc -g -o test main.c test1.o test2.o由于test1.c和test2.c中都定义了全局变量init,链接时会报错:
test2.o.data+0x0): multiple definition of `init'test1.o:/root/work/test/test1.c:4: first defined herecollect2: ld returned 1 exit status但是,如果将文件test1.c中的int init = 0改为int init,对于test1.c来说,这个改动不影响其逻辑,因为init如果未初始化,其值也应该是0。修改后的test1.c
- #include int init;
- void init1(){
- if (0 == init) {
- init = 1;
- printf("init1\n");
- }
- }
重新编译链接gcc -g -c test1.cgcc -g -Wall -o test main.c test1.o test2.o此时即使使用-Wall打开所有warning,也没有任何警告和错误,已经生成了输出文件test。执行test,输出如下:
[root@Lnx99 test]init2可以看到test1.c中并未打印出期望的init1,这是为什么呢?第一种情况,当test1.c中的init被初始化为0时,尽管init被放置在.bss段,但是它是一个强符号。而test2.c中,定义了init为1,也是一个强符号,所以引发了错误。
第二种情况,当test1.c中的init不进行初始化,尽管其值仍然为0,但是其被被保存在common段,为一个弱符号。当test2.c中定义了init为1一个强符号,那么在链接的过程中,gcc会用这个强符号覆盖掉弱符号,并不会引起链接冲突错误。但是在运行阶段,进入init1时,这个init的值却并不是其所期望的值,因此导致没有打印init1。
4.3 全局变量定义的注意事项和建议
从以上分析中可以看到:当定义全局变量时,有两点需要注意:
1. 如果只有本文件使用,那么需要添加上static以限制其作用域;2. 如果不能使用static,那么一定要为该全局变量定义初值,即使这个值就是0。
这样可以保证该变量为强符号,当名字冲突时,可以在链接被发现,而不是被未知的值覆盖。当然最好能够避免使用全局变量,或者定义一个独一无二的名字。
另外,在编译阶段,我们还可以通过编译器的-fno-common选项来禁止将未初始化的全局变量放入到common段。
比如,同样的文件,若添加-fno-common的
编译选项,则链接时将能够发现全局变量init的重复定义错误:- [root@Lnx99 test]#gcc -g -fno-common -c test1.c
- [root@Lnx99 test]#gcc -g -fno-common -c test2.
- [root@Lnx99 test]#gcc -g -fno-common -o test main.c test1.o test2.o
- test2.o:(.data+0x0): multiple definition of `init'
- test1.o:/root/work/test/test1.c:6: first defined here
- collect2: ld returned 1 exit status
文章评论(0条评论)
登录后参与讨论