1. 程式人生 > >利用__attribute__((section()))構建初始化函式表【轉】

利用__attribute__((section()))構建初始化函式表【轉】

轉自:

https://mp.weixin.qq.com/s?__biz=MzAwMDUwNDgxOA==&mid=2652663356&idx=1&sn=779762953029c0e0946c22ef2bb0b754&chksm=810f28a1b678a1b747520ba3ee47c9ed2e8ccb89ac27075e2d069237c13974aa43537bff4fba&mpshare=1&scene=1&srcid=0111Ys4k5rkBto22dLokVT5A&pass_ticket=bGNWMdGEbb0307Tm%2Ba%2FzAKZjWKsImCYqUlDUYPZYkLgU061qPsHFESXlJj%2Fyx3VM#rd

問題匯入

傳統的應用編寫時,每新增一個模組,都需要在main中新增新模組的初始化

 

 

使用__attribute__((section()))構建初始化函式表後,由模組告知main:“我要初始化“,新增新模組再也不需要在main程式碼中顯式呼叫模組初始化介面。

以此實現main與模組之間的隔離,main不再關心有什麼模組,模組的刪減也不需要修改main。

那麼,如何實現這個功能呢?如何實現DECLARE_INIT呢?聯想到核心驅動,所有核心驅動的初始化函式表在哪裡?為什麼新增一個核心驅動不需要修改初始化函式表?

下文會從 構建初始化函式表的原理分析、分析核心module_init實現、演練練習 的3個角度給小夥伴分享。

複製程式碼

構建初始化函式表的原理分析

__attribute__((section(”name“)))是gcc編譯器支援的一個編譯特性(arm編譯器也支援此特性),實現在編譯時把某個函式/資料放到name的資料段中。因此實現原理就很簡單了:

 

1.       模組通過__attribute__((section("name")))的實現,在編譯時把初始化的介面放到name資料段中

2.       main在執行初始化時並不需要知道有什麼模組需要初始化,只需要把name資料段中的所有初始化介面執行一遍即可

 

首先: gcc -c  test.c -o test.o

此時編譯過程中處理了__atribute__((section(XXX))),把標記的變數/函式放到了test.o的XXX的資料段,可用 readelf命令查詢。

最後:ld -T <ldscript> test.o -otest.bin

連結時,test.o的XXX資料段(輸入段),最終儲存在test.bin的XXX資料段(輸出段),如此在bin中構建了初始化函式表。

由於自定義了一個數據段,而預設連結指令碼缺少自定義的資料段的宣告,因此並不能使用預設的連結指令碼。

ld連結命令有兩個關鍵的選項:

ld -T <script>:指定連結時的連結指令碼

ld --verbose:打印出預設的連結指令碼

在我們下文的演練中,我們首先通過”ld --verbose”獲取預設連結指令碼,然後修改連結指令碼,新增自定義的段,最後在連結應用時通過“-T<script>” 指定我們修改後的連結指令碼。

下文,我們首先分析核心module_init的實現,最後進行應用程式的演練練習。

分析核心module_init實現

核心驅動的初始化函式表在哪裡?為什麼新增一個核心驅動不需要修改初始化函式表?為什麼所有驅動都需要module_init?
1.      module_init的定義

module_init定義在<include/linux/init.h>。程式碼如下:

程式碼中使用的“_section_”,是一層層的巨集,為了簡化,把其等效理解為“section”。

分析上述程式碼,我們發現module_init由__attribute__((section(“name”)))實現,把初始化函式地址儲存到名為".initcall6.init" 的資料段中。
2.      連結核心使用自定義的連結指令碼

我們看到核心目錄最上層的Makefile,存在如下程式碼:

# Rule to link vmlinux - also used during CONFIG_KALLSYMS

# May be overridden by arch/$(ARCH)/Makefile

quiet_cmd_vmlinux__ ?= LD      
[email protected]
cmd_vmlinux__ ?= $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux) -o [email protected] \ -T $(vmlinux-lds) $(vmlinux-init) \ --start-group $(vmlinux-main) --end-group \ $(filter-out $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o FORCE ,$^) 本文的關注點在於:-T $(vmlinux-lds),通過“ld -T <script>”使用了定製的連結指令碼。定製的連結指令碼在哪裡呢?在Makefile存在如下程式碼: vmlinux-lds := arch/$(SRCARCH)/kernel/vmlinux.lds 我們以”ARCH=arm“ 為例,檢視連結指令碼:arch/arm/kernel/vmlinux.lds: 在上述程式碼中,我們聚焦於兩個地方: __initcall6_start = .; : 由__initcall6_start指向當前地址 *(.initcall6.init) : 所有.o檔案的.initcall6.init資料段放到當前位置 如此,“__initcall6_start”指向“.initcall6.init”資料段的開始地址,在應用程式碼中就可通過“__initcall6_start”訪問資料段“.initcall6.init”。 是不是如此呢?我們再聚焦到檔案<init/main.c>中。 “.initcall.init”資料段的使用 在<init/main.c>中,有如下程式碼: static initcall_t *initcall_levels[] __initdata = { __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end, }; ...... int __init_or_module do_one_initcall(initcall_t fn) { ...... if (initcall_debug) ret = do_one_initcall_debug(fn); else ret = fn(); ...... } ...... static void __init do_initcall_level(int level) { ...... for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn); } 按0-7的初始化級別,依次呼叫各個級別的初始化函式表,而驅動module_init的初始化級別為6。在“for (fn = initcall_levels[level]; fn <initcall_levels[level+1]; fn++)”的for迴圈呼叫中,實現了遍歷當前初始化級別的所有初始化函式。 module_init的實現總結 通過上述的程式碼追蹤,我們發現module_init的實現有以下關鍵步驟: 通過module_init的巨集,在編譯時,把初始化函式放到了資料段:.initcall6.init 在連結成核心的時候,連結指令碼規定好了.initcall6.init的資料段以及指向資料段地址的變數:_initcall6_start 在init/main.c中的for迴圈,通過_initcall6_start的指標,呼叫了所有註冊的驅動模組的初始化介面 最後通過Kconfig/Makefile選擇編譯的驅動,實現只要編譯了驅動程式碼,則自動把驅動的初始化函式構建到統一的驅動初始化函式表 演練練習 分析了核心使用__attribute__((section(“name”)))構建的驅動初始化函式表,我們接下來練習如何在應用中構建自己的初始化函式表。 下文的練習參考了:https://my.oschina.net/u/180497/blog/177206 1. 應用程式碼 我們的練習程式碼(section.c)如下: #include <unistd.h> #include <stdint.h> #include <stdio.h> typedef void (*init_call)(void); /* * These two variables are defined in link script. */ extern init_call _init_start; extern init_call _init_end; #define _init __attribute__((unused, section(".myinit"))) #define DECLARE_INIT(func) init_call _fn_##func _init = func static void A_init(void) { write(1, "A_init\n", sizeof("A_init\n")); } DECLARE_INIT(A_init); static void B_init(void) { printf("B_init\n"); } DECLARE_INIT(B_init); static void C_init(void) { printf("C_init\n"); } DECLARE_INIT(C_init); /* * DECLARE_INIT like below: * static init_call _fn_A_init __attribute__((unused, section(".myinit"))) = A_init; * static init_call _fn_C_init __attribute__((unused, section(".myinit"))) = C_init; * static init_call _fn_B_init __attribute__((unused, section(".myinit"))) = B_init; */ void do_initcalls(void) { init_call *init_ptr = &_init_start; for (; init_ptr < &_init_end; init_ptr++) { printf("init address: %p\n", init_ptr); (*init_ptr)(); } } int main(void) { do_initcalls(); return 0; } 在程式碼中,我們做了3件事: 使用__attribute__((section()))定義了巨集:DECLARE_INIT,此巨集把函式放置到初始化函式表 使用DELCARE_INIT的巨集,聲明瞭3個模組初始化函式:A_init/B_init/C_init 在main中通過呼叫do_initcalls函式,依次呼叫編譯時構建的初始化函式。其中,“_init_start”和“_init_end”的變數在連結指令碼中定義。 2. 連結指令碼 通過命令”ld --verbose”獲取預設連結指令碼: GNU ld (GNU Binutils for Ubuntu) 2.24 支援的模擬: elf_x86_64 ...... 使用內部連結指令碼: ================================================== XXXXXXXX (預設連結指令碼) ================================================== 我們擷取分割線”=====“之間的連結指令碼儲存為:ldscript.lds 在.bss的資料段前添加了自定義的資料段: _init_start = .; .myinit : { *(.myinit) } _init_end = .; ”_init_start“和”_init_end“是我們用於識別資料段開始和結束的在連結指令碼中定義的變數,而.myinit則是資料段的名稱,其中: .myinit : { *(.myinit) }:表示.o中的.myinit資料段(輸入段)儲存到bin中的.myinit資料段(輸出段)中 前期準備充足,下面進行編譯、連結、執行的演示

複製程式碼

 

 

3.      編譯

執行:gcc -c section.c -o section.o 編譯應用原始碼。

執行:readelf -S section.o 檢視段資訊,截圖如下:

 

可以看到,段[6]是我們自定義的資料段

4.      連結

執行:gcc -T ldscript.lds section.o -o section 連結成可執行的bin檔案

執行:readelf -S section 檢視bin檔案的段分佈情況,部分截圖如下:

在我連結成的可執行bin中,在[25]段中存在我們自定義的段

5.      執行

執行結果:

本文後面跟著的一篇文章是關於這篇文章對應的高清思維導圖。