在之前的文章中有介绍U-Boot的编译流程,但我们知道,不同的存储介质可能会接在不同的接口上,如NOR Flash、EMMC和SDRAM等内存的接口是不同的,而不同的接口对应CPU就会映射到不同的内存中。所以如果我们需要运行U-Boot的话,我们就应该根据映射的内存,然后将程序链接到指定的位置。
在分析之前,建议先学习lds链接脚本的语法,可以参考我的这一篇文章:lds链接脚本基础与例子分析。
SECTIONS {
...
secname start ALIGN(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
secname
和contents
是必须的,前者用来命名这个段,后者用来确定代码中的什么部分放在这个段中。start
:段重定位地址,也称为VMA
,即运行地址。如果代码中有位置相关的指令,程序在运行时,这个段必须放在这个地址上。ALIGN(align)
:虽然start
指定了运行地址,但是仍可以使用BLOCK(align)来指定对齐的要求一这个对齐的地址才是真正的运行地址。(NOLOAD)
:用来告诉加载器,在运行时不用加载这个段。这个选项只有在有操作系统的情况下才有意义。AT (ldadr)
:指定这个段在编译出来的映象文件中的地址,称为LMA
,即加载地址。若不指定默认加载地址等于运行地址。通过这个选项,可以控制各段分别保存在输出文件中不同的位置。>region :phdr =fill
:没用到,不作介绍。u-boot的链接脚本即目录下的u-boot.lds
:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
*(.__image_copy_start)
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
}
.__efi_runtime_start : {
*(.__efi_runtime_start)
}
.efi_runtime : {
*(.text.efi_runtime*)
*(.rodata.efi_runtime*)
*(.data.efi_runtime*)
}
.__efi_runtime_stop : {
*(.__efi_runtime_stop)
}
.text_rest :
{
*(.text*)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : {
*(.data*)
}
. = ALIGN(4);
. = .;
. = ALIGN(4);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(4);
.efi_runtime_rel_start :
{
*(.__efi_runtime_rel_start)
}
.efi_runtime_rel : {
*(.rel*.efi_runtime)
*(.rel*.efi_runtime.*)
}
.efi_runtime_rel_stop :
{
*(.__efi_runtime_rel_stop)
}
. = ALIGN(4);
.image_copy_end :
{
*(.__image_copy_end)
}
.rel_dyn_start :
{
*(.__rel_dyn_start)
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
.end :
{
*(.__end)
}
_image_binary_end = .;
. = ALIGN(4096);
.mmutable : {
*(.mmutable)
}
.bss_start __rel_dyn_start (OVERLAY) : {
KEEP(*(.__bss_start));
__bss_base = .;
}
.bss __bss_base (OVERLAY) : {
*(.bss*)
. = ALIGN(4);
__bss_limit = .;
}
.bss_end __bss_limit (OVERLAY) : {
KEEP(*(.__bss_end));
}
.dynsym _image_binary_end : { *(.dynsym) }
.dynbss : { *(.dynbss) }
.dynstr : { *(.dynstr*) }
.dynamic : { *(.dynamic*) }
.plt : { *(.plt*) }
.interp : { *(.interp*) }
.gnu.hash : { *(.gnu.hash) }
.gnu : { *(.gnu*) }
.ARM.exidx : { *(.ARM.exidx*) }
.gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}
我们简单地分析一下这个链接脚本:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
指定了输出文件的格式为 elf32-littlearm
。OUTPUT_ARCH(arm)
指定了输出文件的体系结构为 ARM。1、ENTRY(_start)
ENTRY(_start)
指定了程序的入口点为 _start
的值指向的地址。这个肯定就是我们的中断向量表的地址了,我们可以在arch/arm/lib
中发现这个标号,果然在中断向量表定义之前:
接着往下看
SECTIONS
{
. = 0x00000000;
. = ALIGN(4); #地址4字节对齐,由于前面指定为0,所以已经对齐
.text :
{
*(.__image_copy_start)
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
}
...
.image_copy_end :
{
*(.__image_copy_end)
}
...
}
.
为定位器符号,不指定默认为0。存放了某个段后,定位器符号会往后移动这个段的大小长度,所以后面text
段就从0地址开始放。下面的text
段包括__image_copy_start
段、vectors
段和arch/arm/cpu/armv7/start.o
文件的代码段。最后定义了一个__image_copy_end
段。
_*attribute_((section*(name)))
等关键字链接一些特定的函数、变量、文件到特定的段中。2、__image_copy_start
和__image_copy_end
看名字就知道这两个标号用于拷贝操作,分别保存拷贝的起始地址和终止地址,也就是说我们需要拷贝代码,拷贝的范围就是所有的声明在这两个标号中间的段。我们知道链接脚本中的标号是可以直接在C语言中用extern关键字获取的,但这里直接声明了一个段,我们应该如何获取这个地址呢?我们搜索一下:
发现在sections.c
中有如下两个声明:
char __image_copy_start[0] __attribute__((section(".__image_copy_start")));
char __image_copy_end[0] __attribute__((section(".__image_copy_end")));
也就是说定义了两个大小为0的数组,但是将它们链接到了__image_copy_start
和__image_copy_end
段中,所以这两个变量的地址就对应于我们要拷贝的地址。实际上我们可以在链接脚本中声明:
__image_copy_start = .;
__image_copy_end = .;
然后在C语言中通过extern来获取,这样起到的效果也是一样的。
extern unsigned int __image_copy_start;
extern unsigned int __image_copy_end;
代码的拷贝操作在relocate.c
文件中实现。
3、vectors
和arch/arm/cpu/armv7/start.o
的text
段
前面的__image_copy_start
的数组为0,只是起到一个标号的作用,并不占据内存,接下来的链接脚本定义了vectors
和text
段,看一下u-boot.map文件:
可以看到紧接着的正是.vectors
和arch/arm/cpu/armv7/start.o
的text
段。
我们可以在vectors.S
文件中看到.vectors
段的声明:
也就是说在程序的开头放的是向量表的首地址,同时我们注意到第一条指令是b reset
,也就是后面就直接跳转到reset
标号中执行并不再返回了。
紧接着就是我们的程序,程序就保存在arch/arm/cpu/armv7/start.S
文件中。如下图所示,正是reset
标号函数的实现:
主要就是进入特权模式对系统做一些初始化。具体完成了什么操作,在后续的文章中会详细的分析,本篇文章就不介绍了。
在没有链接脚本的情况下,我们可以通过arm-linux-ld
类似的指令的参数指定代码段、数据段和bss段的地址:
-Ttext startaddr #直接指定代码段地址
-Tdata startaddr #直接指定数据段地址
-Tbss startaddr #直接指定bss段地址
例子:
arm-linux-gcc -c -o link.o link.s
arm-linux-ld -Ttext 0x00000000 link.o -o link_elf_0x00000000//启动后PC=0x00000000
arm-linux-ld -Ttext 0x30000000 link.o -o link_elf_0x30000000//启动后PC=0x30000000
而在有链接脚本后,我们同样可以通过-T
来指定链接脚本:
arm-linux-ld -T u-boot.lds -o link_elf_lds link.o
但是这里我们主要是想解决一个疑问,在前面的u-boot.lds
中一开始的起始标号为0,但最终程序链接到了0x87800000处,这是怎么回事呢?这是因为我们可以通过参数对这个地址做一个调整,这样链接脚本中的地址实际上就是一个相对的地址。来看一个例子:
LDSCRIPT := /u-boot.lds
LDFLAGS += -T $(LDSCRIPT) -Ttext 0x87800000
all: u-boot
u-boot: $(OBJECTS)
$(LD) $(LDFLAGS) -o $@ $(OBJECTS)
# 其他 Makefile 规则...
LDSCRIPT
变量指定了链接脚本的路径,然后通过LDFLAGS
将其传递给链接器。另外,-Ttext 0x87800000
参数指定了链接器的起始地址为0x87800000。所以我们只要在Makefile中再定义一下-Ttext
到0x87800000就可以了,来看一下U-Boot的Makefile中的对应的定义:
其中CONFIG_SYS_TEXT_BASE
就是0x87800000,这个值来源于.config
文件,具体参考下面。
前面我们知道,默认的配置中,U-Boot链接到了0x87800000处,所以我们就用grep "87800000" * -nr
在目录搜索一下,发现在.config
中有定义一个宏定义:
也就是说U-Boot的链接地址实际上是在xxx_defconfig
文件中定义的,这个宏定义在Makefile中被读取,在编译的时候就会将Text段链接到这个位置。所以我们只需要更改这个值就能改变U-Boot的链接地址了。
这里我们将其修改为0x87900000
,编译后打开u-boot.map文件看一下向量表的映射地址:
果然已经改到0x87900000了。