u-boot链接脚本解析

一、关于编译连接脚本

连接脚本是用来描述输出文件的内存布局。

GNU编译器生成的目标文件缺省为elf格式,elf文件由若干段(section)组成,如不特殊指明,由C源程序生成的目标代码中包含如下段:.text(正文段)包含程序的指令代码;.data(数据段)包含固定的数据,如常量、字符串;.bss(未初始化数据段)包含未初始化的变量、数组等。C++源程序生成的目标代码中还包括.fini(析构函数代码)和.init(构造函数代码)等.链接器的任务就是将多个目标文件的.text、.data和.bss等段连接在一起,而连接脚本文件是告诉链接器从什么地址开始放置这些段.简而言之,由于一个工程中有多个.c文件,当它们生成.o文件后如何安排它们在可执行文件中的顺序,这就是链接脚本的作用.

gcc等编译器内置有缺省的连接脚本;但采用缺省脚本,则生成的目标代码需要操作系统才能加载运行。而对于uboot这种需要在嵌入式系统上直接运行的程序,就不能使用编译器缺省脚本、而必须由我们编写连接脚本。

源代码经过编译器编译后包含如下段:

正文段text:包含程序的指令代码;

数据段data:包含固定的数据,如常量和字符串;

未初始化数据段:包含未初始化的变量、数组等。

连接器的任务是将多个编译后的文件的text、data和bass等段连接在一起;而连接脚本文件就是告诉连接器从什么地址(运行时地址)开始放置这些段。

二、arm程序的加载时域和运行时域

简单地说:程序的加载时域就是指程序被加载到什么地方、是SDRAM还是nor flash,运行时域是指程序执行时的地址。

(1)镜像文件的组成

镜像文件包含加载时域和运行时域;

加载时域包含RO和RW段,运行时域包含RO、RW和ZI三个段。

其中RO和RW段的内容在加载时和运行时时一样的,但存储空间可能不同;而ZI段是运行时由初始化函数创建的。

(2)代码、数据和变量在镜像文件中的位置

代码:一般是只读的,由编译器分配存储空间并放置镜像文件的RO段。

数据:这里的数据指常量、指针常量,它们属于只读数据、由编译器分配存储空间放在镜像文件的RO段。

变量:主要根据生存期划分;

1.全局变量和静态变量:由编译器分配存储空间,已初始化的放到RW段、未初始化的放置ZI段。

2.动态变量:局部变量,占用栈空间。

三、编译地址与运行地址

关键词: 地址无关
术语
地址无关: 编译地址不等于运行地址.
地址相关: 编译地址等于运行地址.
常见的一些Boot(如, U-Boot, VIVI)和Linux Kernel代码开始的一段是位置无关的, 意思就是说运行地址与编译地址无关. 如, Kernel编译地址是0xc0008000, 而运行地址是0x30008000.
为什么?
为什么代码的编译地址和运行地址会不相等呢? 原因主要有以下几种: 1) 对于Boot, 用于存放Boot代码的存储器容量小于代码量. 如, Boot片有4K, 而代码通常有50-60K. 这样, 通常会在前4K代码里, 让Boot把自己复制到RAM, 再接着运行.这里我们需要作出一个选择, 是让前面的代码与地址相关, 还是让后面的代码与地址相关呢? 显然我们会选择前面一段代码量小的与地址无关. 2) 对于Linux Kernel, 它是运行在虚拟地址空间的, 如0xc0008000, 但在MMU打开之前, 通常这个地址是
不存在的, 也就是说在MMU打开之前, Kernel的代码必须是地址无关的.
怎么办?
对于位置无关的代码, 寻址是基于pc值的, 在pc值上+/-一个偏移值, 得到运行地址.以ARM为例, 用adr来寻址, adr的实际上是一个伪指令, 在代码编译时, 会被编译器替换成对pc的+/-运算
这里要注意, 对pc的+/-运行显然是有一个地址范围的, 所以我们在上面选择代码量小的地址无关, 是很明智的.
而访问地址相关的代码, 只需要使用其它的寻址指令就行了. 但在这之前, 必须保证代码被放在正确的地址上, 所以通常都会有一个复制代码的过程, 然后就是跳转到一个标号, 地址相关代码就开始运行了.

四、uboot.lds文件解析

lds 文件说明
主要符号说明
1. OUTPUT_FORMAT(bfdname)
   指定输出可执行文件格式.
2.  OUTPUT_ARCH(bfdname)
   指定输出可执行文件所运行 CPU 平台
3. ENTRY(symbol)
   指定可执行文件的入口段


段定义说明
1.  段定义格式
   SECTIONS { ...
      段名 : {
          内容
      }
      ...
   }


u­boot.lds 文件说明

对于.lds文件,它定义了整个程序编译之后的连接过程,决定了一个可执行程序的各个段的存储位置。虽然现在我还没怎么用它,但感觉还是挺重要的,有必要了解一下。

先看一下GNU官方网站上对.lds文件形式的完整描述:

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}

secname和contents是必须的,其他的都是可选的。下面挑几个常用的看看:

1、secname:段名

2、contents:决定哪些内容放在本段,可以是整个目标文件,也可以是目标文件中的某段(代码段、数据段等)

3、start:本段连接(运行)的地址,如果没有使用AT(ldadr),本段存储的地址也是start。GNU网站上说start可以用任意一种描述地址的符号来描述。

4、AT(ldadr):定义本段存储(加载)的地址。

看一个简单的例子:(摘自《2410完全开发》)

/* nand.lds */
SECTIONS { 
firtst 0x00000000 : { head.o init.o } 
second 0x30000000 : AT(4096) { main.o } 
}

    以上,head.o放在0x00000000地址开始处,init.o放在head.o后面,他们的运行地址也是0x00000000,即连接和存储地址相同(没有AT指定);main.o放在4096(0x1000,是AT指定的,存储地址)开始处,但是它的运行地址在0x30000000,运行之前需要从0x1000(加载处)复制到0x30000000(运行处),此过程也就用到了读取Nand flash。

这就是存储地址和连接(运行)地址的不同,称为加载时域和运行时域,可以在.lds连接脚本文件中分别指定。

编写好的.lds文件,在用arm-linux-ld连接命令时带-Tfilename来调用执行,如
arm-linux-ld –Tnand.lds x.o y.o –o xy.o。也用-Ttext参数直接指定连接地址,如
arm-linux-ld –Ttext 0x30000000 x.o y.o –o xy.o。

 

既然程序有了两种地址,就涉及到一些跳转指令的区别,这里正好写下来,以后万一忘记了也可查看,以前不少东西没记下来现在忘得差不多了。。。

ARM汇编中,常有两种跳转方法:b跳转指令、ldr指令向PC赋值。

我自己经过归纳如下:

(1)       b step1 :b跳转指令是相对跳转,依赖当前PC的值,偏移量是通过该指令本身的bit[23:0]算出来的,这使得使用b指令的程序不依赖于要跳到的代码的位置,只看指令本身。

(2)       ldr pc, =step1 :该指令是从内存中的某个位置(step1)读出数据并赋给PC,同样依赖当前PC的值,但是偏移量是那个位置(step1)的连接地址(运行时的地址),所以可以用它实现从Flash到RAM的程序跳转。

(3)       此外,有必要回味一下adr伪指令,U-boot中那段relocate代码就是通过adr实现当前程序是在RAM中还是flash中。仍然用我当时的注释:

relocate: /* 把U-Boot重新定位到RAM */
    adr r0, _start /* r0是代码的当前位置 */ 
/* adr伪指令,汇编器自动通过当前PC的值算出 如果执行到_start时PC的值,放到r0中:
当此段在flash中执行时r0 = _start = 0;当此段在RAM中执行时_start = _TEXT_BASE(在board/smdk2410/config.mk中指定的值为0x33F80000,即u-boot在把代码拷贝到RAM中去执行的代码段的开始) */
    ldr r1, _TEXT_BASE /* 测试判断是从Flash启动,还是RAM */ 
/* 此句执行的结果r1始终是0x33FF80000,因为此值是又编译器指定的(ads中设置,或-D设置编译器参数) */
    cmp r0, r1 /* 比较r0和r1,调试的时候不要执行重定位 */

    下面,结合u-boot.lds看看一个正式的连接脚本文件。这个文件的基本功能还能看明白,虽然上面分析了好多,但其中那些GNU风格的符号还是着实让我感到迷惑。。。

OUTPUT_FORMAT("elf32­littlearm", "elf32­littlearm", "elf32­littlearm")
  ;指定输出可执行文件是 elf 格式,32 位 ARM 指令,小端
OUTPUT_ARCH(arm)
  ;指定输出可执行文件的平台为 ARM
ENTRY(_start)
  ;指定输出可执行文件的起始代码段为_start.
SECTIONS
{
          . = 0x00000000  ; 从 0x0 位置开始
          . = ALIGN(4) ; 代码以 4 字节对齐
          .text      :  ;指定代码段
          {
             cpu/arm920t/start.o   (.text) ; 代码的第一个代码部分
             *(.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 段, uboot 把所有的 uboot 命令放在该段.
          __u_boot_cmd_end = .;把__u_boot_cmd_end 赋值为当前位置,即结束位置
          . = ALIGN(4);
          __bss_start = .; 把__bss_start 赋值为当前位置,即 bss 段的开始位置
          .bss : { *(.bss) }; 指定 bss 段
          _end = .; 把_end 赋值为当前位置,即 bss 段的结束位置
}

五、U-Boot移植过程中的运行地址和装载地址的区别

uboot移植涉及到底层硬件的设置,因此需要掌握UART、系统时钟频率、NOR FLASH、NAND FLASH、SDRAM、网卡、存储控制器等硬件的功能及配置,这些都可以参照相应开发板的芯片手册来完成,没有什么大的问题。在移植过程中,一直困扰我的是PIC(代码无关性)问题,即运行地址和加载地址的区别,看过网上很多关于这两者的介绍,感觉懂一点,却一直不知所然。在参考大量的文献下,算是得了一点心得。
    首先来了解下运行地址及加载地址的区别
    运行地址:也叫链接地址,是程序定位的绝对地址,即在编译连接时确定的地址。如果程序中有位置相关指令,程序在运行时,程序必须在运行地址上。
    加载地址:程序放置的位置。
    运行地址和加载地址的值有时相等,有时却不相等,所以这给初学者带来很大的困扰。为了弄清楚这个问题,还得从NOR FLASH,NAND FLASH,S3C2440内部4KB RAM的映射说起。
   u-boot链接脚本解析_第1张图片
    左边表示从NOR FLASH启动时的映射,右边表示从NAND FLASH启动时的映射。
    这里只讨论从NOR FLASH启动的情况,从图中可以看出NOR FLASH映射到了0X00000000的起始位置,假如UBOOT的代码存放在NOR FLASH上,即装载地址为0X00000000。再来看看UBOOT的链接地址,代码在board/smdk2410/U-Boot.lds里。
u-boot链接脚本解析_第2张图片
    连接脚本文件lds中没有设置LMA,只是设置了VMA。VMA的设置是通过顶层目录下的config.mk文件中的LDFLAGS实现的
     
    在board/smdk2410/config.mk定义了TEXT_BASE = 0x33F80000(SDRAM),即程序的运行地址
查看u-boot.map文件,代码的连接地址是从0x33F80000开始的。

167 .text         0x33f80000        0x232c8
168        cpu/arm920t/start.o(.text)
169        .text                0x33f80000                0x4a0 cpu/arm920t/start.o
170                                0x33f80048                _bss_start
171                                0x33f8004c                _bss_end
172                                0x33f80044                _armboot_start
173                                0x33f80000                _start
174        board/samsung/fs2410/lowlevel_init.o(.text)
175    .text          0x33f804a0         0x64 board/samsung/fs2410/lowlevel_init.o
176                                0x33f804a4                lowlevel_init
177        board/samsung/fs2410/nand_read.o(.text)
178    .text               0x33f80504        0xe8 board/samsung/fs2410/nand_read.o
179                                0x33f80504                wait_idle
180                                0x33f80518                nand_read_ll

    此时装载地址和运行地址明显不一样,为什么程序还能运行呢?这里就涉及到PIC----代码无关设计方面的知识了。在汇编语言中,像bl、b、adr(adr属于伪指令,一般被编译器解释成sub指令)指令属于位置无关指令,不管程序装载在哪个位置上,bl、b、adr指令都能正确的运行,其原因是bl、b、adr指令的地址域是基于PC的相对偏移寻址,相当于[pc+offset]。当ARM启动时,ARM自动取0x00000000位置上的指令,此时PC=0x00000000。
基于PC偏移量的指令都能正确的执行。所以uboot第一阶段指令都能执行的原因在于此。
    但我们回顾一下u-boot的启动过程中的第一阶段有将u-boot代码复制到SDRAM中,并跳到SDRAM中去运行,因为SDRAM映射到了BANK6,其地址为0x30000000,此时uboot代码的地址范围从 TEXT_BASE----TEXT_BASE+size(u-boot),程序是如何跳转的呢?跳转到SDRAM为何还能运行呢?这里就需要看下cpu/arm920t/start.S中的relocate标号。


    relocate: 把norflash中的代码复制到_TEXT_BASE处,在board/smdk2410/config.mk定义了TEXT_BASE = 0x33F80000,这个地址属于BANK6的范围。也就是把代码复制到从_TEXT_BASE地址开始的SDRAM中,当然在复制之前是要初始化SDRAM的,要不然SDRAM没法使用。至此,代码已复制到SDRAM中,那么就要跳到SDRAM中去运行,跳转之前要做stack设置,清BSS,这些就不说了。下面来说如何跳转的,请看下面这条指令。
    ldrpc, _start_armboot
    ldr伪指令中目的寄存器如果是pc,则ldr是与位置相关的指令,u-boot.map文件可以看出,_start_armboot=0x33f80044, 即pc=0x33f80004。这样uboot就跳到SDRAM上去运行了,且这条指令刚好处在其运行地址处,所以程序就能正确的运行

你可能感兴趣的:(u-boot)