本节书摘来自华章计算机《C语言编程魔法书:基于C11标准》一书中的第2章,第2.4节,作者: 陈轶 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.4 地址与字节对齐
由于C语言是一门接近底层硬件的编程语言,它能直接对存储器地址进行访问(当前大部分处理器在操作系统的应用层所访问到的逻辑地址,而部分嵌入式系统由于不含带存储器管理单元,因此可直接访问物理地址)。在计算机中,所谓“地址”就是用来标识存储单元的一个编号,就好比我们住房的门牌号。没有门牌号,快递就没法发货;如果门牌号记错了,那么快递就会把货物送错地方。计算机中的地址也是一样,我们为了要访问存储器中特定单元的一个数据,那么我们首先要获悉该数据所在的地址,然后我们通过这个地址来访问它。访问存储器,我们也简称为“访存”(Memory Access)。访问地址,我们也简称为“寻址”(Addressing)。我们在图2-1中也看到,一般计算机架构中都会有地址总线和数据总线。CPU先通过地址总线发送寻址信号,以指定所要访问存储器单元的地址。然后再通过数据总线向该地址读写数据,这样就完成了一次访存操作。这好比于快递送货,我们先打电话告诉快递通信地址,然后快递员把货送到该地址(写数据),或者去该地址拿货(读数据)送到别家。
一般对于32位系统来说,处理器一次可访问1个(8比特)字节、2个字节或4个字节。当访问单个字节时,对CPU不做对齐限制;而当访问多个字节时,比如要访问N个字节,由于计算机总线设计等诸多因素,要求CPU所访问的起始地址满足N个字节的倍数来访问存储器。如果在访问存储器时没有按照特定要求做字节对齐,那么可能会引发访存性能问题,甚至直接导致寻址错误而引发异常(引发异常后通常会导致当前应用意外退出,在嵌入式系统中可能就直接死机或复位)。
下面我们给出一张图2-8来描述,看看一般对32位系统而言如何正确地做到访存字节对齐。
图2-8展示了如何正确对齐访问1个字节、2个字节和4个字节的情况。图中画出了6个存储单元内容,地址低16位从0x1000到0x1005,每个存储单元为1个字节。对于仅访问1个字节的情况,图2-8所有地址都能直接访问并满足字节对齐的情况。对于一次访问2个字节的情况,要满足对齐要求,只能访问0x1000、0x1002、0x1004等必须要能被2整除的地址。对于一次访问4字节的情况,要满足对齐要求,则只能访问0x1000、0x1004等必须要能被4整除的地址。
然而,并不是说要访问多少字节,就必须要保证访问能被多少整除的地址才能满足对齐要求。如果一次访问8字节,对于32位系统而言,通过32位通用目的寄存器来读写存储器的话,某些CPU会自动将8字节的访存分为两次进行操作,每次为4字节,因此只要保证4字节对齐就能满足对齐要求。这些都根据特定的处理器来做具体处理。
就笔者用过的一些处理器而言,像x86、ARM等处理器,当访存不满足对齐要求时并不会引发总线异常,但是访问性能会降低很多。因为原本可一次通信的数据传输可能需要拆分为多次,并且前后还要保证数据的一致性,所以还可能会有锁步之类的操作。而像Blackf?in DSP则会直接引发总线异常,导致整个系统的崩溃(如果不对此异常做处理的话)。另外,像ARMv5或更低版本的处理器,在对非对齐的存储器地址进行访问时,CPU会先自动向下定位到对齐地址,然后通过向右循环移位的方式处理数据,这就使得传输数据并不是原本想一次传输的数据内容,也就是说写入的或读出的数据是失真的。比如,根据图2-8所示内容,如果我们要对一款ARM7EJ-S处理器(ARMv5TEJ架构)从地址0x1002读4字节内容,那么实际获取到的数据为0x02010403;而在x86架构或ARMv7架构的处理器下,则能获得0x06050403。