边看边翻译,其中敲字错误或表述有误再所难免。如果您发现错误,而且也有时间,敬请留言告之,以便更正。 请尊重作者及译者工作之艰辛,若转摘,请务必注明出处如下:
------------------------------------------------------------------
《代码反汇编:IDA Pro与Soft ICE之应用》
英文名称:Disassembling Code IDA Pro and Soft ICE
译者:罗祥勇 <E-mail:solo_lxy@126.com>
出自:CSDN Blog <背你走天涯>专栏
------------------------------------------------------------------
第一章 反汇编入门
汇编器和反汇编器是一个事物的两方面。汇编器将用汇编语言写的程序转换成机器能够理解的二进制代码,而反汇编器将二进制模块转换成连续的汇编指令。因此,为了分析反汇编的代码就有必要了解机器码,他们是汇编语言的二进制表示。同时,了解数据在计算机内存中的呈现结构也是极为重要的,这里所说的数据呈现结构即为 Windows 系统所写的程序的结构。这里讨论的所有主题都将在本章中予以提及。
1.1 计算机内存中的信息表示
本节的主要目的是为了描述做为数字的数据在计算机中内存中是如何表示的。
1.1.1 审视内存
列表 1.1 一个能输出内存转储的简单程序
注意:所有的程序都使用 Microsoft Visual C++ 编译器(在翻译的过程中我使用的是 VC 6.0 和 GCC 4.3.3, 而原作者使用的是 Virtual Stdio .net 2003, 之所以选择 VC 6.0 ,主要是因为我实验用的 Window Xp 系 统是装在虚拟机中的,要是再装 Virtual Stdio .net 2003 空间就不够了)。在我看来, 它是可用的最好的编译 器。某些特殊情况下我会再介绍。
列表 1.1 中的程序将会输出指定内存区域的内容,这块区域从存储变量的区块开始。这里的内存区域内容可以输出到任何设备,俗称内存转储 (dump) 。输出到屏幕的内存区域用于存储程序中的变量。
编译程序,然后开启控制台会话并运行之。控制台屏幕上将会显示由十六进制数据( hex )组成的一个列表。 ( 图 1 - 1)
图 1 - 1 列表 1 - 1 程序输出的内存转储
观察这段内存转储,我们会发现它包含了我们在硬编码到程序中的变量值( 0x1667, 依倒序存储,低位字节存储在低地址处)。这些数据是什么?怎么可能理解这些十六进制的数据呢?这些问题(也就是上面的,数据在计算机内存的呈现形式)可能一些高级用户会觉得很低级。大部分的读者可能已经掌握了这些概念,如果是这的话,你可以略过 1.1.2 和 1.1.3 小节。
1.1.2 数值表示法
十进制表示法
大部分人都知道在孩提的时候都知道十进制表示法。二进制表示法对人类来说并不自然,但对计算机来说就非常自然了。计算机内存由一个有两种状态的元素组成。按照惯例,其中一中状态被指派为 0 ,另外一种状态被指派为 1 。结果,内存中的所有信息都由二进制的 0 、 1 组成。另外,计算机内存被分为块 (block) ,每个块由八项组成。这些块被称之为单元( cell )或字节 (byte) 。二进制表示法中的一个数字称之为位 (bit ,代表二进制数字 ) 。因此,每个内存单元由八个二进制数字组成,或由八个位组成。
回到十进制系统上来,它基于 10 个数字。也就是,每个十进制数字都可以用十的幂表示,每个数字所在的位置作为这种表示法的系数。考虑如下的例子:
4567 = 4×l03 + 5×l02 + 6×l01 + 7×100
还句话说,每个数字的贡献的大小取决于它在整个数值中的位置,位置计数从右至左。这样的数字系统被称之为基于位置的数字系统。
二进制表示法
二进制表示法也是一种基于位置的数字系统。一次,任何二进制数字可以用 2 的幂表示,例如:
11101001 =1×27 +1×26 +1×25 +0×24 +1×23 +0×22 +0×21 +1×20
这种书写二进制数值的方法实际上是将它转换为其他数字系统的方法。例如,如果你将它转换成十进制表示法,你将得到数字 233 。
将十进制数值转换为二进制表示有一点困难。我们可以通过如下的算法完成:
1 、将数值除以 2 ,将余数作为下一个最重要的位。
2 、如果结果大于 1 ,返回到步骤 1
3 、最终的二进制数字由每次除法后的余数组成和最后的结果组成。
经过以上的算法计算后,我们可以得到 350 的二进制表示法 101011110 。
为了将不同表示法的数字在汇编语言中区分出来,单字符 B 后缀被用
来表示二进制数。对于十进制数,使用后缀 D ,但整个前缀可以被忽略。对于十六进制数,后缀为 H 。例如: 10000B 、 345H 、 100 ,等等。
类似十进制分数,二进制分数也可以使用类似的方法表示。例如,二进制数 1001.1101 可以如下表示:
1×23 +0×22 +0×21 +1×20 +1×(1/21 )+1×(1/22 ) + 0×(l/23 ) + 1×(l/24 )
二进制数字也可以使用简单的算术操作将其转换为十进制数。例如,为了将 1001.1101 转换为十进制数,可以使用如上的表示法进行转换,结果你会得到: 9.8125 。
十进制数也可以简单的转换为二进制。数字的小数部分和整数部分分开处理。转换整个部分的算法上面已经提及。小数部分按照如下的方法进行转换:
1 、将小数部分乘以 2 。
2 、将结果得到的数字的整数部分分割出来(为 1 或 0 )。将其作为小数点后的第一个数字。
3 、如果结果数字的小数部分不为 0, 返回到步骤 1 ;否则,结束计算。指定计算的精度也是可以的-也 就是说小数点以后的数字多少-当精度达到时可以停止计算。
现在,考虑一个将十进制数转换为二进制数时的实际情况。如将 105.406 转换为二进制。整数部分的转换上面已经介绍过了。因此, 105 的二进制表示为 1101001 。下面使用上面介绍的算法转换小数部分。计算过程表述如下。注意,在这个例子中,如果达到你想要的精度后可以停止计算。
0.406×2 0×(1/21 )
0.812×2 1×(1/22 )
0.624×2 1×(1/23 )
0.248×2 0×(1/24 )
0.496×2 0×(1/25 )
0.992×2 1×(1/26 )
0.984×2 1×(1/27 )
0.968×2 1×(1/28 )
0.936×2 1×(1/29 )
通过如上的计算,得到如下的结果:
105.406≈1101001. 011001111
因此,将十进制数字转换为二进制数存储到计算机内存中是有一定的精度损失的。
十六进制系统
十六进制表示法比十进制表法更为紧凑,而且将其转化为二进制数时更为容易,反之也是一样。最终,十六进制较其他表示法更贴近计算机的内存结构。十六进制数使用数字 :0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F 。将十进制数转换为十六进制数的方法和上面小节介绍的方法是一样的;唯一不同的是,这里基于 16 而不是 2 。希望你能很容易的将相应的算法熟练掌握。
下面考虑将十六进制数转换为二进制数,或者相反。这里使用的主要原则其实很简单:二进制的 4 个数字( a quaternion, 四元组),对应一个十六进制数,反之亦然。图 1.2 显示了将 10101101 二进制数转换为十六进制数的过程:
图 1.2 将二进制数转换为十六进制数
图 1.3 显示了将 14A 转换为二进制的反向过程。
图 1.3 将十六进制数转换为二进制数
正如刚刚提及到的,十六进制表示法更适合于计算机内存结构。计算机内存可以很容易被分为包含八个位的单元。因而, 8 位相当于两个十六进制数。例如, 1245H 将占用两个单元,低位单元包含 45H ,而高位单元包含 12H 。
将十六进制小数转换为二进制小数,或者反之,也是非常简单的;同转换十进制数的方法是一样的。小数部分的转换依照一下原则:一个十六进制数对应 4 个二进制数。考虑二进制数 101.10001 ,并将其转换为十六进制表示法。依照这种方法,结果将是 101>0101>5 。小数部分,可以如下表示: 10001>1000100>88( 注意,小数部分,四元组从左至右计数 ) 。结果二进制的 101.10001 以十六进制表示就为 5.88 。较之于整数部分,小数部分的转换要从左至由将二进制数 4 个分组,不足四个的以 0 填充,而整数部分则是从由至左。
1.1.3 计算机内存中的数字表示
无符号的整数
无符号的整数在内存中的表示原则有点琐碎:
1 、数字必须转换为二进制
2 、必须考虑存放数字的内存大小。就像刚刚提过的,最方便的方法是将它转换为十六进表示,之后它要占用多少内存就比较清楚了。依据惯例,内存被划分为一个个的字节,双字节( word ) , 四字节( double word) 。汇编语言提供指定的指令为将要存储的数字和变量保留内存:
■ Name1 DB value1; 保留一个字节
■ Name1 DW value1; 保留两个字节
■ Name1 DD value1; 保留四个字节
■ Name1 DQ value1; 保留八个字节
■ Name1 DT value1; 保留十个字节
当处理变量的时候,必须指定其范围,在这个范围内变量可变,变量信息可从此范围中获取。因为现代 Intel 处理器倾向于操作 32 位数字,所以现在最好的方法是将变量的大小控制到和它一样。
考虑如下的 C 代码片段,列表 1.2
列表 1.2 C 代码片段
BYTE e = 0xab;
WORD c = 0x1234;
DWORD b = 0x34567890
__int64 a = 0x6178659812324572
这个片段定义了四个变量: e 为一字节变量, c 为二字节变量, b 为四字节变量1 , a 为八字节变量。使用列表 1.1 中的程序,输出保存变量的内存区域的内容。你将会看到如下的一系列字节:
ab 00 00 00 34 12 00 00 90 78 56 34 00 00 00 00 72 45 32 12 98 56 78 61
仔细观察这一系列字节,你将会好不困难的发现它们。通过观察这一系列字节我们可以得到如下比较重要的结论:
■ 调用列列表 1.1 的程序,内存中的内容将会从低地址向高地址依次显示。因此,每个数字的低位 字节在内存中存放在低地址处。四字节数字的低位字节在低地址。 64 位的数字也是一样的道理。这个 问题对于分析二进制代码非常重要。稍后,我们将在内存区域中识别一下变量。
■ 正如你所看到的,每个变量占用的内存大小都是 4 的倍数。每一个变量初始化以后,编译器插入 一个特殊的指令,将其 32 位对齐。然而,情况并不这么简单,对齐的方式根据变量出现的先后顺序不 同也是不一样的。我们将在 3.1.1 对此再做详细阐述。
例子 像 A890H 这样的 16 位数,在内存中以: 90 A8 的序列曾现。 32 位数 67896512H ,将存储为: 12 65 89 67 。而 64 位数 F5C68990D1327650H, 存储为 50 76 32 D1 90 89 C6 F5 。
带符号整数
因为内存只存储二进制数字,因此可以使用一个单独的位来表示数字的符号。例如,如果你有一个内存字节,你可以在- 127 至 +127 之间 (11111111~0111111) 进行算术运算。这种方法并不是太糟,然而,这必须得为无符号和带符号数引入单独的附加操作。还有一种可替代的表示带符号数的方法。在这种构建数字的算法中,某个确定的数字被当作正,与之相反的符号可以通过如下的式子计算得来: a + ( -a ) ≡ 0 。
当处理单字节数字时,很自然的就会想到 1 与二进制的 00000001 相等。通过解求 00000001 + x = 00000000, 然而的出来的结果第一眼看上去相当诡异: x = 11111111 。还句话说,使用这种方法, -1 必须要等于 11111111 (十进制的 255 用十六进制表示为 FFH )。现在有必要对这种理论做更细致的说明了。很显然, -1-(1)=2 ,然而根据如上理论, -2 必须等于 11111110,00000010 必须等于 +2 。检查一下这些是数字是否符合如上理论,结果你会发现 11111110 + 00000010 = 00000000 。因此,不证的等式 +2 + (-2) = 0 是正确的。这就以为着被选用的方法是可行的 ( 表 1.1) 。
表 1.1 无符号单字节数
-
正数
二进制表示
负数
二进制表示
+0
00000000
-0
00000000
+1
00000001
-1
11111111
+2
00000010
-2
11111110
+3
00000110
-3
11111101
+4
00000100
-4
11111100
+5
00000101
-5
11111011
…
…
…
…
+120
01111000
-120
10001000
+121
01111001
-121
10000111
+122
01111010
-122
10000110
+123
01111011
-123
10000101
+124
01111100
-124
10000100
+125
01111101
-125
10000011
+126
01111110
-126
10000010
+127
01111111
-127
10000001
+128
一字节数无法表示
-128
10000000
仔细观察表 1.1 。根据这种理论我们会得出怎么样的结论呢?代符号单字节数的范围为 -127 至 +127 。
单字节数既可以解释为带符号的,也可以被当做无符号的数。根据第一种方法(带符号), 11111111 等于 -1 ;若看作无符号数则为 255 。因此,所有事情依赖这种解释。更加有趣的是,无论是带符号还是无符号数的加法、减法处理方式都是一样的。因此,处理器对每种操作只对应一个指令: ADD 和 SUB 。当处理指定操作时,可能出现溢出或出现了不存在的位的情况2 ;这个问题值得单独关注。我们可以通过保留一个或多个单元来解决此事。如扩展到 2 和 4 个字节。通过计算,无符号 16 位数的最大值为 65535 ,带符号十六位数表示范围从 -23768 至 +32767 。
考虑列表 1.3 中的变量。
列表 1.3 一系列变量
signed char e = -2;
short int c = -3;
int b = -4;
__int64 = -5;
正如你看到的,这里所有的数字都是带符号的数,而且都为负。当显示这块内存时,我们会获得如下字节序列:
FE 00 00 00 FD FF 00 00 FC FF FF FF 00 00 00 00 FB FF FF FF FF FF FF FF
可以看到, 8 位变量 -2 在内存中表示为 FEH,16 为数 -3 表示为 FFFDH,32 位数- 4 表示为 FFFFFFFCH,64 位数 -5 表示为 FFFFFFFFFFFFFFFBH 。而且回忆以前提及的,我们可以看到低位 4 字节比高位字节在内存中的地址要小。
实数
为了在 Intel 处理器(算术协处理器3 )中使用实数,他们在内存中必须依照正规的格式表示。一般来所说,一个数字的正规格式表示如下:
A = ( NS )× M × N q
这里, NS 表示数字符号; M 代表 mantissa (尾数),用于小于 1 的情况; N 表示数字系统的底数; q 表示指数,并且可以带正或负。使用这种表示法的数字被称之为浮点数 (Floating-point numbers) 。可考虑一个浮点数的例子,将 5.75 表示为正规形式。首先,必须将此数字表示为二进制。这个任务简单: 5 的二进制表示为 1001,0.75 等于 (1/2) + (1/4) 。还句话说, 5.75 = 1001.11B 。此外, 1001.11B = 1.00111 × 2 3 。因此,最终的正规表示法由一下各个部分组成: NS = +1, M = 1.00111, N = 2 , q=3 。注意使用这种表示法的数字的尾数第一个数字一定为 1 ;这样就没必要存储它了。 Intel 使用的格式就基于这种可能。除此之外,还必须记住, q 在内存中以数字形式存储,并且必须保证他一定为正。 Intel 处理器可以处理一下三种类型的实数:
■ Short real number -用于保存短实数,分配 32 位, 0~22 位用于尾数, 23~30 为用于存储 q 与 127 的和。位 31 用于保存符号( 1 为负, 0 为正)。
■ Long real number -这里,分配 64 位。 0~51 位用于尾数, 52~62 为用于存储 q 与 1024 的和。位 63 用于保存符号( 1 为负, 0 为正)。
■ Extend real number -分配 80 位。 0~63 位用于尾数, 64~78 为用于存储 q 与 16383 的和。位 79 用于保存符号( 1 为负, 0 为正)。
考虑一个内存中表示的一个浮点数的例子。假设下面的变量是某段 C 程序中的片段 :
float a = -80.5;
浮点数类型为短实数。其内存占用 32 位。现在,使用以前的方法查看起内存。以下为其十六进制表示:
00 00 a1 c2
为了使这种表示法容易理解,将其转换为二进制:
00000000 00000000 10100001 11000010
为了使这种表示法更容易理解,重新组织数据使其以重要为开始,以便强调尾数,指数和符号:
11000010 10100001 00000000 00000000
现在,分割出尾数。回想一下, 23 位用于保存尾数。因此,我们得到 :0100001 。注意,尾数位从最重要为开始计数(本例中为位 22 ),尾部的 0 被丢弃,因为真个尾数位于十进制小数点的右边。然而,我们获得的数字并不完全精确的代表尾数。上面曾经提到过,尾数的第一个数字一直为一, Intel 表示法中将其忽略了,所以为了获取完整的尾数,我们必须将其恢复。这样以来我们得到尾数: 1.0100001B 。因为地 31 位为 1 ,所以该数为负数。接下来是 p ,其二进制为 10000101B ,十进制表示为 133 。为了获得短实数的指数,将这个数字语 127 相减,
结果为 6 。结果,从尾数中我们得到分数部分,十进制的小数点为需要向右移动 6 位。结果为 1010000.1B 。其十六进制表示为 50.8H ;如果你将此数字转换为十进制,结果为 80.5 。
为了亲手实践一下,转换如下字节:
00 80 FB 42
看看你得到的结果是否为 125 。 75 。
通过本节内容,我们可以得出如下结论,如果在程序中使用实数,在处理其他动作之前需要先处理实数,因为在将实数存储到内存之前需要对其进行正规化。
二进码十进数 Binary-Coded Decimals
二进码十进数( BCD )表示法是一种在计算机内存中保存十进制数的特殊方法。在这种方法中,每个十进无符号数用 4 为位二进制数(半字, nibble )表示。 Intel 处理器支持两种类型的 BCD 数字:打包的和未打包的。
■ 打包数的每个数字用半个字节编码。这种情况下,高位的四个 bit 保存高位 BCD 数。因此,一个字 节能表示的数值范围为 0~99 。例如, 56 可以用 01010110B 表示。
■ 未打包数字使用一个字节表示。这种情况下,只使用低位的 4 个 bit 保存 BCD 数字,高位的 4bit 区 全部为 0 。因此一个字节可以表示的数值范围为 0~9 。
BCD 码如今很少用在编程中;因此,对这个主题,我将不再涉及。
1 BYTE 为一个简单的无符号字节, WORD 为无符号的短整形( unsigned short int ), DWORD 为无符号的整形( unsigned int )。对于这些类型的定义,可以在头文件 windows.h 中找到。
2 可以很容易的证明同时表示无符号和带符号数是可能的,因为每个数字被限制为 1 或多个字节。
3 从 Intel 486 以后,算术协处理器是必须的,而且内置于微处理器中。