转载自ARM非对齐访问和Alignment Fault - 者旨於陽 - 博客园 (cnblogs.com)
1、指令对齐
A64指令必须word对齐。尝试在非对齐地址取值会触发PC alignment fault。
1.1、PC alignment checking
PC(Program Counter)寄存器用来存放下一条执行指令地址,对于AArch64架构,如果PC寄存器低2位不为0,则触发PC alignment fault。
类似于Instruction Aborts异常,将非对齐地址加载到PC寄存器并不会直接触发PC alignment fault,只有当CPU尝试从该地址取指令时才会触发异常。
当取指异常发生在EL0时,CPU切换到EL1。当取指异常发生时,HCR_EL2.TGE位为1且EL2使能时,CPU切换到EL2。当取指异常发生时,CPU处于其他模式,CPU运行模式不切换。
当发生PC alignment fault时,ESR(Exception Syndrome Register)的EC域置0x22,表明异常级别。
伪代码AArch64.CheckPCAlignment()执行PC alignment check,伪代码AArch64.PCAlignmentFault()则主动触发异常。
2、数据访问对齐
2.1、数据非对齐访问
如果被访问的内存地址不按照被访问的数据类型的位宽对齐,称为非对齐访问。比如int型占4个字节,则访问int型数据的内存地址需要按照4字节对齐。
2.2、硬件非对齐访问支持
MIPS架构不支持非对齐访问。
X86架构支持非对齐访问,其实现机制是将非对齐访问指令拆分成多条指令执行,结合拼接(或者拆分)指令获取数据。缺点是牺牲性能。
ARMv5架构不支持非对齐访问。
ARMv6架构开始参考X86架构实现方式支持非对齐访问,但是是部分内存访问指令支持。
ARMv7-M架构中CCR.UNALIGN_TRP位控制是否使能对齐检查(Alignment Check),ARMv7-A、ARMv7-R、ARMv8架构中SCTLR.A位控制是否使能对齐检查,默认情况下不使能对齐检查。
如果使能对齐检查,则任何指令的非对齐访问均触发非对齐异常。
对于A32/T32代码,如果不使能对齐检查,则大部分指令的非对齐访问由CPU处理,如LDR, LDRH, STR, STRH,LDRSH, LDRT, STRT,LDRSHT,LDRHT,STRHT,TBH。其他的数据访问指令的非对齐访问都会触发非对齐异常,如STRD,LDRD。
对于A64代码,如果不使能对齐检查,则所有的load和store指令的非对齐访问均由CPU处理,但是exclusive load/store, load acquire和store release指令的非对齐访问则会触发非对齐异常,包括LDAXR, LDAXRB, LDAXRH, LDAXP, STLXR, STLXRB, STLXRH, STLXP。
对于SIMD指令,一般都是强制64字节或者128字节对齐,如果发生非对齐访问,则触发非对齐访问异常。
对于任何内存访问,如果使用SP指针作为基地址,就必须quadword对齐,否则触发栈对齐异常。
2.3、软件非对齐访问支持
部分MIPS架构,通过在VxWorks内核中对非对齐访问异常进行处理,通过多次访存操作和拼接操作来实现非对齐访问,代价是牺牲性能。ARM架构内核中也有类似的处理方式,可以通过相关的配置来控制其处理方式,详细内容查看第四节。
2.4、编译器非对齐访问支持
2.4.1、GCC编译器
使能非对齐访问:-munaligned-access
禁止非对齐访问:-mno-unaligned-access
默认情况下,ARM都是aligned-access的,如果代码中使用__attribute__((packed))定义的结构体,会出现结构体成员是非对齐的,此时如果没有使能非对齐访问会导致触发abort异常。
2.4.2、编译器优化
编译器一般支持对非对齐访问代码的优化,即在编译阶段通过多次内存访问操作拆分和拼接从而规避非对齐访问。
GCC编译选项-Ox用来指定代码优化级别,-O0表示不优化,其他优化级别下会对非对齐访问代码进行优化,比如将LDRD指令的非对齐访问拆分成多条LDR指令。
3、栈对齐
3.1、SP alignment checking
当SP(Stack Pointer)寄存器的低4位不为0时,如果当前指令使用SP作为基地址,则触发栈非对齐异常。
伪代码AArch64.CheckSPAlignment()执行stack pointer check,AArch64.SPAlignmentFault()则触发栈对齐异常。
类似于Data Aborts异常,将非对齐值加载到SP寄存器并不会直接触发异常,只有当尝试从非对齐地址取数据时才会触发异常。
4、ARM Linux内核非对齐访问
4.1、/proc/cpu/alignment
Linux内核只针对arm架构实现了非对齐访问处理机制,主要是针对LDR, STR, LDRD, LDRD, STRD, LDM, STM, LDRH, STRH指令的非对齐访问进行处理。对于arm64架构,因为ARMv8架构CPU可以处理所有LDR/STR类内存访问指令的非对齐访问,因此没有实现该机制。这样就会导致如果在arm64架构上运行64位Linux内核,而用户态为32位应用程序时,如果发生非对齐访问,则会触发异常。
值 | 宏定义 | 说明 |
0 | UM_WARM | 内核打印Alignment Trap警告,打印进程名,pid,pc,指令,地址,和异常错误码等 |
1 | UM_FIXUP | 内核尝试修复用户代码非对齐访问 |
2 | UM_SIGNAL | 发生非对齐访问时,内核发送SIGBUS信号量给对应进程 |
ARMv5架构,该节点默认值为0,即忽略非对齐访问。
ARMv6及以上架构,CPU本身部分支持非对齐访问,因此基本不关心该节点值,但是对于LDM, STM, LDRD和STRD等复合指令进行非对齐访问时,仍然需要软件处理,此时,可以将该节点值设置为1,即fix up模式。
上述三种值是位映射,也可以是组合值。
4.2、非对齐访问异常处理流程
4.2.1、ARM架构
Linux内核初始化时,通过fs_initcall(alignment_init)加载非对齐访问处理模块。alignment_init()函数中首先创建/proc/cpu/alignment节点,然后通过hook_fault_code(FAULT_CODE_ALIGNMENT, do_alignment, SIGBUS, BUS_ADRALN, “alignment exception”)挂do_alignment钩子函数。当发生非对齐访问异常时,进入do_alignment中处理异常,根据获取的/proc/cpu/alignment节点值,分别进入不同的处理分支。
该部分代码位于arch/arm/mm/alignment.c中。
4.2.2、ARM64架构
Linux内核在fault_info[]中注册钩子函数,当发生非对齐访问(BUS_ADRALN)时,进入do_bad()函数,给对应进程发送SIGBUS信号量。
该部分代码位于arch/arm64/mm/fault.c中。
5、什么情况下容易发生非对齐访问?
出现alignment fault问题,通常是用户编写的代码导致。估计很多程序猿在编写代码(特别是c/c++代码)时,从未考虑过这样的问题,那是因为多数可能都在X86架构下的进行代码开发,而且没有考虑过代码的移植性,如前面所说X86硬件会自动处理非对齐问题,用户感知不到,但这种情况下,由此带来的性能损耗,用户可能也关注不到了。另一方面,部分情况下,编译器也会自动做padding处理(如对结构体的自动填充对齐),这也进一步让程序猿们减少了对alignment fault的关注。
最常见的可能导致alignment fault的代码编写方式如:
1) 指针转换
将低位宽类型的指针转换为高位宽类型的指针,如:将char * 转为int *,或将void *转为结构体指针。这类操作是导致alignment fault的最主要的来源,在分析定位问题时,需要特别关注。对于出现异常却又必须这样使用的场景,对这类转换后的指针进行访问时,如果不能确认其对应的地址是对齐的,则应该使用memcpy访问(memcpy方式不存在对齐问题)。另外,建议转换后立即使用,不要将其传递到其他函数和模块,防止扩展,带来潜在的问题。
2) 使用packed属性或者编译选项
这样的操作会关闭编译器的自动填充功能,从而使结构体中各个字段紧凑排列,如果排列时未处理好对齐,则可能导致alignment fault。一些场景下(内核中也较常见)确实需要用户自行紧凑排列结构体,可节省空间(在内存资源稀缺的场景下,很有用),此时需要特别关注对齐问题,建议通过填充的方法尽量对齐,如此可能会导致空间浪费,但是会提升访问性能,典型的“以空间换时间”的思路。如果对空间有强烈要求,而可以接受性能损失,也可以不考虑对齐,不做padding,但在访问这些结构体的数据时,需要全部使用memcpy的方式。
6、测试代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void sigbus_handler(int sno)
{
printf("signal %d captured\n", sno);
exit(1);
}
int main(int argc, char *argv[])
{
char intarray[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
signal(SIGBUS, sigbus_handler);
printf("int1 = 0x%08x, int2 = 0x%08x, int3 = 0x%08x, int4 = 0x%08x\n",
*((int *)(intarray + 1)),
*((int *)(intarray + 2)),
*((int *)(intarray + 3)),
*((int *)(intarray + 4)));
return 0;
}