前几个程序编译链接时, 会使用ld -Ttext=0x42C00000
指定.text
段的起始地址;
以串口回显的工程为例,查看编译后的文件大小;
再看看.elf文件的段表,arm-linux-objdump -h
LMA: load address,加载地址;
VMA: virtual address,在RAM中的运行地址;
Loader,装载器的作用:
1。从二进制文件中读出对应的段的信息,比如text,data,bss等段的信息,
将内容拷贝到对应的LMA的地址处。此谓,装载(对应内容)到装载地址(LMA)。
2。如果发现VMA!=LMA, 即 程序运行时候的地址,和刚刚把程序内容拷贝到的地址LMA,两者不一样,
那么就要把对应的内容,此处主要是data,数据段的内容,从刚刚装载到的位置,LMA处,拷贝到VMA处,
这样,程序运行的时候,才能够在执行的时候,找到对应的VMA处的变量,才能找到对应的值,程序才能正常运行。
一般情况下是LMA = VMA,只有小数情况是不相等:
CPU 从ROM,比如常见的NorFlash中读取代码的速度,要远远小于从RAM,比如常见的SDRAM,中读取的速度,所以,才会牵扯到将代码烧写到ROM 里面,然后代码的最开始,将此部分程序reaload,重载,也就是从此处的ROM的地址,即LMA,重新拷贝到SDRAM中去,也就是VMA的地方,然后从那里运行。更详细的点击查看
执行objcopy
命令生成.bin
文件时,相当于,
- 拷贝
.elf
文件的0x10000 ~ 0x114db
放到目标文件的0x0 ~ 0x14db
处; - 拷贝
.elf
文件的0x114dc ~ 0x11773
放到目标文件的0x14db ~ 0x1173
处; - 拷贝
.elf
文件的0x11774 ~ 0x11784
放到目标文件的0x11774 ~ 0x11784
处;
注意第3部分的.data
段在.bin
文件中起始地址是0x11774
,这就造成了.bin
文件的0x1174 ~ 0x11774
部分浪费了,被0填充;
所以.bin
文件的大小就是 0x11784 = 71556 Byte
;
如何杜绝这种浪费;
同样可以使用 -Tdata
指定 .data
段的起始地址;ld -Ttext=0x42C00000 -Tdata=0x42C01774
;
这样,.data
段的数据就会紧接在.rodata
段的后面;
看看效果;
减小了 0x10000 Byte;
再看看段表:
但这不是通用的方法,这时就要引入链接脚本 .lds
LDS基本语法
SECTIONS
{
secname : {contents}
}
secname 表示 输出段的段名,后面必须有一个空格,使得输出段名没有歧义,后面根一个冒号和一对大括号;
大括号里面的contents描述了一套规则和条件,表示符合这种条件的输入段合并到输出段中。
以下脚本将输出文件的text 段定位在0×10000, data 段定位在0×8000000:
SECTIONS
{
. = 0×10000; /*把定位器符号置为0×10000 (若不指定, 则该符号的初始值为0).*/
.text : { *(.text) } /*将所有(*符号代表任意输入文件)输入文件的.text 段合并成一个.text 段, 该段的地址由定位器符号的值指定, 即0×10000.*/
. = 0×8000000; /*把定位器符号置为0×8000000*/
.data : { *(.data) } /*将所有输入文件的.data 段合并成一个.data 段, 该段的地址被置为0×8000000.*/
.bss : { *(.bss) } /*将所有输入文件的.bss 段合并成一个.bss 段,该段的地址被置为0×8000000+.data 段的大小.*/
}
连接器每读完一个段描述后, 将定位器符号的值*增加*该段的大小. 注意: 此处没有考虑对齐约束.
下面写出满足咱们工程需求的.lds
SECTIONS
{
. = 0x42C00000;
. = ALIGN(4); /*4字节对齐*/
.text :{ *(.text) }
. = ALIGN(4);
.rodata :{ *(.rodata) }
. = ALIGN(4);
.data :{ *(.data) }
. = ALIGN(4);
.bss :{ *(.bss) }
}
还可以使用 read -S
查看段表,排版比 objdump
好看;
####C语言访问 .lds
中的变量
比如获取.text段的开始地址和结束地址:
SECTIONS
{
. = 0x42C00000;
. = ALIGN(4); /*4字节对齐*/
__TEXT_START = .; /*将当前地址赋值给__TEXT_START*/
.text :{ *(.text) }
__TEXT_END = .;
. = ALIGN(4);
.rodata :{ *(.rodata) }
. = ALIGN(4);
.data :{ *(.data) }
. = ALIGN(4);
.bss :{ *(.bss) }
}
在C语言里要先声明外部变量,取值时要加取地址符;
extern __TEXT_START;
extern __TEXT_END;
printf(".text start: %x end: %x\r\n", &__TEXT_START, &__TEXT_END); // 输出.text段的开始地址和结束地址;
输出:
准确的说__TEXT_START, __TEXT_END
并不属于变量,在链接时他们的值就已经确定了,在符号表中记录;加&
是为了得到它在符号表中的值;
下面是输出部分的反汇编代码:
42c000e0: e30104fc movw r0, #5372 ; 0x14fc
42c000e4: e34402c0 movt r0, #17088 ; 0x42c0
42c000e8: e3001000 movw r1, #0
42c000ec: e34412c0 movt r1, #17088 ; 0x42c0
42c000f0: e30124f0 movw r2, #5360 ; 0x14f0
42c000f4: e34422c0 movt r2, #17088 ; 0x42c0
42c000f8: eb0002da bl 42c00c68 <printf>
可以看到,传入的是具体的值;
####关于.bss段
因为.bss段并不储存在目标文件中,又因为裸机环境没有加载器,所以C程序中的未初始化的全局变量和未初始化的静态变量并不会被初始化为0;除非RAM上本身就是0;
用程序来看现象;
在链接脚本中定义bss段的起始和结束地址,输出;
在C程序中定义一个int型全局变量g_a,输出其地址和值;
//.lds
SECTIONS
{
. = 0x42C00000;
. = ALIGN(4); /*4字节对齐*/
__TEXT_START = .; /*将当前地址赋值给__TEXT_START*/
.text :{ *(.text) }
__TEXT_END = .;
. = ALIGN(4);
.rodata :{ *(.rodata) }
. = ALIGN(4);
.data :{ *(.data) }
. = ALIGN(4);
__BSS_START = .;
.bss :{ *(.bss) }
__BSS_END = .;
}
//mian.c
extern __TEXT_START, __TEXT_END;
extern __BSS_START, __BSS_END;
int g_a;
int main(){
printf(".text start: %x end: %x\r\n", &__TEXT_START, &__TEXT_END);
printf(".bss start: %x end: %x\r\n", &__BSS_START, &__BSS_END);
printf("&g_a:%x g_a:%x\r\n", &g_a, g_a);
}
在拷贝.bin文件到SD卡时,将SD卡上.bin结束之后的4字节改为0x12345678
输出结果:
所以在裸机环境,C程序的.bss
段需要我们自己来初始化;
//main.c
void bss_init(){
int *start = &__BSS_START;
int size = &__BSS_END - &__BSS_START;
int i = 0;
for(; i < size; i += 4){
*(start + i) = 0;
}
}
//start.s
bl bss_init ; main()之前执行
####工程源码:
码云_5_lds