uboot啓動流程詳解(5)-_main

前言

  _main標號中主要調用的函數有三個,board_init_f,relocate_code,board_init_r,這裏先貼出_main的代碼並註釋,然後對這三個函數的流程及原理進行詳細介紹。

1、代碼註釋

ENTRY(_main)

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */
/*
*這裏首先爲調用board_init_f準備一個臨時堆棧,CONFIG_SYS_INIT_SP_ADDR這個宏
*就是cpu片上內存的高地址(片上內存的大小減去GD_SIZE)。然後將堆棧初始的地址保存在
*r9,所以r9就是gd的起始地址,後面需要靠r9訪問gd中的成員。然後將r0賦值成0,r0就是
*要調用的board_init_f函數的第一個參數!
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
    ldr sp, =(CONFIG_SPL_STACK)
#else
    ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    sub sp, sp, #GD_SIZE    /* allocate one GD above SP */
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    mov r9, sp      /* GD is above SP */
    mov r0, #0
    bl  board_init_f

#if ! defined(CONFIG_SPL_BUILD)

/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_moni). Trick here is that we'll return
 * 'here' but relocated.
 */

/*
*這段代碼的主要功能就是將uboot搬移到內存的高地址去執行,爲kernel騰出低端空間,
*防止kernel解壓覆蓋uboot。
*adr這個指令非常有意識,可以自己問度娘瞭解以下。這裏這三行代碼
*adr    lr, here
*ldr    r0, [r9, #GD_RELOC_OFF]
*add    lr, lr, r0
*的功能就是,將relocate後的here標號的地址保存到lr寄存器,這樣等到relocate
*完成後,就可以直接跳到relocate後的here標號去執行了。
*relocate_code函數的原理及流程,是uboot的重要代碼,下面詳解!
*/
    ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    ldr r9, [r9, #GD_BD]        /* r9 = gd->bd */
    sub r9, r9, #GD_SIZE        /* new GD is below bd */

    adr lr, here
    ldr r0, [r9, #GD_RELOC_OFF]     /* r0 = gd->reloc_off */
    add lr, lr, r0
    ldr r0, [r9, #GD_RELOCADDR]     /* r0 = gd->relocaddr */
    b   relocate_code
here:

/* Set up final (full) environment */
     /*
     *relocate完成後,uboot的代碼被搬到了內存的頂部,所以必須重新設置異常向量表的
     *地址,c_runtime_cpu_setup這個函數的主要功能就是重新設置異常向量表的地址。
     */
    bl  c_runtime_cpu_setup /* we still call old routine here */
     
     /*
     *在relocate的過程中,並沒有去搬移bss段。bss段是auto-relocated的!爲什麼?
     *可以自己思考一下,又或許看完我後面介紹的relocate的原理後你會明白!
     */
    ldr r0, =__bss_start    /* this is auto-relocated! */
    ldr r1, =__bss_end      /* this is auto-relocated! */

    mov r2, #0x00000000     /* prepare zero to clear BSS */
     
     /*
     *清空bss段。
     */
clbss_l:cmp r0, r1          /* while not at end of BSS */
    strlo   r2, [r0]        /* clear 32-bit BSS word */
    addlo   r0, r0, #4      /* move to next */
    blo clbss_l
     
     /*
     *這兩行代碼無視之,點燈什麼的,和這裏要講的uboot的原理及過程沒有半毛錢關係。
     */
    bl coloured_LED_init
    bl red_led_on
     /*
     *將relocate後的gd的地址保存到r1,然後調用board_init_r函數,進入uboot的新天地!
     */
    /* call board_init_r(gd_t *id, ulong dest_addr) */
    mov     r0, r9                  /* gd_t */
    ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
    /* call board_init_r */
    ldr pc, =board_init_r   /* this is auto-relocated! */

    /* we should not return here. */

#endif

ENDPROC(_main)

2、board_init_f

  這個函數的主要功能就是初始化一些硬件設備(串口、定時器等)並且設置gd結構體中的成員。執行完後,gd中一些重要成員的指向如下圖所示:
  這裏寫圖片描述
  剛開始uboot在編譯地址TEXT_BASE處執行,relocate完成後,uboot會被搬移到gd->relocaddr這個地址執行。gd->start_addr_sp是relocate完成後新的堆棧起始地址。接下來對其調用到的主要函數進行介紹。
  
  1、獲取整個uboot的大小到gd->mon_len

static int setup_mon_len(void)
{
#ifdef __ARM__
    gd->mon_len = (ulong)&__bss_end - (ulong)_start;
#elif defined(CONFIG_SANDBOX)
    gd->mon_len = (ulong)&_end - (ulong)_init;
#else
    /* TODO: use (ulong)&__bss_end - (ulong)&__text_start; ? */
    gd->mon_len = (ulong)&__bss_end - CONFIG_SYS_MONITOR_BASE;
#endif
    return 0;
}

  __bss_end ,_start這兩個標號在鏈接腳本u-boot.lds中定義。__bss_end -_start uboot就是整個uboot的大小。如果對鏈接腳本不瞭解,可以參考《編譯及連接過程 》
  
  2、獲取環境變量的起始地址到gd->env_addr

int env_init(void)
{
    /* use default */
    gd->env_addr = (ulong)&default_environment[0];
    gd->env_valid = 1;

    return 0;
}

  default_environment是全局初始化變量,保存在uboot的data段中!

  3、設置gd->baudrate
  從環境變量中獲取波特率到gd->baudrate成員中。

static int init_baud_rate(void)
{
    gd->baudrate = getenv_ulong("baudrate", 10, CONFIG_BAUDRATE);
    return 0;
}

  
  4、獲取內存的實際大小到gd->ram_size

int dram_init(void)
{
    gd->ram_size = get_ram_size((void *)PHYS_SDRAM, PHYS_SDRAM_SIZE);

    return 0;
}

  5、獲取gd->relocaddr
  重新設置內存大小,預留出內存頂部的4K不用。
  

static int setup_dest_addr(void)
{
    debug("Monitor len: %08lX\n", gd->mon_len);
    /*
     * Ram is setup, size stored in gd !!
     */
    debug("Ram size: %08lX\n", (ulong)gd->ram_size);
#if defined(CONFIG_SYS_MEM_TOP_HIDE)
    /*
     * Subtract specified amount of memory to hide so that it won't
     * get "touched" at all by U-Boot. By fixing up gd->ram_size
     * the Linux kernel should now get passed the now "corrected"
     * memory size and won't touch it either. This should work
     * for arch/ppc and arch/powerpc. Only Linux board ports in
     * arch/powerpc with bootwrapper support, that recalculate the
     * memory size from the SDRAM controller setup will have to
     * get fixed.
     */
    gd->ram_size -= CONFIG_SYS_MEM_TOP_HIDE;
#endif
#ifdef CONFIG_SYS_SDRAM_BASE
    gd->ram_top = CONFIG_SYS_SDRAM_BASE;
#endif
    gd->ram_top += get_effective_memsize();
    gd->ram_top = board_get_usable_ram_top(gd->mon_len);
    gd->relocaddr = gd->ram_top;
    debug("Ram top: %08lX\n", (ulong)gd->ram_top);
#if defined(CONFIG_MP) && (defined(CONFIG_MPC86xx) || defined(CONFIG_E500))
    /*
     * We need to make sure the location we intend to put secondary core
     * boot code is reserved and not used by any part of u-boot
     */
    if (gd->relocaddr > determine_mp_bootpg(NULL)) {
        gd->relocaddr = determine_mp_bootpg(NULL);
        debug("Reserving MP boot page to %08lx\n", gd->relocaddr);
    }
#endif
    return 0;
}

  6、設置gd->reloc_off
  設置relocate的偏移量,並將舊gd中的內容拷貝到新gd中。

static int setup_reloc(void)
{
    gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
    memcpy(gd->new_gd, (char *)gd, sizeof(gd_t));

    debug("Relocation Offset is: %08lx\n", gd->reloc_off);
    debug("Relocating to %08lx, new gd at %08lx, sp at %08lx\n",
          gd->relocaddr, (ulong)map_to_sysmem(gd->new_gd),
          gd->start_addr_sp);

    return 0;
}

3、relocate_code

3.1 原理介紹

  u-boot在啓動過程中,會把自己拷貝到RAM的頂端去執行。這一拷貝帶來的問題是執行地址的混亂。代碼的執行地址通常都是在編譯時有鏈接地址指定的,如何保證拷貝前後都可以執行呢?
  一個辦法是使用拷貝到RAM後的地址作爲編譯時的鏈接地址,拷貝前所有函數與全局變量的調用都增加偏移量。(如VxWorks的bootloader)儘量減少拷貝前需要執行的代碼量。
  另一個辦法是把image編譯成與地址無關的程序,也就是PIC - Position independent code。編譯器無法保證代碼的獨立性,它需要與加載器配合起來。U-boot自己加載自己,所以它自己就是加載器。PIC依賴於下面兩種技術:
1) 使用相對地址
2) 加載器可以自動更新涉及到絕對地址的指令
  對於PowerPC架構,u-boot只是在編譯時使用了-fpic,這種方式會生成一個.got段來存儲絕對地址符號。對於ARM架構,則是在編譯時使用-mword-relocations,生成與位置無關代碼,鏈接時使用-pie生成.rel.dyn段,該段中的每個條目被稱爲一個LABEL,用來存儲絕對地址符號的地址。

3.2 調試過程

  爲了理解rel.dyn段的作用,在uboot源代碼的common/main.c文件中,加入如下代碼,並在main_loop函數中調用rel_dyn_test。

void test_func(void)  
{  
    printf("test func\n");  
}  

static void * test_func_val = test_func;  
static int test_val = 10;   

void rel_dyn_test()  
{  
    test_val = 20; 
    test_func(); 

    printf("test = 0x%x\n", test_func);  
    printf("test_func = 0x%x\n", test_func_val);       
} 

  然後重新編譯uboot,並且對新生成的elf文件(u-boot)進行反彙編:
arm-fsl-linux-gnueabi-objdump -D u-boot > u-boot.huibian。然後在反彙編文件中找到對應的代碼段、數據段以及rel.dyn段。如下:
代碼段:

/*這裏是test_func()函數的反彙編*/
87802fe0 <test_func>:
87802fe0:   e59f300c    ldr r3, [pc, #12]   ; 87802ff4 <test_func+0x14>
87802fe4:   e3a01007    mov r1, #7
87802fe8:   e59f0008    ldr r0, [pc, #8]    ; 87802ff8 <test_func+0x18>
87802fec:   e5831000    str r1, [r3]
87802ff0:   ea003a4a    b   87811920 <printf>/*注:以下兩行爲Lable1*/
87802ff4:   8784004c    strhi   r0, [r4, ip, asr #32]
87802ff8:   8783127f            ; <UNDEFINED> instruction: 0x8783127f


/*這裏是rel_dyn_test函數的反彙編*/
878031c0 <rel_dyn_test>:
878031c0:   e59f302c    ldr r3, [pc, #44]   ; 878031f4 <rel_dyn_test+0x34>
878031c4:   e3a02014    mov r2, #20
878031c8:   e92d4010    push    {r4, lr}
878031cc:   e59f4024    ldr r4, [pc, #36]   ; 878031f8 <rel_dyn_test+0x38>
878031d0:   e5832000    str r2, [r3]
878031d4:   ebffff81    bl  87802fe0 <test_func>
878031d8:   e59f001c    ldr r0, [pc, #28]   ; 878031fc <rel_dyn_test+0x3c>
878031dc:   e1a01004    mov r1, r4
878031e0:   eb0039ce    bl  87811920 <printf>
878031e4:   e59f0014    ldr r0, [pc, #20]   ; 87803200 <rel_dyn_test+0x40>
878031e8:   e1a01004    mov r1, r4
878031ec:   e8bd4010    pop {r4, lr}
878031f0:   ea0039ca    b   87811920 <printf> /*注:以下四行爲Lable2*/
878031f4:   87838c3c            ; <UNDEFINED> instruction: 0x87838c3c
878031f8:   87802fe0    strhi   r2, [r0, r0, ror #31]
878031fc:   8783129b            ; <UNDEFINED> instruction: 0x8783129b
87803200:   878312a8    strhi   r1, [r3, r8, lsr #5]

  這些函數末尾存儲變量地址的內存空間稱爲Label(編譯器自動分配),這裏記<test_func>的最後兩行爲Lable1,<rel_dyn_test>的最後四行爲Lable2。等一下可以看到這些Lable的地址是存儲在rel.dyn段中的。
數據段

87838c3c <test_val>:
87838c3c:   0000000a    andeq   r0, r0, sl

8784004c <jimmy_val>:
8784004c:   00000000    andeq   r0, r0, r0

rel.dyn段

/*Lable1的rel.dyn段*/
8783b4a0:   87802ff4            ; <UNDEFINED> instruction: 0x87802ff4
8783b4a4:   00000017    andeq   r0, r0, r7, lsl r0
8783b4a8:   87802ff8            ; <UNDEFINED> instruction: 0x87802ff8
8783b4ac:   00000017    andeq   r0, r0, r7, lsl r0

/*Lable2的rel.dyn段*/
8783b4c8:   878031f4            ; <UNDEFINED> instruction: 0x878031f4
8783b4cc:   00000017    andeq   r0, r0, r7, lsl r0
8783b4d0:   878031f8            ; <UNDEFINED> instruction: 0x878031f8
8783b4d4:   00000017    andeq   r0, r0, r7, lsl r0
8783b4d8:   878031fc            ; <UNDEFINED> instruction: 0x878031fc
8783b4dc:   00000017    andeq   r0, r0, r7, lsl r0
8783b4e0:   87803200    strhi   r3, [r0, r0, lsl #4]
8783b4e4:   00000017    andeq   r0, r0, r7, lsl r0

  接下來看一下test_val = 20;這條c語句對應的彙編代碼爲:

ldr r3, [pc, #44] -----(1)
mov r2, #20       -----(2)
str r2, [r3]      -----(3)

  由於arm的流水線架構,所以執行第(1)條指令時pc是指向第(3)條指令的,再加上44(也就是再往下數11條指令,因爲一條指令4個字節),即指向了地址爲<878031f4>的語句,然後將其內容加載到r3中(即r3=0x87838c3c,對照上面的數據段,可知這就是變量test_val的地址);接着將立即數20放到r2;最後將r2的內容放到以r3爲地址的空間。
  也就是說,代碼在取符號(全局變量、函數等)的時候,會使用相對地址的方法去函數末尾的Lable中取到該符號的地址(說明白點,就是這些Lable存儲了要查找符號的地址),然後再根據該地址讀取符號的內容。所以當data段搬移時,Lable中的內容也要跟着改變!而這些Lable的地址保存在rel.dyn段中,所以代碼relocate時要用到rel.dyn段,對Lable中的內容進行修改!

3.3 代碼分析

ENTRY(relocate_code)
        /*
        *拷貝__image_copy_start到__image_copy_end之間的內容到新地址,即只拷貝了代碼段
        *和data段,沒有拷貝bss段和rel.dyn段。
        */
    ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
    subs    r4, r0, r1      /* r4 <- relocation offset */
    beq relocate_done       /* skip relocation */
    ldr r2, =__image_copy_end   /* r2 <- SRC &__image_copy_end */

copy_loop:
    ldmia   r1!, {r10-r11}      /* copy from source address [r1]    */
    stmia   r0!, {r10-r11}      /* copy to   target address [r0]    */
    cmp r1, r2          /* until source end address [r2]    */
    blo copy_loop

    /*
     * fix .rel.dyn relocations
     */
    /*
    *根據rel.dyn段設置Lable的內容,relocate後運行地址和編譯地址不一致還能跑,
    *全靠這裏啦。
    */
    ldr r2, =__rel_dyn_start    /* r2 <- SRC &__rel_dyn_start */
    ldr r3, =__rel_dyn_end  /* r3 <- SRC &__rel_dyn_end */
fixloop:
    ldmia   r2!, {r0-r1}        /* (r0,r1) <- (SRC location,fixup) */
    and r1, r1, #0xff
    cmp r1, #23         /* relative fixup? */
    bne fixnext

    /* relative fix: increase location by offset */
    add r0, r0, r4
    ldr r1, [r0]
    add r1, r1, r4
    str r1, [r0]
fixnext:
    cmp r2, r3
    blo fixloop

relocate_done:

#ifdef __XSCALE__
    /*
     * On xscale, icache must be invalidated and write buffers drained,
     * even with cache disabled - 4.2.7 of xscale core developer's manual
     */
    mcr p15, 0, r0, c7, c7, 0   /* invalidate icache */
    mcr p15, 0, r0, c7, c10, 4  /* drain write buffer */
#endif

    /* ARMv4- don't know bx lr but the assembler fails to see that */

#ifdef __ARM_ARCH_4__
    mov        pc, lr
#else
    bx        lr
#endif

ENDPROC(relocate_code)
發佈了57 篇原創文章 · 獲贊 80 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章