RT-thread 链接阶段如何把段排列到内存里,然后运行阶段如何遍历这些函数指针并调用。
目录
1.编译的时候如何将函数定义到相同等级的段里面
2.链接阶段如何把段排列到内存里,然后运行阶段如何遍历这些函数指针并调用。
3.start/end 边界的作用
4.运行阶段如何遍历调用
5.不同等级的调用阶段
6.最后总结自动初始化机制的意义
1.编译的时候如何将函数定义到相同等级的段里面
定义一个函数指针变量,把初始化函数 fn 的地址保存进去,并把这个变量放到指定的链接段 .rti_fn.xxx 中。
#define INIT_EXPORT(fn, level) \ rt_used const init_fn_t __rt_init_##fn rt_section(".rti_fn." level) = fn
INIT_EXPORT(my_can_init, "3");
宏展开
rt_used const init_fn_t __rt_init_my_can_init rt_section(".rti_fn." "3") = my_can_init;
问题:它放在 .rti_fn.0 段的后面、.rti_fn.1 段的前面,用来作为 board 初始化阶段的起始边界。
static int rti_board_start(void){ return 0;}INIT_EXPORT(rti_board_start, "0.end");
为什么放在它放在 .rti_fn.0 段的后面、.rti_fn.1 段的前面呢?
keil编译,链接器会将输入段按特定顺序防止在执行区域内,
各章节按顺序排列:
1.只读代码
2.只读数据
3.读写代码
4.读写数据
5.零初始化数据
如果输入的节名称具有相同的属性,则进行比较。名称区分大小写,并按 ASCII 字符排序规则的字母顺序进行比较。
那么使用GCC 编译的话需要自己去定义linker script?
Keil/ARM 编译工具链: ARMCC 或 ARMCLANG + armlink + scatter 文件 GCC 编译工具链: arm-none-eabi-gcc + arm-none-eabi-ld + linker script
GCC:靠 linker script 里的 SORT(.rti_fn*)
如果用的是GCC 编译,那么就需要去在工程中查看link.lds 文件,通常会定义好编译规则顺序
Keil:靠 scatter 文件里的 .ANY(+RO) 收集 RO 段,然后 armlink 按段属性和段名排序放置
.ANY (+RO) 为什么仅仅一个这个符号在.sct 文件下,就可以实现编译的时候把这些段的信息放在一起呢?
.ANY(+RO) 是 Keil ARM Linker 的 scatter 文件语法,意思是:
把所有还没有被其他规则匹配走的只读输入段,统一放到当前执行区域里。
把所有没有特殊指定位置的只读段,都放到 ER_IROM1 这个 Flash 代码区里。
rt_used const init_fn_t __rt_init_xxx rt_section(".rti_fn.x") = xxx;
能放到 .rti_fn.x 这个指定段里,关键靠的是 rt_section(".rti_fn." level),也就是编译器的 section 属性。
如果没有 rt_section 会怎样?
它大概率会被编译器放到普通只读数据段,比如:
.rodata .constdata
2.链接阶段如何把段排列到内存里,然后运行阶段如何遍历这些函数指针并调用。
在编译阶段,每个使用 INIT_EXPORT 修饰的初始化函数,都会生成一个函数指针变量,并通过 rt_section(".rti_fn.x") 放入指定的输入段中。
在 Keil 工程中,链接器会根据 scatter 文件中的 .ANY(+RO) 规则,将这些只读输入段统一收集到 Flash 的只读执行区域中。并且是连续的地址下。
3.start/end 边界的作用
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");
为了让系统知道某一类初始化函数的起始地址和结束地址,RT-Thread 定义了若干个空函数作为边界标记。
这些函数本身不完成实际初始化工作,它们的主要作用是生成一个确定的符号地址。
例如
低地址 ↓ __rt_init_rti_board_start -> .rti_fn.0.end __rt_init_gpio_init -> .rti_fn.1 __rt_init_uart_init -> .rti_fn.1 __rt_init_can_init -> .rti_fn.1 __rt_init_rti_board_end -> .rti_fn.1.end 高地址
rti_board_start 放在 "0.end",不是因为它表示 board 的结束,而是因为它要排在 .rti_fn.1 之前,用来作为 board 初始化函数区间的起始边界。
4.运行阶段如何遍历调用
运行阶段如何遍历调用?
遍历函数指针表的方式调用。
fn_ptr 从 __rt_init_rti_board_start 开始,依次向后移动,每移动一次就取出一个函数指针并执行,直到到达 __rt_init_rti_board_end 为止。
typedef int (*init_fn_t)(void); volatile 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)(); }
5.不同等级的调用阶段
board 阶段主要用于硬件相关初始化,例如 CPU、时钟、中断控制器、GPIO、串口、CAN 等。这些初始化需要在系统线程调度启动之前完成。
当内核调度器启动后,RT-Thread 会创建初始化线程 init_thread,在该线程中继续执行设备、组件、环境和应用层初始化。
这样可以将底层硬件初始化和上层软件组件初始化分开,保证系统启动顺序清晰。
6.最后总结自动初始化机制的意义
RT-Thread 自动初始化机制的核心思想是:
通过宏定义在编译阶段生成函数指针变量,通过 section 属性将函数指针放入指定初始化等级的段中;链接阶段再由 scatter 文件将这些段统一放入 Flash 只读区域;运行阶段系统根据 start/end 边界符号遍历函数指针表,从而自动调用各个初始化函数。
