天天看點

第7課,代碼重定位

注:以下内容學習于韋東山老師arm裸機第一期視訊教程

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

    1.1 重定位的引入

        2440架構圖如下

第7課,代碼重定位

        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;  
           

            編譯後檢視反彙編檔案,如下圖

第7課,代碼重定位

            是以可以看出程式還包含bss段,rodata段,最後的common段表示注釋段

            總結,代碼中的段:

                代碼段        .text

                資料段        .data

                隻讀資料段 .rodata

                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 [email protected] $<
                
            %.o:%.S
                arm-linux-gcc -c -o [email protected] $<
                
            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 [email protected] $<
            
        %.o:%.S
            arm-linux-gcc -c -o [email protected] $<
            
        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,對應的反彙編如下圖

第7課,代碼重定位

            bl    30000c40 <SdramInit>

            這一句的意思并不是調到0x30000c40,這時候sdram沒有初始化,跳過去執行肯定GG

            修改連結腳本中的. = 0x30000000修改為 .=0x32000000

            再次編譯檢視反彙編檔案如下圖

第7課,代碼重定位

            兩次跳轉的機器完全一緻,這并不是調到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中常量的值,使用時加上"&"得到它的值