liunx 內核動態模塊初始化 __attribute__((section(XXX)))

一 概述

傳統的應用編寫時,每添加一個模塊,都需要在main中添加新模塊的初始化。也就是說增加的一個不能算是真正的獨立模塊,得在main中修改代碼才能集成這模塊功能。有沒有什麼辦法可以實現main跟其他模塊之間隔離呢?main不再關心有什麼模塊,模塊的刪減也不需要修改main?

二 liunx內核模塊初始化

如果你對liunx模塊有一定了解,你應該知道liunx模塊都是獨立加載,加載模塊,不需要修改main代碼。甚至不需要重新編譯代碼。那麼內核是如何實現的呢?

1. module_init 函數

模塊初始化都會調用 module_init ,那麼這函數做了哪些東西呢?我們着入點就從這個module_init 函數開始

// module_init定義在<include/linux/module.h> 
#define module_init(x)	__initcall(x);
// __initcall 定義在<include/linux/init.h> 
#define __initcall(fn) device_initcall(fn)

#define device_initcall(fn)		__define_initcall(fn, 6)

#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn;

// 例如 初始化一個iptables 模塊  module_init(ip_tables_init) 其實就等效於
// static initcall_t  _initcall_ip_tables_init_6   __attribute__((unused, section(".initcall6.init"))) = ip_tables_init ;

__ attribute__ ((section(”name“)))是gcc編譯器支持的一個編譯特性(arm編譯器也支持此特性),實現在編譯時把某個函數/數據放到name的數據段中。原理如下

  • 模塊通過__ attribute__((section(“name”))) ,會構建初始化函數表。放到你命名的name數據段中
  • 而默認鏈接腳本缺少自定義的數據段的聲明,需要在鏈接腳本添加你定義的數據段的聲明
  • 而main在執行初始化時,只需要把name數據段中的所有初始化接口執行一遍即可.

那麼這裏有兩個問題 :

  • 如何再鏈接腳本中添加自己定義的數據段的聲明呢?
  • main是如何將放入數據段的,模塊接口都執行了一遍了呢?

2. 鏈接腳本處理

內核是根據不同的架構,調用內核自己寫的對應的鏈接腳本。而這部分本人也沒有完全理解,這裏就也深入內核討論這塊。
ld鏈接命令有兩個關鍵的選項如下

ld -T <script>:指定鏈接時的鏈接腳本
ld  --verbose:打印出默認的鏈接腳本 

內核最終其實用了 ld -T arch/$(SRCARCH)/kernel/vmlinux.lds 指定架構對應的鏈接腳本。我們以”ARCH=arm“ 爲例,查看鏈接腳本:arch/arm/kernel/vmlinux.lds
可以發現其實就在.bss 數據段前添加了 自己定義數據段的聲明,如下

_initcall_start = .;
_initcall6_start =.; *(.initcall6.init)
_initcall_end = .;
// 當然內核初始化的數據段不止module_init 中要初始的 還有好幾個不同的初始數據段,這裏代碼只是簡化的列出 module_init 中的數據段。

3. main 執行初始化函數

typedef int (*initcall_t)(void);

static initcall_t *initcall_levels[] __initdata = {
      __initcall0_start,
      __initcall1_start,
      __initcall2_start,
      __initcall3_start,
      __initcall4_start,
      __initcall5_start,
      __initcall6_start,
      __initcall7_start,
      __initcall_end,
};

static void __init do_initcalls(void)
{
	int level;

	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}

 ......
static void __init do_initcall_level(int level)
{
    ......
    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);
}
 ......
int __init_or_module do_one_initcall(initcall_t fn)
{
    	......
    if (initcall_debug)
        ret = do_one_initcall_debug(fn);
    else
        ret = fn();
        ......
}
......

按0-7的初始化級別,依次調用各個級別的初始化函數表,而驅動module_init的初始化級別爲6。在“for (fn = initcall_levels[level]; fn <initcall_levels[level+1]; fn++)”的for循環調用中,實現了遍歷當前初始化級別的所有初始化函數。

通過上述的代碼追蹤,我們發現module_init的實現有以下關鍵步驟:

  • 通過module_init的宏,在編譯時,把初始化函數放到了數據段:.initcall6.init
  • 在內核自定義的鏈接,申明瞭.initcall6.init的數據段存放的位置,以及指向數據段地址的變量:_initcall6_start
  • 在init/main.c中的for循環,通過_initcall6_start的指針,調用了所有註冊的驅動模塊的初始化接口
  • 最後通過Kconfig/Makefile選擇編譯的驅動,實現只要編譯了驅動代碼,則自動把驅動的初始化函數構建到統一的驅動初始化函數表

三 自己實現動態模塊初始化

分析了內核使用__ attribute__((section(“name”)))構建的驅動初始化函數表。自己就可以把這部分動態初始化應用到自己的項目了。
簡單一個例子: 今天的行程 你可能會有多個安排,比如休閒的時候去圖書館看會書 晚上又想去打個籃球運動一下 。因此你需要加載兩個行程安排。

ldsdefine.h

#ifndef LDSDEFINE_H_
#define LDSDEFINE_H_


typedef void (*init_call)(void);  // 初始化函數指針函數

#define _self_init  __attribute__((unused,section(".myinit"))) 

#define DECLAER_INIT(func) static init_call _fn_##func _self_init = func   // 表示初始化了函數放到 .myinit 數據段中  

#endif /* LDSDEFINE_H_ */

加載看書,打球行程安排

// move_fuction.c  運動模塊
#include <unistd.h>
#include <stdio.h>
#include "ldsdefine.h"

static void move_init(void)
{
   printf("move module >>>>> palying basketball\n");
}
DECLAER_INIT(move_init);

//arder_fuction.c    休閒模塊
#include <unistd.h>
#include <stdio.h>
#include "ldsdefine.h"

static void arder_init(void)
{
   printf("arder module >>>>> reading book\n");
}
DECLAER_INIT(arder_init);

調用了DECLAER_INIT 表示休閒模塊,運動模塊的初始化函數都放入到了數據段myinit中了。而鏈接腳本中還沒有聲明定義的數據段。
上面提到鏈接腳本 ld鏈接命令有兩個關鍵的選項
通過命令”ld --verbose”獲取默認鏈接腳本:

GNU ld version 2.20.51.0.2-15.fc13 20091009
  Supported emulations:
   elf_i386
   i386linux
   elf_x86_64
   elf_l1om
using internal linker script:
==================================================

XXXXXXXX (缺省鏈接腳本)

==================================================

我們截取分割線”=======“之間的鏈接腳本保存爲:ldsmodule.lds
在.bss的數據段前添加了自定義的數據段:

_init_start = .;
  .myinit : { *(.myinit) }
 _init_end = .;

表示聲明瞭自定義數據段myinit , _init_start 指向自定義數據段 myinit 開始位置,_init_end 指向自定義數據段 myinit 結束位置。

你知道了數據段放入的開始結束位置。那麼你在main.c 中就可以動態加載裏面初始化的函數了。通過extern引用鏈接腳本聲明的數據段,就能獲取到數據段內所有的函數。

mian.c

#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include "ldsdefine.h"

extern init_call _init_start;
extern init_call _init_end;

int main(void)
{
	init_call *init_ptr = &_init_start;
	printf("staring loading today schedule>>>>>>\n");
	for (;init_ptr < &_init_end; init_ptr++) {
		(*init_ptr)();
	}
	printf("ending loading today schedule>>>>>>\n");
    return 0;
}

但是編譯你會發現編譯不成功,會顯示如下錯誤

./src/main.o: In function `main':
/home/song/workspace/module_init_test/Debug/../src/main.c:20: undefined reference to `_init_start'
/home/song/workspace/module_init_test/Debug/../src/main.c:22: undefined reference to `_init_end'

那是因爲你用的鏈接腳本是默認的,默認腳本並不知道你定義的數據段。你得鏈接你剛保存的ldsmodule.lds 腳本。
需要在IDE自動生成的 makefile 中
原本是 gcc -o"module_init_test" $(OBJS) $(USER_OBJS) $(LIBS)
添加 -T src/ldsmodule.lds 如下

# Tool invocations
module_init_test: $(OBJS) $(USER_OBJS)
	@echo 'Building target: $@'
	@echo 'Invoking: GCC C Linker'
	gcc -T src/ldsmodule.lds -o"module_init_test" $(OBJS) $(USER_OBJS) $(LIBS) 
	@echo 'Finished building target: $@'
	@echo ' 

最後運行結果如下,可以看到休閒(arder) ,運動(move)模塊都加載了。

[root@wus Debug]# ./module_init_test 
staring loading today schedule>>>>>>
arder module >>>>> reading book
move module >>>>> palying basketball
ending loading today schedule>>>>>>

至此功能已實現。。。。本篇完

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