重定位和链接

来源:互联网 发布:java使用 编辑:程序博客网 时间:2024/05/17 04:26

链接和重定位是嵌入式C中很重要的部分,对于这一块掌握的越精细越好。


指令位置分类

指令分为两种:

  • 位置无关编码(PIC):汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关
  • 位置相关编码:汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)相关

在程序设计编译链接过程会给程序一个运行地址,而且必须给编译连接器指定这个地址,最后得到的二进制程序是和指定的链接地址相关的,这个地址叫做”链接地址”。
所以我们在程序编译时其实就已经知道程序将来运行时的地址,这个地址叫做”运行地址”,运行地址和链接地址相关,但是不一定是同一个,程序执行时必须放在指定的链接地址下,否则不能运行,这些程序指令就是位置相关代码,我们之前使用的ld链接器指定的“-Ttext 0x0”就是这个作用,意味着这个程序将来会放在0x0地址去运行。
但是有个别的指令可以和链接地址无关,这些代码在实际运行时放在哪里都可以正常运行,这些指令就是位置无关指令。
链接地址和运行地址可能相同也可能不同,例如我们的“-Ttext 0x0”期望在0x0地址运行,但是实际上我们程序是在内存地址0xd0020010位置运行的,这两个地址看起来不同,但实际上是同一个,因为S5PV210内部做了映射,把SRAM0xd0020000位置映射到了0x0这个地址,因为我们把代码烧写在了这个内存区域,但是大部分的指令都是位置相关代码,这就决定了运行地址和链接地址必须相同,否则一定出错。
在S5PV210中,三星推荐的启动方式,bootloader必须小于96K,并且必须大于16K,如果bootloader是80K,则启动过程为:

  • 上电运行iROM中的BL0,BL0加载bootloader中的前16K到SRAM中作为BL1来运行,
  • BL1运行时加载剩余的BL2部分,也就是80-16=64K,到SRAM中去运行
  • BL2运行会初始化DDR并将OS搬运到DDR中去运行,

在Uboot启动过程中,Uboot的大小是没有限制的,假设Uboot是200K,则启动过程为:

  • 上电运行iROM中的BL0,BL0加载Uboot中的前16K到SRAM中作为BL1来运行,
  • BL1运行时初始化DDR,然后将整个Uboot搬运到DDR中,然后使用长跳转指令从SRAM跳转到DDR中继续执行Uboot16K之后剩余的部分,直到Uboot完全启动。
  • Uboot启动后,在Uboot中使用命令启动OS。

为什么需要重定位?

链接地址和运行地址有时候不能相同,而且不能全部使用位置无关指令,则需要重定位来解决该问题,

运行地址和链接地址

运行地址是在程序运行时决定的,在编译链接阶段没有权利也没有办法决定程序的运行地址。
链接地址是程序在编译链接阶段由-Ttext或者lds链接文件决定,程序在编译连接阶段,程序员期望程序运行在一个合适的地址,就把这个地址作为程序的链接地址。

程序编译步骤

  • 预编译:执行宏扩散,文件包含,条件编译和注释处理等动作,为编译做准备
  • 编译:将源码编译为机器目标代码,文件以.o结尾,目标代码中包含了程序最初的二进制程序,只是还没有被链接起来
  • 链接:将目标代码按照-Ttext或者lds链接成可执行的二进制程序整体,链接时按照链接脚本指定的规则来进行
  • (可选)strip:删除二进制可执行程序中的符号表,也就是函数表,以减少程序体积,符号表可以在程序反编译的时候提供程序的函数信息,但是在实际运行中符号表是没有用的
  • (可选)objcopy:将二进制可执行程序转换为可下载烧写的bin文件,以便烧写到NAND Flash或者其他介质中

程序段

程序段是程序在编译之后由源码得到的程序的组成部分,每个二进制程序被分成了若干段,并对每个段命名,段名分为两种:
- 内置段名:由编译连接器内部的命名规则对段的命名
- 自定义段名:由程序员自己命名的段名

内置段名一般有以下几种:

  • 代码段(.text):又称作文本段,程序中函数被编译之后会被放在代码段中
  • 数据段(.data):程序中被显示初始化且值不为0的全局变量会被编译进数据段
  • BSS段(.bss):又称作ZI(zero initial)程序中,没有被初始化的全局变量或者初始化为默认值的全局变量会被编译进BSS段

链接脚本

链接脚本指定了链接的地址和规则,整个链接操作都按照链接脚本指定的规则进行,程序员通过链接脚本来指挥链接器来处理.o目标代码的段,将其链接到合适的位置,从而形成可执行的程序,链接脚本中关键内容有:
- 段名:定位.o目标代码中的段
- 地址:作为链接地址的内存地址,

将指定的段名防止到指定的连接地址,就完成了该段的链接.
连接脚本示例:

SECTIONS{    . = 0xd0024000; // .代表当期位置    .text : {        start.o        * (.text)    }    .data : {        * (.data)    }    bss_start = .;     .bss : {        * (.bss)    }    bss_end  = .;   }

连接脚本由一个或者多个SECTIONS{}组成:

  • . = 0xd0024000;表示将当前位置的地址设置为0xd0024000,
  • .text : {}表示该段为代码段,下面的.data : {},.bss : {}等同
  • start.o表示start.o在前面,所以start.S会被首先执行
  • *(.text)匹配模式,表示剩余的代码都属于代码段并依次排列
  • bss_start = .;表示将bss_start的值设置为当前地址,bss_end等同,这两个符号之所以定义在这里是为了可以在别的文件中引用,以知道bss段的起止地址

代码重定位

目标:将代码从0xd0020010重定位到0xd0024000,
代码本来是从0xd0020010地址开始运行的,因为BL1从这里开始运行,但是因为特殊原因,我们又需要代码从0xd0024000开始执行,这时候就需要重定位,在某些情况下,重定位是必须的,例如Uboot.

思路

  • 通过链接脚本把代码链接到0xd0024000,因为这个地址是我们期望运行的地址,将上面的链接代码作为link.lds链接,原来的-Ttext 0x0修改为-Tlink.lds即可
  • 烧写的时候,将代码烧写到0xd0020010,在dnw的options中设置烧写地址,这样代码实际运行在0xd0020010,但是却链接在0xd0024000,为重定位做了准备
  • 在位置无关代码执行完毕之前和位置相关代码开始之前,必须将代码搬移到链接地址上去,否则后面的位置相关代码将会出错,所以才需要进行重定义
  • 使用一个长跳转跳转到0xd0024000处的代码继续执行,重定位完成

长跳转

长跳转也就是一个跳转,在ARM中常用分支指令(b,bl)来完成短跳转,跳转通常是指令通过给PC寄存器赋值,从而完成代码跳转执行,长跳转和普通跳转的区别在于长跳转的目标地址和当前地址的距离较大,范围较宽

重定位之后为什么要使用长跳转?

代码重定位之后,我们需要去代码被拷贝的目的地的代码区域执行,这个 目标地址通常会距离我们当前地址较远,所以才需要使用重定位,长跳转示例:ldr pc,=led_blink,这样我们就会跳转到目的地的led_blink函数来执行了,这里不能使用短跳转(b,bl),短跳转会跳转到运行地址附近的led_blink函数,只有使用长跳转,才能跳转到链接地址上的led_blink

重定位

重定位其实就是,在运行地址位置执行一段位置无关代码,这段代码将整个程序拷贝到链接地址上,然后使用一个长跳转,跳转到链接地址的对应函数上去,在链接地址上继续执行,从而实现重定位之后的无缝连接。

重定位实现

重定位代码如下:

// 重定位开始adr r0,_start       // adr 用于加载_start运行地址到r0中ldr r1,=_start      // ldr 加载_start的链接地址到r1中// 获取BSS段起始地址,等于重定位代码的结束地址ldr r2,=bss_start   // bss_start是在lds链接脚本中指定的cmp r0,r1       // 比较_start的运行地址和链接地址beq clean_bss       // 如果相等就直接跳转到clean_bss函数// 如果上一句不相等,则进入代码拷贝阶段copy_loop:ldr r3,[r0],#4      // 拷贝源str r3,[r1],#4      // 存储到目的,长度是bss_start - _start,代码段+数据段的长度cmp r1,r2       // 比较拷贝是否完成,也就是到没到重定位代码的结束地址bne copy_loop       // 拷贝没有完成就继续循环// 如果拷贝完成,进入clean_bss函数,该函数用于清理bssclean_bss:ldr r0,bss_start    // 为了满足C语言的要求,需要清理BSS的ldr r1,bss_end      // 一般是不需要手动清理的,这里我们做了重定位,所以需要手动清理cmp r0,r1       // 比较bss段起始地址和结束地址beq run_on_dram     // 如果相等则直接进入run_on_dram函数mov r2,#0       // 不相等的话就把r2设置为0clear_loop:         // 开始清理bssstr r2,[r0],#4      // 把r2赋值给r0然后r0+=4cmp r0,r1bne clear_loop      // 循环,直到bss_end// 从这一句开始就是位置相关代码了,必须在指定的连接地址开始执行// 所以在这一句之前,必须保证代码已经拷贝完毕run_on_dram:// 长跳转到led_blink开始第二阶段ldr pc,=led_blink   // 长跳转// 重定位完毕

重定位放置在icache开关之后。

SDRAM

SDRAM是DRAM的一种,前面的S代表Sync,表示具有可同步性,常见的DDR就是SDRAM的一种,SDRAM属于动态内存,需要进行初始化之后才能使用,这点和SRAM是有很大的不同。
SDRAM属于SOC外部外设,通过地址总线和数据总线和SOC通信。

SDRAM内存地址

SDRAM使用之前必须要初始化,S5PV210共有两个内存端口,分别是DRAM0和DRAM1,地址范围分别是:

  • DRAM0对应Port1:0x20000000-0x3fffffff,大小为512MB,引脚为xm1
  • DRAM1对应Port2:0x40000000-0x7fffffff,大小为1G,引脚为xm2

可以看到整个S5PV210最多支持1.5G内存,X210开发板上共有512MB内存,DRAM0/1上各有256MB,所以可以得到X210上内存地址为:

  • DRAM0:0x20000000到0x2fffffff,大小为256MB
  • DRAM1:0x40000000到0x4fffffff,大小为256MB

DDR初始化完毕之后,这些地址都是可用的,其他地址则不可用。
每个DDR端口都有三类总线组成,分别是地址总线ADDR14个,控制总线,数据总线32个。
X210开发板共使用了4片内存,每片128MB,数据总线为16bit,通过两个内存之间进行并联,从而实现了数据总线32bit。X210上的每片内存分为8个Bank,CPU通过DDR端口中的BA0-BA2来选择Bank,每个Bank有128Mbit的范围,通过行地址和列地址进行寻址,寻址范围是2的24次方(16MB = 128Mbit)。

代码初始化SDRAM

在进行重定位之前,先去执行SDRAM初始化,可以直接跳转到SDRAM的初始化函数:

// 初始化SDRAMbl sdram_asm_init   // bl短跳转到sdram_asm_init函数进行SDRAM初始化

SDRAM的初始化需要先初始化DRAM0,然后初始化DRAM1,步骤在手册的1.2.1.3

  • 设置IO端口驱动强度,DDR和SOC通过总线链接,物理表现上就是引脚,工作时需要驱动信号,驱动强度就是设置了该信号的强度
  • 设置端口时钟,设置PHYCONTROL寄存器,从而使内存芯片的时钟和CPU的时钟保持一致
  • 设置DDR2,DMC_CONCONTROL,DMC_MEMCONTROL,DMC_MEMCONFIG等一系列寄存器

DRAM初始化之后可以使用DRAM进行重定位,步骤和之前完全一致。

0 0
原创粉丝点击