利用__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

本文詳細講解了利用__attribute__((section()))構建初始化函數表,以及Linux內核各級初始化的原理。

 

作者簡介:

    廖威雄,2016年本科畢業於暨南大學,目前就職於珠海全志科技股份有限公司從事linux嵌入式系統(Tina Linux)的開發,主要負責文件系統和存儲的開發和維護,兼顧linux測試系統的設計和持續集成的維護。

    拆書幫珠海百島分舵的組織長老,二級拆書家,熱愛學習,熱愛分享。

歡迎投稿:

2018年給Linuxer投稿原創Linux技術文章,一經錄取,贈送人民郵電出版社任意在售圖書,獲得讀者紅包打賞,和公眾號站長宋寶華200元微信紅包。

Linuxer-"Linux開發者自己的媒體"第五月稿件和贈書名單

 

歡迎關注Linuxer

問題導入

傳統的應用編寫時,每添加一個模塊,都需要在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      $@  

cmd_vmlinux__ ?= $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux) -o $@ \

      -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.      執行

執行結果:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM