前言:
为了方便查看博客,特意申请了一个公众号,附上二维码,有兴趣的朋友可以关注,和我一起讨论学习,一起享受技术,一起成长。
1. 简述
计算机中内存空间都是按照字节(byte)划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,以 2、4、或 8 的倍数的字节块来读写内存,这样就会对基本数据类型的合法地址作出一些限制,即它的地址必须是 2,4 或 8 的倍数。各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,即需要对齐。
关于需要对齐的原因:
(1)为了提高存取效率,如读取一个 4 字节的数据,若起始地址为 4 的倍数,CPU 只需一次读取,如果不是 4 的倍数,既需要分成两次读取在拼接,这就造成了 CPU 效率的降低。
(2)处理器本身的限制,对某些操作,要求数据的内存地址是 2 、4 等整数倍,如果不是,常会造成一些硬件异常(HardFault)。数据的起始地址应具有“对齐”特性。比如:4 字节数据的起始地址应位于 4 字节边界上,即起始地址能够被 4 整除。
2. 数据的存储方式
以下面的结构体为例:
typedef struct
{
uint8_t data_buff[5];
uint32_t data_length;
uint8_t data_flag;
}st_data_test;
st_data_test g_data_test = {0};
sizof(g_data_test ) = 16。以结构体中最大的类型进行对齐,即 uint32_t 。
占据存储空间如下所示:
结构体各变量的首地址如下,均以 4 对齐:
如果我们把结构体的定义稍稍调整,如下:
typedef struct
{
uint8_t data_buff[5];
uint8_t data_flag;
uint32_t data_length;
}st_data_test;
st_data_test g_data_test = {0};
sizof(g_data_test ) = 12 。
在 C 语言中,内存的分配和管理是由操作人员控制的。在嵌入式开发过程中,尤其是存储资源有限时,就必须合理的分配内存,使用内存。
typedef struct
{
uint8_t type;
uint16_t length;
uint8_t value;
}st_tlv;
st_tlv g_data_tlv = {0};
sizof(g_data_tlv ) = 6 。结构体 g_data_tlv 的大小为 6 字节,而不是 4 字节。因为结构体中适宜最大的长度来对齐的,此处就是按照 uint16_t 的长度来对齐。
字节对齐遵从系统字节数与要求的对齐字节数相比,最小原则,在四字节对齐时,局部会按照 2 字节对齐。
3. 数据的对齐方式
(1)数据类型自身的对齐值: char 型数据自身对齐值为 1 字节,short 型数据为 2 字节,int / float 型为 4 字节,double型为 8 字节;
(2)结构体或类的自身对齐值: 其成员中自身对齐值最大的那个值;
(3)指定对齐值: #pragma pack (value) 时的指定对齐值 value;
(4)数据成员、结构体和类的有效对齐值: 自身对齐值和指定对齐值中较小者,即有效对齐值 = min {自身对齐值,当前指定的pack值}。
每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。
3.1 pragma pack 关键字
#pragma pack 的主要作用就是改变编译器的内存对齐方式。
C 编译器可通过下面的方式改变对齐边界。
使用伪指令 #pragma pack(n):C 编译器将按照 n 个字节对齐;
使用伪指令 #pragma pack(): 取消自定义字节对齐方式。
(1)结构体对齐
#pragma pack(1)
typedef struct
{
uint8_t data_buff[5];
uint32_t data_length;
uint8_t data_flag;
}st_data_test1;
#pragma pack()
st_data_test1 g_st_data_test1;
如上结构体将按照 1 字节对齐,sizeof(g_st_data_test1) = 10。
结构体每个变量首地址如下:
注:GCC 编译器可以通过 __ attribute((aligned (n))), __ attribute __ ((packed))来修改对齐长度。
(2)栈对齐
栈的对齐方式不受结构体成员对齐选项的影响。总是保持对齐且对齐在 4 字节边界上。
//--------------------------------------------------------------------------------------------------------
// 函 数 名: stack_test
// 功能说明: 栈测试
// 形 参: 无
// 返 回 值: 无
// 日 期: 2020-04-04
// 备 注: 测试平台,STM32F103
// 作 者: by 霁风AI
//--------------------------------------------------------------------------------------------------------
void stack_test(void)
{
#pragma pack(push, 1) // 1/2/4/8
struct st_test
{
uint8_t val1;
uint32_t val2;
};
#pragma pack(pop)
uint8_t tmp1;
uint16_t tmp2;
uint32_t tmp3;
double data[2];
struct st_test tag_test;
printf("tmp1 address: %p \r\n", &tmp1);
printf("tmp2 address: %p \r\n", &tmp2);
printf("tmp3 address: %p \r\n", &tmp3);
printf("data[0] address: %p \r\n", &(data[0]));
printf("data[1] address: %p \r\n", &(data[1]));
printf("tag_test address: %p \r\n", &tag_test);
printf("tag_test.val2 address: %p \r\n", &(tag_test.val2));
}
结果输出:
可以看到变量均是按照 4 字节对齐,每个变量是独立对齐的,不同于结构体中会将挨着的 uint8_t 和 uint16_t 拼接成 4 字节。另外,可以看到地址是在减小,因为栈是向下生长的。
3.2 对齐处理
在不同编译平台或处理器上,字节对齐会造成消息结构长度的变化。编译器为了使字节对齐可能会对消息结构体进行填充,不同编译平台可能填充为不同的形式,大大增加处理器间数据通信的风险。
如下以 32 位处理器为例,提出一种内存对齐方法以解决上述问题:
(1)对于本地使用的数据结构,为提高内存访问效率,采用四字节对齐方式;同时为了减少内存的开销,合理安排结构体成员的位置,减少四字节对齐导致的成员之间的空隙,降低内存开销;
(2)对于处理器之间的数据结构,需要保证消息长度不会因不同编译平台或处理器而导致消息结构体长度发生变化,使用一字节对齐方式对消息结构进行紧缩;为保证处理器之间的消息数据结构的内存访问效率,采用字节填充的方式自己对消息中成员进行四字节对齐。
(3)数据结构的成员位置要兼顾成员之间的关系、数据访问效率和空间利用率。顺序安排原则是:四字节的放在最前面,两字节的紧接最后一个四字节成员,一字节紧接最后一个两字节成员,填充字节放在最后。
typedef struct
{
uint32_t parm1;
uint32_t parm2;
uint16_t length;
uint8_t flag;
uint8_t data_pad; //填充一字节,保证4字节对齐
}s_msg, *p_msg;
3.3 __align(num)
__align(num) 用于修改最高级别对象的字节边界。在汇编中使用 LDRD 或 STRD 时就要用到此命令 __align(8) 进行修饰限制。来保证数据对象是相应对齐。
这个修饰对象的命令最大是 8 个字节限制,可以让 2 字节的对象进行 4 字节对齐,但不能让 4 字节的对象 2 字节对齐。
__align 是存储类修改,只修饰最高级类型对象,不能用于结构或者函数对象。
示例:
__align(4) uint8_t g_data_buff[DATA_SIZE]; //保证分配的数组空间4字节对齐,同时保证数组首地址可被4整除
3.4 __packed
__packed 进行一字节对齐。
需注意:
(1)不能对 packed 的对象进行对齐;
(2)所有对象的读写访问都进行非对齐访问;
(3)float 及包含 float 的结构联合及未用 __packed 的对象将不能字节对齐;
(4)__packed 对局部整型变量无影响;
(5)强制由 unpacked 对象向 packed 对象转化时未定义。整型指针可以合法定义为 packed,如 __packed int* p(__packed int 则没有意义)
如下结构体:
__packed struct send_msg
{
uint8_t head;
uint32_t length;
uint16_t crc;
uint8_t flag;
};
测试程序:
struct send_msg g_send_msg = {0};
data_size = sizeof(g_send_msg);
printf("struct test size is %d \r\n", data_size);
printf("g_send_msg addr is %p \r\n", &g_send_msg);
printf("g_send_msg.head addr is %p \r\n", &g_send_msg.head);
printf("g_send_msg.length addr is %p \r\n", &g_send_msg.length);
printf("g_send_msg.crc addr is %p \r\n", &g_send_msg.crc);
printf("g_send_msg.flag addr is %p \r\n", &g_send_msg.flag);
测试结果:
我们可以看到 sizeof(g_send_msg) = 8(单字节对齐),另外 uint32_t length 的起始地址为 0x20000007 非 4 字节对齐,针对此,对齐访问的操作中,必须注意,可能导致访问硬件错误。定义一个局部的变量(位于 stack),也可能引发错误,因为栈是完全 4 字节对齐的。
4. 补充
(1) RISC 指令集处理器( MIPS / ARM):这种处理器的设计以效率为先,要求所访问的多字节数据 (short/int/ long) 的地址必须是为此数据大小的倍数,如 short 数据地址应为 2 的倍数,long 数据地址应为 4 的倍数,需是对齐的。