溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

嵌入式C語言自我修養(yǎng) 06:U-boot鏡像自拷貝分析:se

發(fā)布時間:2020-07-15 22:40:49 來源:網(wǎng)絡(luò) 閱讀:655 作者:宅學(xué)部落 欄目:系統(tǒng)運維

6.1 GNU C 的擴(kuò)展關(guān)鍵字:attribute

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

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

__atttribute__((ATTRIBUTE))

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

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

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

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

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

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

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

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

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

6.2 屬性聲明:section

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

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

程序的編譯、鏈接過程

一個可執(zhí)行目標(biāo)文件,它主要由代碼段、數(shù)據(jù)段、BSS 段構(gòu)成。代碼段主要存放編譯生成的可執(zhí)行指令代碼,數(shù)據(jù)段和 BSS 段用來存放全局變量、未初始化的全局變量。代碼段、數(shù)據(jù)段和 BSS 段構(gòu)成了一個可執(zhí)行文件的主要部分。

除了這三個段,可執(zhí)行文件中還包含其它一些段。用編譯器的專業(yè)術(shù)語講,還會包含其它一些 section,比如只讀數(shù)據(jù)段、符號表等等。我們可以使用下面的 readelf 命令,去查看一個可執(zhí)行文件中各個 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 環(huán)境下,使用 GCC 編譯生成一個可執(zhí)行文件 a.out,使用上面的 readelf 命令,就可以查看這個可執(zhí)行文件中各個 section 的基本信息,比如大小、起始地址等等。在這些 section 中,其中 .text section 就是我們常說的代碼段,.data section 是數(shù)據(jù)段,.bss section 是 BSS 段。

我們知道一段源程序代碼在編譯生成可執(zhí)行文件的過程中,函數(shù)和變量是放在不同段中的。一般默認(rèn)的規(guī)則如下。

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

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

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

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

接著,我們使用 GCC 編譯這個程序,并查看生成的可執(zhí)行文件 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

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

編譯器在編譯程序時,是以源文件為單位,將一個個源文件編譯生成一個個目標(biāo)文件。在編譯過程中,編譯器都會按照這個默認(rèn)規(guī)則,將函數(shù)、變量分別放在不同的 section 中,最后將各個 section 組成一個目標(biāo)文件。編譯過程結(jié)束后,鏈接器接著會將各個目標(biāo)文件組裝合并、重定位,生成一個可執(zhí)行文件。

鏈接器是如何將各個目標(biāo)文件組裝成一個可執(zhí)行文件的呢?很簡單,鏈接器首先會分別將各個目標(biāo)文件的代碼段整合,組裝成一個大的代碼段;將各個目標(biāo)文件中的數(shù)據(jù)段整合,合并成一個大的數(shù)據(jù)段;接著將合并后的新代碼段、數(shù)據(jù)段再合并為一個文件;最后經(jīng)過重定位,就生成了一個可以運行的可執(zhí)行文件了。

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

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

$ ld --verbose

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

屬性 section 編程示例

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

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

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

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

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

搞嵌入式的都知道 U-boot,U-boot 的用途主要是加載 Linux 內(nèi)核鏡像到內(nèi)存、給內(nèi)核傳遞啟動參數(shù)、然后引導(dǎo) Linux 操作系統(tǒng)啟動。

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

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

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源碼。

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

鏈接器在鏈接各個目標(biāo)文件時,會按照鏈接腳本里各個 section 的排列順序,將各個 section 組裝成一個可執(zhí)行文件。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 的前面、數(shù)據(jù)段 .data 的后面,作為 U-boot 拷貝自身代碼的起始地址和結(jié)束地址。而在這兩個 section 中,我們除了放2個零長度數(shù)組外,并沒有再放其它變量。根據(jù)前面的學(xué)習(xí)我們知道,零長度數(shù)組是不占用存儲空間的,所以上面定義的兩個零長度數(shù)組,其實就分別代表了 U-boot 鏡像要拷貝自身鏡像的起始地址和結(jié)束地址。

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 上,我們只要知道了這兩個地址,就可以直接調(diào)用相關(guān)代碼拷貝。

接著在 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 分別表示要拷貝鏡像的起始地址和結(jié)束地址,R0 表示要拷貝到 RAM 中的地址,R4 存放的是源地址和目的地址之間的偏移,在后面重定位過程中會用到這個偏移值。

ldr r1, =__image_copy_start

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

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

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI