学习任何一门语言都离不开调试,汇编也是。调试程序执行过程接下来,我们将根据这些函数跟踪程序的执行过程。调试对我们来说非常重要。有很多肉眼可以观察到的代码细节和问题。一些简单的程序问题我们或许可以用肉眼判断,但是对于很多隐藏的问题,我们还是要靠debug来发现。下面是一段汇编代码,我在上一篇文章中也给大家写过。假设cs:codesgcodesgsegmentmovax,0123hmovbx,0456haddax,bxaddax,axmovax,4c00hint21hcodesgendsend新建一个文本文件,把代码cv放过去,然后右键保存,用dosbox编译成1.obj文件,链接到1.exe文件后,我们使用debug1.exe命令分析这个程序,使用-r命令查看初始寄存器情况。在程序的初始状态,可以看到CX中的数据是000F,也就是程序的长度是000F。1.exe中有15个字节,CX中的内容为000FH。好了,现在我们知道程序成功加载到内存并运行了,但是我们先想一想,挂入EXE的程序会从哪里加载到内存呢?我们怎么知道程序加载到哪里了?程序加载的过程分为以下几个步骤:首先,程序会从内存中找一块区域,记录为起始地址SA,此时的偏移地址为0,这样一块容量充足的内存区域.在该区域的前256个字节中,创建了一个称为程序段前缀(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,转为十六进制就是SA+10H,所以物理地址就是SA+10H:0。我们调试上面的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。程序继续向下执行。当执行到int21H时,程序执行完毕。这时应该使用-p命令来结束程序的执行,如下图所示。当显示Programterminatednormally时,表示程序正常结束。这里不用去想为什么执行-p命令要等到int21执行完,不用关心movax,4c00,int21的意思,先记住就行了。由于程序加载过程是命令加载程序到内存,然后调试程序跟踪exe程序,所以程序退出后,先从exe程序退出到调试程序,再返回到命令来自调试程序的程序。我们再分析一段程序,把原代码组装起来假设cs:codesgcodesgsegmentmovax,2000Hmovss,axmovsp,0addsp,10popaxpopbxpushaxpushbxpopaxpopbxmovax,4c00Hint21Hcodesgendsendstill是将其保存为test.txt,然后进行编译和链接操作,生成一个可执行文件test.exe,观察其执行过程。我们先用-r查看初始寄存器的内容。主要观察CX、DS、CS、IP的值是否和我们上面描述的一致。CX存放程序长度,DS存放程序段地址,CS存放程序首地址,IP存放程序偏移地址。然后用-u查看exe程序的源代码。这个exe程序是一个编译和链接的程序。我们来分析一下这一部分。这是一个压入和弹出堆栈段的程序。首先,movax,2000Hmovss,axmovsp,0是设置栈段栈顶的指令。执行完成后,物理地址为20000H,即SS:SP=2000:0000。在执行这个程序的过程中,为什么没有出现movsp,0指令呢?我们错过了吗?查了一下,源码里确实有这条指令,是不是没有执行?为了验证这个假设,我们重新调试这个程序,然后先修改SP的值,如下图所示。一开始我们用-r把sp的值改成0002,然后单步执行。执行完movss,ax后,我们发现sp的值变成了0000,也就是说movsp,0指令实际上是被执行了,只是在debug模式下没有显示。程序继续向下执行,下面是两次pop操作。popax和popbx做两件事:清除寄存器;栈顶+2,所以ax和bx寄存器的内容为0,且SP=SP+2,执行后SP=000E。之后是两次入栈操作,将已经出栈的两个寄存器压回栈中,如下图所示。push操作也做了两件事,将寄存器压入栈,SP=SP-2,由于ax和bx已经出栈,寄存器内容为0,最后执行pop操作,然后结束程序的执行。我们再来看看PSP。由于程序加载时前256字节被PSP占用,此时DS(SA)为PSP的起始地址,CS=SA+10H,即CS=076AH。调试循环程序让我们调试循环程序以查看一些有趣的细节。现在有一个问题,将位置ffff:0006中的数字乘以3,并将结果存储在dx中。针对这个问题,有几点需要考虑:我们知道8086汇编语言中单个存储单元最多可以存储8位,一个字节的长度范围是0到255,而一个寄存器dx可以store可容纳的最大值为16位,长度为两个字节,范围为0-65535。即使255*3也小于65535。显然,乘以3后,可以存储到dx中。一个数乘以3相当于在循环中把自己加3次,所以需要用加法来实现乘法。可以直接用dx来积累,但是需要斧子来转账。ffff:6内存单元是字节单元,ax寄存器可以存放字单元,不能直接赋值。怎么做?因为ax可以看做al和ah,而al和ah是两个独立的寄存器,它们之间不会有值溢出,所以设ah=0,al=内存单元的值。所以这段汇编代码写成如下假设cs:codesgcodesgsegmentmovax,0ffffhmovds,axmovah,0moval,[6]movcx,3s:adddx,axloopsmovax,4c00hint21hcodesgendsend之后完成后,编译链接成一个exe程序,对其进行debugxxx.exe操作。我们来看一下程序的执行过程。前两段没问题,设置DS段寄存器的值为FFFF。然后继续往下执行到moval,[6]的时候,我发现为什么AX寄存器里的内容变成了0006?我不是故意把06放到ax里的,我是想把内存单元ffff:06的值放到ax里,突然发现编译器是白痴。仔细耐心地排查问题后,我终于明白自己是个傻子!不知道大家能看出我代码的问题吗?我怎么敢在源程序中使用立即数作为内存偏移地址?必须使用bx转账!也就是说,编译器编译完源码后,会把06作为一个立即数。如果要让06代表一个内存地址,就必须用bx来传递。修改后的源码如下:assumecs:codesgcodesgsegmentmovax,0ffffhmovds,axmovbx,6movah,0moval,[bx]#必须用bx来表示内存地址movdx,0#清除累加寄存器为0movcx,3s:adddx,axloopsmovax,4c00hint21hcodesgendsend然后relink成为exe程序,我们一步步调试。在执行moval,[bx]的时候,我们发现右边有ds:0006=31,这段代码说明ds:0006处的内存单元的值是31,说明我们的程序是正确的。继续下程序。前两条指令执行完成后,(dx)=0,(cx)=3,完成累加寄存器的清零和循环计数器的赋值。最后一条指令为第一条循环操作指令,此时CS:IP指向076A:0012,继续向下执行。可以看到,第一次添加dx后,执行ax,IP=0014H。此时指向的指令是LOOP0012,这条指令的意思是让程序再次执行(IP)=0012H处的指令,即Executeadddx,ax一次,可以看到cx的值变成了0002,因为循环指令执行完(cx)=(cx)-2,再往下执行,发现后面的循环指令还是LOOP0012,再执行一次adddx,ax直到(cx)=0并结束程序执行。如下图所示,可以发现整个程序一共循环了3次,dx中的最终值为93,程序执行到int21H,使用-p命令结束程序执行。
