韦一之重定位、位置无关、链接脚本、elf格式(013课)

重定位的引入

nor启动和nand启动都需要进行重定位。
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第1张图片
由图片可知,对2440,芯片内部有cpu.内存控制器,内存控制器外面可以接有sdram,norflash。内部还有4k的sram,nand flash控制器。
CPU发出的命令,地址可以直接到达sdram,norflash,4k sram还有各种控制器(包括nand flash控制器),但是不能直接到达外部nand flash芯片,也就是说2440不能直接给Nand Flsh发送命令。
所以我们的程序可以放在(外部芯片)sdram,nor flash上面,可以直接运行。但是如果程序(bin文件)烧写nand flash,cpu无法直接从nand中取出代码执行。
① nand启动需要重定位
为什么我们还能设置为nand启动呢?
因为nand启动时硬件!会将nand前4K字节复制到2440内部的4k的sram,cpu从sram的0开始运行。
如果程序大于4K呢?前4k的代码需要把整个程序读出,放到sdram(只能放到sdram)。
这就是重定位!!!重新确定程序的地址。
② nor启动更需要进行重定位
如果使用nor启动(nor和nand都是外部的),cpu看到的0地址就是nor上的。片内sram就是从地址0x40000000开始
nor可以像内存一样读,不能像内存一样写!!!!
所以对于nor启动,
mov R0,#0
LDR R1,[R0] 可以读nor flash的0地址数据
STR R1,[R0] 则是无效的,不可以写。虽然CPU执行了这个命令,但是不会影响nor的。(否则程序就会很容易被破坏)

如果程序中含有需要写的全局变量或者是静态变量,不能直接操作,需要把变量重定位!!!放到sdram。
局部变量放在栈中,也就是内部sram中,可读可写。但是全局变量是存放在bin中,烧写在nor flash中的,直接修改变量无效

问:nand是怎样读写呢?可以直接读写吗?

用例子观察现象:

#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"

char g_Char = 'A';  //定义一个全局变量
const char g_Char2 = 'B'; //定义固定的全局变量
int g_A = 0;
int g_B;
int main(void)
{
     
    uart0_init();
    while (1)
    {
     
        putchar(g_Char); /*让g_Char输出*/
        g_Char++;         /* nor启动时, 此代码无效 */
        delay(1000000);
    }
    return 0;
}

编译出来在ubuntu中ls,看到此时和之前讲的一样,bin文件超级大,33909字节,33K,显然不对。查看sdram.dis文件,发现data数据段放在了0x00008474这个地址导致。
在makefile中加入这么一句话
arm-linux-ld -Ttext 0 ** -Tdata 0x800 ** start.o led.o uart.o init.o main.o -o sdram.elf
16进制的800就是十进制的2048。
这时我们的bin文件大小就变为2049。

烧写程序:
烧写在NORFlash 和 烧写在NANDFlash观察这两种的效果。
设置成NANDFlash启动没有问题 显示ABCDE…
设置成NORFlash启动显示AAA…。这是错误现象。
原因:**对于NOR启动时g_Char++; 此代码无效。**因为翻译成汇编是下面这样:
在这里插入图片描述

要保存到r2所指的地方,也就是全局变量g_Char。可是在nor启动时,是不能对nor中内存直接写的,这里写不进去的!!!!
(而如果是sram,sdram中执行是没有问题的,所以nand运行正常。)

查看反汇编码摘取部分内容如下:

Disassembly of section .data:
00000800 <__data_start>:
 800:   Address 0x800 is out of bounds.  //数据段,这里超出界限是啥意思呢?????
Disassembly of section .rodata:
                            //放在只读数据段内
00000474 <g_Char2>:         //const char g_Char2 = 'B';
 474:   Address 0x474 is out of bounds.

Disassembly of section .bss:    //bss段

00000804 <g_A>:             //int g_A = 0;

 804:   00000000    andeq   r0, r0, r0

00000808 <g_B>:             //int g_B;
 808:   00000000    andeq   r0, r0, r0
Disassembly of section .comment:
.。。。。。。

bss段不存在于bin文件中,怎么理解呢?
参考文章http://blog.chinaunix.net/uid-27018250-id-3867588.html
.bss在文件中不占据空间。上例中bin文件正好包含了0X800这个数据段,804和808这个bss变量并不在bin文件中。但是程序执行的时候这些变量也是有实际位置的,只不过初始值都是0,在代码中有bss段清0的语句。
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第2张图片

链接脚本(.lds)引入

链接通过简单的指定选项,参数无法满足需求,需要引入脚本。
参考资料:
Using LD, the GNU linker
http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html
还有一个很详细的博客:https://www.cnblogs.com/li-hao/p/4107964.html

一个有赋值的全局变量,如果设置程序为NOR启动的话,则代码中对这个全局变量赋值是无效的,以为NOR启动是直接从NOR中取代码运行,此全局变量也在bin中存在NOR中,是不可以写的。而如果是NAND启动,是先把前4K读进内存然后取代码执行,内存是可以读写的,所以可以对这种全局变量赋值。
如何使NOR启动时也能赋值呢?
修改makefile,指定数据段-全局变量放在SDRAM中!!!!!(貌似2440必须外接NOR NAND 和SDRAM)
arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o init.o main.o -o sdram.elf(会有空洞,生成的bin有800M)
(上节课的学习,内存sdram是需要初始化的)

(地址是统一编制的)在NOR启动例子中,一个g-char全局变量存在地址0x800,这还是NOR的地址范围。如果把它放在0x30000000,这就是放在SDRAM中了。

如果仅仅这样修改,那么bin文件会从底0开始一直延伸到0x30000000地址,然后此变量占据0x30000001,查看bin文件大小会发现有800M。
(我们的nor flash只有2M,也不可能烧写这么大)
问题就在于bin的代码段和数据段之间存在了一个巨大的间隔,hole空洞。
然后就引入了链接脚本的概念,作用与编译时通过-Ttext 0 -Tdata 0x30000000简单指定参数地址类似,不过简单的选项不能满足需求了。

解决黑洞有两个办法:
第一个方法
把数据段的g_Char和代码段靠在一起;
烧写在Nor Flash上面;
运行时把g_char(全局变量)复制到SDRAM,即0x3000000位置(重定位);
第二个方法
让整个bin文件直接从0x30000000开始,全局变量在0x3……;
烧写Nor Flash上 0地址处;
运行会把整个代码段数据段(整个程序)从0地址复制到SDRAM的0x30000000(重定位);
这两个方法的区别是前者只重定位了数据段,后者重定位了数据段和代码段。
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第3张图片
重定位决定了pc取指令的地址,但是很显然SoC芯片刚刚上电时,重定位还没有实现,SoC需要执行一段代码来实现重定位,所以这时PC指针并没有指向重定位地址的地方,执行重定位代码后才对。

① 第一种办法如何实现
修改Makefile的代码段地址,使用链接脚本sdram.lds指定。
#arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o init.o main.o -o sdram.elf 改编为:
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
(sdram.lds对应同目录下一个文件,就是链接脚本文件,新建,修改内容)
sdram.lds可用notepad编辑,如下:

SECTIONS {
     
   .text   0  : {
      *(.text) } (一开始放代码段,放在0位置。*(.text)指明所有文件的.text代码段)
   .rodata  : {
      *(.rodata) }	(接着放所有文件的rodata只读数据段)
   .data 0x30000000 : AT(0x800) {
      *(.data) } (所有文件的的数码段)(AT(0x800)指定)
   .bss  : {
      *(.bss) *(.COMMON) }
}

其中最重要的一句: .data 0x30000000 : AT(0x800) { *(.data) } //数据段放在0x800,但运行时在0x3000000!!!!!!!

这样加入连接脚本后bin文件大小小了,正常了,但是程序运行不对,因为连接脚本设置了这个全局变量编译后的所在的地址就是0x30000000,main中调用它从汇编代码中看就是从0x30000000取数据,但我们此时并没有在0x30000000放数据。还需要在代码程序中加入重定位代码,将数据段数据(在0x801处的)存入(重定向到)0x30000000。
在汇编代码start.s的main调用之前加入代码。

bl sdram_init (先执行sdram初始化,这个函数是在一个C文件定义的,汇编文件可以随便调用C的函数,不用声明吗?????)
/* 重定位data段 */
	mov r1, #0x800
	ldr r0, [r1]
	mov r1, #0x30000000
	str r0, [r1]
	bl main然后跳转执行main
//初级代码,**仅示意原理,实际不是这样用的**,仅仅在已知1个变量从nor数据段重定位到了sdram
//这是我们肉眼去看连接脚本才知道0x800的值写到30000000,这种方式并不通用!

上面的这种方法,只能复制0x700处的一位数据,不太通用,下面写一个更加通用的复制方法:
链接脚本修改如下:

SECTIONS {
     
   .text   0  : {
      *(.text) }
   .rodata  : {
      *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   /*(写重定位代码时就需要把data_end-data_start这么长的数据段从800复制到0x30000000)*/
   {
      //下面这是添加了几个变量,汇编文件的变量是不需要定义和声明的呀。。
      data_load_addr = LOADADDR(.data);//一个宏,获取.data段的链接地址!!!
      data_start = . ;//(.是当前位置) 等于当前位置。data_start肯定就是等于0x30000000。
      //data段在重定位地址, 运行时的地址!!!!
      *(.data)  //等于数据段的大小.(data段长度就是data_end-data_start)
      data_end = . ;//(.当前位置,链接的时候才能确定!!)
   }
   . = ALIGN(4);
   bss_start = .;
   .bss  : {
      *(.bss) *(.COMMON) }
   bss_end = .;
}

注意: data_load_addr = LOADADDR(.data);//一个宏,data段在bin文件中的地址, 加载地址!!!

问:连接脚本的变量是不需要定义和声明的呀?????而且汇编中就可以随意用这个变量了??????貌似是这样的。

修改start.S:

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 //r1+1
    add r2, r2, #1 //r2+1
    cmp r2, r3 //r2 r3比较
    bne cpy //如果不等则继续拷贝

    bl main

关注一下data_load_addr和data_start 这两个变量,
ldr r1, =data_load_addr在反汇编文件中对应下图第一行:
在这里插入图片描述

在这里插入图片描述
可以看到
data_load_addr的值就是800!!从800地址处取那个bin文件中的全局变量给了r1。

② 第二种方法:将text段和data段都重定位在SDRAM中
其实就是nand启动的时候的操作了,一样的。
(视频第五节课代码重定位与位置无关码讲得就是这个,权且放到这里吧。)
补:重定位的好文章https://blog.csdn.net/u014069939/article/details/81054382
nand启动重定位:https://blog.csdn.net/jiaruitao777/article/details/82906875
注:重定位拷贝代码后应该有两份代码,一份在flash中,一份在SDRAM中

先梳理下把整个程序复制到SDRAM需要哪些技术细节:

  1. 把程序从Flash复制到运行地址,链接脚本中就要指定运行地址为SDRAM地址;
    (在nor flash中最开始的代码,需要完成这样的动作,将整个nor中的代码拷贝到SDRAM中)
  2. 编译链接生成的bin文件,需要在SDRAM地址上运行,但上电后却必须先在0地址运行,这就要求重定位之前的代码与位置无关(是位置无关码);

(上面的方式是分体的程序,即代码段和数据段是分开来存放的,对于我们上面的示例,就是代码段保存在nor flash,数据段保存在sdram中,这种方式适用于单片机,它们本身含有内部flash,内存空间较小,如果将代码段也拷贝到内存中,会造成内存剩余可用空间变小;合体的方式就是我们这个示例中的链接脚本,它适用于嵌入式系统,因为嵌入式系统的内存空间较大)

SECTIONS

{
     
   . = 0x30000000;
   . = ALIGN(4);
   .text      :
   {
     
    *(.text)
   }
   
   . = ALIGN(4);
   .rodata : {
      *(.rodata) }

   . = ALIGN(4);
   .data : {
      *(.data) }

   . = ALIGN(4);                          
   __bss_start = .;

   .bss : {
      *(.bss) *(.COMMON) }
   _end = .;
}

现在我们写的这个链接脚本,称为一体式链接脚本,对比前面的分体式链接脚本区别在于代码段和数据段的存放位置是否是分开的

例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss段,都是连续在一起的。
分体式链接脚本则是代码段、只读数据段,中间相关很远之后才是数据段、bss段。

我们以后的代码更多的采用一体式链接脚本,原因如下:

  1. 分体式链接脚本适合单片机,单片机自带有flash,不需要再将代码复制到内存占用空间。
    (代码段保存在nor flash,数据段保存在sdram中,这种方式适用于单片机,它们本身含有内部flash,内存空间较小,如果将代码段也拷贝到内存中,会造成内存剩余可用空间变小。)
    而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,就需要从Nand Flash或者SD卡复制整个代码到内存;
  2. JTAG等调试器一般只支持一体式链接脚本;

链接脚本只是告诉程序需要将某些段拷贝到哪些内存位置,但是它本身不完成拷贝的动作,这个动作是start.S完成的,因此在start.S文件中,我们需要将text段、rodata段,data段整个拷贝到sdram对应的位置。

    /* 重定位text, rodata, data段整个程序 */
    mov r1, #0
    ldr r2, =_start         /* 第1条指令运行时的地址 */
    ldr r3, =__bss_start    /* bss段的起始地址 */

cpy:
    ldr r4, [r1]
    str r4, [r2]
    add r1, r1, #4
    add r2, r2, #4
    cmp r2, r3
    ble cpy


    /* 清除BSS段 */
    ldr r1, =__bss_start
    ldr r2, =_end
    mov r3, #0
clean:
    str r3, [r1]
    add r1, r1, #4
    cmp r1, r2
    ble clean

   // bl main  //注:之前是这样写的,但是到现在的情景,这里是错误的!!!!
    //这里是位置无关码,相对跳转指令,后面会说,所以还是跳到nor中的main所在地方执行的。而我们本意是想跳到sdram中的main函数执行,所以这里应该用绝对跳转指令!!!!!!!
    //应该使用:
	**ldr pc, =main**/*绝对跳转,跳到SDRAM*/
	//重定位之后,可以使用绝对地址ldr pc, =xxx跳转到runtime addr。
halt:
    b halt


上面的_start哪里来的标号呢?
汇编启动文件的最开始:.global _start。
Linux寻找这个_start标签作为程序的默认进入点。_start是一个函数的起始地址,也是编译、链接后程序的起始地址。
由于程序是通过加载器来加载的,必然要找到_start名字的函数,因此_start必须定义成全局的(.global)。

最后的绝对跳转这样也可以:

ldr pc, =sdram  /* 跳转到SDRAM执行 */
sdram:	//这样
	bl main 

韦一之重定位、位置无关、链接脚本、elf格式(013课)_第4张图片
bin文件,开始的运行地址是30000000。
烧写是烧到nor flash,nor flash启动从0地址运行。nor中前部分代码把整个程序复制到sdram,实现重定位。

看一下反汇编:其中有一句:
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第5张图片

这里的bl 30000478不是跳转到30000478!!因为代码执行到这里还没有进行sdram的初始化呢,跳到sdram就死了。
为了验证,我们做另一个实验,修改连接脚本sdram.lds, 链接地址改为0x32000478,编译,查看反汇编:
在这里插入图片描述
变成了32000478,但是两个的机器码eb000105都是一样的,机器码一样,执行的内容肯定都是一样的。

因此这里并不是跳转到显示的地址,而是跳转到: 当前pc + 某个offset,这个offset是由链接器算出来的,至于跳到哪里,是个当前PC决定的。
假设程序从0x30000000执行,当前指令地址就是:0x3000005c ,那么就是跳到0x30000478;如果程序从0运行,当前指令地址:0x5c 调到:0x00000478。
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第6张图片
跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定。反汇编显示这个值只是为了方便读代码。
重点:
反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。

所以,上面这个代码指定的运行地址是0x30000000,但是我们把它放在0地址也是可以运次,因为它使用的是偏移地址,仍然可以跳到正确的代码位置,跟链接地址没有关系。
这就是位置无关码的作用!!位置无关码:程序在任何位置都可以运行,不需要放到运行地址。
我们想写位置无关码,只能用相对跳转命令 b或bl。不可以直接跳转到绝对位置。

相对跳转:b/bl
说明:相对跳转,它是相对于当前运行时所处的环境而言的。比如说现在在NorFlash运行代码,则会跳到NorFlash上去执行代码。而不会去SDRAM上去执行代码
eg:
bl main

绝对跳转:ldr pc, =???
当NorFlash上完成了SDRAM的初始化和重定位后,就可以使用绝对跳转。使程序从NorFlash上跳转到SDRAM上。
eg:
ldr pc, =main

位置无关码/相对跳转指令

怎么写位置无关码?
① 使用相对跳转命令 b或bl;
重定位之前,必须都用位置无关码写,不可使用绝对地址,
也就是不可访问全局变量、静态变量,也不可访问有初始值的数组(因为初始值放在rodata里,会使用绝对地址来访问);
③ 重定位之后,使用ldr pc = xxx,跳转到/runtime地址;
写位置无关码,其实就是不使用绝对地址,判断有没有使用绝对地址,除了前面的几个规则,最根本的办法看反汇编。

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

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

补充:
代码中有bl sdram_init,bl是相对跳转,仍然在nor中进行。看C语言写的 sdram_init函数代码:

void sdram_init(void)
{
     
	BWSCON = 0x22000000;
	BANKCON6 = 0x18001;
//。。。。。。都是一些寄存器的赋值
}

对应反汇编:
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第7张图片

寄存器的赋值应该=算是位置无关码,和绝对相对地址没关系的。所以这段代码在nor中一样可以运行。
而如果sdram_init函数是下面这样写的,就不行了,用到了有初始值的数组就不行!!
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第8张图片
(《韦东山的书籍里面对位置无关也有详细论述》)

怎样判断有没有用到位置有关码,最根本的办法就是看反汇编,怎么看呢?
反汇编段中有:
在这里插入图片描述
这样的就是位置无关码,根据当前PC的值通过offset计算得到一个地址,后面的30005a8仅是方便我们看的。
根据当前pc去取变量值,无论放到什么地址都是可以执行的,不用非得弄到运行地址。
但是下面这样就是位置有关了:
在这里插入图片描述
r3的值就是30000708。接着:
在这里插入图片描述
去30000708的地方读内存,放到r0-r3,这就是绝对位置了!!这就是位置有关码了!!
而这时候这时sdram初始化过程中,还没初始化完,数据也还没有复制到sdram,这肯定是有问题的!!!

回头看,链接脚本的格式

在链接脚本中,我们都需要指出这几个段在内存中的分配情况。

韦一之重定位、位置无关、链接脚本、elf格式(013课)_第9张图片
(划掉的表示一般都不用的) secname和contents是必须的,其他可选。
① secname:段名,用来命名此段
② start :是段的重定位地址的起始地址,即本段运行的地址。
运行时的地址(runtime addr);重定位地址(relocate addr)。两个名字都是一个意思。
start可以用在任意一种描述地址的符号来描述??
③ AT ( ldadr ) :加载地址,也就是本段在bin文件中原本的地址,重定位之前的地址不写时LoadAddr = runtime addr,加载地址等于运行地址。通过这个选项可以控制隔断分别保存于输出文件中不同的位置。
④ contents:决定哪些内容放在本段,可以是整个目标文件(.o)也可以是目标文件中的某段(代码段,数据段等)。
start.o //内容为整个start.o文件
*(.text) //所有文件的代码段都放这里

start.o *(.text) // start.o 放在前面,所有o文件的text段后面接着放。

BLOCK(align)指定快对齐。比如,前面一个段从0x30000000到0x300003F1,此处标记ALIGN(4),表示此处最小占用4Bytes,即使下一个段是紧挨这个段,那么下一个段的起始位置(也就是运行地址)为0x0x300003F4。
NOLOAD:告诉加载器程序运行时不加载该段到内存???

elf文件格式,以及和bin区别

makefile中:
在这里插入图片描述
用链接脚本链接生成elf格式文件,这个elf中就含有那些地址信息,比如加载地址loadaddr。
AT ( ldadr ) :加载地址。如果没有,那么加载地址就等于前面的运行时地址。

ELF文件一种UNIX文件格式,全称是executable and linkable format,即可执行链接格式,在UNIX系统及linux、BSD等类UNIX系统中广泛使用。这种文件格式主要作为链接目标文件用。
elf格式有个头部,结构体。在linux中执行的时候根据头部获得一些信息。我们先只关注一个点吧:
首先是e_entry,这个成员描述的是可执行文件加载到内存中以后机器可执行的程序的入口地址!首先,e_enty的值是一个地址,指向内存中的一个单元;其次,e_entry指向的内存单元是存储CPU执行该ELF文件时执行的第一条指令的。简单说就是一个程序源代码中无论如何都会有一个唯一的入口,一般情况下,在有OS的情形下,对应C程序中的main,而在裸奔的情况下,一般都会用一个_start来指定程序的入口,这些标示符在源代码被链接成可执行文件并被加载到内存中之后,都会有一个明确的地址,CPU从该地址而且仅能从该地址开始执行这个程序才能正确执行,这个地址在链接成ELF文件时就被连接器写到ELF文件头中的e_entry成员中了,CPU正是通过e_entry才知道程序加载到内存之后该从什么地址(CPU是不认识程序符号的,比如main,_start之类的,CPU只知道地址)开始执行这个程序。

elf和bin:
Gcc 编译出来的是ELF文件。通常gcc –o test test.c,生成的test文件就是ELF格式的,在linuxshell下输入 ./test就可以执行。elf(executable and link format)文件里面包含了符号表,汇编等。

Bin 文件是经过压缩的可执行文件,去掉ELF格式的东西。是直接的内存映像的表示。在系统没有加载操作系统的时候可以执行。Bin文件是最纯粹的二进制机器代码。
BIN文件是将elf文件中的代码段,数据段,还有一些自定义的段抽取出来做成的一个内存的镜像。如果下载运行,则下载到编译时的地址即可。可以直接在裸机上运行。
在Embedded中,如果上电开始运行,没有OS系统,如果将ELF格式的文件烧写进去,包含一些ELF格式的东西,arm运行碰到这些指令,就会导致失败,如果用arm-softfloat-linux-gnu-objcopy生成纯粹的汇编 bin文件,程序就可以一步一步运行。

可由elf文件转化为bin文件,bin不能转化为elf文件,因为elf的信息量要大。
bin文件可以在裸机上运行,而ELF文件是在有OS的环境中运行的。

两种文件都可以运行
机器最终只认BIN,之所以有ELF格式是在有操作系统时,操作系统会根据ELF解析出代码、数据等等,最终仍是以BIN运行。由于elf文件的信息比较全,所以可以用来以单步跟踪的方式运行。关键是看loader。

BIN文件需要用objcopy工具,将ELF中的上述信息拷贝出来。(objcopy只是完成两个obj文件内容的拷贝,那如何控制elf生成bin呢?)

elf执行过程:我们主要关注加载地址,运行地址。
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第10张图片
(使用加载器把elf文件读入内存的加载地址(load addr),然后运行程序。)

我们在裸机中没有加载器的时候,就要用bin文件,靠硬件机制启动:
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第11张图片

再看链接脚本:
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第12张图片
前两个段没有用AT,所以加载地址=运行地址。使用nor 和nand启动时,代码段和iodata段都会读到0地址开始的地方,它们是吻合的,不需要重定位。
*(.text)表示放所有文件的代码段,各个文件顺序是怎样呢?
按照makefile中指定的来放的!也就是下面这个顺序。
arm-linux-ld -Ttext 0 -Tdata 0xe80 start.o led.o uart.o lib1funcs.o my_printf.o main.o -o uart.elf

实验说明为什么bss段要清零:(对应代码\012_relocate_013\005_013_003)

int g_A = 0;
printHex(g_A);
void printHex(unsigned int val)
{
     
	int i;
	unsigned char arr[8];

	/* 先取出每一位的值 */
	for (i = 0; i < 8; i++)
	{
     
		arr[i] = val & 0xf;
		val >>= 4;   /* arr[0] = 2, arr[1] = 1, arr[2] = 0xF */
	}

	/* 打印 */
	puts("0x");
	for (i = 7; i >=0; i--)
	{
     
		if (arr[i] >= 0 && arr[i] <= 9)
			putchar(arr[i] + '0');
		else if(arr[i] >= 0xA && arr[i] <= 0xF)
			putchar(arr[i] - 0xA + 'A');
	}
}

很简单,就是把g_A按照十六进制打印出来。最终g_A等于莫名奇妙的值,并不等于0。所以需要清理bss段。
修改lds链接文件:

SECTIONS {
     
   .text   0  : {
      *(.text) }
   .rodata  : {
      *(.rodata) }
   .data 0x30000000 : AT(0x700) 
   {
      
      data_load_addr = LOADADDR(.data);
      data_start = . ;
      *(.data) 
      data_end = . ;
   }
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   bss_start = .; //bss开始地址是当前位置
   .bss  : {
      *(.bss) *(.COMMON) }
   bss_end = .; //bss结束地址也是当前位置
}

修改start.s,清除bss段:

/* 清除BSS段 */
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
    strb r3, [r1]
    add r1, r1, #1
    cmp r1, r2
    bne clean

    bl main
halt:

现在的代码全局变量就是为0,通过几行代码,就可以少几十个甚至上千个全局变量的存储空间。因为加入要把bss加入bin文件,那么很多的0初值的变量都要占用空间了。

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

前面重定位时,需要ldrb命令从的Nor Flash读取1字节数据,再用strb命令将1字节数据写到SDRAM里面。

cpy:
    ldrb r4, [r1] /*首先从flash读出一个字节*/ 
    strb r4, [r2] /*让后把数据写到SDRAM*/
    add r1, r1, #1
    add r2, r2, #1
    cmp r2, r3
    bne cpy

JZ2440上的Nor Flash是16位,SDRAM是32位。
假设现在需要复制16byte数据,
采用ldrb命令每次只能加载1byte,因此CPU需要发出16次命令,内存控制器每次收到命令后,访问硬件Nor Flash,因此需要访问硬件16次;
同理,访问SDRAM时,CPU需要执行strb 16次,内存控制器每次收到命令后,访问硬件SDRAM,也要16次,这样总共访问32次。

现在对其进行改进,使用ldr从Nor Flash中读,ldr命令每次加载4字节数据,因此CPU只需执行4次,但由于Nor Flash是16位的,内存控制器每次收到CPU命令后,需要拆分成两次访问,因此需要访问硬件8次
使用str写SDRAM,CPU只需执行4次,内存控制器每次收到命令后,直接硬件访问32位的SDRAM,因此这里只需要4次,这样总共访问只需要12次。
在整个操作中,花费时间最长的就是硬件访问,改进后代码,减少了硬件访问的次数,极大的提高了效率。

代码编译烧写,最后程序运行有问题,现象就是:
我们定义的有初始值的全局变量char g_Char = ‘A’;最后用void printHex(unsigned int val)输出的时候也全部成了0。(而且用int putchar(int c)打印根本没输出)。
原因:在bss段清0的时候把全局变量给破坏了。

clean:
    str r3, [r1] //就是这句话搞得问题!str不仅把bss段清除,把全局变量这些也清除了。注释掉此句话,可以正常输出全局变量了。
    add r1, r1, #4
    cmp r1, r2
    ble clean

    bl main

问题出在哪里呢?打开dis反汇编看一下:

0000006c <cpy>:
  6c:	e5914000 	ldr	r4, [r1]
  70:	e5824000 	str	r4, [r2]
  74:	e2811004 	add	r1, r1, #4	; 0x4
  78:	e2822004 	add	r2, r2, #4	; 0x4
  7c:	e1520003 	cmp	r2, r3
  80:	dafffff9 	ble	6c <cpy>
  //关键从这里开始了:
  84:	e59f1038 	ldr	r1, [pc, #56]	; c4 <.text+0xc4>
  88:	e59f2038 	ldr	r2, [pc, #56]	; c8 <.text+0xc8>
  8c:	e3a03000 	mov	r3, #0	; 0x0

00000090 <clean>:
  90:	e5813000 	str	r3, [r1]//把r3的0存到【30000002】,以四字节存放。
  94:	e2811004 	add	r1, r1, #4	; 0x4
  98:	e1510002 	cmp	r1, r2
  9c:	dafffffb 	ble	90 <clean>
  a0:	eb000149 	bl	5cc <main>
  ......
  c4:	30000002 	
  c8:	3000000c 	

问题所在:
90: e5813000 str r3, [r1]//把r3的0存到【30000002】,以四字节存放。
但是30000002这个地址并不是四字节对齐的!!str命令会把0存放到向四取整的地方去(???问什么呢,可能是它的特性吧)。
所以实际是str r3, [30000000]。
而30000000正是我们全局变量的地址(回想sdram起始地址就是30000000,全局变量需要重定位就弄到这里了。)

这是怎么回事呢?因为在链接脚本中bss段就是紧接着data段放的。
这时候就需要加上对齐的指令了:
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第13张图片
. = ALIGN(4); //让当前地址向4对齐
上面data段的向四对齐可以省略,因为我们手工指定了30000000这个地址,本来就是对齐的。
但是如果是把代码段和数据段都重定位到sdram的那种情况,在data段也要考虑对齐!!
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第14张图片

代码重定位与位置无关码

本节的内容就是将整个代码,包括代码段和数据段,都重定位到sdram运行。内容已经写在了前面。

重定位清除BSS段的C函数实现及C代码引用脚本变量

清除BSS段的汇编并不复杂,不过老师说了“能用C的时候就不用汇编”。

1.打开start.S把原来的汇编代码删除改为调用C函数。
原来的:

    /* 重定位text, rodata, data段整个程序 */
    mov r1, #0
    ldr r2, =_start         /* 第1条指令运行时的地址 */
    ldr r3, =__bss_start    /* bss段的起始地址 */

cpy:
    ldr r4, [r1]
    str r4, [r2]
    add r1, r1, #4
    add r2, r2, #4
    cmp r2, r3
    ble cpy


    /* 清除BSS段 */
    ldr r1, =__bss_start
    ldr r2, =_end
    mov r3, #0
clean:
    str r3, [r1]
    add r1, r1, #4
    cmp r1, r2
    ble clean

改为

    /* 重定位text, rodata, data段整个程序 */
    mov r0, #0
    ldr r1, =_start         /* 第1条指令运行时的地址 */
    ldr r2, =__bss_start    /* bss段的起始地址 */
    sub r2, r2, r1          /*长度*/

    bl copy2sdram  /* src, dest, len */

    /* 清除BSS段 */
    ldr r0, =__bss_start
    ldr r1, =_end

    bl clean_bss  /* start, end */

在init.c 实现如上两个C函数:

void copy2sdram(volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)  /* src, dest, len */
{
     
    unsigned int i = 0;

    while (i < len)
    {
     
        *dest++ = *src++;
        i += 4;
    }
}


void clean_bss(volatile unsigned int *start, volatile unsigned int *end)  /* start, end */
{
     
    while (start <= end)
    {
     
        *start++ = 0;
    }
}

汇编中,为C语言传入的参数,依次就是R0、R1、R2。

插入:反思:bss段到底是啥意思?
初值为0的变量不放入bin文件中。
在Bin文件的代码中,或者说汇编代码中,涉及到这些变量的仍然是一个地址值,只不过是这个地址值不在bin的包含范围内。程序运行起来,对这些变量的取值和操作还是在这个固定的地址上的。(不知道咋说了,算了。)

我们假设不想汇编传入参数,而是C语言直接取参数。也就是
C代码直接取链接脚本中的变量来用!
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第15张图片
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第16张图片
韦一之重定位、位置无关、链接脚本、elf格式(013课)_第17张图片

总结:
*C函数怎么使用lds文件总的变量abc?
① 在C函数中声明改变量为extern外部变量类型,比如:extern int abc;
② 使用时,要取址,比如:int p = &abc ;//p的只即为lds文件中abc的值

注意为什么是&abc???
汇编文件中可以直接使用外部链接脚本中的变量,但C函数中要加上取址符号。
比如在汇编中很多类似于:ldr r3, =__bss_start 。都是直接使用的。

解释一下原因:
C函数中,定义一个全局变量int g_i;,程序中必然有4字节的空间留出来给这个变量g_i。假如我们的lds文件中有很多变量,如果我们C程序只用到几个变量,完全没必要全部存储lds里面的所有变量,C程序是不保存lds中的变量的!
对于万一要用到的变量?
编译程序时,有一个symbol table符号表:
(符号表中不仅仅是链接脚本的变量哦!!!所有C用到的变量都有!!只是顺便也放下链接脚本的变量)

韦一之重定位、位置无关、链接脚本、elf格式(013课)_第18张图片

韦一之重定位、位置无关、链接脚本、elf格式(013课)_第19张图片
(符号表图中开始几个是C中定义的变量。)
注意:后两个的ADDR本来也是要像普通变量一样放地址的,不过链接脚本的变量特殊,这里放的不是地址,而是变量的值!!!!!

编译链接的时候会确定脚本中变量的值,所以说这里面保存的不是变量了,是个常量!!

如何使用symbol table符号表?
对于常规变量g_i,得到里面的值,使用&g_i得到addr;
(普通的变量编译器就是根据变量名去符号表找到那一项,编译器规定了&g_i的作用就是找到这一项的后半个格子的内容,在这来说也就是地址了。)
为了保持代码的一致,对于lds中的a1,使用&a1得到里面的值!!(根据这个a1的变量名去符号表里找到a1这一项,也是认为&a1取出的是这一项的后半部分内容,本来应该是地址的,脚本变量用来装值了,所以得到变量值)

这挺有意思!!!!!!!!!!学习一下!!!!!!!!!!!!!

这只是一个编译器的小技巧,不用深究。
结论:
C程序中不保存lds文件中的变量,lds再大也不影响;
借助symbol table保存lds的变量,使用时加上”&”得到它的值,链接脚本的变量要在C程序中声明为外部变量,任何类型都可以;

如果再不理解,可以参考如下:
C代码中如何使用链接脚本中定义的变量
http://www.100ask.org/bbs/forum.php?mod=viewthread&tid=16231&highlight=%C1%B4%BD%D3%BD%C5%B1%BE
上面这个帖子是参考下面这个翻译出来的:
参考文章:https://sourceware.org/ml/binutils/2007-07/msg00154.html

暂时没看的一个文档,行行注释,uboot的lds文件分析,也不是很长:
https://www.cnblogs.com/liulipeng/archive/2013/07/19/3200496.html

测试一下csdn代码中使用加粗会有效果吗:

撒大声地
回车
**ldr pc, =main**会加粗吗?

貌似是没有效果的,但是以前我发的博客不想改了,看到代码段有前后两对星号的,知道是想加粗没成功就行了。

你可能感兴趣的:(韦东山linux笔记(第一期))