🎨 颜色标记说明

  • 粉色:特别重要、主线必掌握、优先复盘的内容。
  • 蓝色:核心概念、术语、分类名。
  • 橙色:需要关注的边界、易错点、注意事项。
  • 绿色:解释类内容、帮助理解知识点。

返回:全书总览 · 上一章:变量与内存 · 下一章:结构体和类

数组的内存本质

数组是一段连续的同类型元素。第 i 个元素地址为:

1
element_address = base + i * sizeof(element)

x86/x64 的比例因子寻址非常适合表达这个公式:

1
mov eax, dword ptr [base+index*4]

比例因子只能直接取 1、2、4、8,其他元素宽度常由 IMULLEA、移位和加法组合计算。

局部数组与全局数组

局部数组通常基于栈帧地址连续访问,全局数组通常基于模块静态地址连续访问。识别时观察:

  • 多次访问是否共享同一个基址。
  • 索引是否按固定元素宽度增长。
  • 相邻元素是否使用一致的操作宽度。
  • 是否存在初始化循环或批量复制。

连续内存不一定就是数组,也可能是结构体、对象或临时缓冲区,仍需结合“元素类型是否一致”判断。

数组作为函数参数

C/C++ 中,数组作为普通函数参数时通常退化为指向首元素的指针:

1
2
3
void print(const int values[], size_t count);
// ABI 层面通常等价于:
void print(const int* values, size_t count);

函数不能仅从指针本身知道元素个数,因此常见附加信息有:

  • 独立的长度参数。
  • 结束指针。
  • 终止符,如 C 字符串末尾的 \0
  • 固定协议或结构体中的长度字段。

恢复数组参数时,指针和长度必须一起追踪;只恢复一个裸指针会丢失最重要的边界信息。

指针步长

高级代码中的 p + 1 不是地址只加 1,而是增加所指类型的大小:

1
new_address = p + 1 * sizeof(*p)

反汇编只能看到数值运算。分析者需要从步长、解引用宽度和后续用途反推出指针类型。

多维数组

C/C++ 多维数组默认按行优先连续存储。对于 T a[rows][cols]

1
&a[i][j] = base + (i * cols + j) * sizeof(T)

三维数组继续展开:

1
&a[i][j][k] = base + ((i * dim2 + j) * dim3 + k) * sizeof(T)

逆向时先把乘加表达式拆开,找出固定维度和元素宽度,再恢复下标语义。

指针数组与数组指针

1
2
char* names[4];      // 指针数组:四个 char* 元素
char (*grid)[16]; // 数组指针:指向含 16 个 char 的一行

关键区别:

  • 指针数组先按指针宽度取出一个地址,再访问目标数据。
  • 数组指针跨行时步长是整行大小。
  • 二级指针的第二次寻址来自内存中的指针值,多维数组的行地址可由固定公式直接算出。

字符串数组

两种常见布局:

1
2
char table[3][16];
const char* table[3];

前者字符数据位于一块连续缓冲区;后者先读取指针表,再跳到各字符串地址。静态分析时可通过交叉引用和地址落点验证。

函数指针

函数指针通常表现为间接调用:

1
2
call eax
call qword ptr [table+rcx*8]

常见来源包括回调函数、分派表、IAT 和虚函数表。恢复时记录:

  • 间接目标从哪里获得。
  • 调用前怎样准备参数。
  • 目标集合是否有限。
  • 返回值如何使用。

间接 CALL 不等于虚函数。必须先证明基址来自对象的虚表指针,才能把它归类为虚调用。

越界与安全边界

机器指令不会自动理解 C 数组边界。越界访问可能覆盖相邻局部变量、对象字段、堆元数据或控制数据。分析未知程序时重点检查:

  • 索引是否在使用前比较。
  • 长度单位是元素数还是字节数。
  • 乘法是否可能溢出。
  • 终止符是否包含在容量内。

学习检查清单

检查项 状态
能从比例因子寻址恢复一维数组索引 待复盘
能写出二维数组行优先寻址公式 待复盘
能说明数组参数为什么需要额外长度 待复盘
能区分指针数组、数组指针和二级指针 待复盘
能识别间接调用并追踪候选目标 待复盘