现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

linux内核动态模块初始化 __attribute__((section(XXX)))

2019-09-20 18:02 工业·编程 ⁄ 共 5214字 ⁄ 字号 暂无评论

一 概述

传统的应用编写时,每添加一个模块,都需要在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>>>>>>

至此功能已实现。。。。本篇完

给我留言

留言无头像?