《C语言编程魔法书:基于C11标准》——2.4 地址与字节对齐

简介:

本节书摘来自华章计算机《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整除的地址。

image

然而,并不是说要访问多少字节,就必须要保证访问能被多少整除的地址才能满足对齐要求。如果一次访问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。

相关文章
|
1月前
|
存储 编译器 C语言
C语言:数组名作为类型、作为地址、对数组名取地址的区别
在C语言中,数组名可以作为类型、地址和取地址使用。数组名本身代表数组的首地址,作为地址时可以直接使用;作为类型时,用于声明指针或函数参数;取地址时,使用取地址符 (&),得到的是整个数组的地址,类型为指向该类型的指针。
|
1月前
|
NoSQL C语言 索引
十二个C语言新手编程时常犯的错误及解决方式
C语言初学者常遇错误包括语法错误、未初始化变量、数组越界、指针错误、函数声明与定义不匹配、忘记包含头文件、格式化字符串错误、忘记返回值、内存泄漏、逻辑错误、字符串未正确终止及递归无退出条件。解决方法涉及仔细检查代码、初始化变量、确保索引有效、正确使用指针与格式化字符串、包含必要头文件、使用调试工具跟踪逻辑、避免内存泄漏及确保递归有基准情况。利用调试器、编写注释及查阅资料也有助于提高编程效率。避免这些错误可使代码更稳定、高效。
274 12
|
1月前
|
存储 C语言
C语言:设置地址为 0x67a9 的整型变量的值为 0xaa66
在C语言中,可以通过指针操作来实现对特定地址的访问和赋值。要将地址为 0x67a9 的整型变量值设为 0xaa66,可以先定义一个指向该地址的指针,并通过该指针对该内存位置进行赋值操作。需要注意的是,直接操作内存地址具有一定风险,必须确保地址合法且可写。代码示例应考虑字节序及内存对齐问题。
|
2月前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
2月前
|
Linux C语言
C语言 多进程编程(四)定时器信号和子进程退出信号
本文详细介绍了Linux系统中的定时器信号及其相关函数。首先,文章解释了`SIGALRM`信号的作用及应用场景,包括计时器、超时重试和定时任务等。接着介绍了`alarm()`函数,展示了如何设置定时器以及其局限性。随后探讨了`setitimer()`函数,比较了它与`alarm()`的不同之处,包括定时器类型、精度和支持的定时器数量等方面。最后,文章讲解了子进程退出时如何利用`SIGCHLD`信号,提供了示例代码展示如何处理子进程退出信号,避免僵尸进程问题。
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
2月前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
2月前
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
35 3