RT-Thread 学习之自动初始化

写在开始之前

在学习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 */

看起来很长,但是去除注释和重复代码之后,可以发现它只做了两件事情:

  1. 定义了四个“桩子”,并将其分别插入了 .rti_fn.0 .rti_fn.0.end .rti_fn.1.end .rti_fn.6.end 中。
  2. 定义了两个函数,通过从开始桩查到结束桩的方式,依次调用已注册的初始化函数。

到这里,好像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 = .;

看到这里,大家想必已经明白了。不过阿绪还是大致的解释一下:

  1. 开始一个“片段” ,并声明该段为4字节对齐(这是32位cpu中一个指针的长度)。
  2. 在__rt_init_start 中保留段开始地址。
  3. 告诉链接器保留 .rti_fn* 的段,并将其排序。
  4. 在__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 国际许可协议进行许可。
6月 ago

发表评论