Appearance
数据对齐与大小端
2026 大纲 四(四)数据的对齐和大/小端存放方式。⚠️ 这一条在考纲里属于第四章 指令系统(字节序与对齐是指令集体系结构的属性),本文因历史原因留在
data-repr目录下,目录位置不代表大纲归属。
字节序和对齐是两件毫不相干的事
这一节里有两组规则,很多人把它们混在一起想,结果两边都算不对:
🔴 字节序管一个数据"内部"的字节先后(由体系结构决定),对齐管数据"整体"放在哪个地址(由 ABI 规范与编译器决定)。算结构体布局完全用不到字节序,判字节序也不需要对齐规则。
先把两者共用的一个前提说死:
🔴 "数据的地址"指的是它占用的连续单元中最小的那个地址,与字节序无关。字节序决定的只是"这个最小地址上放的是哪一个字节"——大端放最高有效字节(MSB),小端放最低有效字节(LSB)。地址是同一个,内容不同。
一、字节序:多字节数据的排列约定
现代计算机基本都按字节编址,而 int、float 占 4 字节,double 占 8 字节,一个变量要占用连续的若干个存储单元。以 32 位数据 0x12345678 存放在 0x100 起为例:
| 地址 | 0x100 | 0x101 | 0x102 | 0x103 | |
|---|---|---|---|---|---|
| 大端(MSB 在低地址) | 12 | 34 | 56 | 78 | 与人书写顺序一致,即网络字节序(TCP/IP)。代表架构:IBM 360/370、Motorola 68k、MIPS、SPARC |
| 小端(LSB 在低地址) | 78 | 56 | 34 | 12 | 代表架构:Intel 80x86、DEC VAX、ARM(默认)。Alpha、PowerPC 两种都支持,加电启动时选定 |
判别一台机器字节序的原理是取出多字节数据的第一个字节,看它是高位还是低位:
c
int x = 1; // 0x00000001
if (*(char *)&x == 1) {
// 低地址存的是 0x01(LSB),小端
} else {
// 低地址存的是 0x00(MSB),大端
}同样的判据也适用于给定的内存转储:把真值按十六进制写出来,再和内存里从低地址到高地址的字节序列逐一对照。从低位开始拆能对上就是小端,从高位开始拆能对上就是大端。
🔴 小端倒着拼,大端顺着拼。 题面给的内存转储是物理顺序(低地址在左),不是数值的可读形式。从低地址到高地址逐字节读出之后,小端必须倒着拼才是真值——顺着读是这类题最主要的失分点。
⚠️ 两点补充:两种字节序没有优劣,只是体系结构设计者的一次性约定,麻烦只出现在跨系统通信时(网络协议钉死大端,由两端各自转换;音视频与图像的文件格式同样要在规范里写死)。另外字节序不只管内存里的数据——机器码中的立即数、位移量字段同样按字节序摆放,所以反汇编时读出的偏移量也要按同一规则拼。
小端有一个实质上的技术优势值得知道:
🔴 变宽度访问同一个数时,小端下低字节的地址不变。 把一个 int 的低 8 位当 char 读,小端下的地址与读 int 时完全相同,硬件不必做地址调整;大端则要把地址加 3。
二、边界对齐:省的是访存次数
存储器不是一个字节一个字节地读的。一个 8 字节宽的存储器,一次访问取出的是地址
最简单的策略:
| 数据类型 | 大小 | 对齐要求 |
|---|---|---|
char | 1B | 1(任意地址) |
short | 2B | 2 的倍数 |
int / float | 4B | 4 的倍数 |
double | 8B | 8 的倍数 |
数组 T[n] | = 元素类型 T 的对齐要求(不是数组总长) | |
| 嵌套结构体 | 见第三节 | = 其内部成员中最大的对齐要求 |
具体到几字节由 ABI 规定,不是物理定律
上表是最典型的一种策略(Windows 采用),但并非唯一。Linux 遵循的 i386 System V ABI 规定:short 对齐到 2,而 int、float、double、指针一律对齐到 4——double 在这种规范下就可能需要两次访存。
所以做布局计算时,题面给定的对齐要求优先;没给定时按"
一条推论:对齐等价于地址低位恒为 0。"地址是
| 对齐到 | 2 字节 | 4 字节 | 8 字节 |
|---|---|---|---|
| 地址低位 | 末 1 位恒为 0 | 末 2 位恒为 00 | 末 3 位恒为 000 |
既然这几位恒为 0,硬件就可以不存它们。所以"指令按 4 字节对齐、地址空间 32 位"时,PC 只需 30 位。转移指令的偏移量字段同理,可以只存"以指令为单位"的位移量,寻址范围因此扩大 4 倍。
三、结构体的三条对齐规则
结构体由若干基本类型拼成,要保证其中每一个成员都满足自己的对齐要求:
① 整个结构体的对齐要求 = 其中对齐要求最严格的那个成员② 每个成员在满足自身对齐要求的前提下,取最小的可用偏移量 —— 可能导致内部插空③ 结构体的总大小必须是它自身对齐要求的整数倍 —— 可能导致尾部插空
前两条保证结构体内部每个成员都对齐,第三条保证结构体数组里的每个元素都对齐——第 sizeof(struct)」,步长就是含尾部填充的 sizeof。规则 ③ 用一次填充,换来了数组中所有后续元素的对齐;只定义单个结构体变量时它确实是纯开销,但编译器无法预知你会不会把它放进数组。
套规则之前,有三处判断必须先摆正:
🔴 对齐要求由元素类型决定,不是数组总长。
char name[10]的对齐要求是 1 不是 10,int arr[100]是 4 不是 400。判据是:对齐服务于单次访存,而访问数组一次只访问一个元素。把"字节对齐到 "生搬到数组整体上,是结构体布局计算里最主要的错法。
🔴 规则 ① 取的是最大的"对齐要求",不是最大的"大小"。 绝大多数情况下两者相同(基本类型的对齐要求等于其大小),差别只出现在数组成员与嵌套结构体成员上:
char s[16]大小 16、对齐要求 1;struct{char a; int b;}大小 8、对齐要求 4。
🔴 已知各成员大小之和,算不出
sizeof。 成员之间可能插空、末尾可能补齐,sizeof只增不减,而且增多少取决于成员的排列顺序——同样四个成员,顺序不同可以是 24 与 16 的差别。必须逐成员排偏移,没有捷径。 反过来用:按对齐要求从大到小排列成员,填充最少,因为大对齐的成员先占位时,它后面的小成员总能紧贴着放下。
c
struct S {
char a; // 对齐 1,偏移 0
int b; // 对齐 4 → 偏移 1、2、3 都不可用,落到偏移 4(插 3 字节空)
short c; // 对齐 2,偏移 8
};用到偏移 9 为止,共 10 字节;结构体对齐要求是 4(最严格的成员 int b),
| 偏移 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 内容 | a | 空 | 空 | 空 | b | b | b | b | c | c | 空 | 空 |
调整成员顺序可以减少填充:把 int b; short c; char a; 这样从大到小排,用到偏移 6、共 7 字节,补齐到 4 的倍数
未对齐访问的后果
| 架构 | 处理方式 |
|---|---|
| x86 / IA-32 | 允许,硬件自动拆成多次访问,只是变慢 |
| ARM(严格模式) | 产生对齐异常 |
| MIPS | 必须对齐,否则异常 |
底层机器级代码本应支持按任意地址访问,所以 IA-32 上不对齐也能正确工作。但操作系统按对齐方式分配内存、编译器按对齐方式生成代码,因为执行效率更高。
两道演算:从内存转储反推真值,以及逐成员排出结构体布局(想核对自己拼的方向对不对、偏移是不是一格格排出来的时展开)
(一)字节序与反推。 32 位数据 0xAABBCCDD 存放在地址 0x1000 起:
| 0x1000 | 0x1001 | 0x1002 | 0x1003 | |
|---|---|---|---|---|
| 大端 | AA | BB | CC | DD |
| 小端 | DD | CC | BB | AA |
反过来,小端机器上从 0x2000 起连续 4 字节依次是 DC EC FF FF,该 32 位数是多少(按补码解释)?给的是内存物理顺序(低地址在左),小端下低地址存低字节,所以倒着拼:
按 32 位补码读:最高位为 1,0xDCECFFFF 是最典型的错法。
(二)逐成员排布。 按"
c
struct T {
char a;
double b;
int c;
char d;
};| 成员 | 对齐要求 | 偏移 | 占用 | 前面插空 |
|---|---|---|---|---|
char a | 1 | 0 | 0 | — |
double b | 8 | 8 | 8–15 | 1–7(7 字节) |
int c | 4 | 16 | 16–19 | — |
char d | 1 | 20 | 20 | — |
用到偏移 20,共 21 字节;结构体对齐要求 = 8(最严格的 double),补齐到 8 的倍数
按对齐要求从大到小重排 struct T2 { double b; int c; char a; char d; };:
| 成员 | 对齐要求 | 偏移 | 占用 |
|---|---|---|---|
double b | 8 | 0 | 0–7 |
int c | 4 | 8 | 8–11 |
char a | 1 | 12 | 12 |
char d | 1 | 13 | 13 |
用到 14 字节,中间零填充;补齐到 8 的倍数 struct T2 放进数组,sa[i] 的首地址就是 sa
考点速记
- 计算机按字节编址,变量地址取所占单元中最小的那个;大端 MSB 在低地址(网络字节序),小端 LSB 在低地址(变宽度访问时低字节地址不变)。两者没有优劣,只在跨系统通信时必须转换,且内存转储要小端倒着拼、大端顺着拼。
- 对齐省的是访存次数——不对齐的数据会跨越存储器块边界、需要两次访问再拼接;通用策略是
字节数据对齐到 ,数组按元素类型、嵌套结构体按其内部最严格的成员,具体由 ABI 规定。对齐到 等价于地址低 位恒为 0,这几位可以不存。 - 结构体三条规则:① 对齐要求
最严格成员的对齐要求(不是最大的大小);② 取最小可用偏移(内部插空);③ 总大小是对齐要求的整数倍(尾部插空)。尾部填充是为结构体数组存在的;按对齐要求从大到小排列成员填充最少。
这一节在真题里被考过的形式(下方「真题练习」里属于本篇的那几道,本章题量第二集中的一篇):
- 给一个结构体定义与首地址,问某个成员的地址:逐成员排偏移——遇到对齐要求
的成员,把当前偏移向上取整到 的倍数。⚠️ char name[10]这类数组成员的对齐要求是元素类型的(1),不是 10。 - 给一个结构体数组与下标,问某元素某成员的地址:先算出含尾部填充的
sizeof,再算首地址 + i × sizeof + 成员偏移。漏掉尾部填充,从第二个元素起全错。 - 给一个多字节变量的机器数与起始地址,问某个地址上存的是哪个字节:小端低地址放最低有效字节——把机器数按字节从低位往高位数过去。这类题几乎不涉及对齐,纯考字节序。
- 给结构体定义,问按边界对齐分配后某成员的地址:同第一条,只是题面把"边界对齐"四个字明说出来。
- 给一个 double 变量的地址,问读它要几个存储周期:这一条跨到 多模块存储器——不对齐会多出一次访存,起始地址除以一次并行访问宽度看余数。
易错:把数组成员的对齐要求当成数组总长。看元素类型。
易错:用"各成员大小之和"当
sizeof。必须逐成员排偏移并补尾部。
易错:算结构体数组元素地址时漏掉尾部填充。
易错:内存转储小端顺着拼。要倒着拼。
易错:把字节序牵进结构体布局计算。两件事毫不相干。
教材出处
- 按字节编址、变量地址取最小首地址、大端/小端的定义与代表架构、跨系统通信需转换字节序:袁春风《计算机组成与系统结构》第 3 版 §2.5.2 数据的存储和排列顺序,印刷页 p46–p47
- 不对齐导致多次访存的机制(8 字节宽存储器、数据落在第 6~9 字节需访存两次)、Windows 与 i386 System V 两种对齐策略的差异:袁春风《计算机系统基础》§3.5.4 数据的对齐,印刷页 p149
- 结构体对齐的三条规则,以及第 ③ 条"为保证结构体数组中每个元素都满足对齐要求"的说明与
struct SDT尾部插空实例:同书 §3.5.4,印刷页 p149–p150