008、数组和指针的寻址
数组的内存本质
数组是一段连续的同类型元素。第 i 个元素地址为:
1 | element_address = base + i * sizeof(element) |
x86/x64 的比例因子寻址非常适合表达这个公式:
1 | mov eax, dword ptr [base+index*4] |
比例因子只能直接取 1、2、4、8,其他元素宽度常由 IMUL、LEA、移位和加法组合计算。
局部数组与全局数组
局部数组通常基于栈帧地址连续访问,全局数组通常基于模块静态地址连续访问。识别时观察:
- 多次访问是否共享同一个基址。
- 索引是否按固定元素宽度增长。
- 相邻元素是否使用一致的操作宽度。
- 是否存在初始化循环或批量复制。
连续内存不一定就是数组,也可能是结构体、对象或临时缓冲区,仍需结合“元素类型是否一致”判断。
数组作为函数参数
C/C++ 中,数组作为普通函数参数时通常退化为指向首元素的指针:
1 | 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 | char* names[4]; // 指针数组:四个 char* 元素 |
关键区别:
- 指针数组先按指针宽度取出一个地址,再访问目标数据。
- 数组指针跨行时步长是整行大小。
- 二级指针的第二次寻址来自内存中的指针值,多维数组的行地址可由固定公式直接算出。
字符串数组
两种常见布局:
1 | char table[3][16]; |
前者字符数据位于一块连续缓冲区;后者先读取指针表,再跳到各字符串地址。静态分析时可通过交叉引用和地址落点验证。
函数指针
函数指针通常表现为间接调用:
1 | call eax |
常见来源包括回调函数、分派表、IAT 和虚函数表。恢复时记录:
- 间接目标从哪里获得。
- 调用前怎样准备参数。
- 目标集合是否有限。
- 返回值如何使用。
间接 CALL 不等于虚函数。必须先证明基址来自对象的虚表指针,才能把它归类为虚调用。
越界与安全边界
机器指令不会自动理解 C 数组边界。越界访问可能覆盖相邻局部变量、对象字段、堆元数据或控制数据。分析未知程序时重点检查:
- 索引是否在使用前比较。
- 长度单位是元素数还是字节数。
- 乘法是否可能溢出。
- 终止符是否包含在容量内。
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能从比例因子寻址恢复一维数组索引 | 待复盘 |
| 能写出二维数组行优先寻址公式 | 待复盘 |
| 能说明数组参数为什么需要额外长度 | 待复盘 |
| 能区分指针数组、数组指针和二级指针 | 待复盘 |
| 能识别间接调用并追踪候选目标 | 待复盘 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ruiqy~!





















