重定位和链接脚本

大部分指令是“位置有关编码”

  • 位置无关编码:汇编源文件编码成二进制可执行程序时,编码方式与位置无关。

在我们写程序时,必须给编译器链接器指定地址。将来的程序被执行时必须放在当时编译链接时给定的地址才能运行。

  • 位置有关编码:汇编源码编码成二进制可执行程序后和内存地址是有关的。

但是也有一种特别的指令他可以跟指定的链接地址没有关系,这些代码不管放在哪里都可以正常运行。

分析:

  之前的裸机程序中,makefile中用-Ttext 0x0来指定链接地址是0x0;这意味着我们认为将来这个程序会被放在0x0这个地址去运行。但是实际上我们在DNW中,把程序下载到0xd0020010. 在s5pv210中,由于在内部做了映射,所以0xd002_0010和0x0000_0010是一样的。那么为什么放在0x0而不是0x0000_0010依旧可以呢?原因就这个是位置无关编码。

  • 链接地址:链接时指定的地址(指定方式:makefile中用T-text,或者链接脚本)
  • 运行地址:程序实际运行的地址(指定方式:由实际运行时被加载到内存那个位置说了算)

在linux中的应用程序:gcc hello.c -o hello

  • 这时候默认的链接地址就是0x0,所以链接在0地址,因为应用程序运行在操作系统的一个进程中,这个进程独享了4G的内存空间,所以应用程序可以链接到0地址,因为每个进程都是从0地址开始的。
  • 210中的裸机程序运行地址由我们下载时确定,下载时下载到0xd0020010,所以就从这里开始运行。(这个下载地址是iROM中的BL0加载BL1时实现指定好的地址,这个是由CPU设计时候决定的)。所以理论上我们编译链接时应该指定到0xd0020010,但是实际上我们之前的裸机程序都是使用位置无关码PIC,所以链接地址可以是0

再分析s5pv210的启动过程

三星推荐的启动方式:

  bootloader必须大于16KB小于96KB,假定为80KB。启动过程如下:开机上电后BL0运行,BL0加载外部启动设备中的bootloader的前16KB(BL1)到SRAM中运行,BL1运行后会加载BL2到(80-16=64KB)到SRAM中运行,BL2运行时会初始化DDR并且将OS搬到DDR中执行,启动完成。

uboot实际使用方式:

  uboot大小随意,假定为200KB,启动过程:先开机上电后BL0运行,BL0会加载外部启动设备中的uboot的前16KB到SRAM运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中,然后用一句长跳转指令从SRAM直接跳转到DDR中继续执行uboot直到uboot完全启动。uboot启动后在命令行中启动OS

为什么需要重定位?

  • 原因:链接地址和运行地址有时候必须不相同,而且不能全部用位置无关码,这种就只能重定位
  • 扩展:分散加载:把uboot分为2部分(BL1和整个uboot)两个部分分别制定不同的链接地址。启动时将两部分加载到不同的地址(BL1加载到SRAM,整个uboot加载到DDR),这时候不用重定位也能启动
  • 评价:分散加载其实相当于手工重定位,重定位是用代码来重定位,分散加载是手工操作重定位

从源码到可执行程序的步骤:

  1. 预编译:预编译器执行。譬如C中的宏定义就是由预编译处理器处理,注释等也是由预编译器处理的
  2. 编译:编译器来执行。把源码.c  .S编译成机器码
  3. 链接:连接器来执行。把.o文件中的各个程序按照一定的规则(链接脚本指定)累积在一起

strip:strip是把可执行程序中的符号给拿掉,以节省空间(debug版本和release版本)

objcopy:由可执行程序生成可烧录的镜像bin文件

程序段的概念:代码段、数据段、bss段(ZI段)、自定义段

段是程序的一部分,我们把整个程序的所有东西分成了一个个段,给每个段起一个名字,然后再链接时可以用和这个名字来指示这些段

段名分为2种:一种是编译器连接器内部定好的,一种是程序员自己指定的,自定义的段名

先天性段名:

  •   代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西
  •   数据段:(.data),数据段就是C语言中显示初始化为非0的全局变量
  •   bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,对应C语言中初始化为0的全局变量

后天性段名:

   段名由程序员自己定义,段的属性和特征也由程序员自己定义。

分析一些问题,跟这里结合,然后试图明白一些本质

  • C语言中全局变量如果未显示初始化,值为0。本质上是因为C语言中我们将这类全局变量放在了bss段,从而保证了为0
  • C运行时环境如何保证显示初始化为非0的变量在main函数之前就被赋值了,就是因为把这类变量放在了.data段了,而.data段会在main函数之前被处理。

链接脚本就是一个规则文件,他是程序员用来指挥链接器用来工作的。链接器会参考链接脚本并且使用其中规定的规则来处理.o文件中的那些段,将其链接成可执行程序。

链接脚本的关键内容有两个部分:段名+地址(作为链接地址的内存地址)

链接脚本的理解:

SECTIONS{}这个是整体链接脚本

. 点号在链接脚本中代表当前位置

= 等号代表赋值

SECTIONS
{
    . = 0xd0024000;
    
    .text : {
            start.o
            *(.text)
    }
    
    .data : {
        *(.data)
    }

    bss_start = .;
    .bss : {
        *(.bss)
    }
    bss_end = .;
}

举例分析

  • 第一点:通过链接脚本将代码链接到0xd00240000
  • 第二点:dnw下载时将bin文件下载到0xd0020010

这两点就保证了:代码实际下载运行在0xd0020010,但是却被链接在0xd0024000,从而为从定位奠定了基础。

我们把链接地址设置为0xd0024000时,隐含意思就是这段代码将来必须放在0xd0024000位置才能正确执行。

如果实际运行地址不是这个地址就要出事(除非代码是位置无关码)。所以重定位代码的作用就是在PIC执行前(代码中第一句位置有关码执行前)必须将整个代码搬移到0xd0024000去执行。

  • 第三点:代码执行时通过代码前段的少量位置无关码将整个代码搬移到0xd0024000
  • 第四点:使用一个长跳转跳转到0xd0024000处的代码处继续执行,重定位完成

长跳转:首先这局代码是一句跳转指令(ARM的跳转指令就是类似于分支指令B、BL等作用指令),跳转指令通过pc(r15)赋一个新值来完成代码段的跳转执行。长跳转指的是跳转到的地址和当前地址差异比较大,跳转的范围比较宽广。

当我们把执行完代码重定位后,实际上在SRAM中有两份代码的镜像(一份是我们下载到0xd0020010处的开头,另一份是重定位代码复制到0xd0024000处开头的),这两份内容完全一样,仅仅是地址不同。重定位之后使用ldr pc, =led_blink这局长跳转指令直接从0xd0020010跳转到0xd0024000开头的那份代码段的led_blink的函数处去执行led_blink。如果短跳转bl led_blink则会跳转到0xd0020010开头的那一份led_blink.

原文地址:https://www.cnblogs.com/jxjl/p/7003718.html