|
用 BC4.5 写个 C的小程序,想反汇编过去看看,结果一开始不得其法,连相应的工具都找不到。因为是16位的DOS程序,现在一些调试工具似乎不支持。一度去找什么DOS 下的 SOFTICE,TR。而且还专门用虚拟机装了MS-DOS 7的版本,然后把工具拷到虚拟机里调试,反正就是按着工具的说明去做,竟然要么提示无法加载,要么就是 CPU 出错,郁闷! 其实一个调试的好工具近在眼前,就是在安装了 BC45 后自带的 TD4.2,用 BC 写的 C 程序再用自家的调试器是再合适不过了。所以花在找工具的时间相当于浪费掉了。
为了简单起见,也是为了初步掌握 TD 的使用方法及一些特性,所以就写个非常简单的 C程序进行调试。这个程序的主要目的是看反汇编后,汇编器是如何对程序进行子程序的调用,如何安排堆栈,如何对指针进行应用。程序如下(不需要调用任何函数,连基本的输入输出库都省掉了):
int main()
{
unsigned int StkTest[5] = {1,2,3,4,5};
unsigned int *ptr = &StkTest[0];
*ptr = 10;
ptr++;
*ptr = 20;
ptr++;
*ptr = 30;
ptr++;
*ptr = 40;
return 0;
} 为了方便调试时的比较,似乎是要把生成的 exe 文件和源代码文件放在一个目录底下,然后才能在调试器的 CPU 窗口中看到汇编代码以及相应的 C 程序代码。
汇编代码解析:
从进入 _main() 到 unsigned int StkTest[5] = {1,2,3,4,5}; 的代码是:
#1-0
enter 000e, 00
lea ax, [ bp - 0a]
push ss
push ax
push ds
push 0090
mov cx, 000a
call far f_scopy@ 说明:
enter 000e,0 ; 这个指令表明开始进入 _main() 中,指令中的第二个操作数 00 ,是指明 子程序的嵌套层数为0.
这条指令相当于执行了下面 3 条指令:
push bp
mov bp, sp
sub sp, 000e
每当进入一个子程序时,push bp 是必须的,这个压入站的 bp 是父程序的堆栈 bp。由于进入子程序后,子程序也有自己的堆栈,也要用到 bp ,所以要把父程序的 bp 压栈保存起来。
mov bp, sp 是让 bp 指向子程序堆栈的最高端处,也就是 bp 所压的地方,这样一来,以后就可以用 bp 方便的进行寻址了。
sub sp, 000e,这语句是说明腾挪出 0eH 个字节的堆栈空间,这里会往往会存放临时变量参数等东西。
call far f_scopy@ 调用 f_scopy@ 程序正是对应上面的 C 定义语句:unsigned int StkTest[5] = {1,2,3,4,5};
跟进去这个 CALL 看看编译器是如何安排这么一条语句的,相应代码为:
#1-1
push bp
mov bp,sp
push si
push di
push ds ;以上语句均为进入子程序的一般操作
lds si, [ bp + 06 ]
les di, [ bp + 0a ]
cld
shr cx, 1
rep movsw
adc cx, cx
rep movsb
pop ds ;以下为子程序返回时进行堆栈的释放
pop di
pop si
pop bp
retf 0008 说明:lds si, [ bp + 06 ] 和 les di, [ bp + 0a ]
像 lds , les 这样的指令是装载远指针指令。含义为:从源操作数(远指针)的低地址取到指针的偏移量送往目的操作数中,从高地址取到指针的段地址送往ds(lds),es(les) 中。
从上面的堆栈操作中可以看到,lds si,[ bp + 06 ] 从堆栈中取得 0090 送往 SI 中,原来的 DS 同样的送往 DS中,也就是说,DS 并没有发生变化。同样可以分析 les di, [ bp + 0a ] 指令。
至于说问什么是 0090 而不是别的其它什么数,我想这应该是有编译器来安排的,如果改成 0080 也是可以的。
从下面的代码分析中,很快的能知道,其实 DS:0090 处正是编译器把要初始化 StkTest[5] 这个数组的1,2,3,4,5 这 5个数从那连续的存放的,这应该只是个临时的存放点。而真正最终要存入到数组里面的是 es:di 这里。
而 es 就是先前压入栈的 ss,di 的值就是先前压入栈的 ax,而 ax 的值又是 #1-0 中的 bp - 0a 。由于 ss从来没有被改变过,所以1,2,3,4,5 这 5 个数字就是送往 sp - 0e所开辟的这块堆栈中的!换句话说,数组StkTest[]就是安排在这个堆栈中,数组的首地址 &StkTest 就是 ss:ax。
接着分析一下语句 unsigned int *ptr = &StkTest[0]; 在汇编中的处理,对应的代码为:
lea ax, [ bp - 0a]
mov [ bp - 0c ], ss
mov [ bp - 0e ], ax LEA 是装有效地址指令。在 LEA AX, [BP-0A] 语句中,[BP-0A] 是一个内存操作数,而这个内存操作数中 BP - 0A就是其对应的地址,所以 AX = BP-0A。
*ptr = &StkTest[0]-----这是一条给指针赋值的操作。在大模式编译下,ptr 为远指针。所以对远指针进行赋值操作,要对其 段地址 和 偏移量 同时进行赋值。
容易看出,ptr 自身的地址也就是安排在 enter 指令当初开辟的堆栈中(这个堆栈用来存放 5 个整数供10个字节,一个指针供 4 个字节,一共14即 0x0e 个字节)。
mov [ bp - 0c ],ss 是把数组的段地址赋给指针的高字地址处;mov [ bp - 0e ], ax 是把数组的偏移地址赋给指针的低字地址处。这样一来,就完成了赋值操作。
至于后面的指针移动修改操作,原理就是赋值操作,不再重复去分析了。
从上面的汇编代码中可以看到,在实模式下的远指针实质是 32 位的。它包含了它指向数据的 段地址+偏移量。 |
|