80x86汇编与C语言-4(续)

官方信息技术老师·19 页·深入(追求细节与边界)·0 次浏览·2 天前
结构体内存对齐字段偏移汇编视角

80x86汇编与C语言:结构的存储

看懂 struct 字段偏移、对齐填充与汇编级访问方式

按 空格/→ 演示下一步

1 / 19 页

全部页面点击任意一页,跳回舞台从这页播放

结构体内存对齐字段偏移汇编视角

80x86汇编与C语言:结构的存储

看懂 struct 字段偏移、对齐填充与汇编级访问方式

1第 1 页 · 80x86汇编与C语言:结构的存储

为什么需要结构体

上一页我们看到结构体在内存里连续排列、各成员有偏移。那为什么要这样'绑'在一起?回到一个没有结构体的世界。

现实对象有多属性
一个学生有学号、姓名、成绩等不同类型的数据
散装变量的痛点
用多个独立变量管理,参数列表长、易传错、关系不明
结构体:逻辑聚合
把相关数据捆绑为一个整体,用一个名字引用
整体操作更高效
赋值、传参、拷贝按整体进行,连续布局利于批量访问
学生档案袋对应 →结构体

袋子里装不同类型的纸(学号、成绩),作为一个整体存放和传递

2第 2 页 · 为什么需要结构体

结构体在内存中的样子

上一节我们说结构体把多个变量打包存放——但打包不是简单堆叠,编译器要按每个字段的'脾气'摆位置。下面看一个具体例子,它在内存里到底长什么样。

声明顺序即内存顺序
字段按声明先后依次排布,不能被编译器重排
字段对齐边界
每个字段必须从自身对齐值的整数倍地址开始
字段间填充
前一个字段留下的空位用填充字节补齐
整体大小补齐
整个结构体大小补齐到最宽字段对齐值的倍数
整理行李箱对应 →结构体字段对齐

大件放定位置,小件紧挨填缝,整箱尺寸按最大件对齐

addr(fi)0(modalignof(fi))\text{addr}(f_i) \equiv 0 \pmod{\text{alignof}(f_i)}
3第 3 页 · 结构体在内存中的样子

结构体内存布局全景图

自左向右是低地址到高地址;蓝格是字段,橙格是编译器为对齐插入的填充。

图解渲染中…
a2int需4字节对齐,前面补3字节a5short需2字节对齐,故补1字节a7总大小须为最大字段对齐(4)的倍数
4第 4 页 · 结构体内存布局全景图

字段顺序与内存排列

前页看到结构体在内存中是一块连续的字节,但悬着一个问题:字段到底是怎么排进去的?是按我写的顺序吗?

按声明顺序排列
编译器从上到下读你写的字段,依次放入内存
顺序就是内存顺序
第一个字段落在最低地址,最后一个落在最高地址
调换声明即重排
把两个字段位置互换,内存里它们的相对位置也跟着换
顺序影响总大小
不同顺序可能产生不同填充,sizeof 结果会变
电影院排队入场对应 →字段按声明顺序入座

先到的坐前排,编译器不重排,内存顺序就是书写顺序

5第 5 页 · 字段顺序与内存排列

什么是内存对齐

前页看到字段顺序会影响结构体轮廓;那些空隙不是随意出现的,而是数据必须从合法边界开始留下的代价。

自然对齐
普通数据成员从满足自身对齐要求的地址开始,结构体等复合类型通常受最严格成员影响
访问代价
未对齐访问可能拆成多次传输,跨页时甚至失败;80x86普通访问常能容忍,但性能和指令类型仍受限
填充规则
为满足偏移边界,编译器在字段之间或尾部插入填充字节(padding);这些字节不是数据内容
整体取整
结构体总大小(sizeof)通常向最大对齐值取整;数组中的下一个元素也必须从合法边界开始
实现约定
C标准不规定具体偏移和填充;编译器、二进制接口(ABI)或打包选项变化时,布局结果可能不同
停车场按车位线停车对应 →内存对齐

数据像车辆,地址是停入位置;车位线间距对应类型对齐,缝隙对应填充字节

offset(m)0(modalign(m)),sizeof(T)0(modalignmax)\operatorname{offset}(m)\equiv 0\pmod{\operatorname{align}(m)},\qquad \operatorname{sizeof}(T)\equiv 0\pmod{\operatorname{align}_{\max}}
6第 6 页 · 什么是内存对齐

对齐的基本规则

上一节我们明白了为什么要对齐。这一节落到具体规则——不同字节数的数据,分别要求地址是几的倍数。记住这几条,是读懂 struct 内存布局的钥匙。

1字节对齐
char 类型,地址可以是任意值,最自由
2字节对齐
short 类型,地址必须是 2 的倍数
4字节对齐
int / float 类型,地址必须是 4 的倍数
8字节对齐
double 与 64 位指针,地址必须是 8 的倍数
超市货架的格位对应 →数据对齐到地址边界

小瓶 1 格随便摆;大瓶占 4 格只能从 1、5、9 格开始,绝不能骑在格子上

addrmodN=0addr \bmod N = 0
7第 7 页 · 对齐的基本规则

编译器处理对齐的步骤

编译器把源码中的struct一步步翻译成内存布局与机器码。

1
解析声明与指令
读入struct定义和#pragma pack等对齐控制指令
2
字段对齐值
取类型大小与pack值中的较小者作为字段对齐
3
计算偏移填充
累计偏移,不够对齐处插入padding字节
4
确定整体对齐
取所有字段对齐值的最大值作为结构体对齐
5
尾部填充
结构体总大小向上取整到对齐值的整数倍
6
生成访问指令
用[基址+偏移]形式生成访问该字段的指令
8第 8 页 · 编译器处理对齐的步骤

对齐规则代码验证

c

用 offsetof 和 sizeof 亲眼看编译器塞了多少填充字节。

代码高亮加载中…

原顺序 24 字节含 10 字节填充,重排后只剩 2 字节填充,省了 8 字节。

9第 9 页 · 对齐规则代码验证

嵌套结构的对齐

前面我们掌握了「对齐值 = 内部成员对齐值的最大值」。当一个结构体的字段本身又是一个结构体时,这条规则还成立吗?这就是嵌套对齐要回答的问题。

嵌套成员的对齐值
嵌套结构体自身的对齐值,由其内部所有成员中最大的对齐值决定
参与外层 max 计算
把嵌套结构体当成整体成员,它自身的对齐值也要参与外层对齐值的 max
整块占位不拆分
嵌套结构必须从对齐值整数倍地址开始,整块占 sizeof 个字节,不能拆开
外层总大小仍向上对齐
外层结构体总大小还要向上对齐到外层对齐值的整数倍
俄罗斯套娃对应 →嵌套结构的对齐

每一层娃娃都有自己的最小卡位;外层间距要按所有层中最大的卡位来排

align(T)=maxialign(fi),对嵌套字段递归展开\text{align}(T) = \max_i \text{align}(f_i),\quad \text{对嵌套字段递归展开}
10第 10 页 · 嵌套结构的对齐

#pragma pack指令

c

用一段真实可编译的 C 代码,展示 #pragma pack(1) 如何把结构体压缩到 1 字节对齐,并对比默认布局的差异。

代码高亮加载中…

#pragma pack(1) 把对齐系数压成 1,int、double 紧贴前字段;sizeof 从默认 24 缩到 14,但未对齐访问会变慢。

11第 11 页 · #pragma pack指令

自然对齐 vs 指定对齐

自然对齐跑得快却费内存,指定对齐紧凑却有性能代价——什么时候该牺牲什么?

自然对齐
  • 对齐值 = 字段类型大小(int→4, double→8)
  • 字段按自身大小对齐,padding 随声明顺序
  • CPU 单条指令读出,缓存行友好
  • 结构体总大小向最大字段对齐,有尾 padding
指定对齐(#pragma pack)
  • 对齐值 = min(pack n, 字段类型大小)
  • 强制紧贴排列,节省存储与 I/O 带宽
  • n < 字段原始对齐 → unaligned 访问,性能下降
  • 用于硬性兼容:协议头、固件、跨平台 ABI
默认走自然对齐;只有协议头、固件等硬性二进制场景才用 #pragma pack,且 n 不要小于字段原始对齐。
12第 12 页 · 自然对齐 vs 指定对齐

跨平台对齐差异

自然对齐 vs 指定对齐,那是编译器层面的承诺。但硬件认不认账?x86、ARM、ARM64 三种 CPU 的回答完全不同——x86 睁一只眼闭一只眼,ARM64 可要较真。

x86/x64 宽容访问
不对齐也能读写,只是性能下降;x64 的 SSE 指令仍需 16 字节对齐
ARMv7 可配置
SCTLR.A 位:0 关(不报告对齐异常),1 开(不对齐立即抛故障)
ARM64 严格对齐
绝大多数 LD/ST 要求自然对齐;违规直接 SIGBUS,不容商量
编译器默认对齐
MSVC 默认 8 字节,GCC/Clang 在 x86-64 默认 16 字节,同一 struct sizeof 会变
隐藏变量:字节序
x86 恒小端;ARM 默认小端但可配大端——对齐之外的另一坑
各国靠哪侧开车对应 →处理器对齐策略

x86 像美国,乱开能到但被抓罚款;ARM64 像英国,越线直接撞车(崩溃)

13第 13 页 · 跨平台对齐差异

结构体数组的内存布局

一个结构体变量在内存中的样子——字段顺序、padding、对齐起点——我们都已经看清。那 `Student s[30]` 这 30 份数据,又是按什么规则在内存里排开的?

同型副本的连续排列
数组里每个元素的内部布局完全一样:字段顺序、padding、对齐起点都一致
元素大小统一等于 sizeof
每个元素都占 sizeof(结构体) 字节,含 padding,比字段总和略大
元素之间紧密相邻
末尾 padding 是元素自身一部分,已把下一元素起点推至对齐位置,数组内不再补空
每个起点都满足对齐
元素 i 的首地址受结构体对齐要求约束,无需再额外填充即可放下
停车场一排等宽车位对应 →结构体数组的内存排列

最大车型定车位宽;车位首尾相连;找第 i 个车位 = 入口 + i × 车位宽

addr(arr[i])=base+i×sizeof(T)addr(arr[i]) = base + i \times sizeof(T)
14第 14 页 · 结构体数组的内存布局

结构体数组内存图示

横轴为内存地址递增方向。每个结构体占16字节,字段按对齐规则排列,尾填充让下一元素从对齐地址开始。

图解渲染中…
c尾填充4B,sizeof包含这部分,保证数组元素8字节对齐darr[1]从0x10而非0x0C起步,正是尾填充的功劳
15第 15 页 · 结构体数组内存图示

结构体数组的汇编视角

asm

看 gcc 把 arr[i].y 翻译成什么:基址 + i×sizeof + 字段偏移。

代码高亮加载中…

汇编把 arr[i].y 拆成三步:基址 + i×sizeof(Point) + 字段偏移 4。sizeof 和 offsetof 都被编译器烧死成常数 8 和 4。

16第 16 页 · 结构体数组的汇编视角

数据结构存储要点回顾

  • 字段声明顺序直接决定 padding 数量
  • 对齐是 CPU 访问单元带来的物理代价
  • #pragma pack 用对齐换紧凑,有性能成本
  • 结构体数组按 sizeof 紧密排布,无指针数组空隙
延伸主题:位域 bitfield 的存储压缩联合体 union 的内存共享C 与汇编的 ABI 传递约定
17第 17 页 · 数据结构存储要点回顾

结构体存储自测

点击作答

在 32 位系统自然对齐下,struct { char a; int b; char c; } 占多少字节?

18第 18 页 · 结构体存储自测

课后思考

先自己想,再看参考答案。三题分别从回顾、应用、迁移三个层次出发。

1为什么结构体字段顺序会影响内存占用?交换两个同类型字段位置会改变大小吗?

参考答案顺序决定了填充字节的位置和数量。同类型字段交换不改变总大小;不同类型交换可能减少或增加填充,是结构体压缩的切入点。

2若用 #pragma pack(1) 强制 1 字节对齐,CPU 访问结构体成员的效率会怎样变化?为什么?

参考答案成员不再对齐到自然边界,CPU 可能需要多次访存才能拼出一个字。对齐换性能,pack 换兼容性,本质是空间与效率的权衡。

3同一个含指针字段的结构体定义,在 32 位和 64 位 Linux 下大小一定相同吗?为什么?

参考答案不一定。纯 int/char 成员时常见平台一致;含指针时 64 位指针占 8 字节,整体大小会变。课后可用 sizeof 在自己机器上验证。

19第 19 页 · 课后思考