重定位和连接

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


指令位置分类

指令分为两种:web

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

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

  • 上电运行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。

为何须要重定位?

连接地址和运行地址有时候不能相同,并且不能所有使用位置无关指令,则须要重定位来解决该问题,oop

运行地址和连接地址

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

程序编译步骤

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

程序段

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

内置段名通常有如下几种:spa

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

连接脚本

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

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

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函数,该函数用于清理bss
clean_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设置为0
clear_loop:         // 开始清理bss
str r2,[r0],#4 // 把r2赋值给r0而后r0+=4
cmp r0,r1
bne 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的初始化函数:

// 初始化SDRAM
bl 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进行重定位,步骤和以前彻底一致。