写在开始之前
在学习RTT的过程中,它的自动初始化机制一直是笔者很好奇的一点。关于它的资料非常多,但是官方写的过于概念化;而第三方的又往往缺失了一些关键点。在查阅不少资料、源码之后,笔者才基本掌握了它的原理和使用技巧,并将它移植到了一个项目中,于是在这里记录一下。
因为它的用法和介绍网上实在太多了,所以笔者这边就只讲一下原理和注意事项。
一、函数注册
在搜索任意的自动初始化的宏接口可以在 rtdef.h
中找到下面这段代码
/* initialization export */ #ifdef RT_USING_COMPONENTS_INIT typedef int (*init_fn_t)(void); #ifdef _MSC_VER /* we do not support MS VC++ compiler */ #define INIT_EXPORT(fn, level) #else #if RT_DEBUG_INIT struct rt_init_desc { const char* fn_name; const init_fn_t fn; }; #define INIT_EXPORT(fn, level) const char __rti_##fn##_name[] = #fn; RT_USED const struct rt_init_desc __rt_init_desc_##fn SECTION(".rti_fn." level) = { __rti_##fn##_name, fn}; #else #define INIT_EXPORT(fn, level) RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn." level) = fn #endif #endif #else #define INIT_EXPORT(fn, level) #endif /* board init routines will be called in board_init() function */ #define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1") /* pre/device/component/env/app init routines will be called in init_thread */ /* components pre-initialization (pure software initilization) */ #define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2") /* device initialization */ #define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3") /* components initialization (dfs, lwip, ...) */ #define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4") /* environment initialization (mount disk, ...) */ #define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5") /* appliation initialization (rtgui application etc ...) */ #define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
这段代码配合 Link Script 中的一个配置就完成了自动初始化的全功能。关于 Link Script 的内容我们在最后讲述,先来看看这段神奇的代码。
在分析了源码之后,我们不难发现,它一共分为了三个部分:1.申明函数指针(3);2.将函数指针放入数组or结构体数组(7-19);3.申明宏定接口(26+)。
其中,1和3是比较常规的用法,比较有意思的是2,我们逐句对其分析。
第七句:判断是否定义了 RT_DEBUG_INIT
,这是rtt的初始化的debug宏。
第八到第十二句:定义一个结构体,包括一个 const 的字符串指针和一个 cons 的函数指针。
第十三句:定义一个宏接口,fn为函数入口地址,level为启动等级。
第十四句:定义一个 const 的字符串,用于储存函数名称。其中##是C的一种宏,编译器将会在预处理时展开##后的“符号”,形成一个新的“名称” 。
第十五句: RT_USED
是RTT定义的一个宏,其内容为 __attribute__((used))
。 SECTION
也是 RTT定义的一个宏,其内容为 __attribute__((section(x)))
。
__attribute__((...))
是GCC和ARM CC的一种机制,括号内(一定要两个括号)内容的不同代表了不同的作用。这里就只讲一下用到的两个参数。
__attribute__((used))
: 它的作用是告诉编译器,我声明的这个符号是需要保留的。
__attribute__((section(x)))
: 它的作用是告诉编译器,将作用的函数或数据放入指定名为”x”输入段中。
输入段和输出段是相对于要生成最终的 elf 或 binary 时的 Link 过程说的,Link 过程的输入大都是由源代码编绎生成的目标文件.o,那么这些.o 文件中包含的段相对 link 过程来说就是输入段,而 Link 的输出一般是可执行文件 elf 或库等,这些输出文件中也包含有段,这些输出文件中的段就叫做输出段。
输入段和输出段本来没有什么必然的联系,是互相独立,只是在 Link 过程中,Link 程序会根据一定的规则(这些规则其实来源于 Link Script),将不同的输入段重新组合到不同的输出段中,即使是段的名字,输入段和输出段可以完全不同。
这样,我们就可以知道第十五句的作用是建立一个const的结构体,并在将其的字符串指针指向函数名称+标识符建立的字符串,将其的函数指针指向函数入口。同时申明该结构体为保留,而存放到 .rti_fn.level 的输入段中。
第十七到十八句和第十五句类似,只是将结构体变成了指针。作用如下:定义一个名为 (_rt_init+ 需要自动启的函数名) 的函数指针,将其保存在(.rti_fn.level)数据段中,并声明不使用也不允许编译器将其优化掉。
二、初始化
但是有了上面这些还是不够的,我们需要一个地方调用存在 .rti_fn. 下面的函数。于是,在 components.c
中有如下代码。
#ifdef RT_USING_COMPONENTS_INIT /* * Components Initialization will initialize some driver and components as following * order: * rti_start --> 0 * BOARD_EXPORT --> 1 * rti_board_end --> 1.end * * DEVICE_EXPORT --> 2 * COMPONENT_EXPORT --> 3 * FS_EXPORT --> 4 * ENV_EXPORT --> 5 * APP_EXPORT --> 6 * * rti_end --> 6.end * * These automatically initialization, the driver or component initial function must * be defined with: * INIT_BOARD_EXPORT(fn); * INIT_DEVICE_EXPORT(fn); * ... * INIT_APP_EXPORT(fn); * etc. */ static int rti_start(void) { return 0; } INIT_EXPORT(rti_start, "0"); static int rti_board_start(void) { return 0; } INIT_EXPORT(rti_board_start, "0.end"); static int rti_board_end(void) { return 0; } INIT_EXPORT(rti_board_end, "1.end"); static int rti_end(void) { return 0; } INIT_EXPORT(rti_end, "6.end"); /** * RT-Thread Components Initialization for board */ void rt_components_board_init(void) { #if RT_DEBUG_INIT int result; const struct rt_init_desc *desc; for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++) { rt_kprintf("initialize %s", desc->fn_name); result = desc->fn(); rt_kprintf(":%d done\n", result); } #else const init_fn_t *fn_ptr; for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++) { (*fn_ptr)(); } #endif } /** * RT-Thread Components Initialization */ void rt_components_init(void) { #if RT_DEBUG_INIT int result; const struct rt_init_desc *desc; rt_kprintf("do components initialization.\n"); for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc ++) { rt_kprintf("initialize %s", desc->fn_name); result = desc->fn(); rt_kprintf(":%d done\n", result); } #else const init_fn_t *fn_ptr; for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++) { (*fn_ptr)(); } #endif } #endif /* RT_USING_COMPONENTS_INIT */
看起来很长,但是去除注释和重复代码之后,可以发现它只做了两件事情:
- 定义了四个“桩子”,并将其分别插入了 .rti_fn.0 .rti_fn.0.end .rti_fn.1.end .rti_fn.6.end 中。
- 定义了两个函数,通过从开始桩查到结束桩的方式,依次调用已注册的初始化函数。
到这里,好像RTT的自动初始化已经讲完了。大部分第三方资料最多再讲了一下map文件中的排序问题,然后就结束了。但阿绪在将其移植到一个使用裸机的项目中时,出现了链接器抛弃输入端的问题。
关于这点,在本文的上面有过描述。简单的概括就是,输入端和输出端两者间处于有一些关联,但是并不相关。编译器在编译.c文件时,会将已申明的内容保留并放入指定的段中;但链接器在链接的时候,会因为没有显性调用而抛弃该输入段。于是,就有了第三个关键点。
三、Link Script
在有了上面两件事情还不够,RTT还在 Link Script 中的 .text: 段中加入了这样一段话。
/* section information for initialization */ . = ALIGN(4); __rt_init_start = .; KEEP(*(SORT(.rti_fn*))) __rt_init_end = .;
看到这里,大家想必已经明白了。不过阿绪还是大致的解释一下:
- 开始一个“片段” ,并声明该段为4字节对齐(这是32位cpu中一个指针的长度)。
- 在__rt_init_start 中保留段开始地址。
- 告诉链接器保留 .rti_fn* 的段,并将其排序。
- 在__rt_init_end 中保留段结束地址 。
到这里,RTT的自动初始化机制已经全部讲完了,它的自动初始化机制和linux的自动初始化机制如出一辙,都是利用宏接口注册函数、在启动时遍历表、在连接时将指针存放到统一位置。
参考资料
https://www.rt-thread.org/document/site/programming-manual/basic/basic/
https://www.rt-thread.org/qa/thread-2867-1-1.html
https://blog.csdn.net/Fate_Dream/article/details/53809550
https://blog.csdn.net/Longyu_wlz/article/details/82975871
https://www.jianshu.com/p/9d377ddc8acc
http://wen00072.github.io/blog/2014/03/14/study-on-the-linker-script/

本作品采用知识共享署名 4.0 国际许可协议进行许可。