前言
在C语言里,程序的初始化逻辑,通常来说,都是在某个地方(比如 说是main函数中),显式地去主动去调用某一个设备的初始化函数。
比如现在系统里,有一个LED,一个Key,通常需要在main里面去显式地对其初始化。
void main(void){
InitLed();
InitKey();
}
一、问题背景
但如果现在又新添加了一个器件flash,也有需要初始化的函数InitFlash,那能够在main中不添加内容,InitFlash仍然能够被调用吗?
或者说,新增删的器件,能够像高级语言一样,采用注册的方式,自动地被完成初始化吗?
二、思路
1.原理
程序.c被编译后,生成.o
所有的函数被链接后,生成.bin,此时都有一个对应的地址。能在.map文件当中查到。(当然也包括其它变量,等信息)
2.将特殊函数,单独管理
将上面的所有器件的初始化函数,单独拎出来管理,放到一个新的section中,假如取名my_section,这样,所有的初始化函数地址,就拥有了一个共同的新属性(section)
和.data,.text是平级的。然后找到my_section的起始,结束地址。进行遍历,就能逐个的进行隐式初始化。
二、具体实现
1.如果要添加到新的section中,模拟linux内核驱动的加载方式。
详细参考 __attribute__((section(x))) 使用详解_MRKING.的博客-CSDN博客___section()宏
先定义下面几个宏
typedef void (*INIT_CALL)(void);
#define __define_initcall(level,fn,id) \
static const INIT_CALL __initcall_##fn##id \
__attribute__ ((__section__(".initcall."level".init"))) = fn
#define CoreInitCall(fn) __define_initcall("core",fn,1)
再定义几个函数并注册
void InitLed(void)
{
}
void InitKey(void)
{
}
void InitFlash(void)
{
}
CoreInitCall(InitLed);
CoreInitCall(InitKey);
CoreInitCall(InitFlash);
2.在链接脚本中,添加新的红色框部分
15行是通配符 *.o是指所有的.o文件中以.initcall.开头,后面的*是通配符
MY_REGION 是自己需要管理段的Region名字,可以随便取。后面还需要用到此名字。
+0:意思是紧接着上面的来,当然也可以指定绝对位置和长度,类似于04行或06行一样
2.这样编译后,由于没有被显示调用,链接时会被优化掉。所以在map文件中,会看到
还需要在两个地方,按图进行设置
添加链接参数: --keep=*.o(*initcall*)
修改后再编译,再看.map文件。之前被优化的“.initcall.core.init”,已经出现在MY_REGION区域了。注册了3个函数,32位的系统,每个函数指针是4个Byte,3*4=12,size=0xc。刚好相等。
三、使用
1.找到MY_REGION的相关地址信息
在map文件中,我们能一眼看出Base: 0x080074a0, Size: 0x0000000c。但如何在程序中引用呢?直接看代码
extern int Image$$MY_REGION$$Base;
extern int Image$$MY_REGION$$Limit;
void AppCallInit(void){
INT32U *MyRegion_Start = (INT32U *)&Image$$MY_REGION$$Base;
INT32U *MyRegion_End = (INT32U *)&Image$$MY_REGION$$Limit;
for(INT32U *pAddr = MyRegion_Start; pAddr < MyRegion_End;){
INITCALL_DEBUG(("AppCallInit pAddr =%d \r\n", *pAddr));
((INIT_CALL)*pAddr)();
*pAddr++;
}
}
这样,只需要在main函数中调用AppCallInit,以后即使器件有改变,此函数也能动态地根据注册的内容,自动地隐式地调用了。
2 . 引申:Region$$Table$$Base,包含更多链接信息。
总结
当习惯性地一直使用C,就不会觉得直接显式调用有什么不好。
如果使用过高级语言的注册,回调等机制后。便不得不感叹,C语言的年龄真的大了。