C 动态注册 分散加载

前言

在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内核驱动的加载方式。

详细参考     https://blog.csdn.net/qq_42370291/article/details/103639349

先定义下面几个宏

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行一样

C 动态注册 分散加载

2.这样编译后,由于没有被显示调用,链接时会被优化掉。所以在map文件中,会看到

C 动态注册 分散加载

还需要在两个地方,按图进行设置

C 动态注册 分散加载

 

添加链接参数:                       --keep=*.o(*initcall*)

C 动态注册 分散加载

 

修改后再编译,再看.map文件。之前被优化的“.initcall.core.init”,已经出现在MY_REGION区域了。注册了3个函数,32位的系统,每个函数指针是4个Byte,3*4=12,size=0xc。刚好相等。

C 动态注册 分散加载

 


 

 

三、使用

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,以后即使器件有改变,此函数也能动态地根据注册的内容,自动地隐式地调用了。


总结

当习惯性地一直使用C,就不会觉得直接显式调用有什么不好。

如果使用过高级语言的注册,回调等机制后。便不得不感叹,C语言的年龄真的大了。

上一篇:Linux内核 eBPF基础:perf(1):perf_event在内核中的初始化


下一篇:Linux数据报文接收发送总结4