傻孩子(GorgonMeducer) · 2020年06月16日

漫谈C变量——对齐 (1)

作者:GorgonMeducer 傻孩子
首发:裸机思维
 

谈起变量的访问(Access)就不得不谈到对齐(Alignment)的概念;谈论对齐,离开具体的计算机架构又会显得缺乏支撑,如同谈论空中楼阁一般。今天我们就以笔者熟悉的Cortex-M架构为蓝本,聊一聊变量访问的对齐问题。

1. What ?

  在展开后续讨论之前,我们先来记住一个重要的结论,它是后续所有内容的立论之本:

编译器倾向于根据变量的大小(size)将其放置在与其大小对齐的偶数地址上

怎么理解这句话呢?举个例子,如果我们没有给出特别的指示,编译器会倾向于:

  • 将uint32\_t(4个字节)对齐到4字节地址上,0x0、0x4、0x8、0xC...,也就是我们常说的对齐到字Word Aligned);
  • 将uint16\_t(2个字节)对齐到2字节地址上,0x0、0x2、0x4...也就是我们常说的对齐到半字Half-word Aligned);
  • 将uint64\_t(8个字节)对齐到8个字节上,0x0、0x8... 也就是我们常说的对齐到双字Double Word Aligned);

      • *

    ARM的栈帧(Stack Frame)在Cortex-M3刚推出的时候要求“最好”对齐到双字,后来的Cortex-M4、Cortex-M0/M0+以及Cortex-M7干脆要求“一定要”对齐到双字了。

      • *
  • 将uint8\_t对齐到……好吧,byte没啥好对齐的,它已经是C语言变量的最小单位了——你可以认为对齐到字节Byte Aligned)也就是对齐到任意地址。(爱抬杠的兄弟,不要跟我扯位域,那都是要靠编译器生成“读改写”操作来实现的)

2. Why ?

  那么为什么编译器要做这么看似多此一举的事情呢?因为定义Cortex-M的硬件架构把处理器(Processor)对总线的访问(也就是对Memory的访问)分为两种:对齐访问(Aligned Access)和非对齐访问(Unaligned Access)。

  那么为啥处理器要根据变量的地址把访问活生生的拆成对齐和非对齐两类呢?说的太复杂也没什么卵用,你只要记住:相对仅支持对齐访问的情况,实现非对齐的访问,处理器的需要消耗更多的逻辑,对应到空间上就是需要更多的逻辑门,进而占用更大的面积,最后消耗更多的能量。


属于ARMv6-M架构的处理器只支持对齐访问,例如大家熟悉的M0,M0+以及大家不太熟悉的M1;

属于ARMv7-M架构的处理器不仅两种方式都支持,还为不(pi)同(shi'er)需(tai)求(duo)的客户贴(duo)心(yu)的提供了一个选项——你可以通过某个系统寄存器关闭对非对齐访问的支持。这类处理器有,Cortex-M3/M4/M7...


  一方面,我们常说,物质基础决定上层建筑,为处理器服务的编译器自然是把处理器的脾气摸的清清楚楚;另一方面,为了让自己生成的代码体现最大限度的兼容性,即便是为Cortex-M3/M4这样支持非对齐操作的处理器服务,编译器也会默认按照仅支持对齐操作的情况来生成代码。

  进一步来说,ARM Cortex-M 是一个Load/Store 架构(看到L/S的同学不要激动,这和打游戏的L/S大法半毛钱关系都没有),意思是说,处理器的所有算术逻辑运算都只能使用寄存器页中的内容(R0~R15),并不能直接作用于保存在外部存储器中的变量上——这些变量的内容必须通过Load/Store指令在存储器和寄存器之间进行搬运才行。这就是所谓的Load/Store架构——ALU只能操作寄存器页里面的内容;Load/Store指令在寄存器页和外部存储器之间交换数据——是不是非常简单优雅?

  Cortex-M 处理器支持哪些Load/Store指令呢?(这里,指令的缩写和名称不用记忆,只需要知道支持针对哪些数据类型的Load/Store指令即可)

  • LDR, LoaD Word to Register 读取单个Word到指定寄存器的指令
  • STR, STore Wore to Memory From Register 将指定单个寄存器的值以Word的形式保存到存储器 
  • LDRH / STRH 上述指令的Half-word版本
  • LDRB / STRB 上述指令的Byte版本
  • LDRD / STRD 上述指令的Double-word版本
  • LDM / STM 上述指令的加强版本——可以搬运多个数据!

简单的说,在Cortex-M环境下,所谓非对齐操作就是:

LDR / STR 的目标地址没有对齐到Word

LDRH / STRH 的目标地址没有对齐到Half-Word

LDRD / STRD 的目标地址没有对齐到Double-Word的操作[注1]

LDM / STM 的目标地址没有对齐到Word[注2]


注意:

  1. Cortex-M 在开启对非对齐操作的支持时,仅支持 LDRD / STRD 所有非对齐操作中 “未对齐到Double-Word但是对齐到Word” 的非对齐操作 ——对于其它情形是不支持的——一旦发生,立即出发异常(Exception)。
  2. 前面我们说过,ARMv7-M架构下的处理器支持非对齐操作,但LDM / STM特别任性——“管你支不支持非对齐操作,老子只支持对齐操作”——后面有个陷阱,很多人都栽在它的手上,这里暂时不表。
    • *

3. Then ?

  于是我们就看到了以下的情况:

  已知在一个C文件中,我们定义了四个全局变量:

uint8\_t     a;

uint16\_t   b;

uint8\_t     c;

uint32\_t   d;

你觉得编译器最终生成的变量排布(Layout)会是什么样子?

image.png

根据我们前面所学的知识,为了满足对齐访问的要求,很容易理解上述的排布,是不是觉得很浪费?等一等,编译器从来没有给你保证过,你声明变量的顺序就是它Layout变量的顺序哦,所以,实际上,真正的Layout是下面的形式:

image.png

你看,通过改变变量的顺序,编译器成功的替我们节省了不少的存储器空间。

  同样的情况对结构体来说就没有那么幸运了,假设我们有这样一个类似的结构体:

struct {

  uint8\_t     a;
  uint16\_t   b;
  uint8\_t     c;
  uint32\_t   d;
 } Example;

  由于结构体对Layout的顺序是有要求的,因此上述结构在内存中的Layout是第一种情况——浪费大量的空间;我们只能手工调节结构体成员的顺序才能得到第二种情况的结果。这里要小心哦。


性急的人已经开始考虑结构体的对齐问题了,这个我们下次再讲。结论是很清楚的:结构体无论选择何种对齐方式,都不会导致编译器产生错误的非对齐操作(Unaligned Access)


4. What If ?

“在ARMv6-M架构下以及关闭 非对齐操作支持 的ARMv7-M架构下使用非对齐操作会怎样?”

什么?太绕口,我们换种说法:

“在Cortex-M0/M0+或者关闭了 非对齐操作支持 的Cortex-M3/M4/M7 下使用非对齐操作会怎样?”

只有一个字——死!(哈哈,开个玩笑)真正的答案是:触发BusFault,其中由于ARMv6-M没有BusFault的概念,这类硬件错误最终都会归为HardFault;对于ARMv7-M在BusFault被人为屏蔽的情况下也会归为HardFault

——怎么样,看到这个名字是不是精神一震?HardFault !!!!!!!!!!!!!!!

  你也许觉得很委屈,代码逻辑一点问题都没有,为什么C编译器还会产生会触发非对齐操作的机器码呢?

“你,对就是你!不要一脸无辜了,是你自己干的!”

不相信?放学后不要走,我们下回再说~

\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_正文结束\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_

专栏推荐文章*


如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。
推荐阅读
关注数
1480
内容数
119
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息