U-Boot学习(4):u-boot.lds链接脚本分析

发布时间:2024年01月17日

在之前的文章中有介绍U-Boot的编译流程,但我们知道,不同的存储介质可能会接在不同的接口上,如NOR Flash、EMMC和SDRAM等内存的接口是不同的,而不同的接口对应CPU就会映射到不同的内存中。所以如果我们需要运行U-Boot的话,我们就应该根据映射的内存,然后将程序链接到指定的位置。

1 链接脚本分析

1.1 语法介绍

在分析之前,建议先学习lds链接脚本的语法,可以参考我的这一篇文章:lds链接脚本基础与例子分析

SECTIONS {
	...
	secname start ALIGN(align) (NOLOAD) : AT ( ldadr )
	  { contents } >region :phdr =fill
	...
}
  • secnamecontents是必须的,前者用来命名这个段,后者用来确定代码中的什么部分放在这个段中。
  • start:段重定位地址,也称为VMA,即运行地址。如果代码中有位置相关的指令,程序在运行时,这个段必须放在这个地址上。
  • ALIGN(align):虽然start指定了运行地址,但是仍可以使用BLOCK(align)来指定对齐的要求一这个对齐的地址才是真正的运行地址。
  • (NOLOAD):用来告诉加载器,在运行时不用加载这个段。这个选项只有在有操作系统的情况下才有意义。
  • AT (ldadr):指定这个段在编译出来的映象文件中的地址,称为LMA,即加载地址。若不指定默认加载地址等于运行地址。通过这个选项,可以控制各段分别保存在输出文件中不同的位置。
  • >region :phdr =fill:没用到,不作介绍。

1.2 u-boot.lds分析

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、vectorsarch/arm/cpu/armv7/start.otext

前面的__image_copy_start的数组为0,只是起到一个标号的作用,并不占据内存,接下来的链接脚本定义了vectorstext段,看一下u-boot.map文件:

在这里插入图片描述

可以看到紧接着的正是.vectorsarch/arm/cpu/armv7/start.otext段。

我们可以在vectors.S文件中看到.vectors段的声明:

在这里插入图片描述

也就是说在程序的开头放的是向量表的首地址,同时我们注意到第一条指令是b reset,也就是后面就直接跳转到reset标号中执行并不再返回了。

紧接着就是我们的程序,程序就保存在arch/arm/cpu/armv7/start.S文件中。如下图所示,正是reset标号函数的实现:

在这里插入图片描述

主要就是进入特权模式对系统做一些初始化。具体完成了什么操作,在后续的文章中会详细的分析,本篇文章就不介绍了。

2 链接脚本的使用

在没有链接脚本的情况下,我们可以通过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文件,具体参考下面。

3 链接U-Boot到别的地址

前面我们知道,默认的配置中,U-Boot链接到了0x87800000处,所以我们就用grep "87800000" * -nr在目录搜索一下,发现在.config中有定义一个宏定义:

在这里插入图片描述

也就是说U-Boot的链接地址实际上是在xxx_defconfig文件中定义的,这个宏定义在Makefile中被读取,在编译的时候就会将Text段链接到这个位置。所以我们只需要更改这个值就能改变U-Boot的链接地址了。

这里我们将其修改为0x87900000,编译后打开u-boot.map文件看一下向量表的映射地址:

在这里插入图片描述

果然已经改到0x87900000了。

文章来源:https://blog.csdn.net/tilblackout/article/details/135428135
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。