非对齐地址访问的总结和疑问

什么是非对齐访问

在机器指令层面,当尝试从不能被 N 整除 (addr % N != 0) 的起始地址读取 N 字节的数据时即发生了非对齐内存访问。举例而言,从地址 0x10004 读取 4 字节是可以的,然而从地址 0x10005 读取 4 字节数据将会是一个非对齐内存访问。这里 N 就是数据的自然对齐值 (Natural alignment)。

RISC下使用访存指令读取或写入数据单元时,目标地址必须是所访问之数据单元字节数的整数倍,例如MIPS架构是不支持非对齐访问的。

CISC访存时,如果目标地址不对齐,CPU 不会陷入异常,因为其内部有处理非对齐访问的微程序,例如X86是支持非对齐访问的。

ARMV6首次在硬件层面引入了非对齐访问的支持。更早期的ARM处理器需要在软件层面考虑非对齐访问。

编译器在绝大部分时候都能帮我们搞定这个棘手的事情,满足对齐需求。通常编译器会处理好数据对齐的问题,变量分配时的地址都会是自然对齐的。

一些情况下非对齐访问并不会抛出错误,但是会损失性能。

ARM cortex下非对齐访问的问题和结论

在ARM cortexM0内核的单片机中,在使用指针进行内存访问的时候需要特别留意访问地址的合法性问题,否则会进入HardFault。

结论:

1、u8类型的指针,读写任何地址均是合法的,不会进入HardFault

2、u16类型的指针,读写地址必须是2的倍数,否则会进入HardFault

3、u32类型的指针,读写地址必须是4的倍数,否则会进入HardFault

实验

u8访问内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma pack(4)		//预编译指定对齐是4字节
typedef struct {
u8 len; //自身对齐是1,指定对齐是4,因此有效对齐是1, 0地址存放len
u8 data[12]; //自身对齐是1,指定对齐是4,因此有效对齐是1, 1地址存放data[0],2地址存放data[1]...
} TEST;
#pragma pack()

TEST test;

u8 *p8;
u16 *p16;
u32 *p32;
volatile u32 temp;

void align_test_p8(void)
{
p8 = (u8 *)&test.data[0]; //test.data[0]的地址为奇数

temp = p8[0];//访问非对齐地址
temp = p8[1];//访问2字节对齐的地址
temp = p8[2];//访问非对齐地址
temp = p8[3];//访问4字节对齐地址
}

程序不会进入HardFault

u16访问内存

1
2
3
4
5
6
7
8
9
10
11
p16  = (u16 *)&test.data[0];
temp = p16[0];//访问非2字节对齐的地址,肯定会进入HardFault

p16 = (u16 *)&test.data[1];
temp = p16[0];//访问2字节对齐地址,不会进入HardFault

p16 = (u16 *)&test.data[2];
temp = p16[0];//访问非2字节对齐地址,会进入HardFault

p16 = (u16 *)&test.data[3];
temp = p16[0];//访问2字节对齐地址,不会进入HardFault

u32访问内存

1
2
3
4
5
6
7
8
9
10
11
p32  = (u32 *)&test.data[0];
temp = p32[0];//访问非4字节对齐的地址,进入HardFault

p32 = (u32 *)&test.data[1];
temp = p32[0];//访问2字节对齐地址,但不是4字节对齐地址,会进入HardFault

p32 = (u32 *)&test.data[2];
temp = p32[0];//访问非4字节对齐地址,进入HardFault

p32 = (u32 *)&test.data[3]; //根据结构体字节对齐的规则,test.data[3]的地址肯定是2的倍数,是4的倍数
temp = p32[0]; //访问4字节对齐地址,不进入HardFault

RISC-V MCU非对齐访问的问题的发现

我在RISC-V内核的CH32V307上调试base64编码的程序的时候,总是会在编码的时候进入HardFault循环,经过排查,没有内存溢出和其他的可能性,而且都是在第二次循环进入HardFault,于是考虑到了是不是地址的问题,经过查询,确定了应该是非对齐访问的问题。

我使用的Base64编码程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typedef struct{
uint32_t b0: 6;
uint32_t b1: 6;
uint32_t b2: 6;
uint32_t b3: 6;
uint32_t nop: 8;
} byte3;


int to_base64(const uint8_t* src, int len, uint8_t *buf){
uint8_t* buf_init = buf;
while(len >= 3){
*(buf++) = raw_to_base64[((byte3*)(src))->b0];
*(buf++) = raw_to_base64[((byte3*)(src))->b1];
*(buf++) = raw_to_base64[((byte3*)(src))->b2];
*(buf++) = raw_to_base64[((byte3*)(src))->b3];
src += 3;
len -= 3;
}
switch(len){
case 2:
*(buf++) = raw_to_base64[((byte3*)(src))->b0];
*(buf++) = raw_to_base64[((byte3*)(src))->b1];
*(buf++) = raw_to_base64[((byte3*)(src))->b2 & 0x0f];
src += 2;
break;
case 1:
*(buf++) = raw_to_base64[((byte3*)(src))->b0];
*(buf++) = raw_to_base64[((byte3*)(src))->b1 & 0x03];
src += 1;
break;
}
return buf - buf_init;
}

先看byte3这个结构体,结构体内有5个成员,虽然都是uint32_t类型的,但是后面都有:显示位域,所以前四个成员均占用6个位,最后一个成员占用8个位。也就是这个结构体占用32位(4个字节)。

然后再看 raw_to_base64[((byte3 * )(src))->b0]; 这句话,src本来是u8类型的指针,(byte3 * )(src)将src变成了32位的指针,这为非对齐访问买下了伏笔。

经过Debug,我发现每次程序都会卡在while的第二次循环的 * (buf++) = raw_to_base64[((byte3 * )(src))->b0];这句话。经过Debug:

初始化进入这个函数时,src的地址是0x20005c88,当第二次执行上述语句使,src的地址为0x20005c8b。

我们可以发现0x20005c88 % 4 = 0,0x20005c8b % 4 = 3。显然是发生了非对齐访问造成的硬件中断。

RISC-V手册:

http://riscvbook.com/chinese/RISC-V-Reader-Chinese-v2p1.pdf

中对于非对齐访问的描述:

结论:RISC-V架构支持内存非对齐访问,但是CH32V307不支持内存非对齐访问。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!