利用__attribute__((section()))构建初始化函数表与Linux内核init的实现

本文介绍如何使用__attribute__((section()))构建初始化函数表,实现模块化编程。通过内核module_init的实现原理分析,以及应用程序实战,展示如何在不修改main代码的情况下添加新模块的初始化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

传统的应用编写时,每添加一个模块,都需要在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的实现有以下关键步骤:

  1. 通过module_init的宏,在编译时,把初始化函数放到了数据段:.initcall6.init

  2. 在链接成内核的时候,链接脚本规定好了.initcall6.init的数据段以及指向数据段地址的变量:_initcall6_start

  3. 在init/main.c中的for循环,通过_initcall6_start的指针,调用了所有注册的驱动模块的初始化接口

  4. 最后通过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);

 

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);

 

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件事:

  1. 使用__attribute__((section()))定义了宏:DECLARE_INIT,此宏把函数放置到初始化函数表

  2. 使用DELCARE_INIT的宏,声明了3个模块初始化函数:A_init/B_init/C_init

  3. 在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. 执行

执行结果:

6. 修改代码 

例外一个程序:

#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 __start_myinit;
extern init_call __stop_myinit;
 
#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);

void do_initcalls(void)
{
    init_call *init_ptr = &__start_myinit;
    for (; init_ptr < &__stop_myinit; init_ptr++) {
        printf("init address: %p\n", init_ptr);
        (*init_ptr)();
    }
}
 
int main(void)
{
    do_initcalls();
    return 0;
}

gcc  section.c -o section

不用修改指定连接文件,直接编译通过。

 

<think>好的,我现在需要回答用户关于GCC的__attribute__((section))以及asection宏的用法的问题。首先,我得先确认自己对这两个概念的理解是否正确,并整理相关资料。 首先,__attribute__是GCC的一个扩展语法,允许开发者指定变量、函数等的特殊属性。用户提到的section属性,我记得是用来将变量或函数放置在特定的段(section)中,这在嵌入式开发或需要特定内存布局的情况下很有用。例如,将某些变量放在自定义的段中,而不是默认的.data或.text段。 接下来是asection宏。根据用户提供的引用[1],其中提到了#define notrace __attribute__((no_instrument_function)),这说明asection可能是一个宏,用于简化__attribute__((section(...)))的写法。比如,用户可能通过#define asection(name) __attribute__((section(#name)))这样的宏来方便地指定段名,这样在代码中只需使用asection(.mysection)而不用写完整的属性。 接下来需要验证这些信息的正确性。根据GCC文档,__attribute__((section("name")))确实用于指定对象所在的段。而宏的使用则是预处理器的功能,用于简化代码。例如,Linux内核代码中常用类似的宏来管理不同段的变量或函数,如__init、__exit等宏就是通过section属性实现的。 然后,我需要考虑用户可能的实际应用场景。比如,在嵌入式系统中,将关键数据放入特定的内存区域,或者在驱动开发中,将初始化函数放入特定的段以便在启动时执行。同时,需要注意使用section属性时,链接脚本(linker script)中需要有对应的段定义,否则链接阶段会出错。 关于asection宏的具体实现,用户可能指的是类似这样的定义: #define asection(name) __attribute__((section(#name))) 这样使用时,asection(my_section)就会扩展为__attribute__((section("my_section"))),从而将变量或函数放入my_section段中。需要确认这种宏定义是否符合C语言的语法,特别是字符串化操作符#的使用是否正确。是的,#name会将宏参数转换为字符串,因此是正确的。 另外,用户提供的引用[2]提到GCC属性的语法可能有解析上的问题,特别是在复杂的声明中。因此,建议在宏定义中使用GNU C的语法,比如使用双括号来确保属性参数的正确解析。例如: #define asection(name) __attribute__((section(#name))) 这样可以避免因为逗号或空格导致的解析错误。 接下来,需要提供具体的代码示例,展示如何使用__attribute__((section))和asection宏。例如,定义一个变量到自定义段: int my_var __attribute__((section(".mysection"))) = 42; 使用宏后: #define asection(name) __attribute__((section(#name))) int my_var asection(.mysection) = 42; 此外,函数也可以被放入特定段中,例如: void __attribute__((section(".mytext"))) my_function() { ... } 使用宏: void asection(.mytext) my_function() { ... } 需要注意,某些情况下可能需要使用__attribute__在函数声明中的正确位置,通常在函数名之后,返回类型之前,或者在函数定义时。 另外,链接脚本需要定义对应的段,例如在ld脚本中添加: .mysection : { KEEP(*(.mysection)) } 否则,链接器可能不会保留未引用的段,导致变量或函数被优化掉。 还需要提到潜在的问题,比如不同编译器之间的兼容性,因为__attribute__是GCC特有的,其他编译器如MSVC可能不支持。此外,过度使用自定义段可能导致内存碎片化或管理复杂。 最后,根据用户提供的引用[3],可能需要引导用户到提供的链接获取更多信息,但根据规则,引用标识需要自然地添加在引用段落末尾,比如在提到链接脚本时引用相关资源。 总结回答的结构:先介绍__attribute__的用途,然后详细说明section属性的用法,接着解释asection宏的作用和实现,给出代码示例,讨论应用场景和注意事项,最后生成相关问题。</think>### GCC __attribute__((section)) 和 `asection` 宏的用法解析 #### 1. `__attribute__((section))` 的基本功能 `__attribute__` 是 GCC 编译器的扩展语法,用于为变量、函数等添加特殊属性。`section` 属性允许开发者**将变量或函数放置在特定的内存段**中,而非默认的段(如 `.data` 或 `.text`)。语法格式为: ```c __attribute__((section("段名称"))) ``` 例如,将变量放入自定义段 `.myvars`: ```c int my_var __attribute__((section(".myvars"))) = 42; ``` 这常用于嵌入式系统或操作系统内核中,用于控制内存布局[^1]。 #### 2. `asection` 宏的作用 `asection` 通常是一个宏定义,用于简化 `__attribute__((section(...)))` 的写法。例如: ```c #define asection(name) __attribute__((section(#name))) ``` 使用时可直接写入段名: ```c int my_var asection(.myvars) = 42; // 等价于 __attribute__((section(".myvars"))) ``` 这种宏在 Linux 内核代码中广泛使用(如 `__init`、`__exit` 宏)[^2]。 #### 3. 实际应用示例 **场景 1:将函数放入初始化段** ```c // 宏定义 #define INIT_SECTION asection(.init.text) // 函数定义到初始化段 void INIT_SECTION my_init_function() { // 初始化代码 } ``` **场景 2:自定义数据段管理** ```c // 定义只读配置数据段 #define CONFIG_SECTION asection(.config.data) const char CONFIG_SECTION device_name[] = "MyDevice"; ``` #### 4. 链接脚本的配合 需在链接脚本(`.ld` 文件)中声明自定义段,否则可能被链接器丢弃: ```ld SECTIONS { .myvars : { KEEP(*(.myvars)); /* 保留段内所有符号 */ } } ``` 未定义段可能导致变量/函数丢失[^3]。 #### 5. 注意事项 - **兼容性**:`__attribute__` 是 GCC 特有语法,其他编译器(如 MSVC)需使用 `#pragma` 替代。 - **语法细节**:复杂声明中需确保 `__attribute__` 位置正确,例如: ```c void (* __attribute__((section(".mytext"))) callback)(int); // 函数指针的段属性 ``` - **优化风险**:若自定义段未被链接脚本引用,相关代码可能被优化删除。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值