http://blog.163.com/cailing_07@126/blog/static/3391508720111023104948907/
uboot的链接文件分析
详细:今天结合uboot的README帮助文件打开各uboot的文件包,看到基本上每个文件包中都有Makefile,所以第一阶段:读懂makefile文件。
接着要进入第二轮uboot学习的第二阶段(看懂源码结构,加强源码的理解,知道移植需要修改哪些地方)先找u-boot.lds文件。用find . -type f|ls -l|grep *.lds会有很多个路径下的u-boot.lds。再去Makefile这个地图里去找啦,最终搜索到u-boot.lds的路径是在主目录下的config.mk中找到线索的
ifndef LDSCRIPT
#LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds.debug
ifeq ($(CONFIG_NAND_U_BOOT),y)
LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot-nand.lds
else
LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds
endif
endif
假如没有定义LDSCRIPT则什么也不做。那么LDSCRIPT为空,然后再往下看
sinclude $(TOPDIR)/arch/$(ARCH)/config.mk # include architecture dependend rules
sinclude $(TOPDIR)/$(CPUDIR)/config.mk # include CPU specific rules
于是在\arch\arm中的config.mk中最后一行看到
LDSCRIPT := $(SRCTREE)/$(CPUDIR)/u-boot.lds
那么我就到arch/arm/cpu/arm920t下找到了u-boot.lds文件,打开开始分析。昨天已经了解了些lds基本规则及命令,今天
结合实际案例自己分析。第1句:OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
天呢!我第一句就看不懂,定义输出格式为什么有3个选择?于是到官网查出如下帮助说明
===========================================================================
OUTPUT_FORMAT(bfdname)
OUTPUT_FORMAT(default, big, little)
The OUTPUT_FORMAT command names the BFD format to use for the output file (see BFD). Using OUTPUT_FORMAT(bfdname) is exactly like using `--oformat bfdname' on the command line (see Command Line Options). If both are used, the command line option takes precedence.
You can use OUTPUT_FORMAT with three arguments to use different formats based on the `-EB' and `-EL' command line options. This permits the linker script to set the output format based on the desired endianness.
If neither `-EB' nor `-EL' are used, then the output format will be the first argument, default. If `-EB' is used, the output format will be the second argument, big. If `-EL' is used, the output format will be the third argument, little.
=============================================================================
原来如此定义输出格式。根据ld后面的参数 -EB和-EL或不带参数来决定输出文件的格式,所以有3种选择。ok
第2句:OUTPUT_ARCH(arm)
应该就是指定构架,那么除了arm还能有其他什么参数吗?到官网查出如下
=============================================================================
OUTPUT_ARCH(bfdarch)
Specify a particular output machine architecture. The argument is one of the names used by the BFD library (see BFD). You can see the architecture of an object file by using the objdump program with the `-f' option.
=============================================================================
我到BFD中搜索了下,没搜索到。于是想起来了README中的/arch目录下又arm还有avr32,i386等,应该都可以作为参数的,先跳过,继续
第3句:ENTRY(_start)
应该是指定入口函数。官网搜索了下
===========================================================================
The first instruction to execute in a program is called the entry point. You can use the ENTRY linker script command to set the entry point. The argument is a symbol name:
ENTRY(symbol)
============================================================================
_start是一个.o文件吗?program?于是网上搜索了下,起始段的段名是_start
第4句开始:
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
arch/arm/cpu/arm920t/start.o (.text)
*(.text)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
.got : { *(.got) }
. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
. = ALIGN(4);
__bss_start = .;
.bss (NOLOAD) : { *(.bss) . = ALIGN(4); }
_end = .;
}
根据我昨天看的ld脚本基础,以上内容我都明白什么意思。但是还有一些设计方面的疑问。
1,got是什么段不清楚。
2,另外把段这样定义的目的,如果是我自己定义,我可能也不知道如果定义,在内存中就一定要先放test段吗,然后再放ro段吗?
3,段应该怎样放置才最好,另外段名一共有多少种?
4,u_boot_cmd_start定义后,将来会用吗?有什么好处。
5,定义.bss 为(NOLOAD)的目的?
我现在网上找个不错的分析。
=======================================================
下面是u-boot-1.3.4的u-boot.lds(/cpu/arm920t/),简单分析如下:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*指定输出可执行文件是elf格式,32位ARM指令,小端 */
/*OUTPUT_FORMAT("elf32-arm","elf32-arm", "elf32-arm")*/
OUTPUT_ARCH(arm) /* 指定输出文件的平台体系是ARM */
ENTRY(_start) /*指定可执行映像文件的起始段的段名是_start*/
SECTIONS
{
/*指定可执行image文件的全局入口点,通常这个地址都放在ROM(flash)0x0位置。必须使编译器知道这个地址,通常都是修改此处来完成*/
. = 0x00000000; /* 起始地址为0x00000000 */
. = ALIGN(4); /* 字对齐,即就是4字节对齐*/
.text : /* 代码段*/
{
cpu/arm920t/start.o (.text) /* 代码段第一部分代码*/
board/fs2410/lowlevel_init.o (.text) /* 代码段第二部分,这段由自己添加,由于在编译连接时发现,lowlevel_init.o代码段总是被连接在4kB之后,导致start.s执行到该段代码时,总是无法找到这段代码(注明:从nandflash启动才会存在这个问题)。*/
*(.text) /*其余代码段*/
}
. = ALIGN(4);
.rodata : { *(.rodata) } /* 只读数据段,所有的只读数据段都放在这个位置*/
. = ALIGN(4);
.data : { *(.data) } /* 可读写数据段,所有的可读写数据段都放在这里*/
. = ALIGN(4);
.got : { *(.got) } /*指定got段,got段式是uboot自定义的一个段,非标准段*/
. = .;
__u_boot_cmd_start = .; /*把__u_boot_cmd_start赋值为当前位置,即起始位置*/
.u_boot_cmd : { *(.u_boot_cmd) } /* u_boot_cmd段,所有的u-boot命令相关的定义都放在这个位置,因为每个命令定义等长,所以只要以__u_boot_cmd_start为起始地址进行查找就可以很快查找到某一个命令的定义,并依据定义的命令指针调用相应的函数进行处理用户的任务*/
__u_boot_cmd_end = .; /*u_boot_cmd段结束位置,由此可以看出,这段空间的长度并没有严格限制,用户可以添加一些u-boot的命令,最终都会在连接是存放在这个位置。*/
. = ALIGN(4);
__bss_start = .; /*把__bss_start赋值为当前位置,即bss段的开始位置*/
.bss (NOLOAD) : { *(.bss) } /*指定bss段,这里NOLOAD的意思是这段不需装载,仅在执行域中才会有这段*/
_end = .; /*把_end赋值为当前位置,即bss段的结束位置*/
}
===================================================================
看来上面的介绍,我那5个问题中有2个问题已经解决了,接着我先到官网去查查段有多少种类型
===================================================================
We have four output sections:
.text program code; 程序代码段
.rodata read-only data; 只读数据段
.data read-write initialized data; 读写初始化数据段
.bss read-write zero initialized data. 读写未被初始化数据段
====================================================================
原来只有4种。又一个问题解决了。
接着我自己想了想BSS段通常是指用来存放程序中未初始化的全局变量的一块内存区。如果加载了就会浪费内存空间,因为没初始化,所以到运行的时候加载也可以,从节约内存的角度来看,所以设计者加了NOLOAD(空载),当然,如果我把NOLOAD删除后,应该也没问题,就是编译出来的bin文件大小会变大。我之后会试一下。
最后一个问题。为什么要这样排列段?不同的段排列是否会影响到程序的运行?暂时不知道。当然,我可以把之前移植完成的uboot中的.lds文件的段修改下,看看会有什么不同的结果。
不过,今天我还不会去尝试,因为那属于很浪费时间的猜测性学习,今天的还有一个任务就是找些实例来学习objdump,readelf,objcopy命令的实际操作做到灵活运行。这样会对我今后的调试起到很大的帮助。
1,objdump实例学习
首先,想到了刚才搜索bss段的时候看到的一个bss段说明的实例中
程序1:
===================================================
int ar[30000];
void main()
{
......
}
程序2:
int ar[300000] = {1, 2, 3, 4, 5, 6 };
void main()
{
......
}
发现程序2编译之后所得的.exe文件比程序1的要大得多。当下甚为不解,于是手工编译了一下,并使用了/FAs编译选项来查看了一下其各自的.asm,发现在程序1.asm中ar的定义如下:
_BSS SEGMENT
?ar@@3PAHA DD 0493e0H DUP (?) ; ar
_BSS ENDS
而在程序2.asm中,ar被定义为:
_DATA SEGMENT
?ar@@3PAHA DD 01H ; ar
DD 02H
DD 03H
ORG $+1199988
_DATA ENDS
区别很明显,一个位于.bss段,而另一个位于.data段,两者的区别在于:全局的未初始化变量存在于.bss段中,具体体现为一个占位符;全局的已初始化变量存于.data段中;而函数内的自动变量都在栈上分配空间。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化,因此造成了上述情况。
============================================================
那么就想到了我自己在ubuntu下可以编写个.c文件,然后再自己写一个lds连接文件,最后编译
1,用gcc -S 选项生成汇编代码
2,用objdump查看汇编代码,同时熟悉此命令的参数
3,这个时候修改不同段的位置设置。调试看效果。
4,把.c文件编译成elf文件。然后用readelf分析其elf结构。
5,最后再用objcopy命令把elf文件格式复制出一共二进制文件。并且通过使用objcopy参数,生成不同大小的二进制文件。
ok,已经为自己设置了要求,接着就是去完成这个要求。2个小时过去了,明天继续。
另外,看了objdump帮助里面比较吸引眼球的参数如下
=========================================================
-t
--syms
Print the symbol table entries of the file. This is similar to the information provided by the `nm' program, although the display format is different. The format of the output depends upon the format of the file being dumped, but there are two main types. One looks like this:
[ 4](sec 3)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .bss
[ 6](sec 1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 fred
where the number inside the square brackets is the number of the entry in the symbol table, the sec number is the section number, the fl value are the symbol's flag bits, the ty number is the symbol's type, the scl number is the symbol's storage class and the nx value is the number of auxilary entries associated with the symbol. The last two fields are the symbol's value and its name.
=========================================================
http://blog.csdn.net/u011701660/article/details/52584165
ld文件解析:
MEMORY:
它是用来补充SECTIONS命令的,用来描述目标CPU中可用的内存区域。它是可选的,如果没有这个命令,LD会认为SECTIONS描述的相邻的内存块之间有足够可用的内存。其实很容易理解但是却很少用(我没用过,嘿嘿),在SECTIONS中每个段的分布都没有考虑ARM能够寻址的地址中,ROM,RAM,FLASH是不是连续的。如果不是连续的怎么办?MEMORY就是设置各个区的起始位置,大小,属性的命令,在一个脚本中只能有一个。
举一个例子:
如果你的板子有两段存储,而且很遗憾的是不是连续的,一段是从0x0开始,大小为256K,另一段是从0x40000000开始的大小为4M,你可以在脚本中写入如下的代码来描述你的板子的内存信息。
1 MEMORY 2 { 3 rom (rx) : ORIGIN = 0 , LENGTH = 256K 4 ram ( ! rx) : org = 0x40000000 , l = 4M 5 }
很显然下面的一句用了简略标签,这并不重要,重要的是怎样使用它,不过在那之前还是想再仔细研究下MEMORY命令的细节。
MEMORY命令的语法是:
MEMORY
{
name (attr) : ORIGIN = origin, LENGTH = len
...
}
name:一个用户定义的名字,Linker将在内部使用它,所以别把它和SECTIONS里用到的文件名,段名等搞重复了,它要求是独一无二的。
attr :如同它的名字一样,这是内存段的属性描述。
`R' Read-only sections.
`W' Read/write sections.
`X' Sections containing executable code.
`A' Allocated sections.
`I' Initialized sections.
`L' Same as I.
`!' Invert the sense of any of the following attributes.
别怪我懒,确实不想再打一遍这个的翻译,而且很久没用英文的俺翻译的估计也不好。总体来说,它是属性就行了。
ORIGIN:这是起始地址
LENGTH:段长
由此可见上面那段实例显示ROM和RAM的明确位置,而且还显示了他们的只能,一个存代码,一个除了存代码什么都可以。
接着就是老问题了,怎么用这个。如果仅仅是规定我的板子有什么特点又不用的话那就是脱了裤子放屁,多此一举。这个问题留在SECTIONS命令中回顾。
SECTIONS:
它是脚本文件中最重要的元素,不可缺省。它的作用就是用来描述输出文件的布局。
SECTIONS命令的语法:
SECTIONS
{
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
这么多的参数中,只有secname和contents是必须的,其他都是可选的参数。也就说它的最简单的格式就是:
SECTIONS
{
...
secname : {
contents
}
...
}
但是注意:secname前后的两个空格是必须的,否则就是不合法输入。
secname定义了段名,其实最开始就忽略了一个重要的因素,arm-gcc-ld脚本需要描述输入和输出,而表面上一看却看不出来什么是输入什么事输入,其实secname和contents就是描述这两个信息的参数。secname是输出文件的段,即输出文件有哪些段,而contents就是描述输出文件的这个段从哪些文件里抽取而来。明确这个了就不难理解为什么SECTIONS命令什么都可以不要就是不能没有这两个参数了。
secname:定义段,但是别以为定义的段一定要是教科书上写的.data,.text这些科班的必须品,你甚至可以创建一个段来放一个美女的图片。
contents:它的语法开始复杂起来了,但是你可以简单的把输入文件写到代码中:
.data : { main.o led.o}
但是结果被列的目标文件中所有的代码都被链接到.data中去了,显然不大符合我们的要求啊。那么还有一种写法:
.data : {
main.o(.data)
main.o(.text) // 也可以这样写 main.o(.data .text)或者main.o(.data , .text)
led.o(.data)
}
这个写法让只有被选中的文件的特殊段被链接到输出文件的.data段了。当然,我们似乎还有更好的写法:
.data : {
*(.data)
}
这样的话,所有目标文件的.data段都被连接到了输出文件中了(这似乎是最常用的方法)。
核心的部分讲完了,开始回顾前面说到了的那些参数:
start:强制链接地址。也许没有讲清楚的是,在SECTIONS中,各个段是按次序排列的,前一个段用到什么地方下一个段接着用,而start就是强迫链接器将当前的段连接到指定的地址中。
.data 0x400000000 : { ..... }
BLOCK(align):说实话,没看懂。只知道用的时候用的比较多的是ALIGN(4)这样的标记,表示排列地址的时候按4的倍数排列,这样做的理由很简单,系统会快。
AT(addr):实现存放地址和加载地址不一致的功能,AT表示在文件中存放的位置,而在内存里呢,按照普通方式存储。至于用处,目前在下不知。
>region:好戏来了,这个region就是前面说的MEMORY命令定义的位置信息。表明当前section所放置的mem有什么特点,如果不符合会怎么样呢?不晓得嘛。
其他略了吧,累了,主要是没中文资料(屁话,有了还用我刻薄吗),其实有点日文资料也行啊,英文我比较苦手)。
注释:
和C语言一样的哦,/**/
其它:
其实ARM-GCC-LD脚本还真的和别的不一样,它的功能要强一些,从manual看,它有三大功能:
1:设置入口函数
2:定义一个变量并赋值
3:描述输入输出文件的链接规则
其实上面的介绍是功能3,1和2都没有讲过。对于arm-gcc-ld脚本来说设置入口函数和定义变量可以在SECTIONS命令大括号里,也可以在外面。语法是:
ENTRY(symbol)
这个symbol应该是某个函数,或者是汇编代码里的一个入口。然而,其实ARM-GCC-LD有很多种方式定义入口,所以当你看到你的脚本里没有这句话而板子运行的很正常的时候别大吃一斤。
1:在连接的时候使用-e参数。
2:在脚本里使用ENTRY
3:如果定义过start这个入口(如果你在汇编里如果本身就有这个名字叫start的入口,那么不用特别的声明也可以)
4:SECTION中.text的第一个入口函数
5:地址为0的指令
其实看了这个我们可以理解是ARM对入口的一个选择优先级,1和2是一样的,显示的指明入口,这也是推荐的方法,没人会觉得程序员故弄玄虚是什么好事情。3是连接器的智能吧,而4和5就是无奈的选择了,程序员没干好的事情CPU只要猜着来处理了,有.text段的话就从它开始执行吧,连.text都没有的就从0x00000000开始执行,至于执行到哪里去了,火星人知道。
关于定义变量,其实一般的脚本都会有的,目的只有一个,给汇编启动代码提地址信息。比如说,一段需要清零的区域在脚本里定义了,而脚本自己不是变形金刚,他不能主动给你清零的,需要你自己的启动代码来清零,清零的代码当然在汇编的启动代码里,它怎么知道需要清零的内存在什么地方?就靠脚本里定义的变量了。没错,事情就是这样的,那也就说一定会有一个提取地址的方法将地址赋给变量了哦,yes!就是小小的一个点"."。
RAM_START = .;
定义了一个RAM_START变量,地址是当前的地址,什么是当前的地址啊?就是链接器在连接的时候根据前面的段排列后的当前位置。当然也可以设置当前位置的值,不过最好不要小于前面排列需要的最小内存。
. = 0x00000000
定义当前地址为0x0。
常用的基本上就这么多了,看一个实例吧:
1 SECTIONS 2 { 3 . = 0x30000000 ; 4 .text : { * (.text) }9 .data : { * (.data) } 10 .rodata : { * (.rodata) }11 Image_ZI_Base = .; 12 .bss : { * (.bss) } 13 Image_ZI_Limit = .;21 .debug_info 0 : { * (.debug_info) } 22 .debug_line 0 : { * (.debug_line) } 23 .debug_abbrev 0 : { * (.debug_abbrev)} 24 .debug_frame 0 : { * (.debug_frame) } 25 }18 PROVIDE (__stack = .); 19 end = .; 20 _end = .;14 __bss_start__ = .; 15 __bss_end__ = .; 16 __EH_FRAME_BEGIN__ = .; 17 __EH_FRAME_END__ = .; 5 Image_RO_Limit = .; 6 Image_RW_Base = .; 7 Image_RO_Base = .; 8 Image_RW_Limit = .;