嵌入式C語言自我修養 06:U-boot鏡像自拷貝分析:section屬性

6.1 GNU C 的擴展關鍵字:attribute

GNU C 增加一個 atttribute 關鍵字用來聲明一個函數、變量或類型的特殊屬性。聲明這個特殊屬性有什麼用呢?主要用途就是指導編譯器在編譯程序時進行特定方面的優化或代碼檢查。比如,我們可以通過使用屬性聲明指定某個變量的數據邊界對齊方式。

attribute 的使用非常簡單,當我們定義一個函數、變量或類型時,直接在它們名字旁邊添加下面的屬性聲明即可:

__atttribute__((ATTRIBUTE))

這裏需要注意的是:attribute 後面是兩對小括號,不能圖方便只寫一對,否則編譯可能通不過。括號裏面的 ATTRIBUTE 代表的就是要聲明的屬性。現在 attribute 支持十幾種屬性:

  • section
  • aligned
  • packed
  • format
  • weak
  • alias
  • noinline
  • always_inline
  • ……

在這些屬性中,aligned 和 packed 用來顯式指定一個變量的存儲邊界對齊方式。一般來講,我們定義一個變量,編譯器會根據變量類型,按照默認的規則來給這個變量分配大小、按照默認的邊界對齊方式分配一個地址。而使用 atttribute 這個屬性聲明,就相當於告訴編譯器:按照我們指定的邊界地址對齊去給這個變量分配存儲空間。

char c2 __attribute__((aligned(8)) = 4;
int global_val __attribute__((section(".data")));

有些屬性可能還有自己的參數。比如 aligned(8) 表示這個變量按8字節地址對齊,參數也要使用小括號括起來。如果屬性的參數是一個字符串,小括號裏的參數還要用雙引號引起來。

當然,我們也可以對一個變量同時添加多個屬性說明。在定義時,各個屬性之間用逗號隔開就可以了。

char c2 __attribute__((packed,aligned(4)));
char c2 __attribute__((packed,aligned(4))) = 4;
__attribute__((packed,aligned(4))) char c2 = 4;

在上面的示例中,我們對一個變量添加2個屬性聲明,這兩個屬性都放在 atttribute(()) 的2對小括號裏面,屬性之間用逗號隔開。這裏還有一個細節,就是屬性聲明要緊挨着變量,上面的三種定義方式都是沒有問題的,但下面的定義方式在編譯的時候可能就通不過。

char c2 = 4 __attribute__((packed,aligned(4)));

6.2 屬性聲明:section

在本節教程中,我們先講一下 section 這個屬性。使用atttribute 來聲明一個 section 屬性,主要用途是在程序編譯時,將一個函數或變量放到指定的段,即 section 中。

在講解這個功能之前,爲了照顧一下對計算機編譯、鏈接過程不是很瞭解的同學,我們先講一講程序的編譯、鏈接過程。

程序的編譯、鏈接過程

一個可執行目標文件,它主要由代碼段、數據段、BSS 段構成。代碼段主要存放編譯生成的可執行指令代碼,數據段和 BSS 段用來存放全局變量、未初始化的全局變量。代碼段、數據段和 BSS 段構成了一個可執行文件的主要部分。

除了這三個段,可執行文件中還包含其它一些段。用編譯器的專業術語講,還會包含其它一些 section,比如只讀數據段、符號表等等。我們可以使用下面的 readelf 命令,去查看一個可執行文件中各個 section 的信息。

$ gcc -o a.out hello.c
$ readelf -S a.out

  here are 31 section headers, starting at offset 0x1848:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   
  [ 0]                   NULL            00000000 000000 000000
  [ 1] .interp           PROGBITS        08048154 000154 000013
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020
  [ 3] .note.gnu.build-i NOTE            08048188 000188 000024
  [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000020
  [ 5] .dynsym           DYNSYM          080481cc 0001cc 000040
  [ 6] .dynstr           STRTAB          0804820c 00020c 000045
  [ 7] .gnu.version      VERSYM          08048252 000252 000008
  [ 8] .gnu.version_r    VERNEED         0804825c 00025c 000020
  [ 9] .rel.dyn          REL             0804827c 00027c 000008
  [10] .rel.plt          REL             08048284 000284 000008
  [11] .init             PROGBITS        0804828c 00028c 000023
  [13] .plt.got          PROGBITS        080482d0 0002d0 000008
  [14] .text             PROGBITS        080482e0 0002e0 000172
  [15] .fini             PROGBITS        08048454 000454 000014
  [16] .rodata           PROGBITS        08048468 000468 000008
  [17] .eh_frame_hdr     PROGBITS        08048470 000470 00002c
  [18] .eh_frame         PROGBITS        0804849c 00049c 0000c0
  [19] .init_array       INIT_ARRAY      08049f08 000f08 000004
  [20] .fini_array       FINI_ARRAY      08049f0c 000f0c 000004
  [21] .jcr              PROGBITS        08049f10 000f10 000004
  [22] .dynamic          DYNAMIC         08049f14 000f14 0000e8
  [23] .got              PROGBITS        08049ffc 000ffc 000004
  [24] .got.plt          PROGBITS        0804a000 001000 000010
  [25] .data             PROGBITS        0804a020 001020 00004c
  [26] .bss              NOBITS          0804a06c 00106c 000004
  [27] .comment          PROGBITS        00000000 00106c 000034
  [28] .shstrtab         STRTAB          00000000 00173d 00010a
  [29] .symtab           SYMTAB          00000000 0010a0 000470
  [30] .strtab           STRTAB          00000000 001510 00022d

在 Linux 環境下,使用 GCC 編譯生成一個可執行文件 a.out,使用上面的 readelf 命令,就可以查看這個可執行文件中各個 section 的基本信息,比如大小、起始地址等等。在這些 section 中,其中 .text section 就是我們常說的代碼段,.data section 是數據段,.bss section 是 BSS 段。

我們知道一段源程序代碼在編譯生成可執行文件的過程中,函數和變量是放在不同段中的。一般默認的規則如下。

section 組成
代碼段( .text) 函數定義、程序語句
數據段( .data) 初始化的全局變量、初始化的靜態局部變量
BSS段( .bss) 未初始化的全局變量、未初始化的靜態局部變量

比如,在下面的程序中,我們分別定義一個函數、一個全局變量和一個未初始化的全局變量。

//hello.c
int global_val = 8;
int uninit_val;

void print_star(void)
{
    printf("****\n");
}
int main(void)
{
    print_star();
    return 0;
}

接着,我們使用 GCC 編譯這個程序,並查看生成的可執行文件 a.out 的符號表和 section header 表信息。

$ gcc -o a.out hello.c
$ readelf -s a.out
$ readelf -S a.out
符號表信息:
Num:  Value   Size Type    Bind   Vis      Ndx Name
37: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
48: 0804a024     4 OBJECT  GLOBAL DEFAULT   26 uninit_val
51: 0804a014     0 NOTYPE  WEAK   DEFAULT   25 data_start
52: 0804a020     0 NOTYPE  GLOBAL DEFAULT   25 _edata
53: 080484b4     0 FUNC    GLOBAL DEFAULT   15 _fini
54: 0804a01c     4 OBJECT  GLOBAL DEFAULT   25 global_val
55: 0804a014     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
61: 08048450    93 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
62: 0804a028     0 NOTYPE  GLOBAL DEFAULT   26 _end
63: 08048310     0 FUNC    GLOBAL DEFAULT   14 _start
64: 080484c8     4 OBJECT  GLOBAL DEFAULT   16 _fp_hw
65: 0804840b    25 FUNC    GLOBAL DEFAULT   14 print_star
66: 0804a020     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
67: 08048424    36 FUNC    GLOBAL DEFAULT   14 main
71: 080482a8     0 FUNC    GLOBAL DEFAULT   11 _init
section header信息:
Section Headers:
  [Nr] Name         Type        Addr     Off    Size   
  [14] .text        PROGBITS    08048310 000310 0001a2 
  [25] .data        PROGBITS    0804a014 001014 00000c
  [26] .bss         NOBITS      0804a020 001020 000008
  [27] .comment     PROGBITS    00000000 001020 000034
  [28] .shstrtab    STRTAB      00000000 001722 00010a
  [29] .symtab      SYMTAB      00000000 001054 000480
  [30] .strtab      STRTAB      00000000 0014d4 00024e

通過符號表和節頭表 section header table 信息,我們可以看到,函數 print_star 被放在可執行文件中的 .text section,即代碼段;初始化的全局變量 global_val 被放在了 a.out 的 .data section,即數據段;而未初始化的全局變量 uninit_val 則被放在了.bss section,即 BSS 段。

編譯器在編譯程序時,是以源文件爲單位,將一個個源文件編譯生成一個個目標文件。在編譯過程中,編譯器都會按照這個默認規則,將函數、變量分別放在不同的 section 中,最後將各個 section 組成一個目標文件。編譯過程結束後,鏈接器接着會將各個目標文件組裝合併、重定位,生成一個可執行文件。

鏈接器是如何將各個目標文件組裝成一個可執行文件的呢?很簡單,鏈接器首先會分別將各個目標文件的代碼段整合,組裝成一個大的代碼段;將各個目標文件中的數據段整合,合併成一個大的數據段;接着將合併後的新代碼段、數據段再合併爲一個文件;最後經過重定位,就生成了一個可以運行的可執行文件了。

現在又有一個疑問來了,鏈接器在將各個不同的 section 段組裝成一個可執行文件的過程中,各個 section 的順序如何排放呢?比如代碼段、數據段、BSS 段、符號表等,誰放在前面?誰放在後面?

鏈接器在鏈接過程中,會將不同的 section,按照鏈接腳本中指定的各個 section 的排放順序,組裝成一個可執行文件。一般在 Ubuntu 等 PC 版本的系統中,系統會有默認的鏈接腳本,不需要程序員操心。

$ ld --verbose

我們使用上面命令,就可以查看編譯當前程序時,鏈接器使用的默認鏈接腳本。在嵌入式系統中,因爲是交叉編譯,所以軟件源碼一般會自帶一個鏈接腳本。比如在 U-boot 源碼的根目錄下面,你會看到一個 u-boot.lds 的文件,這個文件就是編譯 U-boot 時,鏈接器要使用的鏈接腳本。在 Linux 內核中,同樣會有 vmlinux.lds 這樣一個鏈接腳本。

屬性 section 編程示例

在 GNU C 中,我們可以通過 attribute 的 section 屬性,顯式指定一個函數或變量,在編譯時放到指定的 section 裏面。通過上面的程序我們知道,未初始化的全局變量是放在 .data section 中的,即放在 BSS 段中。現在我們就可以通過 section 屬性,把這個未初始化的全局變量放到數據段 .data 中。

int global_val = 8;
int uninit_val __attribute__((section(".data")));
int main(void)
{
    return 0;
}

通過上面的 readelf 命令查看符號表,我們可以看到,uninit_val 這個未初始化的全局變量,通過attribute((section(".data"))) 屬性聲明,就被編譯器放在了數據段 .data section 中。

6.3 U-boot 啓動過程中的鏡像自拷貝分析

有了 section 這個屬性,我們接下來就可以試着分析,U-boot 在啓動過程中,是如何將自身代碼加載的 RAM 中的。

搞嵌入式的都知道 U-boot,U-boot 的用途主要是加載 Linux 內核鏡像到內存、給內核傳遞啓動參數、然後引導 Linux 操作系統啓動。

U-boot 一般存儲在 Nor flash 或 NAND Flash 上。無論從 Nor Flash 還是從 Nand Flash 啓動,U-boot 其本身在啓動過程中,也會從 Flash 存儲介質上加載自身代碼到內存,然後進行重定位,跳到內存 RAM 中去執行。這個功能一般叫做“自舉”,是不是感覺很牛 X?U-boot 重定位的過程今天就不展開了,有興趣的同學,可以看看我的嵌入式視頻教程《C 語言嵌入式 Linux 高級編程》第3期:程序的編譯、鏈接和運行。今天我們的主要任務是去看看 U-boot 是怎麼完成自拷貝的,或者說它是怎樣將自身代碼從 Flash 拷貝到內存 RAM 中的。

在拷貝自身代碼的過程中,一個主要的疑問就是,U-boot 是如何識別自身代碼的?是如何知道從哪裏拷貝代碼的?是如何知道拷貝到哪裏停止的?這個時候我們不得不說起 U-boot 源碼中的一個零長度數組。

char __image_copy_start[0] __attribute__((section(".__image_copy_start")));
char __image_copy_end[0] __attribute__((section(".__image_copy_end")));

這兩行代碼定義在 U-boot-2016.09 中的 arch/arm/lib/section.c 文件中。在其它版本中可能路徑不同或者沒有定義,爲了分析這個功能,建議大家可以下載 U-boot-2016.09 這個版本的U-boot源碼。

這兩行代碼的作用是分別定義一個零長度數組,並告訴編譯器要分別放在 .imagecopystart 和 .image_copy_end 這兩個 section 中。

鏈接器在鏈接各個目標文件時,會按照鏈接腳本里各個 section 的排列順序,將各個 section 組裝成一個可執行文件。U-boot 的鏈接腳本 u-boot.lds 在 U-boot 源碼的根目錄下面。

OUTPUT_FORMAT("elf32-littlearm",
    "elf32-littlearm",
    "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 . = 0x00000000;
 . = ALIGN(4);
 .text :
 {
  *(.__image_copy_start)
  *(.vectors)
  arch/arm/cpu/armv7/start.o (.text*)
  *(.text*)
 }
 . = ALIGN(4);
 .data : {
  *(.data*)
 }
    ...
    ...
 . = ALIGN(4);
 .image_copy_end :
 {
  *(.__image_copy_end)
 }
 .end :
 {
  *(.__end)
 }
 _image_binary_end = .;
 . = ALIGN(4096);
 .mmutable : {
  *(.mmutable)
 }
 .bss_start __rel_dyn_start (OVERLAY) : {
  KEEP(*(.__bss_start));
  __bss_base = .;
 }
 .bss __bss_base (OVERLAY) : {
  *(.bss*)
   . = ALIGN(4);
   __bss_limit = .;
 }
 .bss_end __bss_limit (OVERLAY) : {
  KEEP(*(.__bss_end));
 }
}

通過鏈接腳本我們可以看到,image_copy_start 和 image_copy_end 這兩個 section,在鏈接的時候分別放在了代碼段 .text 的前面、數據段 .data 的後面,作爲 U-boot 拷貝自身代碼的起始地址和結束地址。而在這兩個 section 中,我們除了放2個零長度數組外,並沒有再放其它變量。根據前面的學習我們知道,零長度數組是不佔用存儲空間的,所以上面定義的兩個零長度數組,其實就分別代表了 U-boot 鏡像要拷貝自身鏡像的起始地址和結束地址。

char __image_copy_start[0] __attribute__((section(".__image_copy_start")));
char __image_copy_end[0] __attribute__((section(".__image_copy_end")));

無論 U-boot 自身鏡像是存儲在 Nor Flash,還是 Nand Flash 上,我們只要知道了這兩個地址,就可以直接調用相關代碼拷貝。

接着在 arch/arm/lib/relocate.S 中,ENTRY(relocate_code) 彙編代碼主要完成代碼拷貝的功能。

ENTRY(relocate_code)
    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

在這段彙編代碼中,寄存器 R1、R2 分別表示要拷貝鏡像的起始地址和結束地址,R0 表示要拷貝到 RAM 中的地址,R4 存放的是源地址和目的地址之間的偏移,在後面重定位過程中會用到這個偏移值。

ldr r1, =__image_copy_start

見上面指令,在彙編代碼中,ARM的 ldr 指令立即尋址,直接對數組名進行引用,獲取要拷貝鏡像的首地址,並保存在 R1 寄存器中。數組名本身其實就代表一個地址。通過這種方式,U-boot 在嵌入式啓動的初始階段,就完成了自身代碼的拷貝工作:從 Flash 上拷貝自身鏡像到 RAM 中,然後再進行重定位,最後跳到 RAM 中執行。

本教程根據 C語言嵌入式Linux高級編程視頻教程 第05期 改編,電子版書籍可加入QQ羣:475504428 下載,更多嵌入式視頻教程,可關注:
微信公衆號:宅學部落(armlinuxfun)
51CTO學院-王利濤老師:http://edu.51cto.com/sd/d344f

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章