汇编语言强训教程

                            汇编语言强训教程

       语言的学习过程是一个建立条件反射的过程,对语言的运用也直接奠基于条件反射,这个原则无论是对自然语言,或是对程序设计语言的学习都是适用的。

       本教程依据人类条件反射构建的规律,对程序设计语言的学习过程进行了崭新的规划和按排,以培养、训练自如的语言运用能力为核心目标,把知识直接转化为技能,达到直接形成程序设计能力的目的。

本教程特色:

1.        分片化,将汇编语言的知识点转化成目标单一、可直接测试目标实现程度的训练片段,反复进行训练以形成条件反射。

2.        直接写出无差错的代码,依循“手未动,码先成!”的设计理念,使程序设计变成一门真正有实用价值的设计艺术。

3.        非分析性,分析是设计艺术的大敌,是左脑思维的产物,本教程立足于右脑的开发运用,追求直觉式的设计艺术。

                                                     

                                                                                                                              内容简介

 

       所谓汇编语言,即是各种CPU机器指令的助记表示及相关汇编编译器的伪指令的集合。通过汇编语言编制的程序可以直接使用机器指令对计算机软、硬件系统进行操作,程序执行效率高,故为系统设计者所喜用。

       通过汇编语言可以直接操控计算机,对于深入理解计算机的运作原理、洞悉计算机底层的工作机理,具有无可替代的重要作用。但是,数十年的程序设计经历告诉我,知识的累积固然重要,却未必是真正的有效途径,经历过一次又一次的学习、遗忘,再学习、再遗忘的过程,我深刻地认识到,只停留在知识层面上的学习是得不偿失的,甚至可以说是基本无效的。知识就其本身而言基本上是无用的,尤其是在今天的网络环境下,鼠标一点,几乎无所不得,那么我们该怎么办呢?

       知识俯拾即是,传播钢琴知识的媒介处处都有,而钢琴家却屈指可数!这似乎在告诉我们,技能无价,只有通过有效的训练将廉价的知识转化为技能、转化为生命的有效组成成份,才能真正创造出价值。所以,如何把知识转化为技能才是学习的重点。

       基于以上的认识,本书试图构造一个训练体系,旨在把汇编相关的知识转化为操控计算机的技能,使我们在举手投足间指点计算机,犹如挥手、扬眉一般随意和自然。

本书分为三大篇:

       第一篇基础强训,包括基础指令的详细解释及其应用示例,bios功能调的详细解释及其应用示例,dos功能调用的详细解释及其应用示例,最后还有基础综合强训。其目标是,能够对这些基础内容了然于心,运用自如。

       第二篇保护模式下32位汇编强训,着重保护模式及相关操作系统的功能调用,通过大量的强化训练,把系统API的框架及详细脉络植根于受训者心中,犹如学习游泳一般,收到如鱼得水之神效。

       第三篇 64位汇编强训,着重多核系统框架及并发程序设计,亦通过大量的强化训练,使程序设计技能更上层楼,诚所谓“手中无剑,心中有剑!”。

 

第一篇基础强训

第一章  电脑自述

       亲爱的朋友,你好,我是电脑,喜欢的话,可以叫我聪聪。虽然我们天天见面,但这样面对面的交流可能还是第一次。

       就我的核心CPU而论,我属于INTEL大家族。我的兄弟、姐妹可多了,从最早的4位微处理器4004,历经808680186802868038680586,再到奔腾处理器以及更高端的处理器,从4位、8位、16位,再到32位、64位,形成了一条完整的产品进化链。而我呢,比它们还要年轻许多,我是至强系列的64位处理器,站在产品链的最前端,真可算是豪气干云了。

       无论是在家里,还是政府机关,甚至公共场所,几乎到处都有我的伙伴,处处都有我们值守的岗位,为什么呢?是因为我们特别聪明吗?或者是因为我们天赋异秉呢?其实都不是,我们都是很苯的机器,除了机械、呆板地执行人们为我们拟定好的指令、程序,我们对一切几乎都是一无所知的,甚至连那些指令、程序是做什么的,我们都无所知。我们只认识一条条的指令,只知道循规蹈矩地照单执行,最终执行的结果是什么,我们既看不见,也不知情。但人们却喜欢把几乎所有的事情都交给我们来做,那是因为我们执行指令的速度特别快,一秒钟可以执行亿万条指令,那是人们用纸、笔所望尘莫及的,甚至是根本不可能的。

       为什么我们执行指令的速度如此之快呢?那是拜现代超大规模集成电路工艺技术所赐。别看我们小巧玲珑的身体,里面却集成了成万上亿的电子管,这么多电子电路协同工作,速度自然就很快了。

      “那你都能执行什么指令,又是如何执行的呢?”

       好吧,下面就以我为范例,来个裸体秀吧!噢,不是裸体秀哟,应该是个体解剖吧,不过没关系,我是机器,不会感觉痛的。

       我的核心是中央处理器,更通俗的叫法是CPU。它的体积呢,大概只有一块小饼干那么大。解剖开来看吧,里边有算术处理部件,逻辑处理部件,执行控制部件,指令解码器,指令缓冲池,地址加法器,数据总线,地址总线,等等精密的电子部件。它的背脊外面有许多突出的针状管脚,用来与外部的电子元器件进行连接,并从外部获得工作电流。

      我是个工作狂,一旦加电运行,我就不断地执行指令。我有一个地址牌,又叫指令指针寄存器IP,从那儿可以得到一个地址,访问这个地址就能取到一条指令,然后把指令交给解码器进行解码,之后交付给执行控制部件执行,执行的过程中可能会用到算术处理部件,逻辑处理部件等其它部件,这些都由执行控制部件进行协调。一条指令执行完毕,IP会自动调整,指向下一条指令,以此循环往复,无穷无尽。当然,这只是一般的单核处理器的指令执行过程,我更厉害,我有四个相对独立的CPU集成在我的CPU里边,通过超线程技术实现八个处理器核心,更加上流水线执行机制,我可以同时执行多条指令。

     “呀,好厉害!只是我还不太懂,你可否先从较简单的地方讲起?”

       那好吧,先从我的老大哥8086说起吧。我的这个老大哥呀,按照你们的观点,可能已经算是古董级的产品了。虽然它只是个16位的处理器,拥有16位的机器字长和数据总线,但它却有20位的地址总线,这使它可以访问的内存地址空间扩大了许多,达到最大1MB

     “什么?1MB,太小了,我朋友的电脑,内存8G呢!”

       那是,我的内存也有8G呢。可我的老大哥是早期的产品,处于产品进化链的较前端,在当时,这么大的内存寻址能力已经非常棒了。

      “倒也是!人类科技日新月异,令非昔比呀!”

       可不是嘛!我的计算能力比当年的巨型计算机都不差分毫呢!算了,有关计算机分类的话题,不是我们今天的主题,我们还是言归正传吧。

      “好嘞!我现在挺想学汇编程序的,你快给我讲讲吧!”

       好吧,先说说地址空间吧!什么是地址空间呢?是这样,我执行指令的结果是没办法长久保存在我这里的,因为什么呢?我家里的壁柜是很小的,只有数量不多的一些小柜,只能放对我最重要的东西。

      因为指令执行的结果只能是暂时地保存在我这里,当需要把这些结果放在别的地方,能够为人们所阅览时,就需要一些更大的箱柜。只要打开我的机箱,就会发现一些长长的条状物,人们把它们称为内存贮器。它们的容量可比我的壁柜大得太多了,可是有一点,我无法直接打开这些巨大的箱柜,我这里有一组称作地址总线和数据总线的东东,或者说,是我可以操作的机械手吧,通过它们的协助才可以把那些大箱柜里的东西搬到我的壁柜里,或者,把我壁柜里的东西搬到那些大大的箱柜里。这就是我与内存沟通的办法,通过不断地把计算结果搬移到内存中去,就可以得到完整的计算结果供人们阅览。

       那么1MB的地址空间又是怎么来的呢?是这样,其实我这里是没有什么数据的,只有具有两种状态的电路单元,人们把其中的一种状态称为0,另一种状态称为1,这样一个电路单元就可以表示一个二进制的数码。也就是说,一个二进制位对应着两种状态,或者说,能够对两种东西进行编号。那么二个二进制位就对应到2*2=4种状态,或者,可对四种东西进行编号。以此类推,20位地址总线就可对220次方,也就是1MB的地址空间进行访问了。这里B是一个字节,亦即8位二进制位,在我这里,把8位二进制位作为一个可编址单元。

       说到这里,你可能急于想知道我的壁柜里都有什么吧,因为它们对我是那么的重要,没有它们我就等于是个废物。

       好吧,先说说我最喜爱的一组吧,对人们而言,它们叫做16位的通用寄存器组。什么是寄存器呢,这是我所有壁柜的通称,人们把我的壁柜进行了简单的分类,什么数据寄存器呀,指令寄存器呀,地址寄存器呀,等等的。我最喜爱的这一组正是通用数据寄存器组,为了操作方便,它们每一个都有自个的名称,分别叫做:AXBXCXDXSIDIBPSP。其中我最亲近的是AX,它叫做累加寄存器。它距我最近,是我闭着眼睛都可以找到的。用到它的指令编码都较紧凑,即是说,用上它的指令编码都比较短,执行效率相对也较高。它的作用可大了,几乎所有的程序中都有它的身影。

        BX又叫指针寄存器,指针是什么呢?打个比方吧,街道上有很多房屋,每户都有各自的门牌号码。为便于管理,我们把这些门牌号码统统放在一个特定的地方,用以定位这个地方的一个指引符,或者说通讯地址,就是指向门牌号码的指针。通过这个指针找到门牌号码,也就找到了家的位置。

        CX又叫计数寄存器,即是用来数数的。呀,你的头发好好漂亮噢,让我来数数有多少根吧,就该用上它了。对于循环计数控制之类的活计,它是真正的行家里手。

        DX是辅助累加寄存器,存放数据的活计它都拿手,特别地,它常常与累加寄存器AX配合执行一些高难度的指令,比如说,两个16位数相乘,32位乘积中超出16位的部分就找它拿了。还有,在直接操控外设的端口操作中,常用它来存放端口号。

     “聪聪啊,你说了这么多,那么什么是端口呢?”

      这个嘛,你已经看到了,我这里能够访问寄存器和内存储器,但你打开我的机箱就可以看到,我肚子里的东西远不至这些,而且还有一些外部的设备,如鼠标、键盘之类的。这么多设备我如何与它们沟通呢?这就是端口,我通过端口与它们进行沟通、彼此交换数据。

     “噢,是这样,听起来有点复杂呢!”

     是呀,这个先放一放吧,我们接着说。你也看到了,这些都是16位的寄存器,可一个字节是8位的,如何操作一个字节呢?

     是这样,在设计的时候,就已经考虑到这一点了。这四个寄存器的每一个都是由两个独立的8位寄存器合成的。用H表征高8位,用L表征低8位,这样一来,就有八个8位的寄存器了。它们分别是:AHALBHBLDHDLCHCL

      再说SIDISI是源变址指针寄存器,DI是目的变址指针寄存器。说它们是变址寄存器,是因为在某些指令中它们能够自动增加或减小,即是说,能够自动移动到上一个或下一个小箱柜里,增加或减小的值的大小,也能够根据操作数据的字节数自动调整。

      如果遇到在内存区中搬家的情况,这两个小兄弟就常常搭配起来,SI守旧家的房门,DI守新家的房门,一间间地搬运,至到把所有的东西都搬完。其实这里的搬家更象是复制,搬运完成后,新家和旧家就是一样的了。

      至于SPBP,它们可有点儿特别,为什么呢?因为它们与堆栈的操作密切相关。那么什么是堆栈呢,这么着说吧?你用过子弹夹吗?最初弹夹是空的,你可以一粒粒地把子弹压进去,最后压进去的子弹在最上面,要是想把子弹给退出来,也是最上面的子弹先退出。即是先压进去的后出来,后压进去的先出来,类似这样的设置,在我这里也叫做后进先出表。

      堆栈就是一种后进先出表,它对应一块预先分配的内存区域,我们对它进行编号,最初编号最大的地方的上方定为栈顶,编号最小的地方设为栈底。数据是像压子弹一样压入栈中的,从栈中取数据也像从弹夹中退子弹一样把栈顶的数据取出来。SP就是用来指示栈顶位置的,由于8086中栈都是16位的,所以压栈时SP先减2,然后取出SP所指示的存贮区中的数据。出栈时先取出SP所指示的存贮区中的数据,然后SP再加2。至于BP嘛,它是在需要绕过栈顶访问栈内数据的时候用的,通过它,既能访问栈中的数据,又不会改栈顶的位置,相当于在弹夹的一边开道小暗门,偷偷地把子弹换成别的子弹。这两个寄存器用途比较特殊,虽然列在通用寄存器之列,建议不作别的用途。

       除了上述的八个通用数据寄存器,8086还有四个16位的段寄存器,它们分别叫做CSDSESSS。那么什么是段呢?你知道,808616位机,却有一条20位的地址总线,那边是16位的,这边却是20位的,怎么连接呢?人们想了一个折衷的办法,就是用两个16位的地址生成一个20位的地址。这就好比一座城市,先分成一个个的街区,对应到我这里就叫做段。每个街区里都会有许多的住户,由门牌号码标识,对应到我这里就叫做偏移地址。我们把一个16位的段地址,向左移动4位,再加上16位的偏移地址,就得到了一个20位的地址。

       这样一来,整个内存区域就被分成一段段的了,就像城市被分成一片片的街区一样。由于段地址是16位,故最多可有64K个段,偏移也是16位的,故最大的偏移量也是64K,故1MB的内存空间最少可分为16个段,最多可分为64K个段(16*64K = 1024K = 1M)。

       现在来说说这四个段寄存器的分工吧,CS是代码段寄存器,是专门用来存放代码段的段地址的,你不能直接对这个寄存器进行操作,对它的改变只能通过某些指令,如转移类指令,进行间接的调整。

         DSES都是数据段寄存器,DS一般叫做基本数据段寄存器,ES叫做扩展数据段寄存器。对它们系统不做硬性的安排,需要在实用中由人们来设置。而最后一个SS,则是堆栈段寄存器,它是专门用来管理堆栈的。前面已经说过,堆栈其实就是一块特别划分出来的内存区域,这块区域的段地址放在SS中,栈顶的偏移量放在SP中,我只管压栈和出栈的操作,由人们安排这块内存区域的具体位置。

     “聪聪啊,这里不是有点奇怪吗?堆栈段用了两个寄存器SSSP,怎么代码段却只有一个SS呢?”

      这怎么可能呢?当然不会了,还有一个指令指针寄存器IP呢,它起的作用与堆栈中的SP所起的作用相仿,存放着代码段的偏移量。

     “噢,是这样,好像你前面提到过的!”

      你的记性真好!是这样,你看,已经有了这么多的寄存器,我是否可以开工执行指令了呢?别急,还有一个问题没有解决呢。我相信你肯定学过算术,知道运算的过程中会产生进位或者借位,这些进位或者借位如果不保存下来,计算的结果就会出偏差,所以,必须得有一个地方存放这些计算相关的东东。

     “我做算术的时候就经常漏掉进位或借位,还有什么正负符号,真烦人!”

      是呀,运算的过程中有许多东西,进、借位呀,正负数呀,等等的。为也把这些东西保存下来,就专门设计了一个标志寄存器EFLAG。它也是16位的,它拥有CFPFAFZFSF等标志位,通过这些标志位对运算结果进行必要的描述,其用法在下面结合指令的运用再详细讲解。

     “噢,看来是万事倶备,只欠指令了!”

      是呀,现在我们可以接触到指令了,对我来说,我只认识二进制的东东,你说是指令或者是数据,对我来说并没有分别,我只会照本宣科地执行一条条的指令,完全不在意它们到底是什么。

       先拿最简单的事情来说吧,比如说,现在要往我壁柜里的小柜子AL里放上一个数,比如说5,你只要把BOO5传给我就好了,我只要对它解码,就能够知道这是把一个数(这种在指令中直接使用的数叫做立即数),拷贝到我的AL小柜子里。但是,这种用16进制数表示的指令对人们来说太不直观了,也确实难以记忆,所以,人们就发明了一种叫做汇编语言的东东,通过一种叫做助记符的比较容易记忆和书写的形式,来描述指令。当然喽,这种指令我是看不懂的,所以这个汇编语言还要把人们编写的程序再翻译成我认识的机器码,也就是16进制数码,再传给我来执行。

      比如刚才说的事情,用汇编语言助记符描述就是mov al,05h,这里怎么多出个h呢?是这样,在汇编语言里,所有的数都要标示它的数制,二进制用b,十进制用d,八进制用q16进制用h等。通常把十进制设置成缺省的数制,所以上面的指令也可以写成mov al,5。这里mov叫做传送指令,其实就是拷贝,它后面跟着的用逗号隔开的东东,叫做操作数,逗号前面的叫做目的操作数,后面的叫做源操作数,所以,这条指令的具体含义就是把源操作数的内容拷贝一份存到目的操作数中,相当于我们上面说过的搬家。

      “咦,目的操作数是al,源操作数是5,还有别的吗?什么可以用作操作数呢?”

       好,问得好,前面我们已经讲过,像通用数据寄存器、段寄存器,还有内存贮器等,再加上立即数,这些都可以用作操作数。但你可以想到,目的操作数像我的壁柜一样,是用来存放东西的,立即数由于指令执行过后就找不到了,没地方可以放东西,所以立即数不能作为目的操作数。另外,你也知道,cs这个段寄存器很特别,也不能在这里用作目的操作数。再者,你也知道,我是不能直接访问内存的,我只能通过我的机械手去访问,但我只有一个机械手,所以一次只能访问一个内存,故不能在一条指令内出现两个内存储器操作数。两者都是段寄存器也不行,这是段寄存器特殊的要求。

       好了,有了一条指令,可以干搬家的活了。可不能没完没了地搬呀,我的确不会感到厌烦,可你会受不了的。

      “那,就讲加法指令吧,我做算术总也离不开计算器,好烦人呀!”

       那好吧,就按你容易理解的汇编格式讲吧,加法指令是,比如说,把刚才的数5加上3,结果存放到al中,你可以这样写add al,3。与mov指令类似,它也有源和目的两个操作数。意思是源和目的操作数相加,并且把结果存放在目的操作数中。这条指令和上面的那条指令顺序结合起来,就可以完成加法运算了。

      “结果是什么呢?我看不见呀!”。

       当然你看不见了,甚至连我也看不见。因为这两条指令实在太少了,你无法在电脑里单独运行这两条指令,除非使用像debug那样的调试工具软件。

       我看这样吧,我们先来看看一个完整的汇编程序的格式吧。正如我们前面所讲的,我们把内存分成了一段一段的,所以,汇编程序也是一段一段的。但我这里并没有描述分段的指令,所以人们就在汇编语言里定义了一些伪指令,用来应对类似的问题。什么是伪指令呢?那是一些只有汇编语言认识,而我却不认识的指令。

       第一个要讲的伪指令,叫做段描述伪指令,它的简省格式是:

段名 segment

;内容

段名 ends

      “咦,你不是说你不认识这些伪指令吗?那你凭什么讲呢?”

       是呀,我凭什么讲呢?我说不认识,是说我肚子里的解码器呢,它的确对这些一无所知!可我不同呀,所有的东西都放在我这儿,我可以偷偷地学呀,我学会了再教你不行吗?

      “哟,听你这么一说,好像有点道理,我把什么东西都存你肚子里了,你要偷偷地篡改那该怎么办?”

       你别说,还真有这么个问题,我当然不会篡改你的资料,但保不齐有个小鬼什么的,比如木马病毒,偷偷地摸进来,我可不能百分百地防范呀。

       “这还真是的,又扯远了,算了,我明天装安全卫士吧!我们接着聊!”

       上面的段名就是给这个段起的名字,具体是什么由你自己定义,上下两个段名必须一样。段定义开始符segment和段定义结束符ends必须配对。由分号开始至到行尾是注释,是写给编程者或阅读此程序的人看的,汇编语言编译器会把这些内容直接丢弃,好似它们根本就不曾存在过。

       好,现在,就把上面我们写的两条指令,改写成一个完整的汇编程序,我们可以这样写:

cseg segment

      assume cs:cseg,ds:nothing,es:nothing

start:

      mov al,5

      add al,3

cseg ends

end start

      “呀,这么多,我又看不懂了!”

       是这样,如我前面讲过的,我这里有四个段寄存器,需要把它们与你所定义的段对应起来,用于描述这种对应关系的伪指令就是assume,这里把cs与我们定义的段cseg对应起来,nothing是空的意思,即把dses置空,也就是不与任何段对应的意思。如start:这样一个标识符后跟个冒号的,叫做标号。标号后面可以跟指令,也可以什么都不跟。最后一个end表明整个程序的结束点,每个程序有且仅有一条end语句,它后面可以跟一个标号,说明这个程序从什么地方开始执行。比如这里,我们的程序是从start:语句开始的。

       把上面的程序以文件名test.asm存盘,我们就有了第一个汇编语言程序。如果你的电脑里有masm汇编语言编译器(或者你也可以从网上下载一个这样的编译器并安装在你的电脑里),打开dos命令窗口,键入下面的命令:

masm test.asm

link test.obj

      就可以看到当前的文件目录下多出了一个名叫test.exe文件,直接键入文件名test就可以装载执行这个文件。

      “呀!怎么回事,dos命令窗口好像死了一般,什么命令都输入不了!”

       确实如此,不过比较幸运的是,我这里的dos命令窗口却能够返回,其它命令仍然可以使用。可能是我们的操作系统版本不同吧,我这里是Win7 32位旗舰版的,可能与你的不一样。

其实dos命令窗口死掉也是很自然的,为什么呢?因为我们的程序里并没有调用返回到dos状态的指令,为了使这个程序可以返回到dos状态,我们可以增加两条指令,把程序改成如下的模样:

cseg segment

      assume cs:cseg,ds:nothing,es:nothing

start:

      mov al,5

      add al,3

      mov ax,4c00h

      int 21h

cseg ends

end start

      这里的int叫做软中断指令,int 21h是调用dos功能调用的指令,这里通过调用4cdos功能调用使程序执行后返回到dos状态。

      “呀,我照你说的改过了,程序执行后还是什么都没有呀!”

       当然什么都没有啦,最终的结果是放在我的壁柜里的,你当然什么都看不见啦。

       “那可不行,我是想要看到最终的结果呀!”

       那好办呀,把程序改成下面就行了。

cseg segment

      assume cs:cseg,ds:nothing,es:nothing

start:

      mov al,5

      add al,3

      mov dl,al

      add dl,030h

      mov ah,2

      int 21h

      mov ax,4c00h

      int 21h

cseg ends

end start

      “噢,出来了,显示了一个8,真是太棒了!”

       看把你高兴的,幼儿园的小朋友都会做的小算术,有什么可乐的。试着把这两数改大一点看看吧。

      “咦,聪聪呀,我说你怎么骗人呢,我只是把第二个数改成11,结果显示的是类似@的符号,而不是数字?”

       这是自然的了,因为呀,5 + 11 = 16,结果太大了,超出最大的十进制数码9了,当然显示就不对了。你看程序里第二个加法指令add dl,030h,知道这条指令是做什么的么?

       “不知道,我刚才还在心里嘀咕呢,怎么多出来一个加法指令呢?”

       这是把计算的结果转化成ASCII码,什么是ASCII码呢?那是最先由美国的工程师创制的一种国际通用的符号表示法,它用7位二进制位来表示常用的一些符号,比如十进制数码、大小写英文字母、常用标点符号等。用ASCII表示的十进制数码’0’030h,所以这里要加上030h。因为这里的02hdos功能调用要求把要显示的ASCII码放在dl中,所以需要把上面的计算结果转化成ASCII码并放在dl中。

       但是这里光有ASCII码还不够,因为你把数改大以后,计算的结果超过了十进制的最大数码9,必须考虑先把一个比较大的数转化成十进制数码,然后再如上面调用dos功能调用,把它们一位一位地显示出来。

       “我知道怎么把二进制数转化成十进制数,好像是叫做层级相除法,可你还没有讲除法指令呢?”

       是的,就我们目前而言,做这样的事情还是有一点儿复杂,我们先来看看除法指令吧,比如说,用5去除8,可以写成下面的指令

mov al,8

mov ah,0

mov bl,5

div bl

     这里的div就是除法指令的助记符,与前面指令不同的是,它有一个隐含的操作数,即ax。其含义是用指令里的操作数bl的内容去除ax的内容,结果有两个,商数放在al中,余数则放在ah中。

     “你说的是字节数据的除法,要是字数据呢?”

     其实这里的字节指的是除数(指令中的操作数),而ax中的被除数本身就是一个字了。要是除数是一个字,被除数就是32位的双字,一个16位的寄存器放不下了,所以就把被除数放在dx:ax中,两个16位的寄存器联合起来表示32位的被除数,dx中存放高16位,ax中存放低16位。指令执行的结果嘛,dx中放余数,ax中放商数。

     “好,我已经明白了,比如可以写成div bx

      反应真快呀,你真是很棒哟,但是你想过没有,层级相除法的结果是怎么取得的?

      “这个我知道,先一级一级地除以10,把余数写在右边,最后逆序,就是第一级除法的余数是个位数,最后一级除法的余数却是最高位数。”

       很好,这样一来,我们做层级相除法时最先得到的余数是个位数,按照我们从左到右、由高至低书写十进制数的习惯,这个可是要到最后才显示出来的。

       “噢,我知道了,先得到的最后才显示,这不是先进后出么?”

       是的,你真聪明,一点即透。但是你也看到了,层级相除法是要重复做除法运算的,这种重复性的操作我们叫做循环,有一条指令是构造循环的,让我们来看看吧。

loop 标号

       该指令的操作是,先把cx寄存器的内容减去一,结果还放在cx中,然后检测cx的内容是否为零,是则结束循环转到该指令的下一条指令,否则,转到标号处再做一轮循环。

       增一或减一的操作也有专门的指令,它们是inc/dec 操作数,意思是把操作数的内容增一或减一,结果还放回到操作数中。

另外,还有,你知道层级相除法什么时候结束吗?

       “当然,当被除数变为零时。”

        所以,程序中还需要检测被除数是否已经变成零了,这可用比较指令来做检测,通常被除数是放在ax中的,写出来就是cmp ax,0。这条指令与加法指令不同,它把目的操作数减去源操作数,但不存放结果,只影响相关的标志位。

        “标志位,都这么长时间了,你都没有提到过标志位,是不是忘了?”

         哎呀,多谢提醒,我还真的忘了。传送指令对标志都没有影响,加法指令就要看计算的结果了,比如高位有进位时,进位标志CF就置为1,否则置为0;如果低三位向第四位有进位,则辅助进位标志AF就置为1,否则置为0;如果计算的结果为零,则零标志位ZF就置为1,否则置为0;如果计算结果的二进制数码中的1的个数为偶数,则奇偶标志位PF就置为1,否则置为0;如果计算结果的最高位为1,则负数标志位SF就置为1,否则置为0

          “啊,这么多标志位都出来了,可你不是说cmp指令执行的是减法吗?”

        是的,减法与加法的情况差不多,只是加法里的进位,在减法里相应地变成了借位,即是说最高位向外有借位时CF置为1,否则置为0;低三位向第四位有借位时,AF置为1,否则置为0,其它的标志位与加法指令类同。

        这里要特别提醒一点,上面提到的增一、减一指令inc/decCF标志位没有影响,比如说,如果CF的值现在是0al中的值是255,则执行inc alal的值是0CF的值也是0,这里产生的进位被丢弃了。

          “哦,我明白了,如果对最大的数做增一操作,就只剩下0了,这一点我记住了。现在有了这么些准备,我们可以开始写程序了吧!”

是的,让我们设想一下,由于出现先进后出的状况,必须要用到堆栈,所以我们可以考虑用字除法,这样得到的余数也是字数据,我们前面的计算结果是放在al中的,这样

mov ah,0

mov bx,10

div bx

       就是把计算结果除以10,结果的余数放在dx中,商数放在ax中,要把余数入栈,可以用push dx(相应的,出栈用pop)。但是这里有一个问题,因为是层级相除法,每一次除法都有余数,每循环都压栈一次,总共压了几次栈,需要一个计数器。

      “这我知道,你说过的,cx是计数专家!”

       很好,你的记性真不错,我这里正打算用cx呢,记住计数器累加前需要初始化为零,让我们把这几部分合起来吧,这样

mov al,5

add al,11

mov cx,0

mov ah,0

mov bx,10

div bx

       这就求出个位数了,但下面还要求高位数呢,必须检测ax是否为零,对应的指令是cmp ax,0,检测的结果如果大于零,就要再试除一次,需要转移到前面去,用到的转移指令是ja标号,把这些结合进前面的程序,加上标号,得到:

mov al,5

add al,11

mov cx,0

mov ah,0

mov bx,10

get_decimal:

  mov dx,0

div bx

push dx

inc cx

cmp ax,0

ja get_decimal

      “为什么要mov dx,0呢?”

       你想想,这里是字除法,被除数是放在dx:ax中的,但我们这里只有ax中有数据,高位是0,所以要置dx0。另外,结果的余数是放在dx中的,如果循环开始时不清零,则下轮循环就错了。ja是条件转移指令,在这里的意思是,如果ax大于0,则转移到get_decimal再试除一遍。

       “好,我明白了,接下来呢?”

        很自然,余数都在栈中了,且正好最高位在栈顶,只需出栈显示就行了

display:

      pop dx

      add dl,030h

      mov ah,2

      int 21h

      loop display

      “嗯,我明白,前面已经把计数累计在cx中了,这里直接用就行了!”

      好,我们把这几部分合并起来,得到一个完整的程序如下:

cseg segment

      assume cs:cseg,ds:nothing,es:nothing

start:

      mov al,5

      add al,11

      mov ah,0

      mov cx,0

      mov bx,10

get_decimal:

      mov dx,0

      div bx

      push dx

      inc cx

      cmp ax,0

      ja get_decimal

display:

      pop dx

      add dl,030h

      mov ah,2

      int 21h

      loop display

      mov ax,4c00h

      int 21h

cseg ends

end start

      “哈,好棒哟,显示出16。但,有一点,噢,你前面不是说过,堆栈只不过一块内存区域,你并不管这块区域如何分配,我们这里也没有分配堆栈,程序中却用到了堆栈,居然不出错,好奇怪哟?”

       太好了,一语中的,你真是聪明!答案也很自然,没有分配堆栈空间必然会出错,可为什么没出呢?那是汇编语言编译器或dos系统帮了你,它悄悄地帮你分配了堆栈空间,使你可以在不知情的状况下,仍然可以正确地使用堆栈。

       “啊,这样呀,它分配的堆栈空间有多大呢?”

       这样吧,我们来检测一下吧。其实也很简单,就是把sssp的值显示出来看看。这就要上面的程序执行两遍,再加一层循环也不太方便,我们把它提出来做成一个子程序吧。

       “那么子程序又是什么呢?有什么特别的地方吗?”

       其实就是一段可以多次重复调用的程序段,与循环和转移不同的是,它有专门的调用和返回指令。调用子程序的指令是:call子程序名;从子程序中返回的指令是:ret。定义子程序的伪指令如下:

子程序名 proc

;子程序体

子程序名 endp

       这里上下两个子程序名必须一致,我们现在就把上面程序中转化十进制数并显示的程序段提出来,取代这里的子程序体,就得到:

disp proc

      push dx

      push cx

      push bx

      mov bx,10

      mov cx,0

get:

      mov dx,0

      div bx

      push dx

      inc cx

      cmp ax,0

      ja get

display:

      pop dx

      add dl,030h

      mov ah,2

      int 21h

      loop display

      mov dl,32

      mov ah,2

      int 21h

      pop bx

      pop cx

      pop dx

      ret

disp endp

      “噢,我仔细看看,怎么一开始就来了三个push,这是做什么的?”

      是这样,在这段子程序中,我们约定要进行转化的数放在ax中,子程序中用到了dxcxbx三个寄存器,也许在主程序中也会用到这三个寄存器,那么一旦通过call指令进入子程序,这些寄存器中的值就会改变,等到子程序中执行返回指令ret再回到主程序中的时候,就必然因为寄存器中的值发生了改变而产生错误,为了避免这种情况的发生,我们把这三个寄存器压栈保存,并在ret指令前出栈恢复。切切注意压栈和出栈的顺序相反,压栈的顺序是dxcxbx,而出栈的顺序却是bxcxdx

       “噢,是这样,明白!还有,mov dl,32是做什么的?”

哟,你还真细心呢!这个嘛,32是空格字符,也就是键盘上那个最长的按键,的ASCII码。这一条指令的意思是把空格字符的ASCII码传送到dl中,其效果是显示一个空格。

       “噢,明白!”

        现在把原来程序中做算术的部分删掉,加上如下的指令:

mov ax,ss

call disp

mov ax,sp

call disp

      再整理成篇,得到:

cseg segment

      assume cs:cseg,ds:nothing,es:nothing

disp proc

      push dx

      push cx

      push bx

      mov bx,10

      mov cx,0

get:

      mov dx,0

      div bx

      push dx

      inc cx

      cmp ax,0

      ja get

display:

      pop dx

      add dl,030h

      mov ah,2

      int 21h

      loop display

      mov dl,32

      mov ah,2

      int 21h

      pop bx

      pop cx

      pop dx

      ret

disp endp

start:

      mov ax,ss

      call disp

      push ax

      mov ax,sp

      call disp

      mov ax,4c00h

      int 21h

cseg ends

end start

      “咦,显示出两个数,后面一个是0,好像是sp吧?”

       是的,sp的值是0,想过为什么吗?

       “不明白,怎么会是0呢?我想想,你前面说过,sp是指向栈顶的,现在主程序里没有栈操作指令,子程序中的栈操作指令都是配对的,有压栈的就有出栈的,所有压进去的都在子程序返回前退出了。所以说,栈应该是空的?”

        对头,栈的确是空的。我前面说过,当栈空的时候,sp指向栈空间边界的上方,比如说,我们把地址10000h~1000Fh的内存空间作为栈空间,则在栈空的时候,sp的值应该是多少?

       “嗯,空间边界的上方,也就是最大偏移再加一,对吧?0fh+1=10h?”

        真聪明,非常正确!那么对16位的二进制数,什么数加一后会是0呢?

       “这个我知道,0000h-0001h,忽略最高位的借位,是ffffh?”

       很好,这就是说,sp的取值范围是0ffffh,就是64K。这就得到结论,系统自动分配的堆栈空间是64KB,由于sp16位的,这64KB的空间就重复循环使用。就是说,如果你一直压栈的话,超出64KB之后也不会报错,而是把最早进栈的数据给覆盖了。

      “噢,这下清楚了。但我觉得64KB太大了,我们根本用不着,怎么办呢?”

好办呀,前面不是说过,我们的程序也应该是一段一段的,现在我们的程序只有一个代码段,那就再加上一个堆栈段,如下:

stack segment para stack ‘stack’

      db 64 dup(0)

stack ends

      “里面的db是什么?”

      我前面说过,指令的操作数还有一种类型是内存储器,这就是定义内存储器变量的伪指令。它的格式为:标识符 类型描述符 长度或值 初始化。这里的标识符是变量名,类型描述符有:db(字节)、dw(字)、dd(双字)、df(三字)、dq(四字)、dt(十字)等,后面的两项是可以省略的。

比如,

fname db 'Jonathan'       ; 定义一个字符串;

value db 09h,78h,56h,0f2h  ; 定义一组字节类型的值

table db 10 dup(0)         ; 定义一个字节型数组,长度为10,初始值全0

mword dw 0988h,7878h    ; 定义一组字类型的值

rare dw 20 dup(?)         ; 定义一字类型数组,长度20,初值不定

more dd 10 dup( 1,10 dup(0) )  ;定义两维数组,长度10*11,每行首1,其余0           

      把这个堆栈段加进程序,我们就有两个段了,仅仅这样加进去就行吗?

      “不行吧,你前面说过,各个段需要与段寄存器建立对应关系的,这里又加了一个段,对应关系就变了?是吧?”

       没错,还需要修改assume伪指令,整理之后,我们得到新一版的程序如下:

stack segment para stack 'stack'

      db 64 dup(0)

stack ends

cseg segment

      assume cs:cseg,ds:nothing,es:nothing,ss:stack

disp proc

      push dx

      push cx

      push bx

      mov bx,10

      mov cx,0

get:

      mov dx,0

      div bx

      push dx

      inc cx

      cmp ax,0

      ja get

display:

      pop dx

      add dl,030h

      mov ah,2

      int 21h

      loop display

      mov dl,32

      mov ah,2

      int 21h

      pop bx

      pop cx

      pop dx

      ret

disp endp

start:

      mov ax,ss

      call disp

      mov ax,sp

      call disp

      mov ax,4c00h

      int 21h

cseg ends

end start

      “运行之后显示,sp的值是64!”

      对呀,定义了堆栈段之后,系统自动调整了sssp的值,我们就可以放心使用堆栈了。

     “噢,我现在已经有些明白了,一个汇编语言程序由一些段组成,至少必须有一个代码码。定义各个段之后,要通过assume伪指令建立段与段寄存器之间的对应关系。还有,可以通过子程序的方式使程序更简练,既清晰、直观,又便于重复调用!”

      很好,这里一个汇编程序的完整框架已经出来了,你还有什么想法吗?

     “噢,我想,我们已经写出了一个小的汇编程序了,但学到的指令只有这么几条,可否系统地讲讲这些指令呢?”

好的,事实上,我已经做过这方面的讲解了,让我们一起去看看吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值