当前位置 博文首页 > cumtchw:S3C2440裸机------代码重定位

    cumtchw:S3C2440裸机------代码重定位

    作者:[db:作者] 时间:2021-07-12 18:45

    目录

    1.段的概念

    2.链接脚本的引入与简单测试

    3.链接脚本的解析

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

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

    6.重定位_清除BSS段的C函数实现


    1.段的概念

    ? ? ? ? 我们的cpu可以直接把地址通过内存控制器发送到norflash,sram,sdram,但是我们的cpu不能直接到达nandflash,只能发送到nandflash控制器,我们的程序可以放到norflash以及sdram上面,它可以直接运行,但是nandflash的程序是不能直接运行的,但是我们仍然可以把程序放到nandflash上面,因为一上电2440内部的硬件会把nandflash的前4K代码复制到片内的SRAM,这个是由硬件做的,然后cpu从0地址开始运行,此时的0地址对应的是sram。那么如果我的程序超过4K怎么办,如果bin文件在nandflash上面超过4K,那么我们不能只复制前面4K代码,所以显然我们前面4K代码应该把整个程序都复制到SDRAM上面。这就是重定位,重新确定程序的地址。

    ? ? ? ? 如果我是用norflash启动,此时的0地址在norflash,此时片内4Ksram的地址是0x4000,0000.norflash可以像内存一样读,但是不能像内存一样的写。如果程序中含有需要更改的全局变量或者静态变量,全局变量是包含在bin文件中,烧在norflash上面(局部变量是在栈中在SRAM上面,可读可写没有问题),直接修改变量无效。所以我们需要把这些全局变量和静态变量重定位放到SDRAM中。

    ? ? ? ? 我们修改之前的main.c,然后在里面定义一个全局的变量,可以发现串口一直打印A,并没有递增的打印ABCD。

    #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++;         /* nor启动时, 此代码无效 */
    		delay(1000000);
    	}
    
    	
    	return 0;
    }

    我们的程序主要分为:

    代码段(text):代码?

    数据段(data):一般的全局变量

    只读数据段(rodata) :const全局变量。

    bss段:初值为0或者无初始值的全局变量。BSS段不保存在bin文件中。

    comment:注释:也不保存在bin文件中。

    2.链接脚本的引入与简单测试

    通过上一节的测试,我们发现当我们把程序烧写到norflash时,由于norflash是不可写的,因此不能修改g_Char,所以我们需要通过修改makefile,让g_Char放到SDRAM里面,我们的代码仍然放在NORFLASH里面,只是把变量保存到SDRAM里面,由于我们的SDRAM的地址是从0X30000000开始的,因此我们修改makefile为如下的形式。

    all:
    	arm-linux-gcc -c -o led.o led.c
    	arm-linux-gcc -c -o uart.o uart.c
    	arm-linux-gcc -c -o init.o init.c
    	arm-linux-gcc -c -o main.o main.c
    	arm-linux-gcc -c -o start.o start.S
    	arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elf
    	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
    	arm-linux-objdump -D sdram.elf > sdram.dis
    clean:
    	rm *.bin *.o *.elf *.dis
    	
    

    但是编译完之后,我们发现我们的bin文件达到了800M,这是因为我们把变量放在了0X30000000这里,但是我们显然不能这么改,?针对这个问题有两个解决方案。

    2.1方案一

    首先我们在bin文件里面,让代码段和全局变量靠在一起,然后把bin文件烧写到norflash上面,然后运行的时候,我们前面的代码需要把全局变量复制到0X30000000的地方, 这就是重定位。

    2.2方案二

    第二种方法是,我们在链接的时候,让代码段和全局变量中间没有那么大的空闲,我们的代码段也从0x30000000开始,然后烧写到norflash,然后运行的时候,我们前面的代码把代码段和全局变量整个程序复制到0x30000000。

    这两种方案的差别在于,第一种方法只是重定位了全局变量,第二种方法是重定位了整个程序。

    下面我们先来看方案一:我们通过引入链接脚本,将本来应该位于0X30000000的变量和代码段拼在一起,我们首先修改makefile为下面的形式:

    all:
    	arm-linux-gcc -c -o led.o led.c
    	arm-linux-gcc -c -o uart.o uart.c
    	arm-linux-gcc -c -o init.o init.c
    	arm-linux-gcc -c -o main.o main.c
    	arm-linux-gcc -c -o start.o start.S
    	#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
    	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
    	arm-linux-objdump -D sdram.elf > sdram.dis
    clean:
    	rm *.bin *.o *.elf *.dis
    	
    

    然后我们来写出这个链接脚本sdram.lds?

    SECTIONS {
       .text   0  : { *(.text) }
       .rodata  : { *(.rodata) }
       .data 0x30000000 : AT(0x800) { *(.data) }
       .bss  : { *(.bss) *(.COMMON) }
    }

    上面的{}里面的*表示所有文件的.text段,所有文件的.rodata段。其中的AT表示at,也就是全局变量运行的时候是在0x30000000,但是在bin文件中我们把它放在0x800这里。

    我们把上面的程序烧写到板子上之后显示乱码,这是因为我们的bin文件中,全局变量是保存在0X800的地方,但是我们的main函数中是以0x30000000为地址去访问全局变量的。而我们的代码中并没有把0x30000000地址的地方用全局变量的值进行初始化,也就是我们的代码中缺少重定位相关的代码,我们需要修改代码,将变量从0x800的地方复制到0x30000000的地方,我们修改start.S,在执行main函数之前进行重定位。

    
    .text
    .global _start
    
    _start:
    
    	/* 关闭看门狗 */
    	ldr r0, =0x53000000
    	ldr r1, =0
    	str r1, [r0]
    
    	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
    	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
    	ldr r0, =0x4C000000
    	ldr r1, =0xFFFFFFFF
    	str r1, [r0]
    
    	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
    	ldr r0, =0x4C000014
    	ldr r1, =0x5
    	str r1, [r0]
    
    	/* 设置CPU工作于异步模式 */
    	mrc p15,0,r0,c1,c0,0
    	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
    	mcr p15,0,r0,c1,c0,0
    
    	/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) 
    	 *  m = MDIV+8 = 92+8=100
    	 *  p = PDIV+2 = 1+2 = 3
    	 *  s = SDIV = 1
    	 *  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
    	 */
    	ldr r0, =0x4C000004
    	ldr r1, =(92<<12)|(1<<4)|(1<<0)
    	str r1, [r0]
    
    	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
    	 * 然后CPU工作于新的频率FCLK
    	 */
    	
    	
    
    	/* 设置内存: sp 栈 */
    	/* 分辨是nor/nand启动
    	 * 写0到0地址, 再读出来
    	 * 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
    	 * 否则就是nor启动
    	 */
    	mov r1, #0
    	ldr r0, [r1] /* 读出原来的值备份 */
    	str r1, [r1] /* 0->[0] */ 
    	ldr r2, [r1] /* r2=[0] */
    	cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */
    	ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
    	moveq sp, #4096  /* nand启动 */
    	streq r0, [r1]   /* 恢复原来的值 */
    
    	bl sdram_init	/*首先要初始化SDRAM,要不然下面的重定位代码不起作用,没法写SDRAM*/
    
    	/* 重定位data段 */
    	mov r1, #0x800
    	ldr r0, [r1]
    	mov r1, #0x30000000
    	str r0, [r1]
    
    	bl main
    
    halt:
    	b halt
    	
    

    上面的重定位data的代码中,我们简单粗暴的从0x800复制了一个字节到0x30000000这里,这种方法并不通用,首先我们程序中有可能有很多个全局变量,另外我们这里的地址0X800和0x30000000都是写死的,这是因为肉眼从链接脚本中观察到的,下面我们写一个通用的程序,首先要在链接脚本中添加一些变量。

    SECTIONS {
       .text   0  : { *(.text) }
       .rodata  : { *(.rodata) }
       .data 0x30000000 : AT(0x800) 
       { 
          data_load_addr = LOADADDR(.data);
          data_start = . ;
          *(.data) 
          data_end = . ;
       }
       .bss  : { *(.bss) *(.COMMON) }
    }
    
    

    其中data_start后面的.表示当前地址,也就是0x30000000, data_end-data_start就等于数据段的长度,LOADADDR(.data)表示data段在bin文件中的地址,也就是0x800.

    然后我们修改start.S如下:

    
    .text
    .global _start
    
    _start:
    
    	/* 关闭看门狗 */
    	ldr r0, =0x53000000
    	ldr r1, =0
    	str r1, [r0]
    
    	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
    	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
    	ldr r0, =0x4C000000
    	ldr r1, =0xFFFFFFFF
    	str r1, [r0]
    
    	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
    	ldr r0, =0x4C000014
    	ldr r1, =0x5
    	str r1, [r0]
    
    	/* 设置CPU工作于异步模式 */
    	mrc p15,0,r0,c1,c0,0
    	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
    	mcr p15,0,r0,c1,c0,0
    
    	/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) 
    	 *  m = MDIV+8 = 92+8=100
    	 *  p = PDIV+2 = 1+2 = 3
    	 *  s = SDIV = 1
    	 *  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
    	 */
    	ldr r0, =0x4C000004
    	ldr r1, =(92<<12)|(1<<4)|(1<<0)
    	str r1, [r0]
    
    	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
    	 * 然后CPU工作于新的频率FCLK
    	 */
    	
    	
    
    	/* 设置内存: sp 栈 */
    	/* 分辨是nor/nand启动
    	 * 写0到0地址, 再读出来
    	 * 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
    	 * 否则就是nor启动
    	 */
    	mov r1, #0
    	ldr r0, [r1] /* 读出原来的值备份 */
    	str r1, [r1] /* 0->[0] */ 
    	ldr r2, [r1] /* r2=[0] */
    	cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */
    	ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
    	moveq sp, #4096  /* nand启动 */
    	streq r0, [r1]   /* 恢复原来的值 */
    
    	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]
    	strb r4, [r2]
    	add r1, r1, #1
    	add r2, r2, #1
    	cmp r2, r3
    	bne cpy
    
    	bl main
    
    halt:
    	b halt
    	
    

    3.链接脚本的解析

    我们上一节用到了链接脚本,现在我们来解析下前面用到的连接脚本。

    首先通过文档 http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html,我们知道链接脚本的格式是这样的:

    SECTIONS {
    ...
    secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
      { contents } >region :phdr =fill
    ...
    }

    在链接脚本格式中,BLOCK(align)很少用到,NOLOAD也很少用到,region:phdr=fill也很少用到。里面的secname名字我们可以随便写,也可以不叫.text .rodata,叫什么名字都可以,start表示运行时的起始地址runtime address,也叫重定位地址relocate address,AT可写也可以不写,表示加载地址,LOADaddress不写时,就等于runtime address.

    然后就是contents的格式,contents格式有下面3种:

    1:start.o:你可以 指定start.o,把整个start.o放到这个段里面。

    2:*(.text):指定所有.o的text段放到这里面。

    3:start.o

    *(.text):指定start.o放到前面,然后剩下所有文件的text段放到

    elf文件地址问题:

    • 我们的makefile中通过链接脚本将一些.o文件生成了elf格式的文件,elf文件中是含有地址信息的,比如加载地址。
    • 我们可以使用加载器把elf文件加载到内存的加载地址load address,对于裸板来说,JTAG调试器就是加载器,对于应用程序来说,加载器本身也是一个应用程序,
    • 然后运行程序。
    • 如果链接脚本中指定了load address ,然后load address不等于runtime address,那么程序本身还需要重定位代码。

    裸板bin文件:

    • elf格式文件生成bin文件。
    • 硬件机制的启动
    • 如果bin文件所在位置不等于运行地址,那么需要程序本身实现重定位。

    ?下面看一下我们前面用到的链接脚本:?

    SECTIONS {
       .text   0  : { *(.text) }
       .rodata  : { *(.rodata) }
       .data 0x30000000 : AT(0x800) 
       { 
          data_load_addr = LOADADDR(.data);
          data_start = . ;
          *(.data) 
          data_end = . ;
       }
       .bss  : { *(.bss) *(.COMMON) }
    }
    
    

    第一行没有写加载地址,那么我们的text的加载地址就等于运行地址,也就是0.不要进行重定位。另外这里text是保存的是所有文件的text段,这里的所有文件的顺序就是按照makefile里面写的先后顺序arm-linux-ld -Ttext 0 -Tdata 0x30000000 ?start.o led.o uart.o init.o main.o -o sdram.elf,然后看data段,这里加载地址是0x800,然后运行地址是0x30000000.那么我们程序启动之后需要把数据段从0x800拷贝到0x30000000,这就是重定位。另外,在elf或者bin文件中都不会存bss段,因为bss中是初始值为零的或者没有初始化的全局变量,假如我们有很多初始化为零的全局变脸,那么我们保存这么多零浪费空间。

    我们通过编写程序,将初始化为零的全局变量打印出来,发现它并不是零,这是因为我们没有把他清零,我们需要写程序 将BSS段对应的内存清零, 要想清零,需要知道BSS空间的地址是什么,首先要修改链接脚本,

    SECTIONS {
       .text   0  : { *(.text) }
       .rodata  : { *(.rodata) }
       .data 0x30000000 : AT(0x800) 
       { 
          data_load_addr = LOADADDR(.data);
          data_start = . ;
          *(.data) 
          data_end = . ;
       }
       
       bss_start = .;
       .bss  : { *(.bss) *(.COMMON) }
       bss_end = .;
    }
    
    

    然后修改start.S清除BSS段。

    
    .text
    .global _start
    
    _start:
    
    	/* 关闭看门狗 */
    	ldr r0, =0x53000000
    	ldr r1, =0
    	str r1, [r0]
    
    	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
    	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
    	ldr r0, =0x4C000000
    	ldr r1, =0xFFFFFFFF
    	str r1, [r0]
    
    	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
    	ldr r0, =0x4C000014
    	ldr r1, =0x5
    	str r1, [r0]
    
    	/* 设置CPU工作于异步模式 */
    	mrc p15,0,r0,c1,c0,0
    	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
    	mcr p15,0,r0,c1,c0,0
    
    	/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) 
    	 *  m = MDIV+8 = 92+8=100
    	 *  p = PDIV+2 = 1+2 = 3
    	 *  s = SDIV = 1
    	 *  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
    	 */
    	ldr r0, =0x4C000004
    	ldr r1, =(92<<12)|(1<<4)|(1<<0)
    	str r1, [r0]
    
    	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
    	 * 然后CPU工作于新的频率FCLK
    	 */
    	
    	
    
    	/* 设置内存: sp 栈 */
    	/* 分辨是nor/nand启动
    	 * 写0到0地址, 再读出来
    	 * 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
    	 * 否则就是nor启动
    	 */
    	mov r1, #0
    	ldr r0, [r1] /* 读出原来的值备份 */
    	str r1, [r1] /* 0->[0] */ 
    	ldr r2, [r1] /* r2=[0] */
    	cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */
    	ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
    	moveq sp, #4096  /* nand启动 */
    	streq r0, [r1]   /* 恢复原来的值 */
    
    	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]
    	strb r4, [r2]
    	add r1, r1, #1
    	add r2, r2, #1
    	cmp r2, r3
    	bne cpy
    
    
    	/* 清除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:
    	b halt
    	
    

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

    我们前面将数据从norflash复制到SDRAM的时候,我们用了如下的代码:

    	/* 重定位data段 */
    	ldr r1, =data_load_addr  /* data段在bin文件中的地址, 加载地址 */
    	ldr r2, =data_start 	 /* data段在重定位地址, 运行时的地址 */
    	ldr r3, =data_end 	     /* data段结束地址 */
    
    cpy:
    	ldrb r4, [r1]
    	strb r4, [r2]
    	add r1, r1, #1
    	add r2, r2, #1
    	cmp r2, r3
    	bne cpy

    我们首先使用ldrb命令从flash中读取一个字节,然后把这个字节写到SDRAM中,我们的norflash是16位的,SDRAM是32位的,这种读取方式效率比较低。

    ? ? ? ? 我们前面用的ldrb和strb命令,每次只能操作一个字节,假设我们要拷贝16个字节,那我们要访问16次norflash,访问16次SDRAM,一共32次访问硬件的操作,这样就比较耗时。并且我们的SDRAM是32位的,那么读写SDRAM的时候,一次性操作只能以32位作为最小操作单位,如果CPU想读一个字节,它会把命令发给内存控制器,然后内存控制器从SDRAM中读到4个字节的数据,然后挑出CPU感兴趣的那个字节返回给它,如果CPU想写一个字节,那么cpu把地址和数据发给内存控制器,然后内存控制器把32位的数据发给SDRAM,同时内存控制器还会发出数据屏蔽信息号DQM,屏蔽掉不需要写的其他三个字节,最终只会写一个字节,这种效率太低了。

    ? ? ? ? 我们使用ldr和str命令进行改进。这两个命令以4个字节为单位进行操作,如果我们要拷贝16个字节,那么我们只需要执行4次ldr和4次str就可以了,但是由于norflash是16位的,我们要访问8次norflash。

    我们修改我们的start.S

    
    .text
    .global _start
    
    _start:
    
    	/* 关闭看门狗 */
    	ldr r0, =0x53000000
    	ldr r1, =0
    	str r1, [r0]
    
    	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
    	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
    	ldr r0, =0x4C000000
    	ldr r1, =0xFFFFFFFF
    	str r1, [r0]
    
    	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
    	ldr r0, =0x4C000014
    	ldr r1, =0x5
    	str r1, [r0]
    
    	/* 设置CPU工作于异步模式 */
    	mrc p15,0,r0,c1,c0,0
    	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
    	mcr p15,0,r0,c1,c0,0
    
    	/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) 
    	 *  m = MDIV+8 = 92+8=100
    	 *  p = PDIV+2 = 1+2 = 3
    	 *  s = SDIV = 1
    	 *  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
    	 */
    	ldr r0, =0x4C000004
    	ldr r1, =(92<<12)|(1<<4)|(1<<0)
    	str r1, [r0]
    
    	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
    	 * 然后CPU工作于新的频率FCLK
    	 */
    	
    	
    
    	/* 设置内存: sp 栈 */
    	/* 分辨是nor/nand启动
    	 * 写0到0地址, 再读出来
    	 * 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
    	 * 否则就是nor启动
    	 */
    	mov r1, #0
    	ldr r0, [r1] /* 读出原来的值备份 */
    	str r1, [r1] /* 0->[0] */ 
    	ldr r2, [r1] /* r2=[0] */
    	cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */
    	ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
    	moveq sp, #4096  /* nand启动 */
    	streq r0, [r1]   /* 恢复原来的值 */
    
    	bl sdram_init	
    
    	/* 重定位data段 */
    	ldr r1, =data_load_addr  /* data段在bin文件中的地址, 加载地址 */
    	ldr r2, =data_start 	 /* data段在重定位地址, 运行时的地址 */
    	ldr r3, =data_end 	     /* data段结束地址 */
    
    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, =bss_end
    	mov r3, #0
    clean:
    	str r3, [r1]
    	add r1, r1, #4
    	cmp r1, r2
    	ble clean
    
    	bl main
    
    halt:
    	b halt
    	
    

    修改完之后发现我们的代码不仅把BSS清零了,还把我们的初始化的全局变量也给清零了。我们看一下我们的反汇编:

    所以我们要修改我们的链接脚本,让我们的BSS以4字节取整,下面的脚本中同样对data段增加了向4对齐。

    SECTIONS {
       .text   0  : { *(.text) }
       .rodata  : { *(.rodata) }
       .data 0x30000000 : AT(0x800) 
       { 
          data_load_addr = LOADADDR(.data);
    	  . = ALIGN(4);
          data_start = . ;
          *(.data) 
          data_end = . ;
       }
       
       . = ALIGN(4);
       bss_start = .;
       .bss  : { *(.bss) *(.COMMON) }
       bss_end = .;
    }
    
    

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

    在前面我们讲了重定位的两种方案,方案一是只把变量重定位到SDRAM,下面我们看一下重定位方案二,怎么把整个程序都重定位到SDRAM。

    1. ?我们要把程序复制到SDRAM中,也就是运行地址中,那么我们要在链接脚本中指定程序的运行地址为SDRAM的地址。
    2. bin文件是烧写到flash里面的,一上电时从0运行,前面一部分代码需要将整个程序复制到SDRAM中, 重定位之前的代码与位置无关,用位置无关码写成。

    首先来修改链接脚本,链接脚本可以参考uboot,uboot是裸机程序的集大成者,我们这里参考boot_projects/u-boot-1.1.6_jz2440/u-boot-1.1.6/board/smdk2410里面的u-boot.lds文件,然后进行修改,得到我们自己的链接脚本。

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

    然后修改我们的start.S,以前我们只是重定位了data段,现在我们把所有的段都进行重定位。

    
    .text
    .global _start
    
    _start:
    
    	/* 关闭看门狗 */
    	ldr r0, =0x53000000
    	ldr r1, =0
    	str r1, [r0]
    
    	/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
    	/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
    	ldr r0, =0x4C000000
    	ldr r1, =0xFFFFFFFF
    	str r1, [r0]
    
    	/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8  */
    	ldr r0, =0x4C000014
    	ldr r1, =0x5
    	str r1, [r0]
    
    	/* 设置CPU工作于异步模式 */
    	mrc p15,0,r0,c1,c0,0
    	orr r0,r0,#0xc0000000   //R1_nF:OR:R1_iA
    	mcr p15,0,r0,c1,c0,0
    
    	/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0) 
    	 *  m = MDIV+8 = 92+8=100
    	 *  p = PDIV+2 = 1+2 = 3
    	 *  s = SDIV = 1
    	 *  FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
    	 */
    	ldr r0, =0x4C000004
    	ldr r1, =(92<<12)|(1<<4)|(1<<0)
    	str r1, [r0]
    
    	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
    	 * 然后CPU工作于新的频率FCLK
    	 */
    	
    	
    
    	/* 设置内存: sp 栈 */
    	/* 分辨是nor/nand启动
    	 * 写0到0地址, 再读出来
    	 * 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
    	 * 否则就是nor启动
    	 */
    	mov r1, #0
    	ldr r0, [r1] /* 读出原来的值备份 */
    	str r1, [r1] /* 0->[0] */ 
    	ldr r2, [r1] /* r2=[0] */
    	cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */
    	ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
    	moveq sp, #4096  /* nand启动 */
    	streq r0, [r1]   /* 恢复原来的值 */
    
    	bl sdram_init
    	//bl sdram_init2	 /* 用到有初始值的数组, 不是位置无关码 */
    
    	/* 重定位text, rodata, data段整个程序 */
    	mov r1, #0
    	ldr r2, =_start 	    /* 第1条指令运行时的地址,也就是最上面_start的地址,也就是0x30000000 */
    	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  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
    	ldr pc, =main  /* 绝对跳转, 跳到SDRAM */
    
    halt:
    	b halt
    	
    

    下面我们结合反汇编文件来分析一下程序的启动过程:

    我们的bin文件中,一开始是代码段,然后是只读数据段,然后是数据段,它的运行地址是0x30000000,我们的bin文件烧写到norflash上面,上电后,前半部分的代码要把整个程序复制到SDRAM,实现重定位整个程序,我们的start.S实现了copy和clean,下面我们看一下反汇编。

    我们再反汇编代码看到了一个bl 30000478<sdram_init>,注意这一条不是跳到0x30000478,我们这时候SDRAM都还没有初始化,这个地址里面都还没有指令,我们不可能跳到这个地址的,我们做个实验,重新修改下链接脚本,将运行地址由0X30000000修改0x32000000,然后发现反汇编文件里面变成了bl 32000478<sdram_init>,但是我们发现bl 30000478<sdram_init>和bl 32000478<sdram_init>这两个对应的机器码都是eb000105,完全一致,如果是跳转命令,那么跳转到不一样的地址机器码是不可能一样的,所以这里并不是跳转命令,而是跳转到当前某个PC值加上offset。这个offset是由连接器帮你算出来的,具体跳转到那里由当前PC值决定,也就是当前指令所在位置决定,假设程序从0x30000000执行,那么从反汇编文件可以看出,当前的bl指令地址是0x3000005c,然后跳转到0x30000478,如果程序从0运行,那么当前的bl指令地址是0x5c,然后跳转到0x04798.如果是从0x32000000开始运行,那么bl指令地址是0x3200005c,然后跳转到0x32000478.所以这里的bl命令并不是跳转到这个地址,具体跳转到那里是取决于当前PC的位置。在反汇编里面写出这个值只是为了方便你去分析代码而已。总结在反汇编文件里面,B/BL某个值,只是起一个方便查看的作用,并不是真正跳转到这个地址。从前面的分析也可以看到,虽然我们的程序指定运行地址是0x30000000,但是我们把它放到0地址也仍然可以运行,因为跳转的时候使用的是偏移地址,仍然可以跳到正确的代码位置,跟链接地址没有关系。所以以后我们想写出跟链接地址没有关系的代码时,跳转命令必须用B命令或者BL命令,用偏移地址,不能使用绝对地址。

    我们继续往下看start.S,我们发现有一个bl main,main函数也是使用相对跳转命令, 然后main函数在0X5C4的地方,然后由于bl是相对跳转,所以这里跳转之后仍然是处于norflash上面,并没有跳到SDRAM上面,前面的代码我们把代码进行了重定位,我们这里应该是跳转到0X300005C4,但是我们使用bl命令时,仍然跳到了0X5C4,所以我们必须使用绝对地址使程序跳转到SDRAM.

    下面我们再举一个例子,我们把sdram_init修改为sdram_init2,代码如下:

    
    #include "s3c2440_soc.h"
    
    void sdram_init(void)
    {
    	BWSCON = 0x22000000;
    
    	BANKCON6 = 0x18001;
    	BANKCON7 = 0x18001;
    
    	REFRESH  = 0x8404f5;
    
    	BANKSIZE = 0xb1;
    
    	MRSRB6   = 0x20;
    	MRSRB7   = 0x20;
    }
    
    
    #if 0
    /**************************************************************************   
    * 设置控制SDRAM的13个寄存器
    * 使用位置无关代码
    **************************************************************************/   
    void memsetup(void)
    {
    	unsigned long *p = (unsigned long *)MEM_CTL_BASE;	
    	p[0] = 0x22111110;		//BWSCON
    	p[1] = 0x00000700;		//BANKCON0
    	p[2] = 0x00000700;		//BANKCON1
    	p[3] = 0x00000700;		//BANKCON2
    	p[4] = 0x00000700;		//BANKCON3	
    	p[5] = 0x00000700;		//BANKCON4
    	p[6] = 0x00000700;		//BANKCON5
    	p[7] = 0x00018005;		//BANKCON6
    	p[8] = 0x00018005;		//BANKCON7
    	p[9] = 0x008e07a3;		//REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f4
    	p[10] = 0x000000b2;		//BANKSIZE
    	p[11] = 0x00000030;		//MRSRB6
    	p[12] = 0x00000030;		//MRSRB7
    }
    #endif
    
    
    
    
    void sdram_init2(void)
    {
    	unsigned int arr[] = {
    		0x22000000, 	//BWSCON
    		0x00000700, 	//BANKCON0
    		0x00000700, 	//BANKCON1
    		0x00000700, 	//BANKCON2
    		0x00000700, 	//BANKCON3	
    		0x00000700, 	//BANKCON4
    		0x00000700, 	//BANKCON5
    		0x18001, 	//BANKCON6
    		0x18001, 	//BANKCON7
    		0x8404f5, 	//REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f4
    		 0xb1,	//BANKSIZE
    		 0x20,	//MRSRB6
    		 0x20,	//MRSRB7
    
    		};
    	volatile unsigned int * p = (volatile unsigned int *)0x48000000;
    	int i;
    
    	for (i = 0; i < 13; i++)
    	{
    		*p = arr[i];
    		p++;
    	}
    	
    }
    
    
    int sdram_test(void)
    {
    	volatile unsigned char *p = (volatile unsigned char *)0x30000000;
    	int i;
    
    	// write sdram
    	for (i = 0; i < 1000; i++)
    		p[i] = 0x55;
    
    	// read sdram
    	for (i = 0; i < 1000; i++)
    		if (p[i] != 0x55)
    			return -1;
    
    	return 0;
    }
    
    

    把程序下载进去之后发现没有任何打印,这是因为sdram_init2不是位置无关的,我们看一下反汇编,找到sdram_init2这个函数的反汇编码,

    30000508 <sdram_init2>:
    30000508:	e1a0c00d 	mov	ip, sp
    3000050c:	e92dd800 	stmdb	sp!, {fp, ip, lr, pc}
    30000510:	e24cb004 	sub	fp, ip, #4	; 0x4
    30000514:	e24dd03c 	sub	sp, sp, #60	; 0x3c
    30000518:	e59f3088 	ldr	r3, [pc, #136]	; 300005a8 <.text+0x5a8>
                                        从地址300005a8取一个值给r3,这个命令依赖于PC当前值,所以虽然用了ldr,
    									但是仍然是位置无关的,300005a8这里的值是30000708,那么r3=30000708 .
    3000051c:	e24be040 	sub	lr, fp, #64	; 0x40
    30000520:	e1a0c003 	mov	ip, r3
                                        ip=r3=30000708 
    30000524:	e8bc000f 	ldmia	ip!, {r0, r1, r2, r3}
                                        去ip=30000708这个地方读内存,把里面的值分别存放到r0,r1,r2,r3.
    									30000708就是SDRAM里面的数据,这时候SDRAM都还没有初始化,你就去
    									读数据肯定出错,
    30000528:	e8ae000f 	stmia	lr!, {r0, r1, r2, r3}

    我们看一下30000708是什么东西。

    这个数组的初始值存放在只读数据段,也就是放在了30000708这个位置,这些初始值保存在只读数据段里面,我们要使用这些初始值来初始化数组,这些初始值要用绝对地址来访问,所以我们要写位置无关码的话,这种数组也不能使用。

    怎么写位置无关码(就是你的程序在任何地址都可以运行,不需要把它放到运行时地址):

    使用位置无关码,不使用绝对地址,最根本的办法是看反汇编。

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