内存对齐是个容易让人糊涂的概念,但我会用最简单的方式解释清楚。只是需要更直观的解释。
一、什么是内存对齐?(用大白话解释)
基本概念
想象一下你家里的书架:
-
不对齐的情况:你把书随便乱放,有的竖着放,有的横着放,中间还留空隙
-
对齐的情况:你把所有书都按固定间隔整齐摆放
在计算机里,内存对齐就是数据在内存中存放时要放在特定的地址上,这些地址必须是某个值(通常是2、4、8等)的倍数。
为什么需要对齐?
简单比喻:CPU读取内存就像你在超市买东西:
-
如果商品都摆放在过道边(对齐位置),你伸手就能拿到
-
如果商品藏在货架深处(不对齐位置),你需要伸手进去掏半天
对于CPU来说:
-
读取速度快:对齐的数据一次就能读完
-
硬件要求 :有些CPU(包括STM32的ARM Cortex-M)必须对齐访问,否则会出错
具体例子
假设我们有个结构体:
cs
struct Example {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
不对齐时(理论上的紧凑布局):
地址 0: a
地址 1: b的第1字节
地址 2: b的第2字节
地址 3: b的第3字节
地址 4: b的第4字节
地址 5: c的第1字节
地址 6: c的第2字节
总大小:7字节
对齐时(实际的内存布局):
地址 0: a // char,1字节
地址 1: (填充) // 空3字节,为了让b从地址4开始
地址 4: b // int,从4的倍数地址开始
地址 8: c // short,从偶地址开始
地址 10: (填充) // 再补2字节,让总大小是4的倍数
总大小:12字节
看到了吗?对齐后多了很多"空白",但CPU读取速度大大加快!
二、STM32中的内存对齐(ARM Cortex-M)
1. STM32的特殊要求
STM32用的ARM Cortex-M内核有严格的对齐要求:
-
字(4字节):必须放在4的倍数地址(如0x00, 0x04, 0x08...)
-
半字(2字节):必须放在2的倍数地址
-
字节:可以放在任何地址
如果不遵守:STM32会产生"硬件错误",程序直接崩溃!
2. 在代码中如何保证对齐?
方法1:使用编译器属性(最常用)
cs
// 方法1:GCC/ARM编译器指令
typedef struct {
uint8_t data[13]; // 13字节数据
} __attribute__((packed, aligned(4))) my_struct_t;
// aligned(4) 表示4字节对齐
// packed 表示取消内部填充(但这里又被aligned覆盖了)
// 更常用的方式:
typedef struct {
uint32_t a; // 4字节,自动对齐
uint16_t b; // 2字节
uint8_t c; // 1字节
} __attribute__((aligned(4))) my_data_t; // 整个结构体4字节对齐
方法2:ARM编译器的特定指令
cs
// 对于ARMCC/IAR等编译器
#pragma pack(push, 1) // 保存当前对齐设置,设置为1字节对齐(紧凑)
typedef struct {
uint8_t a;
uint32_t b;
} my_struct;
#pragma pack(pop) // 恢复原来的对齐设置
// 或者指定对齐
__align(4) uint8_t buffer[100]; // buffer从4的倍数地址开始
方法3:C11标准方法(推荐,通用)
cs
#include <stdalign.h> // C11标准头文件
typedef struct {
alignas(4) uint8_t data[13]; // 这个数据按4字节对齐
} my_struct_t;
// 或者
_Alignas(4) uint8_t buffer[100]; // C11标准写法
三、实际代码示例(STM32常用)
示例1:DMA传输缓冲区(必须对齐!)
cs
// DMA通常需要4字节对齐
#define ALIGN_4_BYTES __attribute__((aligned(4)))
// DMA缓冲区
ALIGN_4_BYTES uint8_t dma_buffer[1024]; // 确保从4字节边界开始
// 或者更明确
#define CACHE_LINE_SIZE 32
typedef struct {
uint32_t data[8];
} __attribute__((aligned(CACHE_LINE_SIZE))) cache_line_t;
示例2:结构体对齐(网络数据包)
cs
// 以太网帧结构
typedef struct {
uint8_t dest_mac[6]; // 目标MAC地址
uint8_t src_mac[6]; // 源MAC地址
uint16_t eth_type; // 以太网类型
} __attribute__((packed)) eth_header_t; // packed确保没有填充
// 但是整个结构体要2字节对齐
static eth_header_t __attribute__((aligned(2))) eth_header;
示例3:联合体对齐(共用内存)
cs
typedef union {
struct {
uint16_t year:12; // 12位年
uint8_t month:4; // 4位月
uint8_t day; // 8位日
} bits;
uint32_t value; // 整个32位
} __attribute__((aligned(4))) date_t; // 整个联合体4字节对齐
四、检查内存对齐
1. 查看大小和对齐
cs
#include <stdio.h>
typedef struct {
uint8_t a;
uint32_t b;
uint16_t c;
} my_struct;
int main(void) {
my_struct test;
printf("结构体大小: %lu 字节\n", sizeof(test));
printf("对齐要求: %lu 字节\n", _Alignof(test));
printf("b的偏移地址: %lu\n", (size_t)&test.b - (size_t)&test);
return 0;
}
2. 强制检查(编译时)
cs
// 如果alignment不是4的倍数,编译会报错
static_assert(alignof(my_struct) == 4, "结构体必须是4字节对齐!");
static_assert(sizeof(my_struct) % 4 == 0, "结构体大小必须是4的倍数!");
五、常见问题及解决方案
问题1:为什么我的DMA传输失败?
cs
// 错误:可能不对齐
uint8_t buffer[100]; // 不一定从对齐地址开始
DMA_Config(buffer); // 可能导致硬件错误
// 正确:强制对齐
__attribute__((aligned(4))) uint8_t buffer[100];
// 或者
uint8_t buffer[100] __attribute__((aligned(4)));
问题2:结构体跨平台通信
cs
// 发送到其他设备的数据结构
#pragma pack(push, 1) // 1字节对齐,取消填充
typedef struct {
uint16_t cmd; // 命令字
uint32_t data; // 数据
uint8_t checksum; // 校验和
} packet_t;
#pragma pack(pop)
// 发送时
packet_t packet __attribute__((aligned(1))); // 1字节对齐,紧凑
六、总结与最佳实践
给STM32编程的建议:
-
默认情况:让编译器自动处理,它通常做得很好
-
DMA/外设数据:明确指定对齐(通常是4字节)
-
结构体定义:
-
按大小排序成员(从大到小)减少填充
-
添加
packed属性时要小心,可能影响性能
-
-
动态内存 :
malloc返回的地址通常是对齐的
简单记忆:
-
大部分情况 :用
__attribute__((aligned(4))) -
紧凑数据 (如通信协议):先用
#pragma pack(1),再恢复 -
不确定时 :查看
.map文件或使用sizeof、alignof检查
记住:对齐是为了速度,不是浪费空间。在嵌入式系统中,正确对齐可以避免很多奇怪的硬件错误。