目录

attribute-section 编译属性-数据拼接

简介

数据拼接就是把分散的数据(全局/只读),通过编译器的编译链接把它们拼接起来放到一段连续的空间内,本人简称为“串表”或叫“分段列表”。如果这些分散的数据都是同一类型,则这段数据就是数组,程序可实现快速访问。对于这类需求,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); //告知编译器将函数地址加入到串表