3.4 $Sub$main钩子详解
2026/4/26大约 5 分钟启动流程钩子main编译器
3.4 $Sub$$main钩子详解
📚 本节导读
学习时长: 约 30 分钟
难度级别: ⭐⭐☆☆☆(基础级)
前置知识: 3.2 启动文件解析、3.3 SystemInit详解
🎯 学习目标
- 理解 $Sub$$main 钩子的作用
- 掌握钩子机制的原理
- 了解 $Super$$main 的用途
一、$Sub$$main钩子概述
1.1 什么是 $Sub$$main?
$Sub$$main 是 Keil MDK 编译器提供的钩子机制,允许在调用用户 main() 之前自动插入代码。
1.2 为什么需要这个钩子?
我们需要满足两个核心需求:
- 用户代码保持简洁 - 用户只需按常规方式写 main(),无需关心内核初始化细节
- 确保执行顺序正确 - 用户代码必须在内核初始化完成后再执行
1.3 两种调用流程对比
关键点说明:
- 左边流程 - 没有使用钩子,__main 直接调用用户 main()
- 右边流程 - 使用钩子,通过 $Sub$$main 先完成内核初始化,再调用用户 main()
- $Sub$$main - 编译器自动调用的钩子函数
- $Super$$main - 编译器为原 main() 函数生成的别名
二、实际代码分析
2.1 $Sub$$main() 函数(第 179-192 行)
$Sub$$main() 函数位于:drivers/driver.c#if defined(__CC_ARM) || defined(__CLANG_ARM)
void $Sub$$main(void)
{
os_kernel_init();
os_kernel_start();
}
#elif defined(__ICCARM__) || defined(__GNUC__)
void entry(void)
{
os_kernel_init();
os_kernel_start();
}
#endif逐行解释
#if defined(__CC_ARM) || defined(__CLANG_ARM)- 解释: 条件编译
- 含义: 只有在 Keil MDK 或 ARMClang 编译器下才编译这段代码
- 作用: 根据不同编译器类型,使用对应的钩子机制实现方式
void $Sub$$main(void)- 解释: 定义钩子函数
- 含义: 这是一个特殊命名约定,函数名格式为
$Sub$$<原函数名> - 作用: 链接器看到这个函数,会自动把对 main() 的调用替换为对 $Sub$$main() 的调用
os_kernel_init();- 解释: 调用内核初始化函数
- 含义: 初始化 OneOS 内核
- 作用: 为后续多任务运行做准备
os_kernel_start();- 解释: 调用内核启动函数
- 含义: 启动调度器,开始多任务运行
- 作用: ⚠️ 注意:此函数一旦调用便不会返回,系统将进入多任务调度状态
#elif defined(__ICCARM__) || defined(__GNUC__)- 解释: 条件编译
- 含义: 检查当前是否使用 IAR 或 GCC 编译器
- 作用: 根据不同编译器类型,使用对应的钩子实现方式
void entry(void)- 解释: 定义入口函数
- 含义: 这是 IAR 和 GCC 编译器下的钩子实现方式
- 作用: 在调用 main() 之前先执行内核初始化
os_kernel_init();- 解释: 调用内核初始化函数
- 含义: 初始化 OneOS 内核
- 作用: 为后续多任务运行做准备
os_kernel_start();- 解释: 调用内核启动函数
- 含义: 启动调度器,开始多任务运行
- 作用: ⚠️ 注意:此函数一旦调用便不会返回,系统将进入多任务调度状态
#endif- 解释: 结束条件编译
- 含义: 结束 if-elif-endif 条件块
- 作用: 结束不同编译器的处理逻辑
2.2 app_entry() 函数(第 194-203 行)
void app_entry(void)
{
#if defined(__CC_ARM) || defined(__CLANG_ARM)
extern int $Super$$main(void);
$Super$$main();
#elif defined(__ICCARM__) || defined(__GNUC__)
extern int main(void);
main();
#endif
}逐行解释
void app_entry(void)- 解释: 定义 app_entry() 函数
- 含义: 这是 OneOS 内核提供的应用入口函数
- 作用: 内核初始化完成后,通过此函数调用用户 main()
#if defined(__CC_ARM) || defined(__CLANG_ARM)- 解释: 条件编译
- 含义: 检查当前是否使用 Keil MDK 或 ARMClang 编译器
- 作用: 根据不同编译器类型,使用对应的钩子实现方式
extern int $Super$$main(void);- 解释: 声明 $Super$$main 函数
- 含义: 声明这是一个外部定义的函数
- 作用: 告诉编译器 $Super$$main 由链接器提供,无需手动实现
$Super$$main();- 解释: 调用 $Super$$main()
- 含义: 通过链接器自动生成的别名,调用原始的 main() 函数
- 作用: 执行用户定义的 main() 函数代码
#elif defined(__ICCARM__) || defined(__GNUC__)- 解释: 条件编译
- 含义: 检查当前是否使用 IAR 或 GCC 编译器
- 作用: 根据不同编译器类型,使用对应的钩子实现方式
extern int main(void);- 解释: 声明 main 函数
- 含义: 使用 extern 关键字声明外部函数
- 作用: 告诉编译器 main 函数在其他文件中定义,当前文件可以调用
main();- 解释: 调用 main()
- 含义: 在 IAR/GCC 下直接调用用户定义的 main() 函数
- 作用: 执行用户代码
#endif- 解释: 结束条件编译
- 含义: 结束 if-elif-endif 条件块
- 作用: 结束不同编译器的处理逻辑
2.3 Keil与GCC/IAR的钩子机制对比
让我们对比一下 Keil/ARMCC 和其他编译器:
| 特性 | Keil MDK/ARMCC | GCC/IAR |
|---|---|---|
| 钩子函数名 | $Sub$$main() | entry()(本项目实现) |
| 调用原函数 | $Super$$main() | 直接调用 main() |
| 钩子机制 | $Sub$$<函数名>命名约定 | 无标准钩子机制,需要自定义实现 |
| 支持情况 | 原生支持,链接器自动处理 | 需要在链接脚本或启动文件中配置调用点 |
三、完整启动流程
3.1 完整启动流程图
💡 本节总结
重点回顾
- $Sub$$main - 编译器钩子,在 main() 前自动执行
- 核心任务 - 调用 os_kernel_init() 和 os_kernel_start()
- $Super$$main - 编译器为原 main() 生成的别名,通过它调用用户原来的 main()
下一步
接下来请学习:3.5 内核初始化详解