• 链接脚本与重定位



    title: 链接脚本与重定位
    tags: ARM
    date: 2018-10-12 19:25:53

    链接脚本与重定位

    学习视频 韦东山

    总结

    1. 尽量使用一体式的链接脚本,方便简单,灵活
    2. 学会使用链接脚本的值
    3. bss段和comm段是需要我们手动去清除的
    4. 位置无关码相关:
      1. 局部数组是位置无关的,但是其数组的初始值在链接地址中,所以over
      2. 不能使用全局变量和局部变量
      3. 跳转使用bl
    5. 重定位代码的第一次跳转需要使用ldr才能跳出去
    6. 重定位之前,只能使用相对地址,使用bl跳转

    链接脚本格式

    SECTIONS {
    ...
    secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
      { contents } >region :phdr =fill
    ...
    }
    
    1. secname, contents是必须的,其他可选
    2. secname:段名,用以命名此段。
    3. contents:决定哪些内容放在本段,可以是整个目标文件(.o),也可以是目标文件中的某段(代码段、数据段等)。start.o或者这样start.o *(.text)
    4. start:是段的重定位地址,即本段运行的地址。如果代码中有位置无关指令,程序运行时这个段必须放在这个地址上。start可以用任意一种描述地址的符号来描述。
    5. BLOCK(align) 指定块对齐。比如,前一个段从0x30000000到0x300003F1,此处标记ALIGN(4),表示此处最小占用4Bytes,即使下一个段是紧挨这个段,那么下一个段的起始地址(也就是运行地址)为0x300003F4。
    6. NOLOAD:告诉加载器程序运行时不加载该段到内存。
    7. AT(ldadr):定义本段存储的地址,如果不使用这个选项,则加载地址等于运行地址,通过这个选项可以控制各段分别保存于输出文件中不同的位置。

    注意 一般需要4字节对齐,如果代码段和数据段的重定位使用4字节读写

    最简单的链接脚本

    SECTIONS {
        . = 0x33f80000;
        .text : { *(.text) }
        
        . = ALIGN(4);
        .rodata : {*(.rodata*)} 
        
        . = ALIGN(4);
        .data : { *(.data) }
        
        . = ALIGN(4);
        __bss_start = .;
        .bss : { *(.bss)  *(COMMON) }
        __bss_end = .;
    }
    

    COMM段BSS段

    对于全局变量来说,如果初始化了不为0的值,那么该全局变量则被保存在data段,如果初始化的值为0,那么将其保存在bss段,如果没有初始化,则将其保存在common段,等到链接时再将其放入到BSS段。关于第三点不同编译器行为会不同,有的编译器会把没有初始化的全局变量直接放到BSS段。

    参考链接

    elf和bin文件

    elf文件

    1 链接得到elf文件,含有地址信息(load addr)

    2 使用加载器(裸机程序是JTAG/应用程序的加载器本身也是个App去加载)

    3 运行程序

    4 如果loadaddr != runtimeaddr程序本身要重定位

    核心程序运行时应该位于 runtimeaddr(reloate addr)或者链接地址

    bin文件

    1 elf生成bin文件

    2 硬件机制启动,此时没有加载器

    3 如果bin文件所在位置 不等于runtimeaddr ,程序本身实现重定位

    bin文件/elf文件都不保存bss段 这些都是初始值为0 或者没有初始化的全局变量,程序运行时把bss段对应的空间清零

    获得链接脚本的值

    https://sourceware.org/ml/binutils/2007-07/msg00154.html 链接脚本的符号表

    1. 使用伪汇编指令 ldr r1, =__bss_start

    2. 存放到一块区域,再去读取

      .global _bss_start
      _bss_start:
      	.word __bss_start
      
      ldr r1, _bss_start	//读取内存,这里是读取label所在内存的数据
      
      //c中这么引用lable
      extern ulong _bss_start;
      
    3. C中获取链接脚本的值,链接的时候会根据lds与C所需要的lds中的值,产生一张内存表,内存表的标号也就是lds中的符号名(__bss_start).===这里是重点

      extern int __bss_start;
      int val =&__bss_start;	//!< 获得__bss_start的值
      int *p=&__bss_start;	//!< p指向这个内存单元
      ------------------------------------------------------------------
      下面是一个一个的符号表 ,都是 name  + 地址的格式
          
      -----
      name: g_i	//!<正常变量,存的是变量的地址
      Addr:xxx
      -----
      name: g_j
      Addr:xxx
      ------
      name:__bss_start //!< lds中,存的就是值了
      Addr:xxx
      ------
      

      img

      常规变量比如我们定义了int g_i,那么就有一个int大小的内存分配出来,地址也就是&g_i,对于lds中的变量,我们也就要是需要先取址,再取值,也就是*(&__bss_start)

      为什么要先取地址?

      1. 对于变量与lds中的变量,C都是先建立一个符号表,符号表正常情况存档地址
      2. 因为在C的符号表中,正常变量g_i的操作是:1.寻找符号表中的g_i,获得其内存地址,然后对内存操作
      3. 而Lds中的变量,实际上并没有对应的内存单元,符号表中的__bss_start他的值(地址值)就是所需要的值.

      总结:

      1. 对于常规变量g_i,得到里面的值,使用&g_i得到addr;
      2. 为了保持代码的一致,对于lds中的a1,使用&a1得到里面的值;
      3. 借助symbol table保存lds的变量,使用时加上"&"得到它的值,链接脚本的变量要在C程序中声明为外部变量,任何类型都可以;

    重定位

    全局变量

    全局变量在放在链接地址指定的位置,所以其链接地址必须是可写的.

    局部变量

    虽然局部变量是在栈中的,但是局部变量的数组他的初始值是从链接地址中取出来的.例如:

    参考 JTAG调试中nand调试章节

    void memsetup()
    {
        unsigned long  const    mem_cfg_val[]={ 0x22011110,     //BWSCON
                                                0x00000700,     //BANKCON0
                                                0x00000700,     //BANKCON1
                                                0x00000700,     //BANKCON2
                                                0x00000700,     //BANKCON3  
                                                0x00000700,     //BANKCON4
                                                0x00000700,     //BANKCON5
                                                0x00018005,     //BANKCON6
                                                0x00018005,     //BANKCON7
                                                0x008C07A3,     //REFRESH
                                                0x000000B1,     //BANKSIZE
                                                0x00000030,     //MRSRB6
                                                0x00000030,     //MRSRB7
                                        };
    
    }
    
    //mem_cfg_val是在栈,是位置无关的,但是他的初始值是去在链接地址取的,也就是0x3000000后面
    
    // ip=300005bc
    30000050:	e1a0400c 	mov	r4, ip
    30000054:	e8b4000f 	ldmia	r4!, {r0, r1, r2, r3}
    //从r4中指向的内存给r0~r3
    
    //也就是说从 300005bc读取一些数据,但是这个时候300005bc(sdram)并没有被初始化且进行代码搬运,所以这里的数据肯定有问题
    
    //这里的其实就是那个局部数组的值 mem_cfg_val
    300005bc <.rodata>:
    300005bc:	22011110 	andcs	r1, r1, #4	; 0x4
    300005c0:	00000700 	andeq	r0, r0, r0, lsl #14
    300005c4:	00000700 	andeq	r0, r0, r0, lsl #14
    300005c8:	00000700 	andeq	r0, r0, r0, lsl #14
    300005cc:	00000700 	andeq	r0, r0, r0, lsl #14
    300005d0:	00000700 	andeq	r0, r0, r0, lsl #14
    300005d4:	00000700 	andeq	r0, r0, r0, lsl #14
    300005d8:	00018005 	andeq	r8, r1, r5
    300005dc:	00018005 	andeq	r8, r1, r5
    300005e0:	008c07a3 	addeq	r0, ip, r3, lsr #15
    300005e4:	000000b1 	streqh	r0, [r0], -r1
    300005e8:	00000030 	andeq	r0, r0, r0, lsr r0
    300005ec:	00000030 	andeq	r0, r0, r0, lsr r0
    Disassembly of section .comment:
    

    1-直接指定数据段位置(代码黑洞)

    arm-linux-ld -Ttext 0 -Tdata 0x30000000
    // 数据段如下
    Disassembly of section .data:
    30000000 <__data_start>:
    30000000:       Address 0x30000000 is out of bounds.
    
    

    这个时候会发现,代码段从0开始,数据段从0x3000,0000开始,bin文件生成有0x3000,0000+1大小(1个全局变量),也就是产生了代码黑洞.代码段和数据段有大量的空白.

    在1中会产生巨大的代码空白,所以需要解决这个问题,也就是数据段和代码段不能有大的空白,必须紧靠着.

    2-分散加载(数据段)

    引入(手动确认数据段大小)
    1. 将数据段与代码段在一起,全部烧写到Nor的0地址上

    2. 运行时将全局变量复制到sdram上,做数据段的重定位

    3. 这里的Makefile需要使用链接脚本了,这里的关键就是实现了数据放置在A,但是实际运行的时候是在B

      arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
      
      // 重点 data 0x30000000 : AT(0x700) { *(.data) } //放在0x700,但运行时在0x3000000
      
      //lds
      SECTIONS {
         .text   0  : { *(.text) }//所有文件的.text
         .rodata  : { *(.rodata) } //只读数据段
         .data 0x30000000 : AT(0x800) { *(.data) } //放在0x700,但运行时在0x3000000
         .bss  : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
      }
      
    4. 需要手动初始化全局变量的值,也就是说先提前复制存储地址的值到运行地址,也就是将0x800的值复制0x3000,0000

      // c语言调用全局变量
      char g_Char = 'A';
      putchar(g_Char);
      // 汇编,从484获取到链接的地址,也就是说从这个地方0x3000,0000取数据
       450:	e59f302c 	ldr	r3, [pc, #44]	; 484 <.text+0x484>
       454:	e5d33000 	ldrb	r3, [r3]
       458:	e1a00003 	mov	r0, r3
       45c:	ebffff6f 	bl	220 <putchar>
       ...
       484:	30000000 	andcc	r0, r0, r0
      
      

      手动复制数据段,注意需要先初始化sdram

      	/* 重定位data段 */
      	mov r1, #0x800
      	ldr r0, [r1]			//读取0x800 的内容到r0
      	mov r1, #0x30000000		
      	str r0, [r1]			//将r0存到0x3000,0000
      
    自动确认数据段大小

    上述方式是看了反汇编,知道代码就一个全局变量,这在具体应用肯定是不行的,需要从链接脚本获得数据段的大小.修改链接脚本如下,其中可以通过data_load_addr获得其存储地址

    SECTIONS {
       .text   0  : { *(.text) }
       .rodata  : { *(.rodata) }
       .data 0x30000000 : AT(0x700) 
       { 
          data_load_addr = LOADADDR(.data);
          data_start = . ;//等于当前位置
          *(.data)  //等于数据段的大小
          data_end = . ;//等于当前位置
       }
       .bss  : { *(.bss) *(.COMMON) }
    }
    

    代码中自动复制相关的重定位的数据段

    	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
    

    3-全局重定位(一体式)

    分散加载和全局重定位的对比

    1. 分体式链接脚本适合单片机,单片机自带有flash,不需要再将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,就需要从Nand Flash或者SD卡复制整个代码到内存;
    2. JTAG等调试器一般只支持一体式链接脚本;

    代码段和数据段重定位,所以必须确保重定位前的代码必须是位置无关码.具体步骤如下

    1. 让文件直接从0x30000000开始,全局变量在0x3......;
    2. 烧写Nor Flash上 0地址处;
    3. 运行会把整个代码段数据段(整个程序)从0地址复制到SDRAM的0x30000000(重定位);
    4. 修改链接脚本如下:
    SECTIONS
    {
    	. = 0x30000000;
    
    	. = ALIGN(4);
    	.text      :
    	{
    	  *(.text)
    	}
    
    	. = ALIGN(4);
    	.rodata : { *(.rodata) }
    
    	. = ALIGN(4);
    	.data : { *(.data) }
    
    	. = ALIGN(4);
    	__bss_start = .;
    	.bss : { *(.bss) *(.COMMON) }
    	_end = .;
    }
    

    代码搬运和清除bss如下

    	/* 重定位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上的,为什么能运行?因为代码在nor其实也没有什么关系
        ldr pc, =main/*绝对跳转,跳到SDRAM*/
    
    halt:
    	b halt
    

    img

    BL跳转指令

    在反汇编中的B或者Bl跳转指令,其并不是跳转的相应的地址

    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>
    

    当我们修改链接脚本的时候,修改了链接地址,机器码也是不变的.实际上的跳转其实是相对pc跳转.由链接器决定.

    假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x30000478;如果程序从0运行,当前指令地址:0x5c 调到:0x00000478
    跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定。反汇编显示这个值只是为了方便读代码。
    重点: 反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。

    bss段处理

    汇编处理

    初始值为0的全局变量是存放在bss段的,需要自己写代码清0

    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结束地址也是当前位置
    }
    

    代码如下:

    	/* 清除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		//这里代码还是在nor上的,为什么能运行?因为代码在nor其实也没有什么关系
    
    halt:
    	b halt
    

    上述代码是字节读写的,cpu是32位的,sdram是32位宽的,可以直接使用32位访问,这里需要对链接脚本进行4字节对齐,否则在clean清bss段的时候,bss的起始地址不是4字节对齐的,那么他向4取整,也就是

    ldr r1, =bss_start   这里地址如果不是4取整,那么比如0x3000,0002
    
    clean:
    	strb r3, [r1]	 //这里是将0 写入 0x3000,0002 ,但是这是str会向4去整,也就是破坏bss段上面的
    					 //2个字节
    
    SECTIONS {
       .text   0  : { *(.text) }
       .rodata  : { *(.rodata) }
       .data 0x30000000 : AT(0x700) 
       { 
          data_load_addr = LOADADDR(.data);
    	  . = ALIGN(4);
          data_start = . ;
          *(.data) 
          data_end = . ;
       }
       
       . = ALIGN(4);//让当前地址向4对齐
       bss_start = .;
       .bss  : { *(.bss) *(.COMMON) }
       bss_end = .;
    }
    

    改进代码搬运为4字节访问

    cpy:
    	ldr r4, [r1]
    	str r4, [r2]
    	add r1, r1, #4 //r1加4
    	add r2, r2, #4 //r2加4
    	cmp r2, r3 //如果r2 =< r3继续拷贝
    	ble cpy
    
    /* 清除BSS段 */ 
    	ldr r1, =bss_start
    	ldr r2, =bss_end
    	mov r3, #0
    clean:
    	str r3, [r1]
    	add r1, r1, #4
    	cmp r1, r2 //如果r1 =< r2则继续拷贝
    	ble clean
    
    	bl main		//这里代码还是在nor上的,为什么能运行?因为代码在nor其实也没有什么关系
    

    C处理

    回头看 获得连接脚本的值 上面的章节

    1. 使用汇编给c传递参数,也就是先使用汇编ldr r1,=__bss_start获得链接脚本的参数

      	/* 重定位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 */
      
    2. c直接从链接脚本取值

      ///lds
      SECTIONS
      {
      	. = 0x30000000;
      
      	__code_start = .; //定义__code_start地址位当前地址
      
      	. = ALIGN(4);
      	.text      :
      	{
      	  *(.text)
      	}
      
      	. = ALIGN(4);
      	.rodata : { *(.rodata) }
      
      	. = ALIGN(4);
      	.data : { *(.data) }
      
      	. = ALIGN(4);
      	__bss_start = .;
      	.bss : { *(.bss) *(.COMMON) }
      	_end = .;
      }
      // c
      void copy2sdram(void)
      {
      	/* 要从lds文件中获得 __code_start, __bss_start
      	 * 然后从0地址把数据复制到__code_start
      	 */
      
      	extern int __code_start, __bss_start;//声明外部变量
      
      	volatile unsigned int *dest = (volatile unsigned int *)&__code_start;
      	volatile unsigned int *end = (volatile unsigned int *)&__bss_start;
      	volatile unsigned int *src = (volatile unsigned int *)0;
      
      	while (dest < end)
      	{
      		*dest++ = *src++;
      	}
      }
      
      void clean_bss(void)
      {
      	/* 要从lds文件中获得 __bss_start, _end
      	 */
      	extern int _end, __bss_start;
      
      	volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
      	volatile unsigned int *end = (volatile unsigned int *)&_end;
      
      
      	while (start <= end)
      	{
      		*start++ = 0;
      	}
      }
      

    位置无关码

    1. 使用相对跳转命令 b或bl;
    2. 重定位之前,不可使用绝对地址,不可访问全局变量/静态变量,也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);
    3. 重定位之后,使用ldr pc = xxx,跳转到/runtime地址;

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

  • 相关阅读:
    新闻
    蜂群
    Quartz.NET的管理工具
    安卓手机开发的学习资料
    Android IOS WebRTC 音视频开发总结(十九)- kurento
    WebRTC实现很难?让我们看看Mozilla是如何做的
    WebRTC流媒体服务器 Kurento
    Webrtc服务器搭建
    crtmpserver组网部署方案
    实现输出h264直播流的rtmp服务器
  • 原文地址:https://www.cnblogs.com/zongzi10010/p/10023561.html
Copyright © 2020-2023  润新知