文章目录
线索栏
- C语言中 struct(结构体)是什么?它的主要目的是什么?
- 结构体的所有成员在内存中如何排列?指向结构体的指针值是什么?
- 如何通过结构体变量和结构体指针访问其成员?->操作符是什么的简写?
- 偏移量与编译:结构体成员的"偏移量"是什么?编译器如何利用它生成访问成员的代码?
- 如何生成一个指向结构体内部某个成员(或数组成员)的指针?
- 结构体可以包含其他结构体吗?其内存布局如何?
- 如何根据结构体声明计算各字段的偏移量和总大小?如何根据汇编代码反推结构体初始化语句?
- 给定一个遍历链表的汇编代码,如何反推出其操作的数据结构(链表)和函数逻辑(求和)?
笔记栏
1. 结构体基础
(1)定义:struct是一种将不同类型的对象聚合到一个单元中的数据类型。用于表示一个逻辑实体。
(2)声明示例:
c
struct rect { // 表示一个长方形
long llx; // 左下角x坐标
long lly; // 左下角y坐标
unsigned long width; // 宽
unsigned long height; // 高
unsigned color; // 颜色
};
(3)使用:
c
struct rect r; // 声明变量
r.llx = 0; // 通过 '.' 操作符访问成员
r.color = 0xFF00FF;
// 声明并初始化
struct rect r2 = {0, 0, 10, 20, 0xFF00FF};
2. 结构体指针与成员访问
1)指针传递
为避免复制开销,常传递结构体指针。
2)成员访问
(1)(*rp).width:先解引用指针,再访问成员。括号必需。
(2)rp->width:等价于上者,是更简洁的写法。
3)示例函数
c
long area(struct rect *rp) { return rp->width * rp->height; }
void rotate_left(struct rect *rp) {
long t = rp->height;
rp->height = rp->width;
rp->width = t;
rp->llx -= t;
}
3. 内存布局、偏移量与代码生成
1)连续存储
所有成员在内存中连续存放。结构体指针指向第一个字节的地址。
2)偏移量
每个成员相对于结构体起始地址的字节距离。编译器在编译时计算并记录所有偏移量。
3)示例分析(struct rec)
c
struct rec { int i; int j; int a[2]; int *p; };
| 成员 | 类型 | 大小 | 偏移量 |
|---|---|---|---|
| i | int | 4 | 0 |
| j | int | 4 | 4 |
| a[0] | int | 4 | 8 |
| a[1] | int | 4 | 12 |
| p | int* | 8 | 16 |
4)总大小
24 字节(注意末尾可能有填充,但此处刚好对齐)。
5)汇编代码生成
访问成员 = 基址 + 偏移量。
(1)设 struct rec *r在 %rdi
(2)r->j = r->i;被编译为:
c
movl (%rdi), %eax # 从偏移0读取 r->i
movl %eax, 4(%rdi) # 存入偏移4,即 r->j
4. 生成指向结构体内部成员的指针
1)原理
地址 = 结构体基址 + 成员偏移量。
2)示例
(1)&(r->a[1]):偏移 = 8 + 4 * 1 = 12。汇编:leaq 12(%rdi), %rax
(2)&(r->a[i]):偏移 = 8 + 4*i。汇编:leaq 8(%rdi, %rsi, 4), %rax(i在%rsi)
5. 嵌套结构体
(1)声明:结构体可以包含其他结构体作为成员。
(2)内存:内嵌结构体的成员按其自身规则连续存放,并作为整体嵌入到外层结构体中。
6. 练习题
练习题3.41
A. 偏移量计算:
c
struct prob {
int *p; // 指针,8字节,偏移0
struct { // 内嵌结构体,含两个int
int x; // 4字节,偏移8
int y; // 4字节,偏移12
} s;
struct prob *next; // 指针,8字节,偏移16
};
(1)p: 0
(2)s.x: 8
(3)s.y: 12
(4)next: 16
B. 总字节数:最后一个成员 next偏移16,加自身大小8,总 24字节。
C. 反推 sp_init代码:
已知 sp在 %rdi,分析汇编:
(1)movl 12(%rdi), %eax:读取偏移12 -> sp->s.y到 %eax。
(2)movl %eax, 8(%rdi):将值存入偏移8 -> sp->s.x。所以 sp->s.x = sp->s.y;。
(3)leaq 8(%rdi), %rax:计算偏移8的地址 -> &(sp->s.x)。
(4)movq %rax, (%rdi):将该地址存入偏移0 -> sp->p。所以 sp->p = &(sp->s.x);。
(5)movq %rdi, 16(%rdi):将 sp(自身地址) 存入偏移16 -> sp->next。所以 sp->next = sp;。
补全的C代码:
c
void sp_init(struct prob *sp) {
sp->s.x = sp->s.y;
sp->p = &(sp->s.x);
sp->next = sp;
}
练习题3.42
给定:结构体 ELE包含 long v和 struct ELE *p。函数 fun的汇编如上。
A. 逆向C代码:
分析汇编控制流:这是一个循环,%rdi作为指针 ptr在移动(第6行 movq 8(%rdi), %rdi),每次将 ptr->v加到 %rax(第5行),直到 ptr为 NULL(第8行测试)。这是典型的链表遍历。
c
long fun(struct ELE *ptr) {
long result = 0;
while (ptr != NULL) {
result += ptr->v;
ptr = ptr->p;
}
return result;
}
B. 数据结构与操作:
(1)数据结构:ELE结构实现了一个单链表。v是节点存储的值,p是指向下一个节点的指针。
(2)fun的操作:函数 fun遍历这个链表,将所有节点的 v值求和,并返回总和。
总结栏
本节深入剖析了C语言结构体的内存表示、访问机制及其在机器代码中的实现,是理解复杂数据组织的基石。
- 结构体是连续的内存块:编译器按照声明顺序,在连续内存中布局各字段,并计算每个字段的固定偏移量。所有访问都通过"基址+偏移"完成。
- 偏移量是编译时常数:这是结构体访问高效的关键。机器指令中直接编码偏移量,运行时无额外开销。编译器完全负责字段名到偏移量的映射。
- 指针访问是标准模式:传递结构体指针(而非副本)是通用做法。->操作符简化了通过指针的访问。生成指向内部成员的指针,是地址计算的直接应用。
- 理解汇编的钥匙:在涉及结构体的汇编代码中,识别出"基址寄存器+偏移量"的模式,就能立刻知道它在访问哪个字段。练习题3.41和3.42是绝佳的实践,将声明、偏移计算、汇编分析与高级逻辑还原完整串联。
核心启示:结构体将类型不同的数据逻辑绑定,而内存布局的确定性(偏移量)和连续性,使得硬件能高效访问。理解这种从抽象描述到具体内存布局的转换,是进行底层编程、调试内存错误和与硬件设备交互的基础。联合体(union)则提供了另一种"重叠"内存的视角,是下一节的延伸。