ZJZJ / ICS NOTES
学习空间整本阅读
COMPUTER SYSTEMS · PEKING UNIVERSITY

07 · 数组、结构体、指针与浮点汇编

来源:第 7 讲 pp.3~51(包括附加材料);小班第 7 讲;教材 §3.8~3.9、§3.11;2025 阶段卷第 18~22 题。

一维数组与指针算术#

T A[N] 为 N 个连续 T 对象分配 N·sizeof(T) 字节。&A[i] = base+i·sizeof(T),A[i]=*(A+i)。指针加 1 是增加一个所指对象的跨度,而不是一字节。

同一数组中的指针相减得到元素个数(类型 ptrdiff_t),不是字节数;跨不相关对象相减不满足 C 的定义。可以形成末尾后一个指针用于比较或循环终止,但不能解引用它。

数组对象不是指针变量。A 在很多表达式中退化为首元素指针,sizeof(A) 和 &A 是重要例外;作为函数形参的 int a[10] 调整为指针参数,所以在函数内 sizeof(a) 是指针大小。

二维、变长与多级数组#

C 的 T A[R][C] 是 R 个“含 C 个 T 的数组”,行优先:

&A[i][j] = base + (i*C+j)*sizeof(T)
A+i 的跨度 = C*sizeof(T)
&A+1 的跨度 = R*C*sizeof(T)

int A[3][5] 中 sizeof(A)=60、sizeof(*A)=20、sizeof(**A)=4、sizeof(&A)=8。这些数值默认本课 LP64。

int fixed(int a[3][5], size_t i, size_t j) { return a[i][j]; }
int variable(size_t n, int a[n][n], size_t i, size_t j) {
    return a[i][j];
}

变长数组的列数要在运算时参与乘法;形参调整后的类型是指向整行的指针,不是 int**。固定列数常能用移位或 LEA 算行偏移。

int *rows[R] 是指针数组:先从表里读 rows[i],再通过所得指针读第 j 项,通常两次数据读取;各行可独立分配、不连续或不同长度。int A[R][C] 只需一次最终元素读取,行地址可直接算出。将连续二维数组强转 int** 不会自动变成行指针表。

类型声明与 sizeof 表#

从变量名出发,先结合括号内的结构,再按 []、() 优先于 * 读声明。

声明 A 是什么 sizeof A sizeof *A sizeof **A sizeof ***A
int A[3][5] 二维 int 数组 60 20 4 非法类型表达式
int *A[3][5] 二维 int 指针数组 120 40 8 4
int (*A)[3][5] 指向整个二维数组的指针 8 60 20 4
int (*A[3])[5] 3 个指针,每个指向 5 个 int 的数组 24 8 20 4
int *(*A[2])[3] 2 个指针,每个指向 3 个 int* 的数组 16 8 24 8

最后一行不是“指针指向指针再指向 int 数组”的随意层叠:括号与数组位置决定了元素类型。再解引用一级 ****A 才得到 int,sizeof 为 4。

sizeof(*p) 对非 VLA 类型通常不求值,因此 sizeof 不会真的解引用 p;但在普通值表达式中读 *p 需要有效对象。课件附表的 “Bad pointer” 是讨论实际访问风险,不应误读成所有 sizeof 都发生访存。VLA 相关 sizeof 可能运行时求值,要单独分析。

结构体布局与对齐#

编译器按声明顺序排列成员,不会为了省空间擅自重排。每个成员从满足其 alignment 的最小偏移开始;最终结构体大小向其最大成员对齐要求的整数倍取整,保证结构体数组每个元素也正确对齐。

align_up(x,a) = ceil(x/a)*a
成员偏移 = align_up(当前末尾, 成员对齐)
结构体大小 = align_up(最后成员末尾, 结构体对齐)
struct S { char c; int i; short s; double d; };
/* 本课 ABI:offset(c)=0, i=4, s=8, d=16, sizeof=24, align=8 */

c 后补 3 字节;s 后结束于 10,再补 6 字节使 d 在 16。如果改成 double d; int i; short s; char c;,偏移 0/8/12/14,总大小 16。这说明程序员改变顺序可节省空间;编译器不能偷偷改变对外布局。

数组和嵌套结构体作为完整成员参与布局。结构体整体起始地址必须满足所有成员的对齐,不能只满足第一个 char。x86 支持许多未对齐访问不意味着 C/ABI 可以忽略对齐;未对齐还可能跨缓存块并影响性能。#pragma pack、不同 ABI 和向量类型可能改变规则。

用 <stddef.h> 的 offsetof(struct S,i) 核对偏移,用 _Alignof 核对对齐;不通过手写空指针解引用技巧猜布局。

从汇编反推布局#

先看访问宽度判断候选类型,再看偏移;对数组维度列出约束而非凭一个地址猜答案。2025 阶段卷第 21 题:

str1: int x[A][B]; long y;       y 在 184
str2: char array[B]; int t; short s[A]; long u;
t 在 8,u 在 32

得到 align_up(B,4)=8,所以 5≤B≤8;align_up(12+2A,8)=32,所以 7≤A≤10;align_up(4AB,8)=184,所以 AB∈{45,46}。唯一满足正整数范围的解是 A=9,B=5。注意尾部 y 的地址不是简单假定为 4AB,必须先考虑 padding。

链表读取中区分取成员地址与读成员值:leaq 8(%rdi),%rax 返回成员地址;movq 8(%rdi),%rax 读该处 8 字节;后面再 movq (%rax),%rax 才是一次额外指针追踪。

SSE / XMM 与 SIMD#

XMM 寄存器宽 128 位,标量运算只处理指定低位标量,packed 运算处理多个元素。YMM、ZMM 是更宽的扩展寄存器;本课代码主要看 XMM。

指令/后缀 意义
ss / sd scalar single / scalar double,单精度/双精度标量
ps / pd packed single / packed double,4 个 float / 2 个 double
movss / movsd 移动浮点标量位模式
addss/addsd、mulss/mulsd、sub、div 同族 相应精度标量运算
cvtss2sd / cvtsd2ss float ↔ double 数值转换
cvtsi2sd / cvtsi2ss 有符号整数转换成浮点
cvttsd2si / cvttss2si 浮点向零截断转换为有符号整数
ucomiss / ucomisd 设置浮点比较结果标志
xorpd %xmm0,%xmm0 清零,形成 +0 位模式

移动位模式不是数值转换;movq 的整数/向量寄存器间形式也不等于 cvt。标量操作对寄存器其他位的效果与具体 SSE/AVX 编码有关,不只凭 ss/sd 后缀泛化。

浮点参数与比较#

double mix(long *a,double *b,float c):a 在 rdi,b 在 rsi,c 在 xmm0;指向浮点的指针仍走整数寄存器,只有浮点值走 XMM。若计算 *a+*b+c,先加载、转换为 double,再加,结果在 xmm0。

对于 ucomis 的逻辑左值与右值比较(AT&T ucomisd src,dst 比较 dst 对 src):

关系 ZF PF CF
大于 0 0 0
小于 0 0 1
相等 1 0 0
无序(NaN) 1 1 1

因此不能仅用 ZF 断言浮点相等,还要排除 PF=1 的无序情形;编译器可能组合 setnp 等指令。OF/SF 清零,不能套整数 signed 比较条件。NaN 的异常细节还需区别 quiet/signaling 和比较指令种类。

自检#

能按类型一步步求 sizeof;能解释数组形参与对象的差异;能列结构体偏移表;能从汇编反推维度;能判断浮点指针和浮点值各在哪类寄存器;能解释 unordered。教材作业 3.66、3.67、3.68。