ARM处理器的内存对齐处理

发布者:安静的夜晚最新更新时间:2018-10-21 来源: eefocus关键字:ARM处理器  内存对齐 手机看文章 扫描二维码
随时随地手机看文章

对齐问题主要有3点:变量对齐、结构对齐和数据对齐。前两点是编译器决定的变量映射和结构布局。最后一点与CPU的架构(CISC/RISC)有关。  在大多数情况下,对齐是编译器和CPU的事情,和程序员没什么关系。但在某些情况下,程序员又必须考虑对齐问题,否则会有一些麻烦。  
0 约定和预备知识 
0.1 地址边界  
  如果把字节看作小房子,内存就是顺序排列的小房子。每个小房子都有一个顺序编号的门牌号码,例如:0,1,2,...,0xffffffff。我们 把这个门牌号码称作地址。本文将2的整数倍的地址记作2n边界,将4的整数倍的地址记作4n边界,依此类推。显然每个地址都是1n边界,每个4n边界都是 2n边界,每个8n边界都是4n边界。  
所谓“对齐”就是把变量放在什么样的地址边界上,例如:1n边界,2n边界,还是4n边界。  

0.2 变量的分类 
  分类源自角度。有多少角度,就有多少分类。最近经常被迫收听“One World, One dream”,其实在我看来,每个生命都有独一无二的梦想,何况国家。如果狗熊有宗教信仰,它心目中的上帝应该是一只相貌儒雅的狗熊吧。  

0.2.1 基本类型和复合类型  
  从构成看,变量可以分为基本类型的变量和复合类型的变量。基本类型就是语言内部支持的简单类型,例如char, short, int, double等。复合类型由基本类型组成,例如结构。本文将基本类型的变量记作基本变量,将复合类型的变量记作复合变量或结构变量。  
  基本变量的长度目前有1、2、4、8字节。以后可能会有更大的基本变量。嵌入式环境通常不支持浮点,常见的长度是1、2、4字节。  

0.2.2 变量的地址  
  从地址看,变量可以分成有确定地址的变量和没有确定地址的变量。所谓“有确定地址”就是指在程序运行前就有确定的地址。而“没有确定地址”的变量,它们的地址是在运行时确定的。  
  全局变量和静态变量都有确定地址。局部变量和动态分配的变量没有确定地址。本文将有确定地址的变量记作有址变量。  


1 变量对齐 
1.1 没有确定地址的变量  
  局部变量是从堆栈分配的,编译器通常会保证每个局部变量的地址都在4n边界上。  
  动态分配的变量是从堆上分配。堆的实现与标准库和操作系统有关。在一些简单的嵌入式系统中,我们需要自己实现动态内存分配,这时我们要保证每次分配的内存块地址都在4n边界上,以避免后面谈到的数据对齐问题。  

1.2 有确定地址的变量  
  有址变量的地址是在链接时确定的。编译器通常有设置变量对齐方式的编译选项,我们通常使用该选项的默认值。在默认情况下,编译器会按照默认方式对齐放置有址变量。 
  所谓按“按默认方式对齐”,就是将长度为1的基本变量放在1n边界上。将长度为2的基本变量放在2n边界上。将长度为4的基本变量放在4n边界上,依此类推。  
  每个结构变量总是由一个个基本变量构成。结构变量按照该结构中最长的基本变量对齐。如果某个结构基本变量的最大长度是1,编译器就可以把这个结构放在1n边界上。如果某个结构基本变量的最大长度是4,编译器就应该把这个结构放在4n边界上。  
  那么结构中的成员变量又是怎样对齐的?  

1.3 变量对齐会带来什么麻烦?  
  我在变量对齐问题上吃过一次亏,可以作为本节的一个例子。不过要理解这个例子,读者必须知道ARM CPU的一个特点:就是长度为m的基本变量必须放在mn边界上,否则读写时会发生数据访问错误,其中m=2或4。这就是第3节要介绍的数据对齐。  
  事情是这样,我定义了几个缓冲区(大数组),然后动态分配这些内存。我的错误在于将这些数组定义为字节数组。我的分配算法是按块分配,每个数据块的大小都是4的整数倍。读者能猜到错误产生的原因了吗?  
  由于我把缓冲区定义为字节数组,编译器就可以把它们放在1n边界。如果缓冲区的起始地址是奇数地址,从缓冲区分配的内存块的起始地址都是奇数地址。 如果这些内存块被用于需要按2或4字节对齐的变量,读写时就会发生数据访问错误。如果编译器恰好把这些缓冲区放在4n边界上,问题就不会暴露出来。所以前 一次编译可能是好的,但是下一次编译就会发生莫名其妙的错误。调试程序与侦破案件差不多,离犯罪现场越远的凶手就越难发现。在我透过各种表象找到根源之前,吃点苦头是难免的。  
  解决问题的方法很简单,将缓冲区定义为unsigned int(下文记作uint32)的数组,编译器自然会把它们放到4n边界。在嵌入式系统中,我们经常要为任务定义堆栈。这些堆栈通常都是uint32类型的数组。你知道为什么要把它们定义成uint32数组,而不能定义成字节数组了吗? 


2 结构对齐  
2.1 基本长度  
  为了描述方便,我们定义一个基本长度的概念。一个基本变量的基本长度就是它的长度,一个结构变量的基本长度就是结构成员中基本变量的最大长度。前面说过:在默认情况下,结构变量就是按照其基本长度对齐的。 

2.2 对齐  
  在默认情况下,可以认为结构的成员按照默认方式对齐,即长度为m的基本变量放在mn边界上,其中m=1,2,4或8。因为要把成员对齐,结构的各成员间就可能出现填充字节,结构的大小可能大于各成员大小之和。
  例如:  
  typedef struct St1Tag  {
          char ch1;
          int num1;
          short sh1;
          short sh2;
          char ch2;  
  } St1;  
  这个结构的基本长度是4,所以这个结构的变量要放在4n边界。成员num1的基本长度为4,所以也要放在4n边界。成员ch1从4n边界开始,只占1个字节,所以在ch1和num1之间有3个填充字节。  
  在对齐时,编译器会将结构长度取整到基本长度的整数倍。这样以该结构为基本类型的数组既可以连续排列,每个元素又可以对齐放置。所以,sizeof(St1)的值是16,在St1的最后一个成员ch2后面还有3个填充字节。 

2.3 紧缩  
  各编译器都支持结构的紧缩,即连续排列结构的各成员变量,各成员变量之间没有任何填充字节。这时,结构的大小等于各成员变量大小的和。紧缩结构的变量可以放在1n边界,即任意地址边界。  
  在gcc中可以这样定义紧缩结构:  
  typedef struct St2Tag  {
          St1 st1;
          char ch2;  
  }  __attribute__ ((packed)) St2;  

  armcc是这样的:  
  typedef __packed struct St2Tag  {
          St1 st1;
          char ch2;  
  } St2;  

  VC的写法最麻烦:  
#pragma pack(1)  
  typedef struct St2Tag  {
          St1 st1;
          char ch2;  
  } St2;  
#pragma pack()  

  如果要同时支持gcc、armcc、VC平台,可以把代码写成这样:  
#ifdef __GNUC__  
#define GNUC_PACKED    __attribute__((packed))  
#else  
#define GNUC_PACKED  
#endif    

#ifdef __arm  
#define ARM_PACKED     __packed  
#else  
#define ARM_PACKED  
#endif    

#ifdef WIN32  
#pragma pack(1)  
#endif  
typedef ARM_PACKED struct St2Tag  {
          St1 st1;
          char ch2;  
}  GNUC_PACKED St2;  
#ifdef WIN32  
#pragma pack()  
#endif  

其中:__GNUC__是gcc的预定义宏,__arm__是ARM编译器的预定义宏(__arm和__arm__都可以),可以用它们识别当前的编译器。  

2.4 全局设置  
  在VC中,有的程序员习惯设置整个工程的struct member alignment,这对应于命令行选项“/Zpi”,其中i=1,2,4,8,16。如果将这个值设为1,工程中所有结构都是紧缩排列。紧缩排列会增大代码量,降低结构访问效率。我们应该仅在必要的时候使用紧缩结构。 
   “/Zp1”是紧缩排列,那么“/Zp2”,“/Zp4”等选项是怎样排列的呢?
  设选项“/Zpi”中设定的长度是i,设某个结构成员的基本长度是m,则该结构成员按照m和i中较小的值对齐。例如:如果我们设置了“/Zp2”,则基本长度不大于2的成员按照基本长度对齐,基本长度大于2的成员按照2对齐。
  其实,我们不应该使用“/Zp2”这么奇怪的选项,除非有非如此不可的理由。  

2.5 紧缩结构的用途  
  其实最常用的结构对齐选项就是:默认对齐和紧缩。在两个程序,或者两个平台之间传递数据时,我们通常会将数据结构设置为紧缩的。这样不仅可以减小通信量,还可以避免对齐带来的麻烦。假设甲乙双方进行跨平台通信,甲方使用了“/Zp2”这么奇怪的对齐选项,而乙方的编译器不支持这种对齐方式,那么乙方就可以理解什么叫欲哭无泪了。  
  当我们需要一个字节一个字节访问结构数据时,我们通常都会希望结构是紧缩的,这样就不必考虑哪个字节是填充字节了。我们把数据保存到非易失设备时,通常也会采用紧缩结构,既减小存储量,也方便其它程序读出。

2.6 细节  
  最后记录一个小细节。gcc编译器和VC编译器都支持在紧缩结构中包含非紧缩结构,例如前面例子中的St2可以包含非紧缩的St1。但对于ARM编译器而言,紧缩结构包含的其它结构必须是紧缩的。如果紧缩的St2包含了非紧缩的St1,编译时就会报错:
  error:  #1031efinition of "struct St1Tag" in packed "struct St1T2g"must be __packed  


3 数据对齐
  3.1 CISC和RISC
  CPU从指令集的特点上可以分为两类:CISC和RISC。CISC和RISC分别是复杂指令集计算机(Complex Instruction Set Computer)和精简指令集计算机(ReducedInstruction Set Computer)的缩写。
  CPU的工作可以看作以下步骤的反复循环:
  step 1: 取指令
  step 2: 取数据
  step 3: 执行指令
  step 4: 输出结果
CISC CPU支持很多寻址模式,因此取数据的时间是不确定的。RISC CPU的最大特点是简化了指令的寻址模式,除了Load/Store指令外,其它指令都采用寄存器寻址,即从寄存器读写数据。这种设计使取数据的时间相对稳定,可以简化指令流水线的设计。
  一般而言,RISC架构可以降低CPU的复杂性以及允许在同样的工艺水平下生产出功能更强大的CPU,但对于编译器的设计有更高的要求。

  3.2 对齐数据访问
  RISC CPU的Load/Store指令要求数据是对齐的。长度为4的数据应放在4n边界上,长度为2的数据应放在2n边界上。以ARM CPU的Load为例:  
  LDR       R5,[R4] 
  LDRSH   R7,[R6]
  LDRB     R9,[R8] 
  LDR、LDRSH、LDRB分别从存储器读取一个字、半字和字节,放到指定寄存器。例如“LDR R5,[R4]”就是从R4指向的存储单元中读一个字(长度为4),放到R5中。 LDR要求数据地址在4n边界上,否则就会发生错误。LDRSH要求数据地址在2n边界上,否则就会发生错误。  
  发生什么错误呢?这与具体的CPU有关,在ARM7TDMI上,非对齐访问会导致程序跳到数据访问错误的处理向量,即地址0x00000010处。在ARM920T上,LDR指令可能返回错误的数据。  CISC的CPU支持非对齐的数据读取。  

3.3 例子  
  我们来看一个例子:  
  // 例子1 
  void test(void) {
         char a[] = {1,2,3,4,5};
         int *pi, i; 

         printf("&a[1]=%p\n", &a[1]);
         pi = (int *)&a[1];
         i = *pi;
         printf("0xx\n", i);
         *pi = 0x11223344;
         for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
         {
              printf("0xx ", a);  
         }

关键是这句:          i = *pi; 和 *pi = 0x11223344;  我们知道地址pi指向的4个字节依次是:0x02,0x03,0x04,0x05。在小尾的CPU上,我们期待的输出是0x05040302和0x01 0x44 0x33 0x22 0x11。让我们看看这段代码在不同平台的运行效果。

3.3.1 PC/Windows  
  输出结果是: 
   &a[1]=0x0012FF25 0x05040302 
   0x01 0x44 0x33 0x22 0x11
   符合我们的预期,也说明PC的CPU支持非对齐的数据读取。 

3.3.2 PC/Linux 
输出结果是:
  &a[1]=0xbfa0c36c 0x05040302  
  0x01 0x44 0x33 0x22 0x11
值得注意的是gcc编译器将局部变量a放在了1n边界(0xbfa0c36b)上。我们希望pi是一个奇数地址,将测试代码修改为:
  // 例子2 
  void test1(void) {
         int a[] = {0x04030201, 0x08070605};
         int *pi, i;

         pi = (int *)&((char *)&a)[1];
         printf("pi=%p ", pi);
         i = *pi;
         printf("x\n", i); 

         *pi = 0x11223344;
         for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
         {
              printf("0xx ", a);  
         }

输出结果是:  pi=0xbfe87fe9 0x05040302 0x22334401 0x08070611符合我们的预期。数据对齐是CPU的问题,和编译器、操作系统没有关系。  

3.3.3 ARM920T/Linux  
输出结果是:  &a[1]=0xbec49e55 0x01040302 0x44 0x33 0x22 0x11 0x05 考虑到小尾,CPU实际读到的4个字节依次是0x02,0x03,0x04,0x01。这个结果不是我们所预期的,CPU出错了。 
为什么呢? 
在ARM中,有ARM和Thumb两种指令。  
ARM指令:每执行一条指令,PC的值加4个字节(32bits).一次访问4字节内容,该字节的起始地址必须是4字节对齐的位置上,即地址的低两位为bits[0b00],也就是说地址必须是4的倍数。  
Thumb指令:每执行一条指令,PC的值加2个字节(16bits).).一次访问2字节内容,该字节的起始地址必须是2字节对齐的位置上,即地址的低两位为bits[0b0],也就是说地址必须是2的倍数。
目前,经过测试发现写内存操作时,会按照地址对齐访问(如上面的*pi = 0x11223344;实际会向((uintptr_t)(pi))& ~(4-1)对齐访问);而读操作没有发现规律。
但是有没有方法进行非对齐访问呢?为此,ARM编译器提供了__packed关键字,__packed是进行一字节对齐,
  void test2(void) {
         char a[] = {1,2,3,4,5};
         __packed int *pi, i; 

         printf("&a[1]=%p\n", &a[1]);
         pi = (int *)&a[1];
         i = *pi;
         printf("0xx\n", i);
         *pi = 0x11223344;
         for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
         {
              printf("0xx ", a);  
         }



输出结果是: &a[1]=0xbec49e55 0x01040302 0x01 0x44 0x33 0x22 0x11

3.3.4 ARM7TDMI 
程序在执行:          i = *pi; 时直接跳回Data Abort的处理向量,即地址0x00000010。  

3.4 对策  
在读取紧缩结构或结构的紧缩成员时,编译器会自动产生按字节读取的代码。我们只要在做强制指针转换时细心一些就可以了。我们不应该将指向窄数据的指针强制转换成指向宽数据的指针。在可能发生数据对齐问题的地方,按字节读取数据。

关键字:ARM处理器  内存对齐 引用地址:ARM处理器的内存对齐处理

上一篇:用STM32F407玩控制—系统组成
下一篇:ARM-Linux启动方式

推荐阅读最新更新时间:2024-03-16 16:17

ARM处理器内存对齐问题
介绍 可以对齐或不对齐的内存访问。对齐的内存访问发生时的数据都位于其自然大小边界。例如,如果该数据类型的大小是4个字节,那么它属于被4整除的内存地址是位于其自然大小边界。未对齐的内存访问发生在所有其他情况下(在上面的例子中,内存地址时,是不能被4整除)。 ARM处理器的设计有效地访问对齐的数据。在ARM处理器上试图访问未对齐的数据会导致不正确的数据或显着的性能损失(这些不同的症状会在稍后讨论)。与此相反,大多数CISC型处理器(即x86)的访问未对齐的数据是无害的。 这份文件将讨论一些比较常见的方式,一个应用程序可能会执行未对齐的内存访问,并提供一些建议的解决方案,以避免这些问题, 。 症状 上述问题,适用于所有ARM架
[单片机]
基于ARM处理器的HDLC通信的DMA实现
    摘要: 以ARM7TDMI为内核的Samsung公司S3C4510B网络微控制器(Networking MCU)为基础,重点论述如何通过DMA(直接内存访问)方式实现HDLC通信。对软件设计中缓冲描述符、DMA状态配置和控制、ISR服务程序设计以及相关的硬件配置进行详细的描述,并讨论如何编写在操作系统下的驱动程序。     关键词: HDLC 缓冲描述符 循环链表 中断服务程序 DMA 目前在嵌入式产品开发设计中,通常是在OS(Operating System)厂商提供的BSP基础上进行开发工作;对于底层硬件的操作,程序设计人员很少关注或只是少量的修改。实际上很多产品,我们完全可以
[应用]
基于ARM高速闪存MCU应对广泛嵌入式需求
  由于采用了ARM7TDMI-S内核,LPC2000系列MCU工作频率达60MHz,与其他8-bit产品相比具有更强的功能延展性。同时它借助片上存储器加 模块实现了“零等待访问”高速闪存功能,提高了指令执行的效率。   此外,LPC2000的外设接口非常丰富,包括UART、SPI、I2C、CAN、ADC、 PWM、RTC等。LPC2000系列MCU应用领域非常广泛,从网络通信、 马达控制,到汽车和消费电子都适合于涉足。   嵌入式系统是面向用户、面向产品、面向应用的,它是将先进计算机技术、半导体技术和电子技术以及各行业的具体应用相结合的产物,因此它是一个高度密集、不断创新的知识集成系统。作为嵌入式系统,它必须能够根据应用的需
[应用]
运用ARM处理器软件工具出错,会带来什么样的结果?
越来越多程序设计人员在设计安全相关应用程序时采用ARM处理器,范围遍及医疗、运输、航空电子与工业领域。因此,透过这些处理器所执行的软件也受到更为严格的检查,因为任何一个小错误都有可能导致严重后果。 为了避免导致这样的后果,包括IEC 61508,还有最近才通过的汽车业ISO 26262等安全标准应运而生,以确保开发人员与客户在软件方面能符合业界最先进的最佳实务准则。 即便如此,要决定标准当中有哪些元素适用,哪些不适用,接下来还要确保整体设计符合标准,整个过程非常耗时以致于令人却步。由于消费类器件的设计周期极短且逐渐与汽车安全系统整合,开发人员也因此面临更大的时间压力,必须在日益紧迫的设计周期期限前完成设计改动。 所幸针
[单片机]
运用<font color='red'>ARM处理器</font>软件工具出错,会带来什么样的结果?
基于ARM处理器的MVB 2类设备研究
1 引 言 列车需要传输大量的设备控制和旅客服务信息,随着这些信息的数量和种类不断地增长,迫切需要一种大容量,高速度的信息传输系统。为此,国际电工委员会(IEC)制定了一项用于规范车载设备数据通信的标准——IEC61375(列车通信网标准),即TCN标准,该标准于1999年6月成为国际标准。目前国际上主要的TCN产品供应商是德国西门子和瑞士Duagon公司,国内的株洲电力机车研究所和大连北车集团电力牵引研究所等单位进行了大量的TCN相关研究工作并取得了丰硕的科研成果。 TCN标准推荐在机车上层使用绞线式列车总线WTB,在下层使用多功能车辆总线MVB。MVB总线和机车中的各种电气设备相连,这些设备按性能可以分为5类,其中二类设备
[工业控制]
报告称英特尔及部分Arm处理器有新的Spectre漏洞
VUSec安全研究小组和英特尔周二披露了一个名为分支历史注入(branch history injection BHI)的Spectre级漏洞。这个新的漏洞影响到近年来发布的所有英特尔处理器,包括最新的Alder Lake CPU,以及一些特定的Arm内核。相比之下,AMD的芯片似乎不受影响。有意思的是,Intel在2021年产品安全报告中直言不讳的表示:2021年Intel处理器共有16个Bug报告,而AMD的Bug报告则有31个,这表示Intel在Bug方面处于领先地位。 BHI是一个Poc验证代码攻击,影响到容易受到Spectre V2漏洞影响的CPU,但已经有各种缓解措施。据Phoronix报道,这种新的漏洞绕过了英特
[嵌入式]
报告称英特尔及部分<font color='red'>Arm处理器</font>有新的Spectre漏洞
Linux 3.11操作系统支持更多ARM处理器
    随着ARM处理器的盛行,Linux系统内核也在逐步加大对ARM处理器的支持力度,Linux 3.11版本中除了Xen、KVM虚拟化将支持64-bit ARM平台之外,还有其它不少细节上的完善。   Linux 3.11操作系统将正式支持瑞芯微RK3xxx系列,包括最新的RK3168、RK3188等等,国产的ARM芯片正在得到越来越广泛的关注。   德州仪器基于Cortex-A15架构的KeyStone系列也已经进入支持列表。   PCI总线方面,开发人员正在尝试使其不再与特定的SoC代码相关联,而是让ARM PCI主控制器驱动直接成为可载入的内核模块。   此外还有其它大量关于ARM平台驱动、跨平台的相关支持,都在陆续添加中
[手机便携]
ARM处理器ARM处理器工作模式
简介:ARM微处理器的工作状态一般有两种,并可在两种状态之间切换: 第一种为ARM状态,此时处理器执行32位的字对齐的ARM指令; 第二种为Thumb状态,此时处理器执行16位的、半字对齐的Thumb指令。 ARM处理器状态 ARM微处理器的工作状态一般有两种,并可在两种状态之间切换: 第一种为ARM状态,此时处理器执行32位的字对齐的ARM指令; 第二种为Thumb状态,此时处理器执行16位的、半字对齐的Thumb指令。 在程序的执行过程中,微处理器可以随时在两种工作状态之间切换,并且,处理器工作状态的转变并不影响处理器的工作模式和相应寄存器中的内容。但ARM微处理器在开始执行代码时,应该处于ARM状态。 ARM处理
[单片机]
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
热门活动
换一批
更多
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

更多精选电路图
换一换 更多 相关热搜器件
更多每日新闻
随便看看
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved