您好,登錄后才能下訂單哦!
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 支持十幾種屬性:
在這些屬性中,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)));
在本節(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 這樣一個鏈接腳本。
在 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 中。
有了 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
免責(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)容。