初始化函数表
2026/4/26大约 10 分钟结构体初始化数据结构
初始化函数表
📚 本节导读
用途: 索引各个初始化级别的函数表
来源: kernel/source/os_startup.c
一、概述
1.1 概念定义
gs_init_call_table 是 OneOS 多级初始化机制中用于索引各个初始化级别函数表的指针数组。
1.2 作用
- 索引 8 个初始化级别的函数表起始地址
- 提供
_k_run_init_call()函数遍历的范围边界 - 利用编译器段机制实现零开销的初始化函数管理
二、数据结构定义
2.1 完整代码(第 127-155 行)
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM MDK Compiler */
static const init_call_entry_t *gs_init_call_table[] = {&_os_init_call_pre_kernel_1_start + 1,
&_os_init_call_pre_kernel_2_start + 1,
&_os_init_call_post_kernel_start + 1,
&_os_init_call_pre_device_start + 1,
&_os_init_call_device_start + 1,
&_os_init_call_component_start + 1,
&_os_init_call_application_start + 1,
#ifdef OS_USING_SMP
&_os_init_call_smp_start + 1,
#endif /*OS_USING_SMP*/
&_os_init_call_os_init_end};
#elif defined(__GNUC__) /* for GCC Compiler */
static const init_call_entry_t *gs_init_call_table[] = {__init_call_pre_kernel_1_start,
__init_call_pre_kernel_2_start,
__init_call_post_kernel_start,
__init_call_pre_device_start,
__init_call_device_start,
__init_call_component_start,
__init_call_application_start,
#ifdef OS_USING_SMP
__init_call_smp_start,
#endif /*OS_USING_SMP*/
__os_init_end};
#else
#error "not supported compiler"
#endif /*__CC_ARM*/2.2 分步详细解释
第一步:编译器分支判断
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM MDK Compiler */- 解释:判断编译器类型,支持 ARM MDK 编译器
- 语法解析:
#if defined():条件编译预处理指令__CC_ARM:ARM 编译器宏__CLANG_ARM:ARM Clang 编译器宏
- 作用:根据不同编译器选择不同的实现方式
第二步:初始化函数表变量声明
static const init_call_entry_t *gs_init_call_table[]- 解释:初始化函数表的变量声明
- 含义:
static:仅在当前文件可见,避免全局命名冲突const:指针指向的内容不可修改,确保初始化函数表在运行时只读init_call_entry_t *:指向init_call_entry_t结构体的指针[]:表示这是一个指针数组,每个元素都是一个init_call_entry_t *指针
- 作用:声明一个用于索引各个初始化级别函数表的指针数组
第三步:ARM MDK 编译器的数组初始化
= {&_os_init_call_pre_kernel_1_start + 1,
&_os_init_call_pre_kernel_2_start + 1,
&_os_init_call_post_kernel_start + 1,
&_os_init_call_pre_device_start + 1,
&_os_init_call_device_start + 1,
&_os_init_call_component_start + 1,
&_os_init_call_application_start + 1,
#ifdef OS_USING_SMP
&_os_init_call_smp_start + 1,
#endif /*OS_USING_SMP*/
&_os_init_call_os_init_end};- 解释:ARM MDK 编译器下的初始化函数表数组初始化
- 含义:
&_os_init_call_pre_kernel_1_start:获取标记函数的地址+ 1:指针向后偏移 1 个init_call_entry_t元素,跳过标记函数本身&_os_init_call_os_init_end:作为结束标记
- 作用:为 ARM MDK 编译器建立初始化函数表,索引各个级别的起始和结束地址
第四步:GCC 编译器的条件分支
#elif defined(__GNUC__) /* for GCC Compiler */- 解释:GCC 编译器的条件分支
- 含义:
__GNUC__:GCC 编译器的宏定义
- 作用:判断是否为 GCC 编译器,选择对应的实现方式
第五步:GCC 编译器的数组初始化
static const init_call_entry_t *gs_init_call_table[] = {__init_call_pre_kernel_1_start,
__init_call_pre_kernel_2_start,
__init_call_post_kernel_start,
__init_call_pre_device_start,
__init_call_device_start,
__init_call_component_start,
__init_call_application_start,
#ifdef OS_USING_SMP
__init_call_smp_start,
#endif /*OS_USING_SMP*/
__os_init_end};- 解释:GCC 编译器下的初始化函数表数组初始化
- 含义:
__init_call_pre_kernel_1_start:直接使用链接器生成的段起始符号__os_init_end:链接器生成的结束符号
- 作用:为 GCC 编译器建立初始化函数表,使用链接器提供的段符号
第七步:SMP 级别可选支持
#ifdef OS_USING_SMP
&_os_init_call_smp_start + 1,
#endif /*OS_USING_SMP*/- 解释:SMP(对称多处理)级别的可选支持
- 含义:
- 仅当
OS_USING_SMP宏定义时才包含 SMP 级别
- 仅当
- 作用:根据配置选择性包含 SMP 初始化级别
第八步:结束标记
&_os_init_call_os_init_end};- 解释:初始化函数表的结束标记
- 含义:
- 作为最后一个元素,标记所有初始化级别的结束
- 作用:为遍历提供结束边界
第九步:编译器不支持的处理
#else
#error "not supported compiler"
#endif /*__CC_ARM*/- 解释:编译器不支持时的错误处理
- 含义:
#error:编译时错误指令,停止编译并输出错误信息
- 作用:确保在不支持的编译器上能及时发现问题
2.3 ARM MDK 编译器版本
static const init_call_entry_t *gs_init_call_table[] = {&_os_init_call_pre_kernel_1_start + 1,
&_os_init_call_pre_kernel_2_start + 1,
&_os_init_call_post_kernel_start + 1,
&_os_init_call_pre_device_start + 1,
&_os_init_call_device_start + 1,
&_os_init_call_component_start + 1,
&_os_init_call_application_start + 1,
#ifdef OS_USING_SMP
&_os_init_call_smp_start + 1,
#endif /*OS_USING_SMP*/
&_os_init_call_os_init_end};2.4 GCC 编译器版本
static const init_call_entry_t *gs_init_call_table[] = {__init_call_pre_kernel_1_start,
__init_call_pre_kernel_2_start,
__init_call_post_kernel_start,
__init_call_pre_device_start,
__init_call_device_start,
__init_call_component_start,
__init_call_application_start,
#ifdef OS_USING_SMP
__init_call_smp_start,
#endif /*OS_USING_SMP*/
__os_init_end};三、使用方式
3.1 索引方式
| 索引表达式 | 说明 |
|---|---|
gs_init_call_table[level - 1] | 该级别函数表的起始地址 |
gs_init_call_table[level] | 该级别函数表的结束地址 |
3.2 遍历方式
volatile const init_call_entry_t *entry;
for (entry = gs_init_call_table[level - 1];
entry < gs_init_call_table[level];
entry++)
{
/* 执行初始化函数 */
ret = (*entry->func)();
}四、实现原理
多级初始化机制的实现核心是利用编译器段机制,将初始化函数在编译链接时就组织好,运行时直接按顺序执行。整个流程可以分为四个关键步骤:
4.1 第一步:编译器段属性宏(os_stddef.h 第 40-71 行)
首先需要定义跨编译器的段属性宏,让不同编译器都能支持将数据放到指定段中:
/* Compiler related definitions */
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* For ARM compiler */
#define OS_SECTION(x) __attribute__((section(x)))
#define OS_USED __attribute__((used))
#elif defined(__IAR_SYSTEMS_ICC__) /* For IAR compiler */
#define OS_SECTION(x) @x
#define OS_USED __root
#elif defined(__GNUC__) /* For GNU GCC compiler */
#define OS_SECTION(x) __attribute__((section(x)))
#define OS_USED __attribute__((used))
#else
#error "Not supported the tool chain."
#endif作用说明:
OS_SECTION(x):将变量/结构体放到指定的段x中示例:
int g_custom OS_SECTION(".my_section") = 200;结果:变量
g_custom会被编译器放到名为.my_section的自定义段中,而不是默认的.data段。OS_USED:告诉编译器不要优化掉这个符号,即使看起来没有被引用
4.2 第二步:初始化函数注册(os_stddef.h 第 108-121 行)
用户通过 OS_INIT_CALL 宏将初始化函数注册到指定级别:
#ifdef OS_INIT_CALL_DEBUG_EN
#define OS_INIT_CALL(fn, level, sublevel) \
OS_USED static const init_call_entry_t _os_init_call_##fn OS_SECTION( \
".init_call." OS_STRING(level) "." OS_STRING(sublevel)) = {#fn, fn}
#else
#define OS_INIT_CALL(fn, level, sublevel) \
OS_USED static const init_call_entry_t _os_init_call_##fn OS_SECTION( \
".init_call." OS_STRING(level) "." OS_STRING(sublevel)) = {fn}
#endif延伸学习
想深入了解 #define 宏?请阅读:#define详解
代码详细解释:
1. 条件编译分支
#ifdef OS_INIT_CALL_DEBUG_EN- 根据
OS_INIT_CALL_DEBUG_EN宏是否定义,选择不同的宏实现 - 调试模式下会记录函数名,方便定位初始化失败问题
2. 宏参数
OS_INIT_CALL(fn, level, sublevel)fn:初始化函数名level:初始化级别(如OS_INIT_LEVEL_DEVICE)sublevel:子级别(如OS_INIT_SUBLEVEL_HIGH),用于同一级别内的执行顺序
3. 变量声明部分
OS_USED static const init_call_entry_t _os_init_call_##fnOS_USED:告诉编译器这个变量会被使用,不要优化掉static:仅在当前文件可见,避免命名冲突const:确保内容在运行时只读_os_init_call_##fn:使用##运算符将前缀与函数名拼接成变量名- 例如
fn=my_driver_init时,变量名为_os_init_call_my_driver_init
- 例如
4. 段属性
OS_SECTION(".init_call." OS_STRING(level) "." OS_STRING(sublevel))OS_SECTION():将变量放到指定的段中OS_STRING(level):将级别宏转换为字符串(例如OS_INIT_LEVEL_DEVICE转为"5")OS_STRING(sublevel):将子级别宏转换为字符串(例如OS_INIT_SUBLEVEL_HIGH转为"1")- 段名格式:
.init_call.<level>.<sublevel>- 例如:
.init_call.5.1 - 字符串自动拼接:C 语言中相邻的字符串字面量会被编译器自动拼接成一个字符串,例如
".init_call." "5" "." "1"会自动拼接成".init_call.5.1"
- 例如:
- 链接器会按段名字符排序,确保初始化顺序正确
5. 结构体初始化
- 调试模式:
{#fn, fn}#fn:使用#运算符将函数名转为字符串(例如"my_driver_init")fn:函数指针
- 非调试模式:
{fn}- 只保留函数指针,节省空间
工作原理:
- 创建一个
init_call_entry_t结构体,包含函数指针(调试模式还包含函数名) - 通过
OS_SECTION将这个结构体放到名为.init_call.<level>.<sublevel>的段中 - 链接器会将同一级别的所有结构体按顺序排列在一起
示例:
/* 用户代码中这样使用 */
static os_err_t my_driver_init(void)
{
/* 驱动初始化代码 */
return OS_SUCCESS;
}
OS_INIT_CALL(my_driver_init, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH);宏展开后(调试模式):
OS_USED static const init_call_entry_t _os_init_call_my_driver_init
OS_SECTION(".init_call.5.1") = {"my_driver_init", my_driver_init};结果:
- 编译时,
_os_init_call_my_driver_init结构体被放到.init_call.5.1段中 - 链接时,同一级别(
OS_INIT_LEVEL_DEVICE)的所有初始化函数按子级别顺序排列 - 系统启动时,
_k_run_init_call(OS_INIT_LEVEL_DEVICE)会遍历该级别并调用my_driver_init()
4.3 第三步:编译器特定的段标记(os_startup.c 第 51-125 行)
为了在代码中访问这些段,需要知道每个段的起始和结束地址。不同编译器有不同的实现方式:
ARM MDK 编译器(第 51-108 行)
ARM MDK 编译器没有直接访问段地址的机制,因此采用标记函数的方式:
static os_err_t pre_kernel_1_start(void)
{
return OS_SUCCESS;
}
OS_INIT_CALL(pre_kernel_1_start, OS_INIT_LEVEL_PRE_KERNEL_1, "");
/* 其他级别类似... */ARM MDK 方式图示:
段名 内容 符号位置
─────────────────────────────────────────────────────────────────
.init_call.1.1 [标记函数 pre_kernel_1_start] ← &_os_init_call_pre_kernel_1_start
.init_call.1.2 [用户初始化函数 1] ← &_os_init_call_pre_kernel_1_start + 1
.init_call.1.3 [用户初始化函数 2]
...
.init_call.2.1 [标记函数 pre_kernel_2_start] ← &_os_init_call_pre_kernel_2_start
.init_call.2.2 [用户初始化函数 3] ← &_os_init_call_pre_kernel_2_start + 1
...
.init_call.5.1 [标记函数 device_start] ← &_os_init_call_device_start
.init_call.5.2 [用户初始化函数 4] ← &_os_init_call_device_start + 1
...
.init_call.9 [标记函数 os_init_end] ← &_os_init_call_os_init_end- 为每个级别定义一个空函数,用
OS_INIT_CALL注册到该级别的最前面 - 这个标记函数会占据段的第一个位置
- 用户的初始化函数会紧跟在标记函数后面
- 使用时需要
+1来跳过标记函数
GCC 编译器(第 109-121 行)
GCC 配合链接脚本可以直接生成段符号:
extern const init_call_entry_t __init_call_pre_kernel_1_start[];
extern const init_call_entry_t __init_call_pre_kernel_2_start[];
extern const init_call_entry_t __init_call_post_kernel_start[];
extern const init_call_entry_t __init_call_pre_device_start[];
extern const init_call_entry_t __init_call_device_start[];
extern const init_call_entry_t __init_call_component_start[];
extern const init_call_entry_t __init_call_application_start[];
#ifdef OS_USING_SMP
extern const init_call_entry_t __init_call_smp_start[];
#endif /*OS_USING_SMP*/
extern const init_call_entry_t __os_init_end[];GCC 方式图示:
地址 段名 内容 符号
─────────────────────────────────────────────────────────────────
0x8001000 .init_call.1.1 [用户初始化函数 1] ← __init_call_pre_kernel_1_start
0x8001008 .init_call.1.2 [用户初始化函数 2]
0x8001010 .init_call.1.3 [用户初始化函数 3]
...
0x8001020 .init_call.2.1 [用户初始化函数 4] ← __init_call_pre_kernel_2_start
0x8001028 .init_call.2.2 [用户初始化函数 5]
...
0x8001060 .init_call.5.1 [用户初始化函数 6] ← __init_call_device_start
0x8001068 .init_call.5.2 [用户初始化函数 7]
...
0x8001100 [结束标记] ← __os_init_end- 链接脚本会为每个段自动生成
__init_call_xxx_start和__os_init_end符号 - 这些符号直接指向段的起始和结束地址
- 无需标记函数,直接使用符号即可
4.4 第四步:初始化函数表构建(os_startup.c 第 127-155 行)
有了段标记后,就可以构建 gs_init_call_table 数组来索引各个级别:
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM MDK Compiler */
static const init_call_entry_t *gs_init_call_table[] = {&_os_init_call_pre_kernel_1_start + 1,
&_os_init_call_pre_kernel_2_start + 1,
&_os_init_call_post_kernel_start + 1,
&_os_init_call_pre_device_start + 1,
&_os_init_call_device_start + 1,
&_os_init_call_component_start + 1,
&_os_init_call_application_start + 1,
#ifdef OS_USING_SMP
&_os_init_call_smp_start + 1,
#endif /*OS_USING_SMP*/
&_os_init_call_os_init_end};
#elif defined(__GNUC__) /* for GCC Compiler */
static const init_call_entry_t *gs_init_call_table[] = {__init_call_pre_kernel_1_start,
__init_call_pre_kernel_2_start,
__init_call_post_kernel_start,
__init_call_pre_device_start,
__init_call_device_start,
__init_call_component_start,
__init_call_application_start,
#ifdef OS_USING_SMP
__init_call_smp_start,
#endif /*OS_USING_SMP*/
__os_init_end};
#endif为什么 ARM MDK 需要 +1,GCC 不需要?
| 特性 | ARM MDK 编译器 | GCC 编译器 |
|---|---|---|
| 段标记方式 | 手动定义标记函数 | 链接器自动生成符号 |
| 标记位置 | 标记函数在段的第一个位置 | 符号直接指向用户函数起始 |
| 访问方式 | &_os_init_call_xxx + 1 | __init_call_xxx_start |
| 偏移原因 | 需要跳过标记函数 | 无需偏移 |
4.5 第五步:遍历执行初始化函数(os_startup.c 第 166-185 行)
最后,_k_run_init_call() 函数根据级别遍历并执行初始化函数:
static os_err_t _k_run_init_call(int32_t level)
{
os_err_t ret;
volatile const init_call_entry_t *entry;
for (entry = gs_init_call_table[level - 1]; entry < gs_init_call_table[level]; entry++)
{
ret = (*entry->func)();
if (ret != OS_SUCCESS)
{
#ifdef OS_INIT_CALL_DEBUG_EN
OS_ASSERT_EX(OS_FALSE, "level %d automatic initialization faild of %s", level, entry->name);
#else
OS_ASSERT_EX(OS_FALSE, "level %d automatic initialization faild", level);
#endif
}
}
return OS_SUCCESS;
}执行流程:
- 从
gs_init_call_table[level - 1]获取该级别的起始地址 - 遍历直到
gs_init_call_table[level]结束地址 - 依次调用每个
entry->func()函数 - 如果执行失败,断言报错(调试模式会打印失败的函数名)
4.6 机制优势
- 零开销: 所有工作在编译链接时完成,运行时无需动态构建函数表
- 顺序保证: 链接时按段名排序,子级别确保同一级别内的执行顺序
- 灵活扩展: 新增初始化级别只需更新链接脚本,无需修改核心代码
- 调试友好: 支持调试模式记录函数名,便于定位初始化失败问题