跳转至

数值数据

游戏黑客需要的大多数值(比如玩家生命值、魔法值、位置和等级)都由数值数据类型表示。由于数值数据类型也是所有其他数据类型的构建块,理解它们极其重要。幸运的是,它们在内存中的表示相对直接:它们可预测地对齐,并具有固定的位宽。表 4-1 显示了你会在 Windows 游戏中遇到的五种主要数值数据类型,以及它们的大小和范围。

类型名大小有符号范围无符号范围 8 位−128 到 1270 到 255 16 位−32,768 到 32,7670 到 65,535 32 位−2,147,483,648 到0 到 4,294,967,295 2,147,483,647 64 位−9,223,372,036,854,0 到 18,446,744,07 775,808 到 9,223,373,709,551,615 2,036,854,775,807 32 位+/−1.17549×10³不适用⁸ 到 +/−3.40282×10³⁸

数值数据类型的大小可能因架构甚至编译器而异。由于本书专注于破解 Windows 上的 x86 游戏,我使用微软标准化的类型名称和大小。除 float 外,表 4-1 中的数据类型都按小端序存储,意味着整数的最低有效字节存储在整数占用的最低地址中。例如,图 4-1 显示 DWORD 0x0A0B0C0D 由字节 0x0D 0x0C 0x0B 0x0A 表示。

图 4-1 小端序排序示意图

float 数据类型可以保存混合数,所以它在内存中的表示不像其他数据类型那么简单。例如,如果你在内存中看到 0x0D 0x0C 0x0B 0x0A,而该值是 float,你不能简单把它转换成 0x0A0B0C0D。相反,float 值有三个组成部分:符号(位 0)、指数(位 1–8)和尾数(位 9–31)。

符号决定数字是负还是正,指数决定小数点要移动多少位(从尾数之前开始),尾数保存值的近似值。你可以通过求值表达式 尾数 × 10ⁿ(其中 n 是指数)来取回存储的值,如果设置了符号位,再把结果乘以 −1。

现在让我们看看内存中的一些数值数据类型。清单 4-1 初始化了九个变量。

unsigned char ubyteValue = 0xFF;
char byteValue = 0xFE;
unsigned short uwordValue = 0x4142;
short wordValue = 0x4344;
unsigned int udwordValue = 0xDEADBEEF;
int dwordValue = 0xDEADBEEF;
unsigned long long ulongLongValue = 
0xEFCDAB8967452301;
long long longLongValue = 0xEFCDAB8967452301;
float floatValue = 1337.7331;

清单 4-1:在 C++ 中创建数值数据类型的变量

从顶部开始,这个示例包含 char、short、int、long long 和 float 类型的变量。其中四个是无符号的,五个是有符号的。(在 C++ 中,float 不能是无符号的。)结合目前学到的知识,仔细研究清单 4-1 中的代码与图 4-2 中内存转储之间的关系。假设变量在全局作用域中声明。

图 4-2 我们的数值数据的 OllyDbg 内存转储

你可能会注意到有些值看起来被任意隔开。由于处理器访问位于地址大小(x86 中是 32 位)倍数地址的值要快得多,编译器用零填充值,以便在这样地址上对齐——因此,填充也叫对齐。单字节值不会被填充,因为访问它们的操作无论对齐如何都执行相同。

记住这一点,看看表 4-2,它提供了图 4-2 中内存转储与清单 4-1 中声明变量之间的一种内存到代码对照。

地址大小数据对象 1 字节 1 字节 2 字节 2 字节 2 字节 2 字节 2 字节

4 字节

4 字节

4 字节

8 字节

8 字节

地址列列出内存位置,数据列告诉你那里确切存储了什么。对象列告诉你每块数据与清单 4-1 中的哪个变量相关。注意 floatValue 在内存中被放在 ulongLongValue 之前,尽管它是清单 4-1 中最后声明的变量。这些变量在全局作用域中声明,编译器可以把它们放在任何地方。这个特殊的移动可能是对齐或优化的结果。