简介
数据拼接就是把分散的数据(全局/只读),通过编译器的编译链接把它们拼接起来放到一段连续的空间内,本人简称为“串表”或叫“分段列表”。如果这些分散的数据都是同一类型,则这段数据就是数组,程序可实现快速访问。对于这类需求,GCC 的编译属性__attribute__((section("name"))
为我们提供了解决方案。
更多的 attribute 编译属性:
《attribute-section 编译属性-数据拼接》
《attribute-aligned 编译属性-地址对齐》
《attribute-packed 编译属性-字节对齐》
《attribute-weak 编译属性-弱符号》
《attribute-un/used 编译属性-未用警告》
《attribute-at 编译属性-地址指定》
用途
数据拼接常用于设备启动时初始化各个模块,其中 linux 的 initcall 机制就是基于此原理实现的。下面对比介绍上电初始化的几种方式:调用方式、列表方式、串表方式。
1、调用方式
在main()
函数最前在分别调用这些初始化函数,当新增一模块就必须在main()
增加代码 ,这种方式与main()
过于耦合。试想一下 linux 系统,少则几百多则上千上万个初始化函数放在一起,那是多么恐怖的事,而且 linux 核心源文件是不给程序员随便改动的,否则会影响系统稳定性。也就是说对于中大型工程这种方式不可取。
1
2
3
4
5
6
7
8
9
10
11
12
|
int main(void)
{
//每当增加模块初始化,都需在这里增加初始化函数
init_a();
init_b();
init_c();
while(1)
{
...
}
}
|
2、列表方式
做一个数组列表,在列表加入这些初始化函数,main()
的上电初始化时用for
循环统一初始化,当新增一模块无须动代码,只需要在数组列表增加内容即可,这种方式比调用方式简洁方便很多。而且这个表格可以独立放到一个文件里,不但集中管理调用初始化函数,还有效阻隔核心代码不被程序员改动。由于集中管理,会存在一定可能性的误操作(如:误删别的模块初始化操作),与独立模块还是存在一定的耦合性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
typedef void (*func_t)(void);
const func_t func_tab[] = {
init_a,
init_b,
init_c
}; //平时只需维护此列表
int main(void)
{
int i;
for (i=0; i<sizeof(func_tab)/sizeof(func_tab[0]); i++)
{
(*(func_tab[i]))();
}
while(1)
{
...
}
}
|
3、串表方式
在main()
的上电初始化时使用for
循环调用事先已预设好的串表,串表内容(组员)分散在各种模块中,依靠编译器的编译链接把它们存到这个串表里(无需人工添加)。
关键字-属性__attribute__
/ə’trɪbjuːt/
关键字-分段section
/’sekʃn/
语法:__attribute__((used, section(".name." "tail")))
.name.
为分段名称,同名分段数据存放到同一段连接的空间内。
tail
为分段名称后缀,先由它决定数据先后顺序,再由代码编译先后顺序决定。
used
向编译器说明这段代码即使没使用也不能优化掉,不能产生警告;
而 unused
则是表示该函数或变量可能不使用,编译器不要产生警告信息。
特别说明:section
修饰的数据段会被编译器和链接器这两道关卡优化掉,需要增加特别语句加以防止!used、unused 只针对编译器而言,但对于链接器无效,也就是说链接器的--gc-sections
还是会优化掉没使用的段,除非在链接脚本中使用KEEP
语句特别指的段才不会被优化掉!关于链接脚本的更多知识,请移步《linux-STM32F开发㈠-makefile 构建与使用》
主程序:main.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//---定义串表---
typedef void (*p_init_fun_t)(void);
struct init_fun_tab
{
p_init_func_t pfun; //初始函数(指针)
const uint8_t *name; //函数名称(指针)
};
// 属性关键字 分段关键字 分段名称 分段名称后缀
// +-----+-----+ +----+----+ +---+---+ +-+-+
#define INITSECTION(level) __attribute__((used, __section__(".init_fn." level)))
#define INIT_FRONT_EXPORT(func,name) INITSECTION("0.front") const struct init_fun_tab init_fn_##func = {func, name} //串表头
#define INIT_TABLE_EXPORT(func,name) INITSECTION("1" ) const struct init_fun_tab init_fn_##func = {func, name}
#define INIT_LIMIT_EXPORT(func,name) INITSECTION("1.limit") const struct init_fun_tab init_fn_##func = {func, name} //串表尾
// +---------+-------+ +----------------------------+------------------------+
// 串表宏 一个表员(结构体)数据
|
主程序:main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//---调用串表---
void tab_front(void) {}
INIT_FRONT_EXPORT(tab_front, "tab_front()"); //串表头
void tab_limit(void) {}
INIT_LIMIT_EXPORT(tab_limit, "tab_limit()"); //串表尾
int main(void)
{
const struct init_fun_tab *p_init;
//执行串表(由编译器维护列表内容)
for (p_init=&init_fn_tab_front+1; p_init<&init_fn_tab_limit; p_init++)
{
printf("run:%s\r\n", p_init->name);
(*(p_init->pfun))();
}
while(1)
{
...
}
}
|
模块a:
1
2
3
4
5
6
7
|
//---加入串表---
include "main.h"
void init_a(void)
{
printf("init_a code\r\n");
}//加入串表
INIT_TABLE_EXPORT(init_a, "init_a()"); //告知编译器将函数地址加入到串表
|
模块b:
1
2
3
4
5
6
7
|
//---加入串表---
include "main.h"
void init_b(void)
{
printf("init_b code\r\n");
}//加入串表
INIT_TABLE_EXPORT(init_b, "init_b()"); //告知编译器将函数地址加入到串表
|
main.c
写好后,就不会改动,如果模块想在main.c
完成初始化工作,只需在其初始化函数后面加入INIT_TABLE_EXPORT()
宏,编译器编译时就可以通过此宏知道将函数指针加入到main.c
的初始化列表上。从代码上看,模块与模块之间没有任何的函数调用关系,高质量实现高内聚性低耦合度。
4、作用对象
__attribute__((section(x)))
作用对象:
主要针对只读变量。
5、语法总结
一、关于__attribute__(())
的参数名称,为了防止与其它对象出现同名影响,强烈建议在参数的前后都加上__
两个下划线。
1
2
3
4
5
6
7
8
|
__attribute__((section(x))) 改为 __attribute__((__section__(x)))
__attribute__((at(a))) 改为 __attribute__((__at__(a)))
__attribute__((packed)) 改为 __attribute__((__packed__))
__attribute__((aligned(n))) 改为 __attribute__((__aligned__(n)))
__attribute__((unused)) 改为 __attribute__((__unused__))
__attribute__((used)) 改为 __attribute__((__used__))
__attribute__((weak)) 改为 __attribute__((__weak__))
|
二、关于__attribute__(())
语句书写位置总体原则:书写到修饰对象名称的后面(★修饰其左边的单元体(非每个元素),放到最前面即是修饰整体★),但考虑要跨编译平台使用,强烈建议使用宏定义并且按下面规则使用。
1
2
3
4
5
6
7
8
9
10
|
//【总体原则】+++++++++++++++++++
#define O2O_SECTION(x) __attribute__((__section__(x))) //数据拼接 (对象名称后明声明)
#define O2O_AT(a) __attribute__((__at__(a))) //地址指定 (对象名称后明声明)
#define O2O_PACKED __attribute__((__packed__)) //字节对齐 (对象名称后明声明,强烈建议改用 #pragma pack(push, 1) ... #pragma pack(pop) 的兼容性更好)
#define O2O_ALIGN(n) __attribute__((__aligned__(n))) //地址对齐 (对象整体最前声明)
#define O2O_UNUSED __attribute__((__unused__)) //未用不警告(对象整体最前声明)
#define O2O_USED __attribute__((__used__)) //未用不优化(对象整体最前声明)
#define O2O_WEAK __attribute__((__weak__)) //弱化对象 (对象整体最前声明)
#define O2O_INLINE static __inline //内联函数 (对象整体最前声明,c/h文件中直接编写函数(体),不能外部声明)
|
三、关于__attribute__(())
的__aligned__(n)
参数对【结构体类型】修饰的特殊表现(只是唯一的特殊个案):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
typedef struct obj_1_
{
uint16_t a;
uint8_t b;
}obj_1_t __attribute__((__aligned__(64))); //1.用于[单体]结构体类型的【起始地址】
typedef struct obj_n_
{
uint16_t a;
uint8_t b;
}__attribute__((__aligned__(64))) obj_n_t; //2.用于结构体[组员]类型的【起始地址】和【大小】,也可用于单体结构体!备注:是组员非成员!
// -┬-
obj_1_t aaaaaa; //1.影响[单体结构体]的起始地址对齐,不能用于数组 ├→ ●只是唯一的特殊个案●
obj_n_t bbb[6]; //2.影响结构体[每个组员]的起始地址对齐和大小限制----------------------┘
//3.影响[数组整体]起始地址,但不影响[其它组员]起始地址和大小!
__attribute__((__aligned__(64))) struct obj_1_ ccc[8]; //←┤
__attribute__((__aligned__(64))) struct obj_n_ ddd[8]; //←┘
|
四、关于__attribute__(())
的__section__(x)
参数被【编译器】与【链接器】优化的问题:
【section】修饰的数据段会被【编译器】和【链接器】这两道关卡优化掉,需要增加特别语句加以防止!【used、unused】只针对【编译器】而言,但对于【链接器】无效,也就是说链接器的【-Wl,–gc-sections】参数还是会优化掉没使用的段,除非在【链接脚本】中使用【KEEP】语句特别指出的段才不会被优化掉!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//1.变量声明
__attribute__((__used__)) const uint8_t aaa __attribute__((__section__(".init.001"))); //增加__used__防止被[编译器]优化
__attribute__((__used__)) const uint8_t bbb __attribute__((__section__(".init.002"))); //增加__used__防止被[编译器]优化
//2.编译链接
gcc -Wl,--gc-sections -o hello hello.c /* -Wl,--gc-sections 会强制优化掉没使用的__section__(函数段/数据段)*/
//3.链接脚本
.text :
{
.............
. = ALIGN(4);
KEEP(*(SORT(.init.*))) /* 使用 KEEP(*(SORT())) 防止被[链接器]优化 */
. = ALIGN(4);
_etext = .;
} >FLASH
|
扩展
数据拼接除了可以用在初始化上,还可以应用于很多场合,只要和列表有关的都有应用的可能性。
1、回调串表
本人编写独立模块时,喜欢通过回调函数的方式来通知其它模块,而且回调函数做成列表,即使用列表触发通知多个第三方模块。拿按键模块作为示例,当检测到有按键动作时,调用回调列表触发【界面模块】和【恢复出厂初始化模块】执行相应动作。
按键模块:key.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
typedef void (*p_key_fun_t)(uint8_t key, uint8_t action, uint32_t time);
struct key_fun_tab
{
p_key_func_t pfun; //回调函数(指针)
};
// 属性关键字 分段关键字 分段名称 分段名称后缀
// +-----+-----+ +----+----+ +--+--+ +-+-+
#define KEYSECTION(level) __attribute__((used, __section__(".key_fn." level)))
#define KEY_FRONT_EXPORT(func) KEYSECTION("0.front") const struct key_fun_tab key_fn_##func = {func} //串表头
#define KEY_TABLE_EXPORT(func) KEYSECTION("1" ) const struct key_fun_tab key_fn_##func = {func}
#define KEY_LIMIT_EXPORT(func) KEYSECTION("1.limit") const struct key_fun_tab key_fn_##func = {func} //串表尾
// +---------+-------+ +---------------------+---------------------+
// 串表宏 一个表员(结构体)数据
|
按键模块:key.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
void ktab_front(uint8_t key, uint8_t action, uint32_t time) {}
void ktab_limit(uint8_t key, uint8_t action, uint32_t time) {}
KEY_FRONT_EXPORT(ktab_front); //串表头
KEY_LIMIT_EXPORT(ktab_limit); //串表尾
void key_scan(void)
{
...
const struct key_fun_tab *p_key;
//按键有动作时回调列表通知其它模块(由编译器维护列表内容)
for (p_key=&key_fn_ktab_front+1; p_key<&key_fn_ktab_limit; p_key++)
{
(*(p_key->pfun))(key, action, time);
}
...
}
|
界面模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//---加入串表---
include "key.h"
void lcd_key_callback(uint8_t key, uint8_t action, uint32_t time)
{
if ((key >= KEYNO_K1)
&& (key <= KEYNO_K4))
{
key -= KEYNO_K1;
if ((action == KEYAT_1CLICK/*单击*/)
|| (action == KEYAT_2CLICK/*双击*/))
{
if (time == 0) /*短按,非长按*/
{
....//收到按键单/双击动作
}
}
}
}//加入串表
KEY_TABLE_EXPORT(lcd_key_callback); //告知编译器将函数地址加入到串表
|
恢复出厂初始化模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//---加入串表---
include "key.h"
void frst_key_callback(uint8_t key, uint8_t action, uint32_t time)
{
if (key == KEYNO_FRST)
{
if (action == KEYAT_1CLICK/*单击*/)
{
if (time == 5000) /*长按5秒*/
{
....//收到按键长按5秒动作
}
}
}
}//加入串表
KEY_TABLE_EXPORT(frst_key_callback); //告知编译器将函数地址加入到串表
|