实用指南:计算系统安全速成之机器级编程(数组和指针)【3】

说明

  • 庄老师的课堂,如春风拂面,启迪心智。然学生愚钝,于课上未能尽领其妙,心中常怀惭愧。
  • 幸有课件为引,得以于课后静心求索。勤能补拙,笨鸟先飞,一番沉浸钻研,方窥见知识殿堂之幽深与壮美,竟觉趣味盎然。
  • 今将此间心得与笔记整理成篇,公之于众,权作抛砖引玉。诚盼诸位学友不吝赐教,一同切磋琢磨,于学海中结伴同行。
  • 资料地址:computing-system-security

一 数组

  • T A[L],表示类型为 T、长度为 L 的数组,在内存中连续分配 L * sizeof(T) 字节的区域。
    • char string[12]:分配 12 字节
    • int val[5]:分配 20 字节(每个 int 占 4 字节)
    • double a[3]:分配 24 字节(每个 double 占 8 字节)
    • char *p[3]:分配 24 字节(每个指针占 8 字节)
      在这里插入图片描述
  • 标识符 A 可作为指向第 0 个元素的指针,类型为 T*
ReferenceTypeValue
val[4]int3
valint *x
val+1int *x + 4
&val[2]int *x + 8
val[5]int??
*(val+1)int5 //val[1]
val + iint *x + 4 * i //&val[i]

在这里插入图片描述

  • 自定义类型 typedef int zip_dig[ZLEN] 等价于 int cmu[5]
  • cmu = {1,5,2,1,3} 分配在 20 字节块。mit = {0,2,1,3,9}ucb = {9,4,7,2,0} 连续分配。

1.1 访问数组元素

在这里插入图片描述

  • 访问数据元素的函数:
    int get_digit
    (zip_dig z, int digit)
    {
    return z[digit];
    }
  • 对应的汇编代码:
    # %rdi = z
    # %rsi = digit
    movl (%rdi,%rsi,4), %eax  # z[digit]
  • %rdi 存储数组起始地址。
  • %rsi 存储索引值。
  • 使用指令 movl (%rdi,%rsi,4), %eax 实现 z[digit],元素地址计算%rdi + 4 * %rsi

1.2 数组循环递增

  • C语言源代码
#define ZLEN 5
void zincr(zip_dig z) {
size_t i;
for (i = 0; i < ZLEN; i++)
z[i]++;
}
  • 对应的汇编
# %rdi = z
movl    $0, %eax          #   i = 0
jmp     .L3               #   goto middle
.L4:                        # loop:
addl    $1, (%rdi,%rax,4) #   z[i]++
addq    $1, %rax          #   i++
.L3:                        # middle
cmpq    $4, %rax          #   i:4
jbe     .L4               #   if <=, goto loop
rep; ret

在这里插入图片描述
在这里插入图片描述

  • A1:编译成功(是),无非法引用风险(否),sizeof(A1) 返回 12(3×4 字节)。
  • *A1:编译成功(是),合法访问首元素(否),sizeof(*A1) 返回 4(一个 int 大小)。
  • A2:编译成功(是),无非法引用风险(否),但未初始化,sizeof(A2) 返回 8(指针大小)。
  • *A2:编译成功(是),存在非法引用风险(是),因指向未分配内存,sizeof(*A2) 返回 4。
    在这里插入图片描述

声明 int A1[3]

  • An(即 A1):大小 12,合法
  • *An(即 *A1):大小 4,合法
  • **An:不合法(否),无法双重解引用

声明 int *A2[3]

  • 含义:包含 3 个 int 指针的数组
  • AnA2):大小 24(3×8 字节),合法
  • *An*A2):返回第一个指针,大小 8,合法
  • **An**A2):解引用得 int,大小 4,但存在非法访问风险

声明 int (*A3)[3]

  • 含义:指向包含 3 个 int 的数组的指针
  • AnA3):大小 8(指针),合法
  • *An*A3):解引用得整个数组,大小 12,但访问有风险
  • **An**A3):再解引用得 int,大小 4,有风险

1.3 多层(嵌套)数组

  • T A[R][C]:表示 R 行 C 列的二维数组。
  • 总大小:R * C * sizeof(T) 字节
  • 内存布局采用行优先顺序(Row-Major Ordering),示例:int A[R][C] 中元素按行依次存储。
    在这里插入图片描述
    在这里插入图片描述

#define PCOUNT 4
typedef int zip_dig[5];
zip_dig pgh[PCOUNT] =
{{1, 5, 2, 0, 6},
{1, 5, 2, 1, 3 },
{1, 5, 2, 1, 7 },
{1, 5, 2, 2, 1 }};

在这里插入图片描述

  • 定义:zip_dig pgh[PCOUNT] 等价于 int pgh[4][5]
  • 数据内容:四个 ZIP 码:{1,5,2,0,6}、{1,5,2,1,3} 等。
  • 内存分配特点:整体连续分配,共 80 字节(4×5×4),每个子数组(每行)连续存放。
  • 所有元素按行优先排列。

1.3.1 行向量访问

行访问机制

  • A[i] 表示第 i 行,是一个包含 C 个 T 类型元素的数组。
  • 起始地址计算公式:A + i * (C * sizeof(T))
  • 示例:pgh[index] 起始地址为 pgh + 20 * index
    在这里插入图片描述

  • 汇编实现
    在这里插入图片描述
  • leaq (%rdi,%rdi,4), %rax → 计算 5 * index(%rdi,%rdi,4)等同于 rdi + rdi*4
  • leaq pgh(,%rax,4), %rax → 计算 pgh + 20 * index p g h + r a x ∗ 4 = p g h + ( 5 ∗ i n d e x ) ∗ 4 = p g h + 20 ∗ i n d e x pgh + rax*4 = pgh + (5*index)*4 = pgh + 20*index pgh+rax4=pgh+(5index)4=pgh+20index

1.3.2 嵌套数组元素访问

  • A [ i ] [ j ] A[i][j] A[i][j]是类型为 T T T 的元素,占用 K K K字节
  • 地址计算公式: A + i ∗ ( C ∗ K ) + j ∗ K = A + ( i ∗ C + j ) ∗ K A + i * (C * K) + j * K = A + (i * C + j) * K A+i(CK)+jK=A+(iC+j)K
  • 对于 i n t A [ R ] [ C ] int A[R][C] intA[R][C],每个元素占 4 字节。
    在这里插入图片描述
leaq	(%rdi,%rdi,4), %rax	# 5*index
addl	%rax, %rsi	# 5*index+dig
movl	pgh(,%rsi,4), %eax	# M[pgh + 4*(5*index+dig)]

1.4 多级数组表示

zip_dig cmu = { 1, 5, 2, 1, 3 };
zip_dig mit = { 0, 2, 1, 3, 9 };
zip_dig ucb = { 9, 4, 7, 2, 0 };
#define UCOUNT 3
int *univ[UCOUNT] = {mit, cmu, ucb};

在这里插入图片描述

int get_univ_digit (size_t index, size_t digit)
{
return univ[index][digit];
}
salq    $2, %rsi            # 4*digit
addq    univ(,%rdi,8), %rsi # p = univ[index] + 4*digit
movl    (%rsi), %eax        # return *p
ret
  • $salq 2 , 2, %rsi 2, d i g i t < < 2 = d i g i t ∗ 4 digit << 2 = digit * 4 digit<<2=digit4
  • a d d q u n i v ( , addq univ(,%rdi,8), %rsi addquniv(, u n i v + 8 ∗ i n d e x + 4 ∗ d i g i t univ + 8*index + 4*digit univ+8index+4digit

1.5 数组元素访问对比

在这里插入图片描述地址计算差异

  • 嵌套数组: M e m [ p g h + 20 ∗ i n d e x + 4 ∗ d i g i t ] Mem[pgh+20*index+4*digit] Mem[pgh+20index+4digit](单次计算,无间接寻址)
  • 多级数组: M e m [ M e m [ u n i v + 8 ∗ i n d e x ] + 4 ∗ d i g i t ] Mem[Mem[univ+8*index]+4*digit] Mem[Mem[univ+8index]+4digit](双重内存访问,需先取指针)

二 结构体(重点)

在这里插入图片描述

# r in %rdi, idx in %rsi
leaq (%rdi,%rsi,4), %rax
ret
  • a 数组从偏移0开始,占用16字节(4个int,每个4字节)
  • i 从偏移16开始,占用8字节
  • next 指针从偏移24开始,占用8字节

存储特性

  • 结构体以连续内存块表示,一块的大小需要保证容纳所有字段。
  • 字段按声明顺序排列,即使采用其他字段排列顺序可能因对齐规则(即将介绍)而获得更紧凑的表示。
  • 编译器决定整体大小和字段位置,机器代码不识别结构体语义。

在这里插入图片描述

在这里插入图片描述

  • %rdi(64位)→ %edi(低32位)→ %dil(低8位)
  • %rax(64位)→ %eax(低32位)→ %al(低8位)
  • %rbx(64位)→ %ebx(低32位)→ %bl(低8位)

2.1 结构体内存对齐规则

规则类型说明示例
基本对齐每个成员按其自身大小对齐int按4字节对齐
结构体对齐整个结构体按最大成员大小对齐含double的结构体按8字节对齐
数组对齐数组按元素类型对齐int数组按4字节对齐
嵌套结构体按内部结构体的最大成员对齐嵌套结构体按其内部最大成员对齐
  • C语言常用数据类型字节占用表。
数据类型32位系统64位系统说明
基本整型
char11字符型
signed char11有符号字符
unsigned char11无符号字符
short22短整型
unsigned short22无符号短整型
int44整型
unsigned int44无符号整型
long48长整型
unsigned long48无符号长整型
long long88长长整型
unsigned long long88无符号长长整型
浮点型
float44单精度浮点
double88双精度浮点
long double8/12/1616长双精度(依赖编译器)
指针类型
void*48任意类型指针
int*48整型指针
char*48字符指针
其他类型
bool11布尔型(C99)
size_t48sizeof返回类型

在这里插入图片描述


#include <stdio.h>
  // 示例1:简单结构体
  struct Example1 {
  char a;        // 1字节
  int b;         // 4字节,需要3字节填充
  char c;        // 1字节
  // 总共:1 + 3(填充) + 4 + 1 + 3(填充) = 12字节
  };
  // 示例2:优化后的结构体
  struct Example2 {
  int b;         // 4字节
  char a;        // 1字节
  char c;        // 1字节
  // 总共:4 + 1 + 1 + 2(填充) = 8字节
  };
  // 示例3:含指针的结构体
  struct Example3 {
  int value;     // 4字节
  char* ptr;     // 32位:4字节,64位:8字节
  double d;      // 8字节
  // 32位:4 + 4 + 8 = 16字节
  // 64位:4 + 4(填充) + 8 + 8 = 24字节
  };

2.2 内存对齐:为何重要?

性能考量

  • 提升访存效率:CPU 按 4/8 字节块读取数据,对齐可减少内存访问周期。
  • 避免缓存行分割:防止单个数据横跨 64 字节缓存行,避免额外的内存访问和延迟。
  • 遵循硬件建议:如 Intel 架构建议避免数据跨越 16 字节边界,以优化指令执行。

简化系统设计

  • 规避跨页问题:确保数据不落在两个 4KB 虚拟内存页上,简化内存管理单元(MMU)的处理逻辑。

编译器的智能处理

  • 自动填充(Padding):编译器会在结构体成员间自动插入填充字节,确保每个成员都满足其对齐要求。
  • 保证对齐:最终结构体的整体大小也会是其最宽成员对齐值的整数倍,保证在数组中也能正确对齐。

2.3 通过结构体实现对齐

一个 N 字节类型的数据,其内存起始地址必须是 N 的倍数。

  • 2 字节类型 (short):地址必须是 2 的倍数,即二进制表示的最低位为 0(偶数地址)。
  • 4 字节类型 (int, float):地址必须是 4 的倍数,即二进制表示的最低 2 位为 00
  • 8 字节类型 (double, long, 指针):地址必须是 8 的倍数,即二进制表示的最低 3 位为 000

结构体内对齐策略

  • 单个元素对齐:每个成员按自身大小对齐

整体结构对齐要求 K

  • K = 结构体中最大成员的对齐要求
  • 示例:struct S1 中因 double v 存在,K = 8

地址与长度约束

  • 起始地址必须是 K 的倍数
  • 结构体总长度也必须是 K 的倍数

内部填充

  • 在 char c 后添加 3 字节填充,使 int i[2] 对齐到 4 字节边界
  • 总大小由 17 字节扩展至 24 字节(8 的倍数)
    在这里插入图片描述

2.4 结构体数组和元素访问

  • 单个结构体对齐占用分析。
    在这里插入图片描述
  • 成员顺序:double v(8B)、int i[2](8B)、char c(1B),K = 8,因此整体长度需为 8 的倍数。

  • 每个结构体实例起始地址均为单个结构体的倍数。数组连续存放,步长等于对齐后结构体大小(如 24 字节)。
    在这里插入图片描述
    在这里插入图片描述

2.5 成员排序优化

在这里插入图片描述

2.6 结构体对齐考试题(重点)

typedef struct {
char a;
long b; # long 类型 8 字节
float c; # float 类型 4 字节
char d[3];
int *e;  # int * 类型 8 字节
short *f; # short * 类型 8 字节
} foo;
  • 展示 foo 在 x86-64 Linux 系统上的内存分配方式。用不同字段的名称标注字节,并清晰地标明结构体的结束。用 X 表示结构体中作为填充分配的空间。
    在这里插入图片描述
  • 调整 foo 的元素顺序以最大程度节省内存空间。用不同字段的名称标注字节,并清晰地标明结构体的结束。用 X 表示结构体中作为填充分配的空间。
    在这里插入图片描述
posted on 2026-01-11 17:16  ljbguanli  阅读(2)  评论(0)    收藏  举报