学习任何一门语言都不能少的了 debug ,汇编也是。
debug 程序执行过程
下面我们就依据这几个功能来跟踪一下程序的执行过程。
debug 对我们来说非常重要,有很多代码细节和问题通过肉眼是观察出来的,我们肉眼可能能够判断一些简单的程序问题,但是对于很多隐藏较深的问题,还是要依据 debug 才能发现。
下面是一段汇编代码,这段汇编代码我之前的文章中也给大家写过。
新建文本文件,把代码 cv 过去,然后右键保存,使用 dosbox 将其编译为 1.obj 文件,链接为 1.exe 文件后,我们使用 debug 1.exe
命令来分析一下这段程序,并用 -r 命令来看一下初始的寄存器情况。
程序初始状态下,可以看到 CX 中的数据为 000F,这也表示着程序的长度是 000F,1.exe 中共有 15 个字节,CX 中的内容为 000FH。
好,现在我们已经知道程序被成功的载入内存并运行起来了,但是我们现在先不妨想一下,被链接成为 EXE 的程序会被装入内存的哪个地方的呢?我们怎么知道程序被装入在哪里呢?
程序装载的过程分下面几步:
- 首先程序会从内存中找到一块区域,记为初始地址 SA,此时的偏移地址为 0 的这样一块足够容量的内存区域。
- 在这段区域内的头 256 个字节中,会创建一块称为程序段前缀(Program Segment Prefix ,PSP)的区域,这块区域被 DOS 用来和被加载的程序进行通信。
- 从这块程序的 256 个字节开始处,也就是在 PSP 程序段前缀的后面,程序会被加载到这里,此时程序的初始地址是 SA + 10H,偏移地址为 0 。也就是 SA + 10H : 0,所以程序的初始地址就是 CS = 076AH ,IP = 0000H。
程序被装入内存后,由 DS 段寄存器存放着内存区的段地址,此时内存区域的偏移量为 0 ,所以此时的物理地址为 SA * 16:0,我们并不用知道真实的 DS 是多少,反正都是由操作系统和 DOS 分配的。
然后这个内存区域的前 256 个字节被用于存放 PSP ,所以程序的物理地址为 SA * 16 + 256 : 0 。
SA * 16 + 256 = SA * 16 + 16 * 16 = (SA + 16) * 16 ,转换为 16 进制就是 SA + 10H,所以物理地址就是 SA + 10H : 0。
我们上面 debug 1.exe 之后可以看到,DS 段寄存器的值为 076AH ,而 CS 段寄存器的值为 076BH ,正好符合 076A * 16 + 10 = 076BH (注意这里的 * 16 就是左移 4 位的意思,之前文章中也解释过原因。)
我们使用 -u 指令可以看到完整的汇编源代码。
上图中用红框圈出来的就是我们这段汇编程序的源代码,可以看到这是一个程序段,程序段的段地址始终为 076A,偏移地址在不断变化。
我们使用 -t 命令来单步执行以下这段程序,如下图所示。
(为了连续的观察一下程序的执行结果,我索性直接把主要的程序步骤执行完了。)
这段程序就是 mov 和 add 的基本使用,将 0123 送入 AX 寄存器,将 0456 送入 BX 寄存器,对 AX 寄存器执行 AX = AX + BX ,再对 AX 执行 AX = AX + AX。
程序继续向下执行,当执行到 int 21H 处,程序执行完毕,此时要使用 -p 命令结束程序的执行,如下图所示。
当显示 Program terminated normally 时,表示程序正常结束,这里大家先不用考虑为什么执行到 int 21 处才执行 -p 命令,也不用关心 mov ax,4c00 和 int 21 是什么意思,大家先记住就行。
由于程序装载的过程是 command 将程序装载进入内存,然后 debug 程序对 exe 程序其进行跟踪,所以程序退出后也是先从 exe 程序退出到 debug 程序中,由 debug 程序再退回到 command 程序中。
下面再分析一段程序,汇编原代码
仍然是将其保存为 test.txt,然后执行编译和链接操作,将其生成可执行文件 test.exe,观察其执行过程。
我们先使用 -r 查看一下初始寄存器的内容。
主要观察一下 CX 、DS 、CS 和 IP 的值,是否和我们上面描述的一致,CX 存放程序长度,DS 存放程序段地址,CS 存放程序初始地址,IP 存放程序偏移地址。
再使用 -u 看一下 exe 程序的源代码,这个 exe 程序是经过编译和链接之后的程序。
我们来分析一下这段,这是一段栈段的入栈和出栈的程序,首先
是设置栈段的栈顶指令,执行完成后会设置栈顶的物理地址为 20000 H ,即 SS:SP = 2000:0000。
我们执行这个程序的过程中,发现 mov sp,0 这个指令为什么没有出现呢?难道是我们漏写了?查看了一下,源代码确实是有这条指令的,难道是没有执行?
为了验证这个假设,我们重新 debug 一下这段程序,然后先把 SP 的值进行修改,如下图所示。
刚开始,我们使用 -r 把 sp 的值改成 0002,然后单步执行,在执行到 mov ss,ax 之后,发现 SP 的值变为 0000,这也就是说 mov sp,0 这条指令其实是执行了的,只是 debug 模式下没有显示而已。
程序继续向下执行,下面是两个 pop 出栈操作。
pop ax 和 pop bx 做了两件事:把寄存器清空;栈顶位置 + 2 ,所以 ax 和 bx 寄存器的内容为 0 ,并且 SP = SP + 2 ,执行后 SP = 000E。
之后是两个 push 操作,把出栈的两个寄存器再进行入栈,如下图所示。
push 操作也做了两件事情,将寄存器入栈,SP = SP - 2,由于 ax 和 bx 已经 pop 出栈了,所以寄存器内容为 0 ,最后再进行 pop 操作,然后再结束程序的执行过程。
我们再来看一下 PSP 的情况,由于程序被装入的时候前 256 个字节是 PSP 所占用的,此时 DS(SA)处就是 PSP 的起始地址,而 CS = SA + 10H ,也就是 CS = 076AH。
debug 循环程序
下面我们来 debug 一下循环程序,看看有哪些有意思的细节。
现在有这样一道问题,计算 ffff:0006 单元中的数乘 3 ,让结果存储在 dx 中。
针对这个问题,有几个点需要思考:
- 我们知道 ,8086 汇编语言中单个存储单元所能存储的最大值是 8 位,一个字节长度,范围是 0 - 255 之间,而一个寄存器 dx 中可容纳的最大值是 16 位,两个字节长度,范围是 0 - 65535,即使 255 * 3 也小于 65535,很显然乘以 3 之后,dx 中能够存放的下。
- 数乘 3 相当于是循环做 add 自身操作 3 次,所以需要用加法来实现乘法,可以直接使用 dx 进行累加,不过需要一个 ax 来进行中转。
- ffff:6 内存单元是一个字节单元,而 ax 寄存器能容纳的是一个字单元,无法直接赋值,该如何做呢?因为 ax 可以看做 al 和 ah ,而 al 和 ah 又是两个单独的寄存器,它们之间不会发生值溢出,所以让 ah = 0 ,al = 内存单元的值即可。
所以这段汇编程序的代码如下
编写完毕,编译链接成 exe 程序后,对其进行 debug xxx.exe 操作。
我们来看下程序的执行过程。
前两段没毛病,设置 DS 段寄存器的值为 FFFF 。然后继续向下执行
执行到 mov al,[6] 的时候我发现,怎么 AX 寄存器中的内容变成 0006 了?我不是想要把 06 放入 ax 中啊,我是想把 ffff:06 内存单元中的值放入 ax 中啊,我突然意识到编译器是个傻子。
经过我认真仔细细心耐心用心的排查了一番问题之后,我方才大悟,原来我是个傻子!不知道各位小伙伴们看出来我代码的问题了吗?
我怎么敢在源程序中把立即数当做内存偏移地址来用呢?必须要用 bx 中转啊!
这也就是说,编译器编译完源代码之后,会把 06 当做立即数使用,如果想要使 06 表示内存地址,必须要用 bx 进行中转,修改之后的源代码如下:
然后再重新链接成为 exe 程序之后,我们一步一步 debug 看一下。
执行到 mov al,[bx] 的时候,我们发现,此时右侧有个 ds:0006 = 31,这段代码表示的是 ds:0006 处内存单元的值是 31,这才表明我们的程序是正确的。
继续向下执行程序。
前两条指令执行完成后,(dx) = 0 ,(cx) = 3,完成对累加寄存器的清空和循环计数器的赋值操作。最后一条指令是第一次循环操作指令,此时 CS:IP 指向 076A:0012 ,继续向下执行。
可以看到,第一次 add dx,ax 执行完成后 IP = 0014H ,此时指向的指令是 LOOP 0012,这条指令的意思是让程序再执行一次 (IP) = 0012H 处的指令,也就是再执行一次 add dx,ax,可以看到 cx 的值变成了 0002,因为循环指令执行后 (cx) = (cx) - 2 ,然后再向下执行,发现后面的循环指令还是 LOOP 0012 ,再执行一次 add dx,ax,一直到 (cx) = 0 后结束程序执行,如下图所示
可以发现,整个程序一共循环三次,最终 dx 中的值是 93 ,程序执行到 int 21H 处,使用 -p 命令结束程序的执行。