﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-任我行-随笔分类-汇编积累</title><link>http://www.cppblog.com/oosky/category/946.html</link><description>一天一个脚印......
&lt;br&gt;每日一句: &lt;script language="javascript" charset="utf-8" src="http://sl.iciba.com/spdshow.php"&gt;&lt;/script&gt;</description><language>zh-cn</language><lastBuildDate>Tue, 20 May 2008 04:55:48 GMT</lastBuildDate><pubDate>Tue, 20 May 2008 04:55:48 GMT</pubDate><ttl>60</ttl><item><title>汇编语言超浓缩教程</title><link>http://www.cppblog.com/oosky/archive/2006/02/20/3354.html</link><dc:creator>任我行</dc:creator><author>任我行</author><pubDate>Mon, 20 Feb 2006 08:17:00 GMT</pubDate><guid>http://www.cppblog.com/oosky/archive/2006/02/20/3354.html</guid><wfw:comment>http://www.cppblog.com/oosky/comments/3354.html</wfw:comment><comments>http://www.cppblog.com/oosky/archive/2006/02/20/3354.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cppblog.com/oosky/comments/commentRss/3354.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/oosky/services/trackbacks/3354.html</trackback:ping><description><![CDATA[<P><FONT size=2>&nbsp;&nbsp;&nbsp; “ 哎哟，哥们儿，还捣鼓汇编呢？那东西没用，兄弟用VB"钓"一个API就够你忙活个十天半月的，还不一定搞出来。”此君之言倒也不虚，那吾等还有无必要研他一究呢？（废话，当然有啦！要不然你写这篇文章干嘛。）别急，别急，让我把这个中原委慢慢道来：一、所有电脑语言写出的程序运行时在内存中都以机器码方式存储，机器码可以被比较准确的翻译成汇编语言，这是因为汇编语言兼容性最好，故几乎所有跟踪、调试工具（包括WIN95/98下）都是以汇编示人的，如果阁下对CRACK颇感兴趣……；二、汇编直接与硬件打交道，如果你想搞通程序在执行时在电脑中的来龙去脉，也就是搞清电脑每个组成部分究竟在干什么、究竟怎么干？一个真正的硬件发烧友，不懂这些可不行。三、如今玩DOS的多是“高手”，如能像吾一样混入（我不是高手）“高手”内部，不仅可以从“高手”朋友那儿套些黑客级“机密”，还可以自诩“高手”尽情享受强烈的虚荣感--#$%&amp; “醒醒!” </FONT></P>
<P><FONT size=2>　　对初学者而言，汇编的许多命令太复杂，往往学习很长时间也写不出一个漂漂亮亮的程序，以致妨碍了我们学习汇编的兴趣，不少人就此放弃。所以我个人看法学汇编，不一定要写程序，写程序确实不是汇编的强项，大家不妨玩玩DEBUG，有时CRACK出一个小软件比完成一个程序更有成就感（就像学电脑先玩游戏一样）。某些高深的指令事实上只对有经验的汇编程序员有用，对我们而言，太过高深了。为了使学习汇编语言有个好的开始，你必须要先排除那些华丽复杂的命令，将注意力集中在最重要的几个指令上（CMP LOOP MOV JNZ……）。但是想在啰里吧嗦的教科书中完成上述目标，谈何容易，所以本人整理了这篇超浓缩（用WINZIP、WINRAR…依次压迫，嘿嘿！）教程。大言不惭的说，看通本文，你完全可以“不经意”间在前辈或是后生卖弄一下DEBUG，很有成就感的，试试看！那么――这个接下来呢？―― Here we go！（阅读时看不懂不要紧，下文必有分解）</FONT></P>
<P><FONT size=2>　　因为汇编是通过CPU和内存跟硬件对话的，所以我们不得不先了解一下CPU和内存：（关于数的进制问题在此不提）</FONT></P>
<P><FONT size=2>　　ＣＰＵ是可以执行电脑所有算术╱逻辑运算与基本 I/O 控制功能的一块芯片。一种汇编语言只能用于特定的CPU。也就是说，不同的CPU其汇编语言的指令语法亦不相同。个人电脑由1981年推出至今，其CPU发展过程为：8086→80286→80386→80486→PENTIUM →……，还有AMD、CYRIX等旁支。后面兼容前面CPU的功能，只不过多了些指令（如多能奔腾的MMX指令集）、增大了寄存器（如386的32位EAX）、增多了寄存器（如486的FS）。为确保汇编程序可以适用于各种机型，所以推荐使用8086汇编语言，其兼容性最佳。本文所提均为8086汇编语言。寄存器（Register）是CPU内部的元件，所以在寄存器之间的数据传送非常快。用途：1.可将寄存器内的数据执行算术及逻辑运算。2.存于寄存器内的地址可用来指向内存的某个位置，即寻址。3.可以用来读写数据到电脑的周边设备。8086 有8个8位数据寄存器，这些8位寄存器可分别组成16位寄存器：ＡＨ&amp;ＡＬ＝ＡＸ：累加寄存器，常用于运算；ＢＨ&amp;ＢＬ＝ＢＸ：基址寄存器，常用于地址索引；ＣＨ&amp;ＣＬ＝ＣＸ：计数寄存器，常用于计数；ＤＨ&amp;ＤＬ＝ＤＸ：数据寄存器，常用于数据传递。为了运用所有的内存空间，8086设定了四个段寄存器，专门用来保存段地址：ＣＳ（Code Segment）：代码段寄存器；ＤＳ（Data Segment）：数据段寄存器；ＳＳ（Stack Segment）：堆栈段寄存器；ＥＳ（Extra Segment）：附加段寄存器。当一个程序要执行时，就要决定程序代码、数据和堆栈各要用到内存的哪些位置，通过设定段寄存器 CS，DS，SS 来指向这些起始位置。通常是将DS固定，而根据需要修改CS。所以，程序可以在可寻址空间小于64K的情况下被写成任意大小。 所以，程序和其数据组合起来的大小，限制在DS 所指的64K内，这就是COM文件不得大于64K的原因。8086以内存做为战场，用寄存器做为军事基地，以加速工作。除了前面所提的寄存器外，还有一些特殊功能的寄存器：IP（Intruction Pointer）：指令指针寄存器，与CS配合使用，可跟踪程序的执行过程；SP（Stack Pointer）：堆栈指针，与SS配合使用，可指向目前的堆栈位置。BP（Base Pointer）：基址指针寄存器，可用作SS的一个相对基址位置；SI（Source Index）：源变址寄存器可用来存放相对于DS段之源变址指针；DI（Destination Index）：目的变址寄存器，可用来存放相对于 ES 段之目的变址指针。还有一个标志寄存器FR（Flag Register）,有九个有意义的标志，将在下文用到时详细说明。</FONT></P>
<P><FONT size=2>　　内存是电脑运作中的关键部分，也是电脑在工作中储存信息的地方。内存组织有许多可存放数值的储存位置，叫“地址”。8086地址总线有20位，所以CPU拥有达1M的寻址空间，这也是DOS的有效控制范围，而8086能做的运算仅限于处理16位数据，即只有0到64K，所以，必须用分段寻址才能控制整个内存地址。完整的20位地址可分成两部份：1.段基址(Segment)：16位二进制数后面加上四个二进制０，即一个16进制０，变成20位二进制数，可设定1M中任何一个64K段，通常记做16位二进制数；2.偏移量(Offset)：直接使用16位二进制数，指向段基址中的任何一个地址。如：2222（段基址）:3333（偏移量），其实际的20位地址值为：25553。除了上述营养要充分吸收外，你还要知道什么是DOS、BIOS功能调用，简单的说，功能调用类似于WIN95 API，相当于子程序。汇编写程序已经够要命了，如果不用MS、IBM的子程序，这日子真是没法过了（关于功能调用详见《电脑爱好者》98年11期）。</FONT></P>
<P><FONT size=2>　　编写汇编语言有两种主要的方法：1.使用MASM或TASM等编译器；2.使用除错程序DEBUG.COM。DEBUG其实并不能算是一个编译器，它的主要用途在于除错，即修正汇编程序中的错误。不过，也可以用来写短的汇编程序，尤其对初学者而言，DEBUG 更是最佳的入门工具。因为DEBUG操作容易：只要键入DEBUG回车，A回车即可进行汇编，过程简单，而使用编译器时，必须用到文本编辑器、编译器本身、LINK以及EXE2BIN等程序，其中每一个程序都必须用到一系列相当复杂的命令才能工作，而且用编译器处理源程序，必须加入许多与指令语句无关的指示性语句，以供编译器识别，使用 DEBUG 可以避免一开始就碰到许多难以理解的程序行。DEBUG 除了能够汇编程序之外，还可用来检查和修改内存位置、载入储存和执行程序、以及检查和修改寄存器，换句话说，DEBUG是为了让我们接触硬件而设计的。（8086常用指令用法将在每个汇编程序中讲解，限于篇幅，不可能将所有指令列出）。</FONT></P>
<P><FONT size=2>　　DEBUG的的A命令可以汇编出简单的COM文件，所以DEBUG编写的程序一定要由地址 100h（COM文件要求）开始才合法。FOLLOW ME，SETP BY SETP（步步回车）：</FONT></P>
<P><FONT size=2>　　输入 A100 ； 从DS：100开始汇编<BR>　　2.输入 MOV DL,1 ； 将数值 01h 装入 DL 寄存器<BR>　　3.输入 MOV AH,2 ； 将数值 02h 装入 DL 寄存器<BR>　　4.输入 INT 21 ； 调用DOS 21号中断2号功能，用来逐个显示装入DL的字符<BR>　　5.输入 INT 20 ； 调用DOS 20号中断，终止程序，将控制权交回给 DEBUG<BR>　　6.请按 Enter 键<BR>　　7.现在已将汇编语言程序放入内存中了，输入 G(运行)<BR>　　8.出现结果：输出一个符号。<BR>　　ㄖ ←输出结果其实不是它，因WORD97无法显示原结果，故找一赝品将就着。<BR>　　Program terminated normally</FONT></P>
<P><FONT size=2>　　我们可以用Ｕ命令将十六进制的机器码反汇编（Unassemble）成汇编指令。你将发现每一行右边的汇编指令就是被汇编成相应的机器码，而8086实际上就是以机器码来执行程序。<BR>　　1.输入 U100,106<BR>　　1FED:0100 B201 MOV DL,01<BR>　　1FED:0102 B402 MOV AH,02<BR>　　1FED:0104 CD21 INT 21<BR>　　1FED:0106 CD20 INT 20<BR>　　DEBUG可以用Ｒ命令来查看、改变寄存器内容。CS：IP寄存器，保存了将执行指令地址。<BR>　　1.输入R<BR>　　AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000<BR>　　DS=1FED ES=1FED SS=1FED CS=1FED IP=0100 NV UP EI PL NZ NA PO NC<BR>　　1FED:0100 B201 MOV DL,01</FONT></P>
<P><FONT size=2>　　当程序由DS：100开始执行，那么终止程序时，DEBUG会自动将IP内容重新设定为100。当你要将此程序做成一个独立的可执行文件，则可以用Ｎ命令对该程序命名。但一定要为COM文件，否则无法以DEBUG载入。<BR>　　输入N SMILE.COM ；我们得告诉DEBUG程序长度：程序从100开始到106，故占用7<BR>　　；字节。我们利用BX存放长度值高位部分，而以CX存放低位部分。<BR>　　2.输入RBX ；查看 BX 寄存器的内容，本程序只有7个字节，故本步可省略<BR>　　3.输入 RCX　 ；查看 CX 寄存器的内容<BR>　　4.输入 7　 ；程序的字节数<BR>　　5.输入 W ；用Ｗ命令将该程序写入（Write）磁盘中</FONT></P>
<P><FONT size=2>　　修行至此，我们便可以真正接触8086汇编指令了。 当我们写汇编语言程序的时候，通常不会直接将机器码放入内存中，而是打入一串助记符号（Mnemonic Symbols），这些符号比十六进制机器码更容易记住，此之谓汇编指令。助记符号，告诉CPU应执行何种运算。 也就是说，助忆符号所构成的汇编语言是为人设计的，而机器语言是对PC设计的。 </FONT></P>
<P><FONT size=2>　　现在，我们再来剖析一个可以将所有ASCII码显示出来的程序。<BR>　　1. 输入 DEBUG<BR>　　2. 输入 A100<BR>　　3．输入 MOV CX,0100 ；装入循环次数<BR>　　MOV DL,00 ；装入第一个ASCII码，随后每次循环装入新码<BR>　　MOV AH,02<BR>　　INT 21<BR>　　INC DL ；INC：递增指令，每次将数据寄存器 DL 内的数值加 1<BR>　　LOOP 0105 ；LOOP：循环指令，每执行一次LOOP，CX值减1，并跳<BR>　　；到循环的起始地址105，直到CX为0，循环停止<BR>　　INT 20<BR>　　4.输入 G即可显示所有ASCII码<BR>　<BR>　　当我们想任意显示字符串，如：UNDERSTAND？，则可以使用DOS21H号中断9H号功能。输入下行程序，存盘并执行看看：<BR>　　1.输入 A100<BR>　　 MOV DX,109 ；DS:DX ＝ 字符串的起始地址<BR>　　 MOV AH,9 ；DOS的09h功能调用<BR>　　INT 21 ；字符串输出<BR>　　INT 20<BR>　　DB 'UNDERSTAND？$'；定义字符串 </FONT></P>
<P><FONT size=2>　　在汇编语言中，有两种不同的指令：1.正规指令：如 MOV 等，是属于CPU的指令，用来告诉CPU在程序执行时应做些什么，所以它会以运算码（OP-code）的方式存入内存中；2.伪指令：如DB等，是属于DEBUG等编译器的指令，用来告诉编译器在编译时应做些什么。DB（Define Byte）指令用来告诉DEBUG 将单引号内的所有ASCII 码放入内存中。使用 9H 功能的字符串必须以$结尾。用Ｄ命令可用来查看DB伪指令将那些内容放入内存。<BR>　　6.输入 D100<BR>　　1975:0100 BA 09 01 B4 09 CD 21 CD-20 75 6E 64 65 72 73 74 ......!. underst<BR>　　1975:0110 61 6E 64 24 8B 46 F8 89-45 04 8B 46 34 00 64 19 and$.F..E..F4.d.<BR>　　1975:0120 89 45 02 33 C0 5E 5F C9-C3 00 C8 04 00 00 57 56 .E.3.^_.......WV<BR>　　1975:0130 6B F8 0E 81 C7 FE 53 8B-DF 8B C2 E8 32 FE 0B C0 k.....S.....2...<BR>　　1975:0140 74 05 33 C0 99 EB 17 8B-45 0C E8 D4 97 8B F0 89 t.3.....E.......<BR>　　1975:0150 56 FE 0B D0 74 EC 8B 45-08 03 C6 8B 56 FE 5E 5F V...t..E....V.^_<BR>　　1975:0160 C9 C3 C8 02 00 00 6B D8-0E 81 C3 FE 53 89 5E FE ......k.....S.^.<BR>　　1975:0170 8B C2 E8 FB FD 0B C0 75-09 8B 5E FE 8B 47 0C E8 .......u..^..G..</FONT></P>
<P><FONT size=2>　　现在，我们来剖析另一个程序：由键盘输入任意字符串，然后显示出来。db 20指示DEBUG保留20h个未用的内存空间供缓冲区使用。<BR>　　输入A100<BR>　　 MOV DX,0116 ；DS:DX ＝ 缓冲区地址，由DB伪指令确定缓冲区地址<BR>　　MOV AH,0A ；0Ah 号功能调用<BR>　　INT 21 ；键盘输入缓冲区<BR>　　MOV DL,0A ；由于功能Ah在每个字符串最后加一个归位码（0Dh由 Enter<BR>　　MOV AH,02 ；产生），使光标自动回到输入行的最前端，为了使新输出的<BR>　　INT 21 ；字符串不会盖掉原来输入的字符串，所以利用功能2h加一<BR>　　；个换行码(OAh)，使得光标移到下一行的的最前端。<BR>　　MOV DX,0118 ；装入字符串的起始位置<BR>　　MOV AH,09 ；9h功能遇到$符号才会停止输出，故字符串最后必须加上<BR>　　INT 21 ；$，否则9h功能会继续将内存中的无用数据胡乱显示出来<BR>　　INT 20<BR>　　DB 20 ；定义缓冲区 <BR>　　送你一句话：学汇编切忌心浮气燥。</FONT></P>
<P><FONT size=2>　　客套话就不讲了。工欲善其事，必先利其器。与其说DEBUG 是编译器，倒不如说它是“直译器”，DEBUG的A命令只可将一行汇编指令转成机器语言，且立刻执行。真正编译器（MASM）的运作是利用文本编辑器（EDIT等）将汇编指令建成一个独立且附加名为.ASM的文本文件，称源程序。它是MASM 程序的输入部分。MASM将输入的ASM文件，编译成.OBJ文件，称为目标程序。OBJ文件仅包含有关程序各部份要载入何处及如何与其他程序合并的信息，无法直接载入内存执行。链结程序LINK则可将OBJ文件转换成可载入内存执行（EXEcute）的EXE文件。还可以用EXE2BIN，将符合条件的EXE文件转成COM文件（COM 文件不但占用的内存最少，而且运行速度最快）。<BR>　　下面我们用MASM写一个与用DEBUG写的第一个程序功能一样的程序。<BR>　　用EDIT编辑一个SMILE.ASM的源程序文件。<BR>　　源程序 DEBUG 程序<BR>　　prognam segment<BR>　　assume cs:prognam<BR>　　org 100h A100<BR>　　mov dl,1 mov dl,1<BR>　　mov ah,2 mov ah,2<BR>　　int 21h int 21<BR>　　int 20h int 20<BR>　　prognam ends<BR>　　end</FONT></P>
<P><FONT size=2>　　比较一下：1.因为MASM会将所有的数值假设为十进制，而DEBUG则只使用十六进制，所以在源程序中，我们必须在有关数字后加上代表进制的字母，如H代表十六进制，D代表十进制。若是以字母开头的十六进制数字，还必须在字母前加个0，以表示它是数，如0AH。2.源程序增加五行叙述：prognam segment 与 prognam ends 是成对的，用来告诉 MASM 及LINK，此程序将放在一个称为PROGNAM(PROGram NAMe)的程序段内，其中段名（PROGNAM）可以任取，但其位置必须固定。assume cs:prognam 必须在程序的开头，用来告诉编译器此程序所在段的位置放在CS寄存器中。end用来告诉MASM，程序到此结束, ORG 100H作用相当于DEBUG的A100，从偏移量100开始汇编。COM 文件的所有源程序都必须包含这五行，且必须依相同的次序及位置出现，这点东西记下就行，千篇一律。接着，我们用MASM编译SMILE.ASM。<BR>　　输入 MASM SMILE ←不用打入附加名.ASM。<BR>　　Microsoft (R) Macro Assembler Version 5.10<BR>　　Copyright (C) Microsoft Corp 1981, 1988. All rights reserved.<BR>　　Object filename [SMILE.OBJ]: ←是否改动输出OBJ文件名，如不改就ENTER<BR>　　Source listing [NUL.LST]: ← 是否需要列表文件（LST），不需要就ENTER<BR>　　Cross-reference [NUL.CRF]: ←是否需要对照文件（CRF），不需要则ENTER<BR>　　50162 + 403867 Bytes symbol space free<BR>　　0 Warning Errors ←警告错误，表示编译器对某些语句不理解，通常是输入错误。<BR>　　0 Severe Errors ←严重错误，会造成程序无法执行，通常是语法结构错误。</FONT></P>
<P><FONT size=2>　　如果没有一个错误存在，即可生成OBJ文件。OBJ中包含的是编译后的二进制结果，它还无法被 DOS载入内存中加以执行，必须加以链结（Linking）。以LINK将OBJ文件（SMILE.OBJ）链结成 EXE 文件（SMILE.EXE）时，。<BR>　　1.输入 LINK SMILE ←不用附加名OBJ<BR>　　Microsoft (R) Overlay Linker Version 3.64<BR>　　Copyright (C) Microsoft Corp 1981, 1988. All rights reserved.<BR>　　Run File [SMILE.EXE]: ← 是否改动输出EXE文件名，如不改就ENTER<BR>　　List File [NUL.MAP]: ← 是否需要列表文件（MAP），不需要则ENTER<BR>　　Libraries [.LIB]: ←是否需要库文件，要就键入文件名，不要则ENTER<BR>　　LINK : warning L4021: no stack segment← 由于COM文件不使用堆栈段，所以错误信息<BR>　　←"no stack segment"并不影响程序正常执行</FONT></P>
<P><FONT size=2>　　至此已经生成EXE文件，我们还须使用EXE2BIN 将EXE文件（SMILE.EXE），转换成COM文件（SMILE.COM）。输入EXE2BIN SMILE产生 BIN 文件（SMILE.BIN）。其实 BIN 文件与 COM 文件是完全相同的，但由于DOS只认COM、EXE及BAT文件，所以BIN文件无法被正确执行，改名或直接输入 EXE2BIN SMILE SMILE.COM即可。现在，磁盘上应该有 SMILE.COM 文件了，你只要在提示符号C：&gt;下，直接输入文件名称 SMILE ，就可以执行这个程序了。</FONT></P>
<P><FONT size=2>　　你是否觉得用编译器产生程序的方法，比 DEBUG 麻烦多了！以小程序而言，的确是如此，但对于较大的程序，你就会发现其优点了。我们再将ASCII程序以编译器方式再做一次，看看有无差异。首先，用EDIT.COM建立 ASCII.ASM 文件。<BR>　　prognam segment ;定义段<BR>　　assume cs:prognam ;把上面定义段的段基址放入 CS<BR>　　mov cx,100h ; 装入循环次数<BR>　　mov dl,0 ; 装入第一个ASCII码，随后每次循环装入新码<BR>　　next: mov ah,2<BR>　　 int 21h<BR>　　 inc dl ;INC：递增指令，每次将数据寄存器 DL 内的数值加 1<BR>　　loop next ; 循环指令，执行一次，CX减1，直到CX为0，循环停止<BR>　　int 20h<BR>　　 prognam ends ;段终止<BR>　　end ;汇编终止<BR>　　在汇编语言的源程序中，每一个程序行都包含三项元素：<BR>　　　 start: mov dl,1 ；装入第一个ASCII码，随后每次循环装入新码<BR>　　　 标识符 表达式 注解</FONT></P>
<P><FONT size=2>　　在原始文件中加上注解可使程序更易理解，便于以后参考。每行注解以“；”与程序行分离。编译器对注解不予理会，注解的数据不会出现在OBJ、EXE或COM文件中。由于我们在写源程序时，并不知道每一程序行的地址，所以必须以符号名称来代表相对地址，称为“标识符”。我们通常在适当行的适当位置上，键入标识符。标识符（label）最长可达31 个字节，因此我们在程序中，尽量以简洁的文字做为标识符。现在，你可以将此ASCII.ASM 文件编译成 ASCII.COM 了。1.MASM ASCII，2.LINK ASCII，3.EXE2BIN ASCII ASCII.COM。</FONT></P>
<P><FONT size=2>　　注意：当你以编译器汇编你设计的程序时，常会发生打字错误、标识符名称拼错、十六进制数少了ｈ、逻辑错误等。汇编老手常给新人的忠告是：最好料到自己所写的程序一定会有些错误（别人告诉我的）；如果第一次执行程序后，就得到期望的结果，你最好还是在检查一遍，因为它可能是错的。原则上，只要大体的逻辑架构正确，查找程序中错误的过程，与写程序本身相比甚至更有意思。写大程序时，最好能分成许多模块，如此可使程序本身的目的较单纯，易于撰写与查错，另外也可让程序中不同部份之间的界限较清楚，节省编译的时间。如果读程序有读不懂的地方最好用纸笔记下有关寄存器、内存等内容，在纸上慢慢比划，就豁然开朗了。 　　下面我们将写一个能从键盘取得一个十进制的数值，并将其转换成十六进制数值而显示于屏幕上的“大程序”。前言：要让8086执行这样的功能，我们必须先将此问题分解成一连串的步骤，称为程序规划。首先，以流程图的方式，来确保整个程序在逻辑上没有问题（不用说了吧！什么语言都要有此步骤）。这种模块化的规划方式，称之为“由上而下的程序规划”。而在真正写程序时，却是从最小的单位模块（子程序）开始，当每个模块都完成之后，再合并成大程序；这种大处著眼，小处著手的方式称为“由下而上的程序设计”。</FONT></P>
<P><FONT size=2>　　我们的第一个模块是BINIHEX，其主要用途是从8086的BX寄存器中取出二进制数，并以十六进制方式显示在屏幕上。注意：子程序如不能独立运行，实属正常。<BR>　　 binihex segment<BR>　　 assume cs:binihex<BR>　　mov ch,4 ;记录转换后的十六进制位数（四位）<BR>　　rotate: mov cl,4 ;利用CL当计数器，记录寄存器数位移动次数<BR>　　rol bx,cl ;循环寄存器BX的内容，以便依序处理4个十六进制数<BR>　　mov al,bl ;把bx低八位bl内数据转移至al<BR>　　and al,0fh ;把无用位清零<BR>　　add al,30h ;把AL内数据加30H，并存入al<BR>　　cmp al,3ah ;与3ah比较<BR>　　jl printit ;小于3ah则转移<BR>　　add al,7h ;把AL内数据加30H，并存入al<BR>　　printit:mov dl,al ;把ASCII码装入DL<BR>　　mov ah,2<BR>　　 int 21h<BR>　　 dec ch ;ch减一，减到零时，零标志置1<BR>　　jnz rotate ;JNZ：当零标志未置1，则跳到指定地址。即：不等，则转移<BR>　　int 20h ;从子程序退回主程序<BR>　　binihex ends<BR>　　 end</FONT></P>
<P><FONT size=2>　　利用循环左移指令ROL循环寄存器BX(BX内容将由第二个子程序提供)的内容，以便依序处理4个十六进制数:1. 利用CL当计数器，记录寄存器移位的次数。2.将BX的第一个十六进制值移到最右边。利用 AND （逻辑“与”运算：对应位都为１时，其结果为１，其余情况为零）把不要的部份清零，得到结果：先将BL值存入AL中，再利用AND以0Fh（00001111）将AL的左边四位清零。由于０到９的ASCII码为30h到39h，而Ａ到Ｆ之ASCII码为41h到46h，间断了7h，所以得到结果：若AL之内容小于3Ah，则AL值只加30h，否则AL再加7h。ADD指令会将两个表达式相加，其结果存于左边表达式内。标志寄存器（Flag Register）是一个单独的十六位寄存器，有9个标志位，某些汇编指令（大部份是涉及比较、算术或逻辑运算的指令）执行时，会将相关标志位置1或清0， 常碰到的标志位有零标志（ZF）、符号标志（SF）、溢出标志（OF）和进位标志（CF）。 标志位保存了某个指令执行后对它的影响，可用其他相关指令，查出标志的状态，根据状态产生动作。CMP指令很像减法，是将两个表达式的值相减，但寄存器或内存的内容并未改变，只是相对的标志位发生改变而已：若 AL 值小于 3Ah，则正负号标志位会置0，反之则置1。 JL指令可解释为：小于就转移到指定位置，大于、等于则向下执行。CMP和JG 、JL等条件转移指令一起使用，可以形成程序的分支结构，是写汇编程序常用技巧。</FONT></P>
<P><FONT size=2>　　第二个模块DECIBIN 用来接收键盘打入的十进制数，并将它转换成二进制数放于BX 寄存器中，供模块1 BINIHEX使用。<BR>　　decibin segment<BR>　　assume cs:decibin<BR>　　mov bx,0 ;BX清零<BR>　　newchar:mov ah,1 ;<BR>　　int 21h ;读一个键盘输入符号入al，并显示<BR>　　sub al,30h ;al减去30H，结果存于al中，完成ASCII码转二进制码<BR>　　jl exit ;小于零则转移<BR>　　cmp al,9d<BR>　　 jg exit ;左&gt;右则转移<BR>　　cbw ;8位al转换成16位ax<BR>　　xchg ax,bx ;互换ax和bx内数据<BR>　　mov cx,10d ;十进制数10入cx<BR>　　mul cx ;表达式的值与ax内容相乘，并将结果存于ax<BR>　　xchg ax,bx<BR>　　 add bx,ax<BR>　　 jmp newchar ;无条件转移<BR>　　exit: int 20 ;回主程序<BR>　　decibin ends<BR>　　 end<BR>　　CBW 实际结果是:若AL中的值为正，则AH填入00h；反之，则AH填入FFh。XCHG常用于需要暂时保留某个寄存器中的内容时。<BR>　　当然，还得一个子程序（CRLF）使后显示的十六进制数不会盖掉先输入的十进制数。<BR>　　crlf segment<BR>　　assume cs:crlf<BR>　　mov dl,0dh ;回车的ASCII码0DH入DL<BR>　　mov ah,2<BR>　　 int 21h<BR>　　 mov dl,0ah ;换行的ASSII码0AH入AH<BR>　　mov ah,2<BR>　　 int 21h<BR>　　 int 20 ;回主程序<BR>　　crlf ends<BR>　　end</FONT></P>
<P><FONT size=2>　　现在我们就可以将BINIHEX、DECIBIN及CRLF等模块合并成一个大程序了。首先，我们要将这三个模块子程序略加改动。然后，再写一段程序来调用每一个子程序。<BR>　　crlf proc near；<BR>　　mov dl,0dh<BR>　　mov ah,2<BR>　　int 21h<BR>　　mov dl,0ah<BR>　　mov ah,2<BR>　　int 21h<BR>　　ret<BR>　　crlf endp</FONT></P>
<P><FONT size=2>　　类似SEGMENT与ENDS的伪指令，PROC与ENDP也是成对出现，用来识别并定义一个程序。其实，PROC 真正的作用只是告诉编译器：所调用的程序是属于近程（NEAR）或远程（FAR）。 一般的程序是由 DEBUG 直接调用的，所以用 INT 20 返回，用 CALL 指令所调用的程序则改用返回指令RET,RET会把控制权转移到栈顶所指的地址，而该地址是由调用此程序的 CALL指令所放入的。<BR>　　各模块都搞定了，然后我们把子程序组合起来就大功告成<BR>　　decihex segment ;主程序<BR>　　assume cs:decihex<BR>　　org 100h<BR>　　mov cx,4 ;循环次数入cx；由于子程序要用到cx，故子程序要将cx入栈<BR>　　repeat: call decibin;调用十进制转二进制子程序<BR>　　call crlf ;调用添加回、换行符子程序<BR>　　call binihex ;调用二进制转十六进制并显示子程序<BR>　　call crlf<BR>　　loop repeat ;循环4次，可连续运算4次<BR>　　mov ah,4ch ; 调用DOS21号中断4c号功能，退出程序，作用跟INT 20H<BR>　　int 21H ; 一样，但适用面更广，INT20H退不出时，试一下它<BR>　　decibin proc near push cx ;将cx压入堆栈，;<BR>　　┇ exit: pop cx ;将cx还原; retdecibin endp binihex proc near push cx<BR>　　┇ pop cx retbinihex endp crlf proc near<BR>　　 push cx<BR>　　┇ pop cx retcrlf endpdecihex ends end</FONT></P>
<P><FONT size=2>　　CALL指令用来调用子程序，并将控制权转移到子程序地址，同时将CALL的下行一指令地址定为返回地址，并压入堆栈中。CALL 可分为近程（NEAR）及远程（FAR）两种：1.NEAR：IP的内容被压入堆栈中，用于程序与程序在同一段中。2.FAR：CS 、IP寄存器的内容依次压入堆栈中,用于程序与程序在不同段中。PUSH、POP又是一对指令用于将寄存器内容压入、弹出，用来保护寄存器数据，子程序调用中运用较多。堆栈指针有个“后进先出”原则，像PUSH AX，PUSH BX…POP BX，POP AX这样才能作到保护数据丝毫不差。</FONT></P>
<P><FONT size=2>　　汇编语言超浓缩教程到这要告一段落了，希望能奠定你独立设计的基础。而更多更好的技巧则全依赖你平时的积累了。祝你成功！</FONT></P><img src ="http://www.cppblog.com/oosky/aggbug/3354.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/oosky/" target="_blank">任我行</a> 2006-02-20 16:17 <a href="http://www.cppblog.com/oosky/archive/2006/02/20/3354.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>关于在8086/88内存寻址方式</title><link>http://www.cppblog.com/oosky/archive/2006/02/20/3353.html</link><dc:creator>任我行</dc:creator><author>任我行</author><pubDate>Mon, 20 Feb 2006 08:15:00 GMT</pubDate><guid>http://www.cppblog.com/oosky/archive/2006/02/20/3353.html</guid><wfw:comment>http://www.cppblog.com/oosky/comments/3353.html</wfw:comment><comments>http://www.cppblog.com/oosky/archive/2006/02/20/3353.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/oosky/comments/commentRss/3353.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/oosky/services/trackbacks/3353.html</trackback:ping><description><![CDATA[
		<table class="tableBorder1" style="TABLE-LAYOUT: fixed; WORD-WRAP: break-word" cellspacing="1" cellpadding="0" width="100%" border="0">
				<tbody>
						<tr>
								<td align="left">
										<strong>
												<span class="FontSizeBig">关于在8086/88内存寻址方式</span>
												<br />
												<br />
										</strong> <font size="2"><span class="FontSizeNormal">Writer:HSLY</span><br /></font> <span class="FontSizeNormal"><font size="2">Excerpt:80x86汇编小站</font></span></td>
						</tr>
						<tr bgcolor="#d1d9e2">
								<td height="1">
								</td>
						</tr>
						<tr>
								<td align="left"> <font size="2"><span class="FontSizeNormal">Preface：</span><br /></font><span class="FontPrefaceSize"><font color="#da7e34">在学汇编时，很多初学者对PC的寻址方式和很不理解......</font></span></td>
						</tr>
						<tr bgcolor="#d1d9e2">
								<td height="1">
								</td>
						</tr>
						<tr>
								<td align="left"> <font size="2"><span class="FontSizeNormal">Content：</span><br /></font><p><font size="2">    在学汇编时，很多初学者对PC的寻址方式和很不理解，甚至是很难理解。的确，这方面的知识是很抽象的，需要比较强的空间想象能力。尤其是我们在输入字符串时，那这些字符是如何进行排列的呢？对于，这个问题，我相信很多初学者也是很难想象是如何排列。但是，我可以这样比喻：内存就是有很多栋“楼房”，“楼房”又是由“单元号”，“门户号”组成，那“楼房”就相当于内存地址的段地址，“单元号”就相当于内存的的 偏移地址，“门户号(家)”就相当于“变地址”，而每个单元有16个"门户号(家)",又当我们找到"门户号(家)"后,走进这个"门户号(家)"就会见到里面会有"人",而我们所说的人就是寄存器所指的"内容"了,我画个图给你们看就会一目了然了。</font></p><p><br /><font size="2">用DEBUG的D命令得出这样的效果：</font></p><p><font size="2">　</font></p><p><br /><font size="2">|----------&gt;0B1F就是"楼房"------&gt;段地址 <br />|<br />|     |------&gt;右边的就是"单元号"---&gt;偏移地址<br />|     |<br />|     |            |--------&gt;这部分就是"门户号"-----&gt;变地址<br />|     |          |&lt;------------------------------------------&gt;|<br />0B1F:0100 00 80 FF 02 75 05 C6 46-00 00 C3 E8 8C EB B4 3B<br />0B1F:0110 CD 21 72 39 8B FA 33 C0-8B C8 49 26 34 00 0E 0B<br />'<br />'<br />'<br />[省略] </font></p><p><font size="2">看完这个图之后,是不是就很明了呢?但是聪明的人就会有疑问,那我们怎么走进"门户号(家)"呢?问得好,所以了为了可以走进"门户号(家)",就出现了一个叫做"寻址方式"的概念!说白了,就是教你如何找到这个"门户号(家)".呵呵!</font></p><p><font size="2">好现在都明白了吗?那你们就看看我是怎么理解PC的寻址方式(通俗易懂):<br />在这我就只介绍比较难理解的: </font></p><p><font size="2">1:寄存器直接寻址: <br />你就想成:其实你已经站在你要找的"门户号(家)"面前了,直接敲门进去就OK了! <br />例子: MOV AX,[2000H] <br />MOV AX,2000H --&gt;2000H为存放操作数单元号的符号地址<br />上面两者是不等效的</font></p><p><font size="2">2:寄存器间接寻址方式:<br />你就想成:你已经站在你要找的"门户号(家)"的"单元号",你要找到它,必须知道它在当前"单元号"几楼.假如它在6楼,那你就上到6楼就OK了!!注意,最高只有16楼,因为什么呢?那就用DEBUG的D命令看看呀,慢慢数哦,呵呵!!<br />例子: MOV AX,[BX]</font></p><p><font size="2">计算公式: 物理地址=16d*(DS)+(BX)<br />物理地址=16d*(DS)+(SI)<br />物理地址=16d*(DS)+(DI)<br />物理地址=16d*(SS)+(BP)</font></p><p><font size="2">3:寄存器相对寻址方式:<br />你就想成:你要找的"门户号(家)"其实就在你家的楼上或者楼下,你要找到它,就 必须知道它在你楼上几楼,或者在楼下几楼!就OK了!<br />例子: MOV AX,COUNT[SI]<br />MOV AX,[COUNT+SI]<br />其中 COUNT为位移量的符号地址</font></p><p><br /><font size="2">计算公式: 物理地址=16d*(DS)+(BX)+8位位移量<br />或+(SI) 或 16位位偏移量<br />或+(DI)</font></p><p><br /><font size="2">物理地址=16d*(SS)+(BP)+8位偏移量</font></p><p><br /><font size="2">4:基址变址寻址方式:<br />你就想成:你要找的"门户号(家)"是跟住在同一栋楼的不同"单元号",你要找到它,就必须知道它是该栋的哪个"单元号",并且住在几楼!那样你就可以找到它了 !<br />例子: MOV AX,[BX][DI]<br />MOV AX,[BX+DI]</font></p><p><font size="2">计算公式: 物理地址=16d*(DS)+(BX)+(SI)<br />或+(DI)<br />物理地址=16d*(SS)+(BP)+(SI)<br />或+(DI)</font></p><p><br /><font size="2">5:相对基址变址寻址方式:<br />你就想成:你就想成:你要找的"门户号(家)"是跟住在同一栋楼的不同"单元号",它比你高几层楼或者低几层楼,然后用的你目前的楼数+/-就可以得出你要找的住在几楼了!<br />例子: MOV,AX,MASK[BX][SI]<br />MOV,AX,MASK[BX+SI]<br />MOV,AX,[MASK+BX+SI]<br />以上三个例子是等效的!!</font></p><p><font size="2">计算公式: 物理地址=16d*(DS)+(BX)+(SI)+8位位移量<br />或+(DI) 或 16位位偏移量<br />物理地址=16d*(SS)+(BP)+(SI)+8位位移量<br />或+(DI) 或 16位位偏移量<br />---------------------------------------------------------------------<br />呵呵,终于写完了这篇教程,好累哦!! 是不是觉得我的思维很另类呀,要创新呀!<br />书上太理论了,我就创新一个,不知道你们看得懂吗?<br />呵呵,反正你们不要</font><a href="mailto:!@###$" target="_blank"><font color="#000000" size="2">!@##)(#$</font></a><font size="2">*!@(@我就行了,我很努力写了!!!</font></p><p><font size="2">下面,我举个程序例子,让你们加深印象!!!</font></p><p><font size="2">----------------------------------------------------------------------<br />编程步骤:<br />1: 建立缓冲区,为输入字符串(最多能输入9个)<br />2: 取缓冲区的首地址,以便后面进行"寄存器间接寻址方式"<br />3: 利用"寄存器间接寻址方式"取得实际输入字符个数,以便确认循环次数<br />4: 利用"寄存器间接寻址方式"输入字符串的最后一个字符<br />5: 利用LOOP指令和2号显示功能来进行倒着显示<br />----------------------------------------------------------------------</font></p><p><font size="2">;程序功能：任意输入几个字符(最多能输入9个)，按回车则倒着输出！</font></p><p><font size="2">data segment<br />user_string db 10,0,10 dup(?)<br />data ends<br />code segment<br />assume cs:code,ds:data<br />start: mov ax,data<br />mov ds,ax <br />lea dx,user_string ;建立输入字符串缓冲区<br />mov ah,0ah<br />int 21h<br />xor si,si<br />xor bx,bx<br />mov bx,dx <br />mov cx,[bx+si+1] ;看这个就是"寄存器间接寻址方式"<br />xor ch,ch ;其目的就是取实际输入字符个数<br />mov di,cx<br />lop: mov ah,2<br />mov dx,[bx+di+1];看这又是"寄存器间接寻址方式"<br />int 21h ;其目的就是取输入字符串的最后一个字符<br />dec di<br />loop lop ;依次循环倒着输出字符<br />mov ah,4ch<br />int 21h<br />code ends<br />end start</font></p><p><font size="2">-----------------------------------------------------------------------<br />完工了</font></p></td>
						</tr>
				</tbody>
		</table>
<img src ="http://www.cppblog.com/oosky/aggbug/3353.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/oosky/" target="_blank">任我行</a> 2006-02-20 16:15 <a href="http://www.cppblog.com/oosky/archive/2006/02/20/3353.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>