在分析之前首先查阅 RT-Thread 的官方文档 [RT-Thread 自动初始化机制](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic?id=rt-thread-自动初始化机制),根据官方文档的讲述在 RTT 源码中一共使用了 6 中顺序的初始化,本文以其中的一个 INIT_APP_EXPORT(fn) 为例进行自动初始化的原理分析,其他顺序的初始化的原理与之一致。
初始化顺序 | 宏接口 | 描述 |
1 | INIT_BOARD_EXPORT(fn) | 非常早期的初始化,此时调度器还未启动 |
2 | INIT_PREV_EXPORT(fn) | 主要是用于纯软件的初始化、没有太多依赖的函数 |
3 | INIT_DEVICE_EXPORT(fn) | 外设驱动初始化相关,比如网卡设备 |
4 | INIT_COMPONENT_EXPORT(fn) | 组件初始化,比如文件系统或者 LWIP |
5 | INIT_ENV_EXPORT(fn) | 系统环境初始化,比如挂载文件系统 |
6 | INIT_APP_EXPORT(fn) | 应用初始化,比如 GUI 应用 |
1 知识点补充
1.1 __attribute__ 关键字
1. 关键字__attribute__ 是 GNU C 实现的编译属性设置机制,也就是通过给函数或者变量声明属性值,以便让[编译器](https://so.csdn.net/so/search?q=编译器&spm=1001.2101.3001.7020)能够对要编译的程序进行优化处理。
2. 关键字 __attribute__((section(x))) 是告诉编译器,将作用的函数或数据放入指定名为 ”x” 输入段中。 举个例子,看下面一段代码:
int a __attribute__(section(“var”)) = 0;
定义了一个整形变量 a,然后将其赋值为0,而中间的 __attribute__(section(“var”)) 语句的作用是将变量 a 放入指定的段 var 中。而如果不指定变量所处的段的话,编译器就会随机将其分配在内存中。
3. __attribute__((used)) 的含义是即使它们没有被引用,也留在目标文件中,也就是告诉编译器,我声明的这个符号是需要保留的。
1.2 函数指针
1.2.1 简单的函数指针的运用
使用简单的函数指针的示例如下
#include
/* 定义了一个指针变量p,该变量指向某种函数,这种函数有两个int类型参数,返回一个int类型的值*/
/* 只有第一句我们还无法使用这个指针,因为我们还未对它进行赋值 */
int (*p)(int, int);
/* 定义了一个求和函数 */
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int sum = 0;
p = add; // 将函数 add() 的地址赋值给变量 p
printf("add = %08X\n", *((unsigned int *)add));
printf("*add = %08X\n", *((unsigned int *)*add));
printf("&add = %08X\n", *((unsigned int *)&add));
printf("p = %08X\n", *((unsigned int *)p));
printf("*p = %08X\n", *((unsigned int *)*p));
printf("&p = %08X\n", *((unsigned int *)&p));
sum = (*p)(1, 2);
//sum = p(1, 2); // 这两种写法都可以
printf("sum = %d\n", sum);
return 0;
}
/* 运行结果 */
add = 0xE5894855
*add = 0xE5894855
&add = 0xE5894855
p = 0xE5894855
*p = 0xE5894855
&p = 0x0040052D // 变量p的地址与函数指针值
sum = 3
从上面的结果看来,我们应该把函数名,当成指针看待。最常见的函数调用方式:fnc1(); 只是 (*fnc1)(); 简写形式而已。我们之所以可以 fnc1(); 这样调用函数,只是编译器帮我们做了调整。对于函数名 fnc1 来说,不管是 *fnc1 还是 fnc1 还是 &fnc1,编译器都认为他是函数指针。
1.2.2 使用 typedef 定义的函数指针
使用 typedef 定义的函数指针的示例如下
#include
/* typedef 的功能是定义新的类型,就是定义了一种 pFunc 的类型 */
/* pFunc 这种类型为指向某种函数的指针,这种函数以两个int类型为参数并返回int类型 */
typedef int (*pFunc)(int, int);
/* 定义了一个求和函数 */
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int sum = 0;
pFunc p = add; // 使用 pFunc 这种类型定义了变量p,将函数 add() 的地址赋值给变量 p
printf("add = 0x%08X\n", *((unsigned int *)add));
printf("*add = 0x%08X\n", *((unsigned int *)*add));
printf("&add = 0x%08X\n", *((unsigned int *)&add));
printf("p = 0x%08X\n", *((unsigned int *)p));
printf("*p = 0x%08X\n", *((unsigned int *)*p));
printf("&p = 0x%08X\n", *((unsigned int *)&p));
sum = (*p)(1, 2);
//sum = p(1, 2); // 这两种写法都可以
printf("sum = %d\n", sum);
return 0;
}
/* 运行结果 */
add = 0xE5894855
*add = 0xE5894855
&add = 0xE5894855
p = 0xE5894855
*p = 0xE5894855
&p = 0x0040052D
sum = 3
1.3 链接脚本解析
摘抄 RT-Thread 链接脚本中和本文有关的内容如下所示
/* section information for initial. */
. = ALIGN(4); /* 按照四字节对齐 */
__rt_init_start = .; /* 开始一个 "片段" */
KEEP(*(SORT(.rti_fn*))) /* 告诉链接器保留 ".rti_fn*" 的段,并将其排序 */
__rt_init_end = .; /* 结束一个 "片段" */
其中 SORT 关键字的含义是链接器会在把文件和 section 放到 输出文件中之前按名字顺序重新排列它们。
该链接脚本部分定义了申明各种自动初始化函数在进行链接时的排列顺序,因为 RT-Thread 源码中一共定义了六种实现自动初始化功能的宏接口,详见本文最开始的表格,所以也就可以解释为什么 INIT_BOARD_EXPORT(fn) 在自动初始化的最开始,而 INIT_APP_EXPORT(fn) 在自动初始化的结尾,就是因为 SORT 关键字在起作用。
2 自动初始化原理分析
RT-Thread 源码中一共有六种不同顺序的自动初始化宏定义,如下所示
/* rt-thread/include/rtdef.h */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
2.1 自动初始化宏定义解析
查看 RT-Thread 的源码中自动初始化宏定义,以 INIT_APP_EXPORT(fn) 为例进行分析, INIT_APP_EXPORT(fn) 的定义如下。其中宏定义中 ## 连接符号由两个井号组成,其功能是在带参数的宏定义中将两个子串(token)联接起来。
/* rt-thread/include/rtdef.h */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
#define INIT_EXPORT(fn, level) RT_USED const init_fn_t __rt_init_##fn RT_SECTION(".rti_fn." level) = fn
其中里面的宏定义如下
#define RT_USED __attribute__((used))
typedef int (*init_fn_t)(void); /* init_fn_t 为函数指针 */
#define RT_SECTION(x) __attribute__((section(x)))
以文件 rt-thread/components/finsh/shell.c 中 Finsh 控制台初始化函数 INIT_APP_EXPORT(finsh_system_init)为例,参照上面的宏定义规则分布展开和最终结果如下
INIT_APP_EXPORT(finsh_system_init)
|-> INIT_EXPORT(finsh_system_init, "6")
|-> RT_USED const init_fn_t __rt_init_finsh_system_init RT_SECTION(".rti_fn." "6") = finsh_system_init
|-> __attribute__((used)) const init_fn_t __rt_init_finsh_system_init __attribute__((section(".rti_fn.6"))) = finsh_system_init
结合第1小节的补充知识,上述宏定义展开的最终结果的含义为:定一个一个 init_fn_t 类型的函数指针变量 __rt_init_finsh_system_init ,将 finsh_system_init() 函数的地址赋值给了定义的变量, 并且将该变量放到了指定的段 ".rti_fn.6" 中。
所以说只要在代码中使用 INIT_APP_EXPORT(fn) 申明的初始化函数最终都会定义在指定的段 ".rti_fn.6" 中。
2.2 组件初始化调用解析
参考官方文档 [RT-Thread 启动流程](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic?id=rt-thread-启动流程),在调度器的启动函数执行时,会调用 rt_components_init() 函数对申明的各种初始化函数进行调用,RT-Thread 的启动流程如下图所示。
函数 rt_components_init() 的源码如下所示,因为没有定义宏 RT_DEBUG_INIT,所以直接将和宏 RT_DEBUG_INIT 有关的代码省略掉
/* rt-thread/src/components.c */
void rt_components_init(void)
{
#if RT_DEBUG_INIT
... ... /* 省略掉与分析无关的代码 */
#else
volatile 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 /* RT_DEBUG_INIT */
}
其中 __rt_init_rti_board_end 和 __rt_init_rti_end 表示不同的段。在系统源码中又定义了几个空函数来申明了一些段,如下所示。
/* rt-thread/src/components.c */
/* 宏定义展开后段名为 ".rti_fn.0" ,函数指针变量为 __rt_init_rti_start */
static int rti_start(void)
{
return 0;
}
INIT_EXPORT(rti_start, "0");
/* 宏定义展开后段名为 ".rti_fn.0.end" ,函数指针变量为 __rt_init_rti_board_start */
static int rti_board_start(void)
{
return 0;
}
INIT_EXPORT(rti_board_start, "0.end");
/* 宏定义展开后段名为 ".rti_fn.1.end" ,函数指针变量为 __rt_init_rti_board_end */
static int rti_board_end(void)
{
return 0;
}
INIT_EXPORT(rti_board_end, "1.end");
/* 宏定义展开后段名为 ".rti_fn.6.end" ,函数指针变量为 __rt_init_rti_end */
static int rti_end(void)
{
return 0;
}
INIT_EXPORT(rti_end, "6.end");
上面几个函数的导出,再加上6个自动初始化宏定义的导出,结合 1.3 小节连接脚本的分析,可以得到各个段名 和 对应的函数指针 / 宏的名字 及 前后顺序如下表所示。
section 名 | 函数指针 / 宏 |
.rti_fn.0 | __rt_init_rti_start |
.rti_fn.0.end | __rt_init_rti_board_start |
.rti_fn.1 | INIT_BOARD_EXPORT(fn) |
.rti_fn.1.end | __rt_init_rti_board_end |
.rti_fn.2 | INIT_PREV_EXPORT(fn) |
.rti_fn.3 | INIT_DEVICE_EXPORT(fn) |
.rti_fn.4 | INIT_COMPONENT_EXPORT(fn) |
.rti_fn.5 | INIT_ENV_EXPORT(fn) |
.rti_fn.6 .rti_fn.6.end |
INIT_APP_EXPORT(fn) __rt_init_rti_end |
我们可以通过编译生成的 map 文件对上述的分析进行验证,map 文件通常位于工程的 Debug 目录下,在 map 文件中搜索 .rti_fn* 可以找到如下所示的内容。可以看到经过排序后的各个自动初始化的段和对应的函数指针,我们前面分析的 Finsh 自动初始化的函数指针 __rt_init_finsh_system_init 就位于段 ".rti_fn.6" 中。并且从里面我们还可以看出每个函数指针都占 4 个字节的空间,因为在 32 位的系统中无论什么样的指针都占 4 个字节的空间。
*(SORT(.rti_fn*))
.rti_fn.0 0x08075220 0x4 ./rt-thread/src/components.o
0x08075220 __rt_init_rti_start
.rti_fn.0.end 0x08075224 0x4 ./rt-thread/src/components.o
0x08075224 __rt_init_rti_board_start
.rti_fn.1 0x08075228 0x4 ./rt-thread/components/utilities/ulog/ulog.o
0x08075228 __rt_init_ulog_init
.rti_fn.1 0x0807522c 0x4 ./drivers/drv_clk.o
0x0807522c __rt_init_clock_information
.rti_fn.1 0x08075230 0x4 ./drivers/drv_spi.o
0x08075230 __rt_init_rt_hw_spi_init
.rti_fn.1 0x08075234 0x4 ./drivers/drv_wdt.o
0x08075234 __rt_init_rt_wdt_init
.rti_fn.1 0x08075238 0x4 ./applications/peripheral/src/hn_psram.o
0x08075238 __rt_init_rt_hw_psram_init
.rti_fn.1.end 0x0807523c 0x4 ./rt-thread/src/components.o
0x0807523c __rt_init_rti_board_end
.rti_fn.2 0x08075240 0x4 ./rt-thread/components/utilities/ulog/backend/console_be.o
0x08075240 __rt_init_ulog_console_backend_init
.rti_fn.2 0x08075244 0x4 ./rt-thread/components/utilities/ulog/ulog.o
0x08075244 __rt_init_ulog_async_init
.rti_fn.2 0x08075248 0x4 ./rt-thread/components/net/lwip-2.0.3/src/arch/sys_arch.o
0x08075248 __rt_init_lwip_system_init
.rti_fn.2 0x0807524c 0x4 ./rt-thread/components/drivers/src/workqueue.o
0x0807524c __rt_init_rt_work_sys_workqueue_init
.rti_fn.2 0x08075250 0x4 ./rt-thread/components/dfs/src/dfs.o
0x08075250 __rt_init_dfs_init
.rti_fn.3 0x08075254 0x4 ./drivers/drv_rtc.o
0x08075254 __rt_init_rt_hw_rtc_init
.rti_fn.4 0x08075258 0x4 ./rt-thread/components/net/sal_socket/src/sal_socket.o
0x08075258 __rt_init_sal_init
.rti_fn.4 0x0807525c 0x4 ./rt-thread/components/libc/pthreads/pthread.o
0x0807525c __rt_init_pthread_system_init
.rti_fn.4 0x08075260 0x4 ./rt-thread/components/libc/compilers/gcc/newlib/libc.o
0x08075260 __rt_init_libc_system_init
.rti_fn.4 0x08075264 0x4 ./rt-thread/components/libc/compilers/common/time.o
0x08075264 __rt_init__rt_clock_time_system_init
.rti_fn.4 0x08075268 0x4 ./rt-thread/components/dfs/filesystems/elmfat/dfs_elm.o
0x08075268 __rt_init_elm_init
.rti_fn.4 0x0807526c 0x4 ./packages/ppp_device-v1.1.0/class/ppp_device_ec20.o
0x0807526c __rt_init_ppp_ec20_register
.rti_fn.4 0x08075270 0x4 ./applications/peripheral/src/hn_spi_flash.o
0x08075270 __rt_init_hn_spi_flash_init
.rti_fn.6 0x08075274 0x4 ./rt-thread/components/finsh/shell.o
0x08075274 __rt_init_finsh_system_init
.rti_fn.6 0x08075278 0x4 ./packages/ppp_device-v1.1.0/samples/ppp_sample.o
0x08075278 __rt_init_ppp_sample_start
.rti_fn.6 0x0807527c 0x4 ./applications/user/src/hn_pvd_detect.o
0x0807527c __rt_init_pvd_detect_init
.rti_fn.6 0x08075280 0x4 ./applications/user/src/hn_smtp.o
0x08075280 __rt_init_hn_smtp_init
.rti_fn.6 0x08075284 0x8 ./applications/peripheral/src/hn_spi_flash.o
0x08075284 __rt_init_hn_spi_flash_filesystem_init
0x08075288 __rt_init_hn_easyflash_init
.rti_fn.6.end 0x0807528c 0x4 ./rt-thread/src/components.o
0x0807528c __rt_init_rti_end
0x08075290 __rt_init_end = .
0x08075290 . = ALIGN (0x4)
[!provide] PROVIDE (__ctors_start__, .)
再结合上面的 rt_components_init() 函数中的具体实现源码分析,我们可以看到函数指针在 for 循环的开始指向了 __rt_init_rti_board_end,也就是指向了函数 rti_board_end(),该函数没有任何操作直接返回了0。
当满足条件fn_ptr < &__rt_init_rti_end 时,fn_ptr每次循环自加1,根据生成的 map 文件可以看出下一次就指向了 __rt_init_ulog_console_backend_init ,也就是指向了函数 ulog_console_backend_init() ,该函数对 ulog输出到控制台进行了初始化。
每次循环过程中fn_ptr自加1,然后执行对应的初始化函数,一直到 fn_ptr 自加后等于 &__rt_init_rti_end时循环结束,在这个过程中就执行了各种自动初始化的代码,完成了自动初始化的任务。
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
为什么fn_ptr = &__rt_init_rti_board_end 这个地方要使用取地址 & 符号呢?因为 fn_ptr是一个指向函数指针的指针,根据本文 2.1 小节的分析 __rt_init_rti_board_end 是一个函数指针变量,也就是说 fn_ptr 最开始指向的是函数指针变量 __rt_init_rti_board_end 的地址,根据 map 文件也就是 0x0807523c,指针每次循环自加1也就是加上4个字节的大小,就指向了下一个位置 0x08075240,(*fn_ptr)(); 也就是把该位置的内容取出来也就是相当于 __rt_init_ulog_console_backend_init();。
3 总结
为什么 RT-Thread 要采用这种复杂的方式来进行自动初始化操作呢?我认为是 RT-Thread 采用和 Linux 一样的机制,为了实现驱动和应用的分层将驱动的初始化操作在 main() 函数之前就进行初始化后注册好,等到运行到 main() 函数后,用户只需要关心应用层的额代码即可,如果不采用这种方式而是采用裸机写法的方式在main() 函数的最开始进行初始化,就需要执行很多驱动层的初始化代码,就不能很好的实现驱动和应用的分层,也就不能很好的实现应用层只需关系应用层的逻辑而不用关系驱动和硬件初始化的分层思想了。
审核编辑:汤梓红
-
初始化
+关注
关注
0文章
49浏览量
11809 -
RT-Thread
+关注
关注
31文章
1256浏览量
39799 -
函数指针
+关注
关注
2文章
56浏览量
3768
发布评论请先 登录
相关推荐
评论