嵌入式Linux学习笔记 1-13 代码重定位

目录

1.程序反汇编中所包含的内容

2. 链接脚本

3.拷贝代码和链接脚本的改进

4.代码重定位与位置无关码

5.重定位,清楚BSS段的C函数实现


引入:2440芯片通过内存控制器直接与NorFlash、SRAM、SDRAM相连接,但是NandFlash与内存控制器非直接连接,当中还需要一个NandFlash控制器且NandFlash连地址线都没有,但是即使这样为何我们可以设置NandFlash启动?

        这是因为,当NandFLash中的代码<4K(SRAM的大小)时,会将4K的数据直接写入SRAM,然后CPU从0地址开始运行,0地址对应的SRAM。当NandFLash中的代码>4K时,会借SRAM为载体,把程序复制到SDRAM中,CPU从SDRAM的地址开始运行。这就涉及到了代码重定位,重新确定程序的地址。

        另外,我们也可以分析一下NorFlash,设置Nor启动时,程序从0地址开始运行,这时候的0地址对应的是NorFlash, 这时SRAM的地址为0x4000_0000。NorFlashNor相当于硬盘,可以像内存一样读,但是不能像内存一样轻易写。因此程序存在Nor的时候,对于局部变量存在SRAM中,对于全局变量这种程序需要读写的变量,由于NorFlash不能随意写,所以我们需要把全局变量存在别的地方,比如SDRAM。这样的话,当程序运行的时候,存放在指定的地方的全局变量就可以被读写。有两种方案:

        具体的可以参考以下这篇文章:

=========================================================================

2440的启动方式有两种,norflash和nandflash,怎么选择?

OM[1:0] = 01,10为norflash启动,分别对应16bit和32bit,OM[1:0] = 00为nandflash启动。

两种启动方式:先贴张图。

可以观察到,s3c2440总共有8个内存banks,6个内存bank可以当作ROM或者SRAM来使用,留下的2个bank除了当作ROM 或者SRAM,还可以用SDRAM,7个bank的起始地址是固定的 还有一个灵活的bank的内存地址,并且bank大小也可以改变。

Norflash启动:选择从NOR FLASH启动,上电,nGCS0控制的bank0直接连接了nor flash,S3C2440芯片就会去运行nor flash上地址为0x0处的指令。读nor flash可以像读内存那样读,但是要用额外的命令向nor flash写入数据。如果nor flash像内存那样读和那样写,那nor flash完全可以被内存所替代。

Nandflash启动:CPU上电后,首先会通过几个引脚的电平确定Nandflash的类型,从而读取flash,cpu会自动从NAND flash中读取前4KB的数据放置在片内SRAM里(s3c2440是soc),同时把这段片内SRAM映射到nGCS0片选的空间(即0x00000000)。cpu是从0x00000000开始执行,也就是NAND flash里的前4KB内容。因为NAND FLASH连地址线都没有,不能直接把NAND映射到0x00000000,只好使用片内SRAM做一个载体。通过这个载体把nandflash中大代码复制到RAM(一般是SDRAM)中去执行。

大概流程:

文章转自:    https://www.cnblogs.com/Rainingday/p/7695957.html

=========================================================================

1.程序反汇编中所包含的内容

  • 代码段:text
  • 数据段(全局变量): data                                                          
  • rodata 只读数据段:const全局变量
  • Bss(Block Started by Symbol):初值为0 ,(无初值的全局变量)
  • common段:common段存放弱引用的符号,即未初始化的全局变量,在链接时再放入bss段(有些编译器也会直接放到bss段,故有些文章也直接说未初始化的全局变量在bss段中,这并不冲突)
  • comment:注释

注:Bss段与comment段是不保存在bin文件中的。

2. 链接脚本

        链接脚本主要用于规定如何把输入文件内的section放入输出文件内, 并控制输出文件内各部分在程序地址空间内的布局.

 格式:

SECTIONS{

       ...

       secname start BLOCK(align)(NOLOAD):AT(ldadr)

            {contents}>region:phdr = fill

       ...

       }

  • 这么多参数中,只有secname 和 contents 是必须的
  • start :表示将某个段强制链接到的地址,(放到哪里去运行),也就是运行时的地址或说是重定位地址。
  • AT(addr):实现存放地址和加载地址不一致的功能,AT表示在文件中存放的位置(下载时的存放位置),而在内存里呢,按照普通方式存储。
  • region:这个region就是前面说的MEMORY命令定义的位置信息。 

***************************************************************************************************************************

        另外,需要注意的是,在写链接脚本之前,要先实现重定位的功能,就是把需要用的全局变量从存放地址复制到运行时的地址。否则没有重定位的话,链接脚本只是让程序访问重定位地址而已,里面实际是空的,没有任何值。

***************************************************************************************************************************

重定位代码

bl sdram_init    

    /* 重定位data段 */
    ldr r1, =data_load_addr  /* data段在bin文件中的地址, 即加载地址 */
    ldr r2, =data_start      /* data段在重定位地址, 运行时的地址 */
    ldr r3, =data_end          /* data段结束地址 */

cpy: //拷贝
    ldrb r4, [r1] //从r1读一个字节到r4
    strb r4, [r2] //将r4的值写到r2存放的位置
    add r1, r1, #1
    add r2, r2, #1
    cmp r2, r3
    bne cpy    //如果不相等,继续拷贝
 

链接脚本

SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800)
   {
      data_load_addr = LOADADDR(.data);
      data_start = . ;//.就是当前位置,即data_start = 0x30000000
      *(.data)
      data_end = . ;//data_end - data_start就是数据段的长度 (data_end由数据段确定大小)
   }
   .bss  : { *(.bss) *(.COMMON) }
}

        因此,我们需要修改全局变量运行的地址时,只需要修改链接脚本即可。

        此外,由以上的链接脚本可知,bss段是放在data段之后的空间里,但是在bin文件,elf文件中都不存在bss段。所以bin文件和elf文件是没有留存储空间给未初始化的全局变量或者初值为0的全局变量的。因此当我们重定位了BSS段,然后读取它的时候,看到的未初始化的变量实际都是一些随机的值,而不是我们一开始定义的0。因此我们需要人为地把BSS段清零。

3.拷贝代码和链接脚本的改进

假设需要复制数据到SDRAM,复制16字节,其中Nor位宽为16bit,SDRAM位宽为32bit

ldrb从nor中得到数据:执行16次,访问nor16次
strb写到sdram:执行16次,访问sdram16次
发现一共32次,效率非常低

ldrb:读取一个字节,CPU会先把命令发给内存控制器,内存控制器从sdram中读取四字节(32bit)数据,挑出CPU感兴趣的1字节进行返回

strb:写一个字节,CPU会把地址和数据发送给内存控制器,内存控制器把32的数据发给sdram,同时也会发出屏蔽信号(DQM),比如只需要写一个字节,则会发出3条DQM屏蔽其他不需要写的3字节,选择最终只会写sdram中的一个字节

改进:
ldr从nor中得到数据:执行4次,访问硬件8次
str写到sdram:执行4次,访问硬件4次
一个12次,得到了改进

但是问题也随之而来,当我们采用多字节来代替单字节操作来提高效率的时候,比如四字节一处理,就会面临没有对齐的问题。比如明明程序内存已经用到了0x3000_0002, 但是由于没有对齐,再执行下一条指令的时候,由于是四字节一操作,所以程序会强制对齐,从0x3000_0000开始执行指令,就会覆盖之前的数据,导致我们数据的丢失。因此,我们在lds文件里为了对齐,会多加一条语句,有了下面这条语句以后,程序就可以从0x3000_0004处继续执行,不会覆盖之前的数据。

. = ALIGN(4) ;  // 当前地址向上取整4

这是官方文档Using LD, the GNU linker 中关于ALIGN的解释。

4.代码重定位与位置无关码

        之前的内容是将data段的内容重定位,接下来我们将代码重定位,即将代码段也移到其他地址上去。既然有移动代码,代码中就不能用绝对地址,要使用位置无关嘛,使得其即使移动到其他地方也一样能够使用。

=============================================================================


在生成的bin文件里,代码保存的位置是0x30000000。随后烧写到NOR Flash的0地址,但代码的结构没有变化。之后再重定位到SDRAM。

查看反汇编:

3000005c:   eb000106    bl  30000478 <sdram_init>

30000060:   e3a01000    mov r1, #0  ; 0x0
30000064:   e59f204c    ldr r2, [pc, #76]   ; 300000b8 <.text+0xb8>
30000068:   e59f304c    ldr r3, [pc, #76]   ; 300000bc <.text+0xbc>

这里的bl 30000478不是跳转到30000478,这个时候sdram并未初始化;
为了验证,我们做另一个实验,修改连接脚本sdram.lds, 链接地址改为0x32000478,编译,查看反汇编:

3000005c:   eb000106    bl  30000478 <sdram_init>

30000060:   e3a01000    mov r1, #0  ; 0x0
30000064:   e59f204c    ldr r2, [pc, #76]   ; 300000b8 <.text+0xb8>
30000068:   e59f304c    ldr r3, [pc, #76]   ; 300000bc <.text+0xbc>

可以看到现在变成了bl 30000478,但两个的机器码eb000106都是一样的,机器码一样,执行的内容肯定都是一样的。
因此这里并不是跳转到显示的地址,而是跳转到: pc + offset,这个由链接器决定。

假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x30000478;如果程序从0运行,当前指令地址:0x5c 调到:0x00000478

跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定。反汇编显示这个值只是为了方便读代码。

重点:反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。

怎么写位置无关码?

(1)使用相对跳转命令 b或bl;

(2)重定位之前,不可使用绝对地址,不可访问全局变量/静态变量(因为需要靠绝对地址访问),也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);

(3)重定位之后,使用ldr pc = xxx 来代替 相对跳转的bl main,跳转到/runtime地址;

写位置无关码,其实就是不使用绝对地址,判断有没有使用绝对地址,除了前面的几个规则,最根本的办法看反汇编。

因此,前面的例子程序使用bl命令相对跳转,程序仍在NOR/sram执行,要想让main函数在SDRAM执行,需要修改代码:

 //bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
 ldr pc, =main/*绝对跳转,跳到SDRAM*/

————————————————
版权声明:本文为CSDN博主「今天天气眞好」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_51118175/article/details/117469515

=============================================================================

5.重定位,清楚BSS段的C函数实现

        程序能用C语言写就尽量用C语言写,来代替汇编。若是一定要用汇编,参数可以汇编定,功能用C写,参数<4个时,将参数的值对应地写在R0~R3即可。

        当然,若是地址参数,也可以通过链接脚本来传递。C函数中只需要用extern关键字申明一下该变量。

extern + 类型 + 变量 是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。

C函数怎么使用lds文件总的变量abc?

(1)在C函数中声明该变量为extern外部变量类型,比如:extern int abc;
(2)使用时,要取址,比如:int *p = &abc;//p的只即为lds文件中abc的值

汇编文件中可以直接使用外部链接脚本中的变量,但C函数中要加上取址符号“&”

以上只是编译器的一个小技巧,无需深究,记住即可。

结论:

  • C程序中不保存lds文件中的变量,lds再大也不影响;
  • 借助symbol table保存lds的变量,使用时加上”&”得到它的值,链接脚本的变量要在C程序中声明为外部变量,任何类型都可以;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值