12 IMX6ULL裸机开发:代码重定位

创建时间:2022/1/17 17:09
更新时间:2022/2/22 18:19
作者:gi51wa2j
标签:100ask_IMX6ULL_v11, bingo, 正文


视频教程:https://www.100ask.net/detail/p_5f857338e4b0e95a89c3cdb0/8
因为imx6ull性能较强,BootRom可以自己完成重定位这一步,所以在imx6ull上理解比较不容易,本文仅用快速查阅。
登陆自己微信账号查看第9章代码重定位(IMX6ULL)加深理解
官方参考链接:http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html
在此之前为了方便理解,我们先对程序运行过程做一个简要阐述,  记住一个关键点
  • 链接地址:Link Address是指编译器指定代码和数据所需要放置的内存地址, 由链接器配置(但是注意链接地址是一个期望运行地址,程序并不一定从这里运行
  • 加载地址:Load Address是指程序被实际加载到内存的位置

一、概念理解

1.1 段的概念

     段是程序的组成元素。将整个程序分成一个一个段,并且给每个段起一个名字,然后在链接时就可以用这个名字来指示这些段,使得这些段排布在合适的位置。
     程序分为以下几个段,在程序上电执行start.S时,程序会根据我们启动文件(start.s)中设置的代码段对代码的存放位置进行分配。
针对imx6ULL这款MCU,其各段的概念如下
段名
名称
存放的指令
是否需要重定位
代码段
.text
存放代码本身,不会被修改
不在链接地址上就需要重定位
只读数据段/存放常量数据
.rodata
只读变量,一些常量字符串放在这里。可以放在ROM上,不需要复制到内存
不在链接地址上就需要重定位
数据段
data
有初值的,且非const属性的全局变量、static静态局部变量,需要从ROM复制到内存
如果不在链接地址上,就需要重定位
零初始化段
.bss/ZI
  • 初始值为0的全局变量或静态变量,没必要放在ROM上,使用前清零。
  • 未初始化的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以
不需要重定位,因为程序里面 不保存bss段
注释段
.comment
存放注释

局部变量
保存在栈中,运行时生成
一块空闲空间,使用malloc函数来管理它,malloc函数可以自己写

1.2 重定位概念(加载地址与链接地址)

1.2.1 什么是重定位

保存在ROM上的全局变量的值,在使用前要复制到内存,这就是数据段重定位。

想把代码移动到其他位置,这就是代码重定位。

1.2.2 配合下图理解代码重定位

在Makefile中我们设置了加载地址
在imx6ull.lds中我们设置了链接地址

1.2.3 谁来做重定位

1.2.4 重定位的实质:移动数据

     把代码段、只读数据段、数据段,移动到它的链接地址处。

也就是复制!

     数据复制的三要素:源、目的、长度。

     这3要素怎么得到?

     在GCC中,使用链接脚本来描述。

     在keil中,跟链接脚本对应的是散列文件,散列的意思就是"分散排列",在STM32F103这类资源紧缺的单片机芯片中:

     但是,在资源丰富的MPU板子上:

1.3 对于imx/img文件的理解

1.4 重温IMX6ULL的启动流程

02 IMX6ULL裸机开发:IMX6ULL启动流程解析

1.5 链接地址与加载地址的区别

程序运行时,应该位于它的链接地址处,因为:

但是: 程序一开始时可能并没有位于它的"链接地址":

当加载地址 != 链接地址时,就需要重定位。

二、链接脚本使用与分析

作用:使用链接脚本来获得各个段的信息

2.1 链接脚本示例

SECTIONS {     . = 0xC0200000;       /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */     . = ALIGN(4);     .text      :     {       *(.text)     }     . = ALIGN(4);     .rodata : { *(.rodata) }     . = ALIGN(4);     .data : { *(.data) }     . = ALIGN(4);     __bss_start = .;     .bss : { *(.bss) *(.COMMON) }     __bss_end = .; }

2.2 链接脚本语法

一个链接脚本由一个SECTION组成
一个SECTION里面,还有一个或多个section
SECTIONS { ... secname start BLOCK(align) (NOLOAD) : AT ( ldadr )   { contents } >region :phdr =fill ...   }
section是链接脚本的核心,它的语法如下:
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )   { contents } >region :phdr =fill

2.3 举例

实际上不需要那么复制,不需要把语法里各项都写完。

2.3.1 示例1

SECTIONS {   .text : { *(.text) }            /* secname为".text",里面是所有文件的".text"段 */   .data : { *(.data) }            /* secname为".data",里面是所有文件的".data"段 */   .bss :  { *(.bss)  *(.COMMON) } /* secname为".bss",里面是所有文件的".bss"段和".COMMON"段 */ }

2.3.2 示例2

SECTIONS {   outputa 0x10000 :     /* secname为"outputa",链接地址为0x10000 */     {     first.o                 /* 把first.o整个文件放在前面 */     second.o (.text)        /* 接下来是second.o的".text"段 */     }   outputb :             /* secname为"outputb",链接地址紧随outputa */     {     second.o (.data)        /* second.o的".data"段 */     }   outputc :             /* secname为"outputc",链接地址紧随outputb */      {     *(.bss)                 /* 所有文件的".bss"段 */     *(.COMMON)              /* 所有文件的".COMMON"段 */     } }

2.3.3 示例3

SECTIONS {   .text 0x10000 : AT (0)       /* secname为".text",链接地址是0x10000,加载地址是0 */   { *(.text) }    .data 0x20000 : AT (0x1000)  /* secname为".data",链接地址是0x20000,加载地址是0x1000 */   { *(.data) }   .bss :                       /* secname为".bss",链接地址紧随.data段,加载地址紧随.data段 */   { *(.bss)  *(.COMMON) } }

2.4 使用链接脚本获取各个段的信息

数据复制3要素:源、目的、长度。

怎么知道某个段的加载地址、链接地址、长度?

2.4.1 确定源

可以用ADR伪指令获得当前代码的地址,对于这样的代码:
.text .global  _start _start:     ......     adr r0, _start
adr是伪指令,它最终要转换为真实的指令。它怎么获得_start代码的当前所处地址呢?

实际上,adr r0, _start指令的本质是r0 = pc - offset,offset是在链接时就确定了。

2.4.2 确定目的地址

也就是怎么确定链接地址?可以用LDR伪指令。对于这样的代码:
.text .global  _start _start:     ......     ldr r0, =_start
ldr是伪指令,它最终要转换为真实的指令。它怎么获得_start的链接地址呢?

_start的链接地址在链接时,由链接脚本确定。

2.4.3 获取更详细的信息

在链接脚本里可以定义各类符号,在代码里读取这些符号的值。

比如对于下面的链接脚本,可以使用__bss_start、__bss_end得到BSS段的起始、结束地址:

    __bss_start = .;     .bss : { *(.bss) *(.COMMON) }     __bss_end = .;
上述代码里,有一个".",它被称为"Location Counter",表示当前地址:可读可写。它表示的是链接地址。
. = 0xABC;       /* 设置当前地址为0xABC */ _abc_addr = . ;  /* 设置_abc_addr等于当前地址 */ . = . + 0x100;   /* 当前地址增加0x100 */ . = ALIGN(4);    /* 当前地址向4对齐 */
注意:"Location Counter"只能增大,不能较小。

三、位置无关码

     发现,在重定位函数copy_data执行之前,已经涉及到了片内RAM上的地址,但此时片内RAM上并没有任何程序,那为什么程序还能正常运行呢?

打开反汇编文件:relocate.dis
07 00900000 <_start>:

08   900000:    e59fd00c                ldr           sp, [pc, #12]   ; 900014 <halt+0x4>

09   900004:    fa00016f                 blx         9005c8 <copy_data>

10   900008:    fb000180                blx          900612 <clean_bss>

11   90000c:    e59ff004                ldr           pc, [pc, #4]    ; 900018 <halt+0x8>

12

13 00900010 <halt>:

14   900010: eafffffe          b              900010 <halt>

15   900014: 80200000     eorhi        r0, r0, r0

16   900018: 009001b3                            ; <UNDEFINED> instruction: 0x009001b3

……

009001b2 <main>:

     dis文件中左边的90000xx是链接地址,表示程序运行应该位于这里。但是实际上,我们一上电,boot ROM把程序放到0x80100000去了。所以一开始运行这些指令时,它们是位于DDR里的。

     第9行的blx命令,并不是跳到0x9005c8。这要根据当前的PC值来计算,在dis里写成9005c8,这只是表示“如果程序从0x900000开始运行的话,第9行就会跳到0x9005c8”。现在程序被boot ROM复制到0x80100000,从0x80100000开始运行,我们需要根据机器码来计算出实际跳转的地址。

     blx是相对跳转指令,要跳到“pc + offset”这个地址去。程序从0x8010000运行,运行到第9行时,如下计算新地址:

PC=当前地址+8=0x8010004+8=0x801000C

offset=机器码“fa00016f”里的bit[23:0]*4=0x16f*4=0x5BC

新PC=PC + offset = 0x80105C8

     在0x80105C8这个位置,确实存有copy_data函数,所以:即使程序并不在链接地址0x900000上,它也可以运行。因为blx是相对跳转指令,它用的不是链接地址,它是“位置无关”的。使用“位置无关码”写出的代码,它可以在任何位置上运行,不一定要在“链接地址”上运行。

3.1 上电后程序的执行

  1. 程序被boot ROM重定位到0x80100000,并从这个地址开始执行第一条指令:

此时pc = 0x80100000 + 8 = 0x80100008。

  1. 执行到第2条指令“fa00016f”时,根据上述算法,它跳到地址0x80105C8去执行copy_data函数

  2. 在执行完copy_data和clean_bss函数后,片内RAM 0x900000上已经有程序了。

  3. 执行绝对跳转命令“ldr pc, =main”,它是一条伪指令,真实指令是“ldr pc, [pc, #4] ; 900018 <halt+0x8>”:

从dis文件里很容易看出,执行完这条指令后,pc等于dis文件中“900018”上的值“009001b3”,所以程序跳到片内RAM去执行main函数了。



    注意:在dis文件中,main函数的链接地址是0x009001b2,往pc寄存器里赋值0x009001b3时,bit0为1,表示main函数的代码是用Thumb指令写的。

那么我们应该如何写位置无关码呢?

答:使用相对跳转命令 b或bl,并注意

 


注意

函数指针在被赋值的时候,使用的函数的链接地址,所以没有重复定位的时候,对于程序无法运行(程序的运行与否由链接地址和加载地址之间的关系决定的)