ARM 内存对齐总结

内存对齐原理与应用
一、啥是内存对齐?为啥要内存对齐?
      现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问都可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就是对齐。
字节对齐的原因大致是如下两条:
      1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
      2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
二、对齐规则
       每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。规则:
       1. 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在偏移量为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行,
即各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的整数倍。
       2. 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
       3. 结合1、2可推断:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。
三、X86对齐实验
       下面再简要回顾解释一下上述的对齐规则,结合实例进行分析:
       1. 数据类型自身的对齐值:对于char型数据,其自身对齐值为1字节,对于short型为2字节,对于
int,float,double类型,其自身对齐值为4字节。
       2. 结构体的自身对齐值:其成员中自身对齐值最大的那个值。
       3. 指定对齐值: #pragma   pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始
地址的偏移量有两种情况,第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。
       4. 数据成员和结构体的有效对齐值:数据成员(数据类型)和数据结构的自身对齐值和指定对齐值中小的那个值,数据成员对齐了数据结构自然也就对齐了。
      了解上述四个基本概念,我们开始讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0"。而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍)。下面结合VS2005中编译环境的例子进行深入了解:
例子B分析:
struct B
{
char b;
int a;
short c;
};
假设B从地址空间0x0000开始排放。该例中没有显式指定对齐值N,VS2005默认值为4。
    成员变量b自身对齐值是1,比指定或默认指定对齐值4小,故有效对齐值为1,其存放地址0x0000符合0x0000%1=0,满足字节对齐原则。
    成员变量a自身对齐值为4,和指定或默认指定对齐值4相等,故有效对齐值也为4,为了保证字节对齐,成员变量a只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0。
    成员变量c自身对齐值为2,比指定或默认指定对齐值4小,故有效对齐值为2,可顺序存放在0x0008至0x0009两个字节空间中,符合0x0008%2=0。
    至此满足了数据成员的字节对齐,接着看数据结构B的对齐。数据结构B的自身对齐值为其变量中最大对齐值(也就是成员变量b)4,故结构体B的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12。
    之所以在变量C补充2字节,是因为要实现编译器快速有效的存取结构数组,试想如果定义B结构数组,第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果不把结构的大小补充为对齐值(4)的整数倍,那下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了。
例子C分析:


__align(2) struct C
{
char b;
int a;
short c;
};
   同理,例子C中成员变量b自身对齐值为1,指定对齐值为2,故效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0,满足字节对齐原则。
   成员变量a自身对齐值为4,指定对齐值为2,故有效对齐值为2,顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0,满足字节对齐原则。
   成员变量c的自身对齐值为2,与指定对齐值相等,故有效对齐值为2,顺序存放在0x0006、0x0007中,符合 0x0006%2=0,满足字节对齐原则。
   从0x0000到0x00007共八字节存放的是结构体C的变量。结构体C自身对齐值为4,比指定对齐值2大,故C的有效对齐值为2,因8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8,完全满足字节对齐原则。除了指定的对齐值不同能导致数据结构的地址存放不同外, 编译器不同存放结构体方式也可能不同。
四、ARM平台的对齐问题
    在ARM中,有ARM和Thumb两种指令。
    ARM指令:每执行一条指令,PC的值加4个字节(32bits).一次访问4字节内容,该字节的起始地址必须是4字节对齐的位置上,即地址的低两位为bits[0b00],也就是说地址必须是4的倍数。
    Thumb指令:每执行一条指令,PC的值加2个字节(16bits).).一次访问2字节内容,该字节的起始地址必须是2字节对齐的位置上,即地址的低两位为bits[0b0],也就是说地址必须是2的倍数。

     遵循以上方式叫对齐(aligned)方式,不遵守这样方式称为非对齐(unaligned)的存储访问操作。
五、ARM平台字节对齐关键字
  1. __align(num)用于修改最高级别对象的字节边界。
    A、在汇编中使用LDRD或者STRD时,就用到此命令__align(8)进行修饰限制。来保证数据对象是相应对齐。
    B、该修饰对象的命令最大是8个字节限制,可让2字节的对象按4字节对齐,但不能让4字节的对象2字节对齐。
    C、 __align是存储类修改,他只修饰最高级类型对象不能用于结构或者函数对象。   
  2. __packed 是进行一字节对齐。
    A、不能对packed的对象进行对齐;
    B、所有对象的读写访问都进行非对齐访问;
    C、float及包含float的结构联合及未用__packed的对象将不能字节对齐;
    D、__packed对局部整形变量无影响;
    E、强制由unpacked对象向packed对象转化是未定义,整形指针可以合法定义为:
       packed __packed int* p; //__packed int 则没有意义。
  3. __unaligned   用于修饰该变量可按照非对齐访问。
六、如何查找与字节对齐方面的问题,如果出现对齐或者赋值问题首先查看:
  1. 编译器的big little端设置;
  2. 看这种体系本身是否支持非对齐访问;
  3. 如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。
七、结论
   针对于32位处理器对于本地使用的数据结构,为提高内存访问效率,采用四字节对齐方式;同时为了减少内存的开销,合理安排结构成员的位置,减少四字节对齐导致的成员之间的空隙,降低内存开销。
   对于处理器之间的数据结构,需要保证消息的长度不因为在不同编译平台和不同处理器导致消息结构的长度发生变化,使用一字节对齐方式对消息结构进行紧缩;为保证处理器之间的消息的数据结构的内存访问效率,采用字节填充的方式自己对消息中成员进行四字节对齐。
   数据结构的成员位置要兼顾成员之间的关系、数据访问效率和空间利用率。顺序安排的原则是:四字节的放在最前面,两字节的紧接最后一个四字节成员,一字节紧接最后一个两字节成员,填充字节放在最后。举例如下:
typedef struct tag_T_MSG{
long ParaA;
long ParaB;
short ParaC;
char ParaD;
char Pad;
} T_MSG;
<think> 我们正在讨论内存对齐的原理。根据引用[1][2][3],内存对齐是指数据在内存中的存放地址必须是该数据长度的整数倍。这样做的原因主要是硬件访问效率和可移植性。 原理详解: 1. 硬件访问效率:现代处理器通常从内存中按“字”(word)读取数据。例如32位CPU的字长为4字节,它每次从内存读取4字节数据。如果数据没有对齐,比如一个4字节的int变量存储在地址0x00000002处(不是4的倍数),那么处理器需要两次内存访问才能读取完整数据:第一次读取0x00000000-0x00000003,第二次读取0x00000004-0x00000007,然后组合出0x00000002-0x00000005的数据。而如果数据对齐(如存储在0x00000004),则一次读取即可。 2. 可移植性:某些硬件平台不支持非对齐内存访问,或者非对齐访问会导致异常。 结构体内存对齐规则(以32位系统为例): 规则1:结构体每个成员相对于结构体首地址的偏移量都是该成员大小与其对齐数中较小值的整数倍。 规则2:结构体的总大小为结构体最宽基本类型成员大小与对齐数中较小值的整数倍(即最大对齐数的整数倍)。 对齐数:编译器通常有一个默认的对齐数(如4字节),但也可以通过预编译命令调整。每个成员的对齐数取自身大小和默认对齐数的较小值。 举例说明: ```c struct example { char a; // 1字节,对齐数min(1,4)=1,偏移0 int b; // 4字节,对齐数min(4,4)=4,所以偏移量必须是4的倍数,因此偏移从4开始(1-3填充) char c; // 1字节,对齐数1,偏移8 }; // 总大小:0(a)+[填充3字节]+4(b)+8(c)=9,但总大小必须是最大对齐数(4)的整数倍,所以填充到12 ``` 计算偏移量的公式: 设当前偏移量为offset,下一个成员的类型大小为size,对齐数为align(align=min(size, 默认对齐数)),则下一个成员的偏移量为: $$ \text{offset} = (\text{offset} + \text{align} - 1) / \text{align} \times \text{align} $$ 或者用位运算(当align是2的幂时): $$ \text{offset} = (\text{offset} + \text{align} - 1) \& \sim(\text{align}-1) $$ 为什么需要内存对齐?总结: 1. 性能:对齐内存访问只需一次内存操作,非对齐可能需要多次,且降低缓存命中率。 2. 硬件要求:某些架构(如ARM)的早期版本不支持非对齐访问,会引发异常。 3. 可移植性:不同平台对齐要求可能不同,对齐保证代码可移植。 在嵌入式系统中,内存对齐尤为重要,因为嵌入式设备通常资源有限,且可能使用不支持非对齐访问的处理器。同时,对齐也影响DMA传输等操作。 相关问题: 1. 如何修改编译器的对齐设置? 2. 结构体内存对齐如何计算?请举例说明。 3. 在嵌入式编程中,如何手动控制结构体的内存对齐? 4. 非对齐内存访问会带来哪些问题? </think>### 内存对齐原理详解 内存对齐是计算机系统中优化内存访问的关键机制,其核心原理是通过调整数据在内存中的存储位置,使数据的起始地址满足特定要求(通常是数据大小的整数倍)。以下是深度解析: #### 一、硬件访问原理 1. **CPU读取机制** 现代CPU通过数据总线访问内存,每次读取固定长度的数据块(如32位CPU读取4字节)。当数据跨越两个内存块时: - 对齐数据(如4字节int存储在0x00000004):**单次读取完成** $$ \text{访问效率} = 1 \times \text{总线周期} $$ - 非对齐数据(如4字节int存储在0x00000002):**需要两次读取+移位拼接** $$ \text{访问效率} = 2 \times \text{总线周期} + \text{移位操作} $$ 引用[3]的案例验证了该原理[^3] 2. **内存粒度约束** 内存芯片以固定粒度(通常4/8字节)组织存储单元。对齐访问可充分利用内存带宽: ```c // 非对齐访问示例(ARM Cortex-M可能触发HardFault) uint32_t *p = (uint32_t*)(0x1001); // 地址非4字节对齐 uint32_t val = *p; // 潜在总线错误 ``` #### 二、对齐规则体系(以结构体为例) 1. **成员对齐规则** 每个成员相对于结构体首地址的偏移量必须是 **min(成员大小, 默认对齐数)** 的整数倍。32位系统默认对齐数通常为4字节: ```c struct SensorData { uint8_t id; // 大小1 → 偏移0 // 填充3字节(对齐到4) uint32_t value; // 大小4 → 偏移4 uint16_t flag; // 大小2 → 偏移8 }; // 总大小=12(非10!) ``` $$ \text{偏移量} = \left\lceil \frac{\text{当前偏移}}{\text{align}} \right\rceil \times \text{align} $$ 2. **结构体整体对齐** 结构体总大小必须是 **最宽成员大小** 的整数倍,末尾可能填充: ```c struct Example { char a; // 偏移0 // 填充3字节 int b; // 偏移4 short c; // 偏移8 }; // 总大小=12(最宽成员int=4,12是4的倍数) ``` #### 三、对齐的底层数学原理 1. **地址约束方程** 对于类型为T的变量,其地址addr必须满足: $$ \text{addr} \mod \text{alignof}(T) = 0 $$ 其中 $\text{alignof}(T)$ 是编译器决定的对齐系数 2. **填充字节计算** 结构体大小计算通式: $$ \text{size} = \sum (\text{member\_size} + \text{padding}) $$ 填充字节数由递推公式决定: $$ \text{padding}_i = \left( \text{align}_i - (\text{offset}_i \mod \text{align}_i) \right) \mod \text{align}_i $$ #### 四、性能影响实测数据 | 访问类型 | CPU周期数(ARM Cortex-M4) | 功耗增量 | |----------|--------------------------|----------| | 对齐访问 | 1周期 | 基准值 | | 非对齐访问 | 3-5周期 | +15%[^2] | > 引用[2]的硬件验证表明:非对齐访问增加内存总线活动,导致功耗显著上升[^2] #### 五、手动控制对齐方法 1. **编译器指令** ```c #pragma pack(1) // 设置1字节对齐 struct Unaligned { int a; char b; }; // 大小=5(无填充) #pragma pack() // 恢复默认对齐 ``` 2. **属性指定(GCC)** ```c struct __attribute__((packed)) SensorPack { uint32_t val; uint8_t id; }; // 大小=5 ``` 3. **C11标准对齐** ```c #include <stdalign.h> alignas(8) uint64_t data; // 强制8字节对齐 ``` #### 六、嵌入式场景特殊考量 1. **DMA传输要求** DMA控制器通常要求缓冲区地址按总线宽度对齐: ```c // STM32 HAL库示例 uint8_t buffer[128] __attribute__((aligned(4))); // 4字节对齐 HAL_UART_Transmit_DMA(&huart1, buffer, sizeof(buffer)); ``` 2. **位带操作约束** ARM位带区要求访问地址按4字节对齐: ```c #define BITBAND(addr, bit) ((0x42000000 + ((addr)-0x20000000)*32 + (bit)*4)) volatile uint32_t *led = (uint32_t*)BITBAND(GPIOA_ODR, 5); // 对齐地址 ``` --- ### 相关问题 1. 如何验证结构体成员的真实内存偏移量? 2. 内存对齐在RTOS任务栈分配中有何特殊要求? 3. 非对齐访问在哪些嵌入式处理器上会导致硬件异常? 4. 如何计算结构体位域的内存对齐? > 关键提示:引用[1]强调内存对齐适用于所有数据类型,结构体只是最典型的分析案例[^1]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值