字节对齐是怎么回事?

由于C语言是一门接近底层硬件的编程语言,它能直接对存储器地址进行访问(当前大部分处理器在操作系统的应用层所访问到的逻辑地址,而部分嵌入式系统由于不含带存储器管理单元,因此可直接访问物理地址)。

在计算机中,所谓“地址”就是用来标识存储单元的一个编号,就好比我们住房的门牌号。没有门牌号,快递就没法发货;如果门牌号记错了,那么快递就会把货物送错地方。

计算机中的地址也是一样,我们为了要访问存储器中特定单元的一个数据,那么我们首先要获悉该数据所在的地址,然后我们通过这个地址来访问它。

访问存储器,我们也简称为“访存”(Memory Access)。访问地址,我们也简称为“寻址”(Addressing)。

一般计算机架构中都会有地址总线和数据总线,CPU 先通过地址总线发送寻址信号,以指定所要访问存储器单元的地址。然后再通过数据总线向该地址读写数据,这样就完成了一次访存操作。这好比于快递送货,我们先打电话告诉快递通信地址,然后快递员把货送到该地址(写数据),或者去该地址拿货(读数据)送到别家。

一般对于 32 位系统来说,处理器一次可访问 1个(8比特)字节、2 个字节或 4 个字节。当访问单个字节时,对 CPU 不做对齐限制;而当访问多个字节时,比如要访问 N 个字节,由于计算机总线设计等诸多因素,要求 CPU 所访问的起始地址满足 N 个字节的倍数来访问存储器。如果在访问存储器时没有按照特定要求做字节对齐,那么可能会引发访存性能问题,甚至直接导致寻址错误而引发异常(引发异常后通常会导致当前应用意外退出,在嵌入式系统中可能就直接死机或复位)。

下面我们给出一张图来描述,看看一般对 32 位系统而言如何正确地做到访存字节对齐。


图:字节对齐

图中展示了如何正确对齐访问 1 个字节、2 个字节和 4 个字节的情况。图中画出了 6 个存储单元内容,地址低 16 位从 0x1000 到 0x1005,每个存储单元为 1 个字节。
  • 对于仅访问 1 个字节的情况,图中所有地址都能直接访问并满足字节对齐的情况。
  • 对于一次访问 2 个字节的情况,要满足对齐要求,只能访问 0x1000、0x1002、0x1004 等必须要能被2整除的地址。
  • 对于一次访问 4 字节的情况,要满足对齐要求,则只能访问 0x1000、0x1004 等必须要能被 4 整除的地址。

然而,并不是说要访问多少字节,就必须要保证访问能被多少整除的地址才能满足对齐要求。如果一次访问 8 字节,对于 32 位系统而言,通过 32 位通用目的寄存器来读写存储器的话,某些 CPU 会自动将 8 字节的访存分为两次进行操作,每次为 4 字节,因此只要保证 4 字节对齐就能满足对齐要求。这些都根据特定的处理器来做具体处理。

就笔者用过的一些处理器而言,像 x86、ARM 等处理器,当访存不满足对齐要求时并不会引发总线异常,但是访问性能会降低很多。因为原本可一次通信的数据传输可能需要拆分为多次,并且前后还要保证数据的一致性,所以还可能会有锁步之类的操作。而像Blackfin DSP则会直接引发总线异常,导致整个系统的崩溃(如果不对此异常做处理的话)。

另外,像 ARMv5 或更低版本的处理器,在对非对齐的存储器地址进行访问时,CPU 会先自动向下定位到对齐地址,然后通过向右循环移位的方式处理数据,这就使得传输数据并不是原本想一次传输的数据内容,也就是说写入的或读出的数据是失真的。

比如,根据上图所示内容,如果我们要对一款 ARM7EJ-S 处理器(ARMv5TEJ 架构)从地址 0x1002 读 4 字节内容,那么实际获取到的数据为 0x02010403;而在 x86 架构或 ARMv7 架构的处理器下,则能获得 0x06050403。