第7课,代码重定位

注:以下内容学习于韦东山老师arm裸机第一期视频教程


一.段的概念和重定位的引入

    1.1 重定位的引入

        2440框架图如下

    

        

        CPU发出的地址可以直接到达SDRAM,SRAM,NOR但是无法直接到达NAND

        因此我们的程序可以直接放在NOR,SDRAM直接运行,假设我们把程序烧录到NAND中,CPU无法直接从NAND取地址运行.

        

        1.1.1 但是我们仍然可以设置为NAND启动,NAND启动时(SRAM的地址对应0,因此NAND启动时NOR无法访问):

              前4K会被复制到SRAM,然后CPU从0地址运行,就对应SRAM

              如果bin文件超过4K怎么办?

              前面的4K需要将整个代码读出来放到SDRAM(这就是重定位)

              

        1.1.2 NOR启动时,0地址对应NOR上面,SRAM的地址对应0x40000000

            

              NOR可以向内存一样的读,但是不能像内存一样的写=>需要发出特定的指令才可以写.

              mov r0, #0
              ldr r1, [r0]     /* 可以读 */
              str r1, [r0]  /* 无效 */  

              因此引入问题,当程序中含有需要写的变量(局部变量在栈中,栈指向SRAM,读写没有问题)

                            但是全局变量,静态变量是包含在bin文件中烧到NOR中,直接写修改变量无效

                            因此,我们需要将这些全局变量,静态变量重定位放在SDRAM中

                            

                 示例码如下:                  

                            #include "my_printf.h"
                            #include "uart.h"
                            #include "sdram.h"
                            #include "SetTacc.h"

                            char g_cA = 'A';

                            int main()
                            {
                                char c;
                                
                                Uart0Init();
                                SdramInit();
                                
                                puts("");

                                while (1)
                                {
                                    putchar(g_cA);
                                    g_cA++;         /* nor启动时代码无效 */
                                }
                                
                                return 0;
                            }

                            分别烧写到NOR和NAND,烧些到NAND会打印出ABCD,NOR启动只会打印出AAA

                            

                NAND启动需要重定位-> 将全部代码重定位到SDRAM中

                NOR启动需要重定位->  将全局变量,静态变量重定位到SRAM中

                
                            

    1.2  段的概念

            程序至少包含代码段和数据段,数据段中存放(全局变量)

            在main.c中定义下面几个变量

            char g_cA = 'A';
            const g_cB = 'B';
            int g_iA = 0;
            int g_iB;  

            编译后查看反汇编文件,如下图

            

            因此可以看出程序还包含bss段,rodata段,最后的common段表示注释段


            总结,代码中的段:

                代码段        .text

                数据段        .data

                只读数据段 .rodata

                /* bss段和注释段不保存在bin文件中 */

                bss段       .bss,未初始化或初始化为0的全局变量

                注释段       .common

                

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

    2.1 修改Makefile使得全局变量存放到SDRAM,0x3000000中

            修改Makefile

            objs = uart.o main.o start.o SetTacc.o my_printf.o lib1funcs.o sdram.o
            A = test

            all:$(objs)
                arm-linux-ld -Ttext -Tdata 0x30000000 0 $^ -o $(A).elf
                arm-linux-objcopy -O  binary -S $(A).elf $(A).bin
                arm-linux-objdump -D  $(A).elf > $(A).dis
                
            %.o:%.c
                arm-linux-gcc -c -o $@ $<
                
            %.o:%.S
                arm-linux-gcc -c -o $@ $<
                
            clean:
                rm *.o *.elf *.bin

            但是这么编译出来的bin文件有800多M,这是因为代码段和数据段之间有一个很大的间隔.

    2.2 两种解决办法:

        2.2.1

                a.bin文件中,让全局变量和代码段在一起,链接在0地址
                b.烧写bin文件在NOR的0地址
                c.运行时前面的代码将全局变量复制到0x30000000处
                

        2.2.2  

                a.让代码段的链接地址在0x30000000开始,全局变量在紧接着后面开始

                b.烧写bin文件在NOR的0地址

                c.运行时前面的代码将代码段和全局变量全部复制到0x30000000

                
                

    2.3 链接脚本

        修改Makefile,在链接时指定链接脚本

        objs = uart.o main.o start.o SetTacc.o my_printf.o lib1funcs.o sdram.o
        A = test

        all:$(objs)
            #arm-linux-ld -Ttext 0 -Tdata 0x800 $^ -o $(A).elf
            arm-linux-ld -T sdram.lds $^ -o $(A).elf
            arm-linux-objcopy -O  binary -S $(A).elf $(A).bin
            arm-linux-objdump -D  $(A).elf > $(A).dis
            
        %.o:%.c
            arm-linux-gcc -c -o $@ $<
            
        %.o:%.S
            arm-linux-gcc -c -o $@ $<
            
        clean:
            rm *.o *.elf *.bin
        
        /* 链接脚本如下 */
        SECTIONS {
        .text   0 : { *(.text) }
        .rodata   : { *(.rodata) }
        .data   0x30000000 : AT(0x1000) { *(.data) }
        .bss    : { *(.bss) *(.COMMON) }
        }
        

        这样代码段会放在0地址,在0x800的地方放了全局变量,但是main函数中访问全局变量时是以0x30000000的地址来访问的

        我们并没有设置0x30000000的内存数值是A,我们的代码中缺少了重定位.


        修改代码Start.S,在执行main函数之前需要进行重定位data段,在前面还要初始化sdram

        /* 重定位了1个字节,0x1000是我们看反汇编确定的地址,并不通用 */
        mov r1, #0x1000
        ldr r0, [r1]
        mov r1, #0x30000000
        str r0, [r1]
        
        /* 我们需要得到通用的重定位的办法 */
        
        修改sdram.lds如下
        SECTIONS {
            .text   0 : { *(.text) }
            .rodata   : { *(.rodata) }
            .data   0x30000000 : AT(0x1000)
            {
                data_load_addr = LOADADDR(.data)
                data_start = .;
                *(.data)
                data_end = .;
            }
            .bss    : { *(.bss) *(.COMMON) }
        }
        
        修改Start.S重定位代码
        ldr r1, =data_load_addr  /* data段在bin文件中的地址,加载地址 */
        ldr r2, =data_start        /* 重定位地址,运行时的地址 */
        ldr r3, =data_end         /* data段的结束地址 */
        str r0, [r1]
    cpy:
        ldrb r4, [r1]
        strb r4, [r2]
        add r1, r1, #1
        add r2, r2, #1
        cmp r2, r3
        bne cpy

三.链接脚本的解析


    链接脚本的格式

    
    SECTIONS {
        secname(段的名字,可以随便写) start->(起始地址,运行时的地址,重定位后的地址) AT(ldadr)->(可以写,可以不写,加载地址,如果不写加载地址等于重定位地址)
        {contents}
    }    
    
    contents,内容:
            格式:a. start.o(指定整个文件)
                  b. *(.text)
                  c. start.o *(.text) (start.o放在最前面,然后是 剩下所有文件的text段)    

    对于elf格式文件

    1.链接得到elf格式的文件,里面含有地址信息(例如加载地址)

    2.使用加载器(对于裸板就是JTAG调试工具,对于应用程序加载器本身是一个应用程序)把elf文件解析,读入内存(读到加载地址)

    3.运行程序

    4.如果loadaddr不等于加载地址,程序本身需要重定位代码

    

    以上面的例子为例,data段的运行地址在0x30000000,在取值是就回去0x300000000取值,但是指定了数据段在0x1000,因此需要将数据段拷贝到0x30000000去

    
     核心: 程序运行时的地址应位于运行时地址(重定位后的地址)(链接地址)
    

    对于bin文件

    1.elf->bin

    2.硬件机制启动

    3.如果bin文件所在位置不等于运行时地址程序本身实现重定位  

   

解析链接脚本

    SECTIONS {
            .text   0 : { *(.text) }              /* 加载地址等于运行地址,所有文件的代码段排在前面,按照Makefile中文件的顺序来排,也可以在链接脚本中指定谁派在前面 */
            .rodata   : { *(.rodata) }            /* 所有文件的只读数据段 */
            .data   0x30000000 : AT(0x1000)     /* 加载地址等于0x1000,运行地址等于0x30000000,会导致data段在bin文件中处于0x10000位置 */
            {                                     /* 我们需要将数据段复制到0x30000000的位置,由前面的代码段来拷贝 */
                data_load_addr = LOADADDR(.data)
                data_start = .;
                *(.data)
                data_end = .;
            }
            .bss    : { *(.bss) *(.COMMON) }    /* bss段紧接着data段排放,放在0x3xxxxxxx,bin文件中不存放bss段 */
        }                                        /* 程序运行时把bss段的数据清0 */    

    
    首先将被设置为0的全局变量的值打印出来,发现数值是一个乱码,因此需要把bss段的数据清0,修改start.S与链接脚本
    SECTIONS {
    .text   0 : { *(.text) }
    .rodata   : { *(.rodata) }
    .data   0x30000000 : AT(0x1000)
    {
        data_load_addr = LOADADDR(.data)
        data_start = .;
        *(.data)
        data_end = .;
    }
    .bss_start = .;
    .bss    : { *(.bss) *(.COMMON) }
    .bss_end = .;
}

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

四.拷贝代码与链接脚本的改进

    4.1 拷贝代码的拷贝(将数据段拷贝代SDRAM)

    
        ldr r1, =data_load_addr  /* data段在bin文件中的地址,加载地址 */
        ldr r2, =data_start        /* 重定位地址,运行时的地址 */
        ldr r3, =data_end         /* data段的结束地址 */
        str r0, [r1]
    cpy:
        ldrb r4, [r1]        /* 每次读取一个字节,但是我们的SDRAM是32位的,NOR是16位的,这样做效率会很低 */
        strb r4, [r2]
        add r1, r1, #1
        add r2, r2, #1
        cmp r2, r3
        bne cpy

        在上面拷贝的时候,我们每次从源地址读取一个字节,写入一个字节,但是我们的SDRAM是32位的,NOR是16位的,这样做效率很低

        ldrb从NOR中得到数据,strb来写SDRAM,假设复制16字节,ldrb需要执行16次访问NOR16次,strb执行16次,访问SDRAM16次,共32次       

        当CPU要读一个字节的时候,将地址发给内存控制器,内存控制器读SDRAM会读出4个字节,从中挑出需要的1个字节返回给CPU

        当CPU要写一个字节的时候,将地址和数据发送给内存控制器,内存控制器将32位的数据发送给SDRAM,同时会向SDRAM发送数据屏蔽信号(三条)DQM,会屏蔽掉不需要写的三个字节


        改进方法:使用ldr 从NOR中读,使用str 写入SDRAM中

                ldr从NOR中读,假设复制16字节,执行4次,访问8次,内存控制器会访问两次NOR,因为一次只能够得到两个字节

                str写SDRAM,执行4次,访问硬件4次,一次可以写入4个字节

                

                修改代码如下

                ldr r1, =data_load_addr  /* data段在bin文件中的地址,加载地址 */
                ldr r2, =data_start        /* 重定位地址,运行时的地址 */
                ldr r3, =data_end         /* data段的结束地址 */
                str r0, [r1]
            cpy:
                ldr r4, [r1]        /* 每次读取一个字节,但是我们的SDRAM是32位的,NOR是16位的,这样做效率会很低 */
                str r4, [r2]
                add r1, r1, #4
                add r2, r2, #4
                cmp r2, r3
                ble cpy
                
                /* Start.S清除bss段 */
                ldr r1, = bss_start    
                ldr r2, = bss_end        /* 查看反汇编,r1 = 0x30000002 r2 = 0x3000000c */
                mov r2, #0
            clean:
                str r2, [r1]            /* 清0的时候以4字节清0,但是0x30000002并不是4字节对齐 */
                                        /* 会将0存放到0x30000000处,str 0, [0x30000000],会破坏别的数据 */
                add r1, r1, #4
                cmp r1, r2
                ble clean
 

    4.2 链接脚本的改进

            修改链接脚本使bss段向4取整

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

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

    5.1 将整个程序的代码段和数据段重定位

        5.1.1 在链接脚本中指定runtime addr指定为SDRAM的地址

        5.1.2 前面的代码需要将整个代码拷贝到SDRAM中

        5.1.3 程序应该位于0x30000000地址,但是刚开始位于0地址,仍然可以运行,因此重定位之前的代码与位置无关,即用位置无关码写成

        
        5.1.4 修改链接脚本如下:
                SECTIONS {
            . = 0x30000000;
            
            . = ALIGN(4);
            .text     :
            {
                *(.text)
            }
            
            . = ALIGN(4);
            .rodata    :
            {
                *(.rodata)
            }
            
            . = ALIGN(4);
            .data    :
            {
                *(.data)
            }
            
            . = ALIGN(4);
            bss_start = .;
            .bss    :
            {
                *(.bss) *(.COMMON)
            }
            bss_end = .;
        }

        一般都是使用上面这种代码段和数据段放在一起的链接脚本

            

        5.1.5 修改Start.S对数据段的重定位为对代码段,数据段,rodata段整个程序重定位

                /* 重定位text, data, rodata段 */
                mov r1, #0
                ldr r2, =_start
                ldr r3, =bss_start
            cpy:
                ldr r4, [r1]
                str r4, [r2]
                add r1, r1, #4
                add r2, r2, #4
                cmp r2, r3
                ble cpy              

    5.2 分析启动代码

        

        5.2.1 在重定位代码之前,需要调用sdram_init函数来初始化sdram,对应的反汇编如下图

            

            bl    30000c40 <SdramInit>

            这一句的意思并不是调到0x30000c40,这时候sdram没有初始化,跳过去执行肯定GG

            

            修改链接脚本中的. = 0x30000000修改为 .=0x32000000

            

            再次编译查看反汇编文件如下图

            

            两次跳转的机器完全一致,这并不是调到0x30000c40地址,而是跳到当前PC值+offset(链接器算出来的)

            具体跳到哪里由当前PC值决定

            

            假设程序从0x30000000执行,当前指令的地址320001ec,那么程序就会跳到0x30000c40执行

            如果程序从0执行,当前指令的地址是0x1ec,那么程序就会跳到0xc40

            如果程序从0x32000000执行,当前指令的地址是0x320001ec,那么程序就会跳到0x32000c40

            

            注意:     

                5.2.1 反汇编文件里的b/bl xxxx只是方便查看的作用,不是调到这个地址

                5.2.2 跳到哪里由PC值+offset决定    

        

        5.2.2 注意到在调用main函数时使用bl main,查看反汇编

             30000230:    ebffffc2     bl    30000140 <main>

            

            由于是bl跳转指令,程序从0开始执行,因此会跳到0x230地址,但是之前已经将代码拷贝到SDRAM中去了,拷贝到0x30000230

            因此这时不对的,我们想让程序调到SDRAM的话必须使用绝对跳转如下

            ldr PC, =main

            反汇编如下

            30000230:    e59ff01c     ldr    pc, [pc, #28]    ; 30000254 <.text+0x254>

           

    5.3 怎么写位置无关码-> 不使用绝对地址,看反汇编有没有用到绝对地址

        5.3.1 使用相对跳转命令(b/bl)

        5.3.2 重定位之前不可使用绝对地址,不可以访问全局变量,静态变量

        5.3.3 重定位之后,使用ldr pc, =xxx来跳转,跳转到runtime addr

        5.3.4 不可访问有初始值数组,因为初始值会存放在rodata或者data中,使用绝对地址来访问具体见下面的例子

        

    5.4 sdram_init函数的写法    

        5.4.1 在sdram_init函数中,直接对寄存器进行赋值,没有访问任何全局变量,静态变量

        

        5.4.2 修改sdram_init函数如下:

            void SdramInit()
            {
                unsigned int arr[] = {
                    0x22000000,
                    0x00000700,
                    0x00000700,
                    0x00000700,
                    0x00000700,
                    0x00000700,
                    0x00000700,
                    0x18001,
                    0x18001,
                    0x8404f5,
                    0xb1,
                    0x20,
                    0x20,
                    };

                volatile unsigned int *p = (volatile unsigned int*)0x48000000;
                int i;

                for (i = 0; i < 13; i++)
                {
                    *p = array[i];
                    p++;
                }

            }   

            这个函数编译烧写后并没有任何反应,表示这个函数并不是位置无关的

            反汇编代码如下

            30000c40 <SdramInit>:
            30000c40:    e1a0c00d     mov    ip, sp
            30000c44:    e92dd800     stmdb    sp!, {fp, ip, lr, pc}
            30000c48:    e24cb004     sub    fp, ip, #4    ; 0x4
            30000c4c:    e24dd03c     sub    sp, sp, #60    ; 0x3c
            30000c50:    e59f3088     ldr    r3, [pc, #136]    ; 30000ce0 <.text+0xce0>
                                    /* 读30000ce0的值,依赖于PC值,如果是0地址运行就是赌0xce0处的值 */
                                    /* 读到30000eb4, r3 = 0x30000eb4 */
            30000c54:    e24be040     sub    lr, fp, #64    ; 0x40
            30000c58:    e1a0c003     mov    ip, r3
                                    /* ip = r3 = 0x30000eb4 */
            30000c5c:    e8bc000f     ldmia    ip!, {r0, r1, r2, r3}
                                    /* 读0x30000eb4的数据,加载到r0,r1,r2,r3上去,但是sdram没有初始化,程序会死掉 */
                                    /* 0x30000eb4存放了寄存器的初始值 */
                                    /* 初始值保存在rodata里面,用初始值来初始化数组,而数组保存在栈里面 */
            30000c60:    e8ae000f     stmia    lr!, {r0, r1, r2, r3}
            30000c64:    e8bc000f     ldmia    ip!, {r0, r1, r2, r3}
            30000c68:    e8ae000f     stmia    lr!, {r0, r1, r2, r3}
            30000c6c:    e8bc000f     ldmia    ip!, {r0, r1, r2, r3}
            30000c70:    e8ae000f     stmia    lr!, {r0, r1, r2, r3}
            30000c74:    e59c3000     ldr    r3, [ip]
            30000c78:    e58e3000     str    r3, [lr]
            30000c7c:    e3a03312     mov    r3, #1207959552    ; 0x48000000
            30000c80:    e50b3044     str    r3, [fp, #-68]
            30000c84:    e3a03000     mov    r3, #0    ; 0x0
            30000c88:    e50b3048     str    r3, [fp, #-72]
            30000c8c:    e51b3048     ldr    r3, [fp, #-72]
            30000c90:    e353000c     cmp    r3, #12    ; 0xc
            30000c94:    ca00000f     bgt    30000cd8 <SdramInit+0x98>
            30000c98:    e51b1044     ldr    r1, [fp, #-68]
            30000c9c:    e51b3048     ldr    r3, [fp, #-72]
            30000ca0:    e3e02033     mvn    r2, #51    ; 0x33
            30000ca4:    e1a03103     mov    r3, r3, lsl #2
            30000ca8:    e24b000c     sub    r0, fp, #12    ; 0xc
            30000cac:    e0833000     add    r3, r3, r0
            30000cb0:    e0833002     add    r3, r3, r2
            30000cb4:    e5933000     ldr    r3, [r3]
            30000cb8:    e5813000     str    r3, [r1]
            30000cbc:    e51b3044     ldr    r3, [fp, #-68]
            30000cc0:    e2833004     add    r3, r3, #4    ; 0x4
            30000cc4:    e50b3044     str    r3, [fp, #-68]
            30000cc8:    e51b3048     ldr    r3, [fp, #-72]
            30000ccc:    e2833001     add    r3, r3, #1    ; 0x1
            30000cd0:    e50b3048     str    r3, [fp, #-72]
            30000cd4:    eaffffec     b    30000c8c <SdramInit+0x4c>
            30000cd8:    e24bd00c     sub    sp, fp, #12    ; 0xc
            30000cdc:    e89da800     ldmia    sp, {fp, sp, pc}
            30000ce0:    30000eb4     strcch    r0, [r0], -r4

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

    6.1 汇编传递参数  

  
  mov r0, #0
    ldr r1, =_start
    ldr r2, =bss_start
    sub r2, r2, r1
    bl Copy2Sdram
    
    void Copy2Sdram(volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)
    {
        unsigned int i = 0;

        while (i < len)
        {
            *dest++ = *src++;
            i += 4;
        }
    }
    
    ldr r0, =bss_start
    ldr r1, =bss_end
    bl CleanBss

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

    6.2 C语言直接获得地址参数

        void Copy2Sdram(void)
        {
            extern unsigned int code_addr, bss_start;

            volatile unsigned int *dest = (volatile unsigned int *)&code_addr;
            volatile unsigned int *src = (volatile unsigned int *)0;
            volatile unsigned int *end = (volatile unsigned int *)&bss_start;
            
            while (dest < end)
            {
                *dest++ = *src++;
            }
        }

        void CleanBss(void)
        {
            extern unsigned int bss_start, bss_end;

            volatile unsigned int *start = (volatile unsigned int *)&bss_start;
            volatile unsigned int *end = (volatile unsigned int *)&bss_end;

            while (start < end)
            {
                *start++ = 0;        
            }
        }
 

        C函数如何使用lds文件中的变量abc(汇编文件中可以直接使用)? 

        a.在C函数中生命该变量未extern类型,比如extern int abc;

        b.使用时,要取址,比如

            int *p = &abc; //p的值即为lds文件中abc的值

            

    6.3 C函数中使用链接脚本变量需要生命,汇编文件中可以直接使用的原因:

        C函数中声明某个变量,必须声明,例如int g_i,那么程序必然有4字节保存这个变量

        

        假设lds文件中有100W个,a1,a2....等变量,C程序完全没有必要存储这些变量

        编译程序时有一个symbol tabel(符号表),保存着变量的名字和地址

        对于链接脚本的变量也使用符号表,保存着lds变量(准确来说是常量,里面的值是固定的)的名字和值(注意,不是地址)

        

        对于普通变量,使用&g_i 得到addr,为了保持代码的一致,对于lds中的a1,使用&a1得到里面的值

        符号表里面的值在链接时确定,符号表不会存放到程序中去

           

        结论:

            6.3.1 C程序中不保存lds文件中的变量

            6.3.2 借助符号表来保存lds中常量的值,使用时加上"&"得到它的值

            
   

猜你喜欢

转载自blog.csdn.net/qq_36521904/article/details/80635231
今日推荐