汇编语言入门教程.docx
《汇编语言入门教程.docx》由会员分享,可在线阅读,更多相关《汇编语言入门教程.docx(20页珍藏版)》请在冰豆网上搜索。
![汇编语言入门教程.docx](https://file1.bdocx.com/fileroot1/2023-5/26/08a84b84-3764-46f2-ade6-8ae3db9192ae/08a84b84-3764-46f2-ade6-8ae3db9192ae1.gif)
汇编语言入门教程
汇编语言入门教程2007-04-2922:
04对初学者而言,汇编的许多命令太复杂,往往学习很长时间也写不出一个漂漂亮亮的程序,以致妨碍了我们学习汇编的兴趣,不少人就此放弃。
所以我个人看法学汇编,不一定要写程序,写程序确实不是汇编的强项,大家不妨玩玩DEBUG,有时CRACK出一个小软件比完成一个程序更有成就感(就像学电脑先玩游戏一样)。
某些高深的指令事实上只对有经验的汇编程序员有用,对我们而言,太过高深了。
为了使学习汇编语言有个好的开始,你必须要先排除那些华丽复杂的命令,将注意力集中在最重要的几个指令上(CMPLOOPMOVJNZ……)。
但是想在啰里吧嗦的教科书中完成上述目标,谈何容易,所以本人整理了这篇超浓缩(用WINZIP、WINRAR…依次压迫,嘿嘿!
)教程。
大言不惭的说,看通本文,你完全可以“不经意”间在前辈或是后生卖弄一下DEBUG,很有成就感的,试试看!
那么――这个接下来呢?
――Herewego!
(阅读时看不懂不要紧,下文必有分解)
因为汇编是通过CPU和内存跟硬件对话的,所以我们不得不先了解一下CPU和内存:
(关于数的进制问题在此不提)
CPU是可以执行电脑所有算术╱逻辑运算与基本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位寄存器:
AH&AL=AX:
累加寄存器,常用于运算;BH&BL=BX:
基址寄存器,常用于地址索引;CH&CL=CX:
计数寄存器,常用于计数;DH&DL=DX:
数据寄存器,常用于数据传递。
为了运用所有的内存空间,8086设定了四个段寄存器,专门用来保存段地址:
CS(CodeSegment):
代码段寄存器;DS(DataSegment):
数据段寄存器;SS(StackSegment):
堆栈段寄存器;ES(ExtraSegment):
附加段寄存器。
当一个程序要执行时,就要决定程序代码、数据和堆栈各要用到内存的哪些位置,通过设定段寄存器CS,DS,SS来指向这些起始位置。
通常是将DS固定,而根据需要修改CS。
所以,程序可以在可寻址空间小于64K的情况下被写成任意大小。
所以,程序和其数据组合起来的大小,限制在DS所指的64K内,这就是COM文件不得大于64K的原因。
8086以内存做为战场,用寄存器做为军事基地,以加速工作。
除了前面所提的寄存器外,还有一些特殊功能的寄存器:
IP(IntructionPointer):
指令指针寄存器,与CS配合使用,可跟踪程序的执行过程;SP(StackPointer):
堆栈指针,与SS配合使用,可指向目前的堆栈位置。
BP(BasePointer):
基址指针寄存器,可用作SS的一个相对基址位置;SI(SourceIndex):
源变址寄存器可用来存放相对于DS段之源变址指针;DI(DestinationIndex):
目的变址寄存器,可用来存放相对于ES段之目的变址指针。
还有一个标志寄存器FR(FlagRegister),有九个有意义的标志,将在下文用到时详细说明。
内存是电脑运作中的关键部分,也是电脑在工作中储存信息的地方。
内存组织有许多可存放数值的储存位置,叫“地址”。
8086地址总线有20位,所以CPU拥有达1M的寻址空间,这也是DOS的有效控制范围,而8086能做的运算仅限于处理16位数据,即只有0到64K,所以,必须用分段寻址才能控制整个内存地址。
完整的20位地址可分成两部份:
1.段基址(Segment):
16位二进制数后面加上四个二进制0,即一个16进制0,变成20位二进制数,可设定1M中任何一个64K段,通常记做16位二进制数;2.偏移量(Offset):
直接使用16位二进制数,指向段基址中的任何一个地址。
如:
2222(段基址):
3333(偏移量),其实际的20位地址值为:
25553。
除了上述营养要充分吸收外,你还要知道什么是DOS、BIOS功能调用,简单的说,功能调用类似于WIN95API,相当于子程序。
汇编写程序已经够要命了,如果不用MS、IBM的子程序,这日子真是没法过了(关于功能调用详见《电脑爱好者》98年11期)。
编写汇编语言有两种主要的方法:
1.使用MASM或TASM等编译器;2.使用除错程序DEBUG.COM。
DEBUG其实并不能算是一个编译器,它的主要用途在于除错,即修正汇编程序中的错误。
不过,也可以用来写短的汇编程序,尤其对初学者而言,DEBUG更是最佳的入门工具。
因为DEBUG操作容易:
只要键入DEBUG回车,A回车即可进行汇编,过程简单,而使用编译器时,必须用到文本编辑器、编译器本身、LINK以及EXE2BIN等程序,其中每一个程序都必须用到一系列相当复杂的命令才能工作,而且用编译器处理源程序,必须加入许多与指令语句无关的指示性语句,以供编译器识别,使用DEBUG可以避免一开始就碰到许多难以理解的程序行。
DEBUG除了能够汇编程序之外,还可用来检查和修改内存位置、载入储存和执行程序、以及检查和修改寄存器,换句话说,DEBUG是为了让我们接触硬件而设计的。
(8086常用指令用法将在每个汇编程序中讲解,限于篇幅,不可能将所有指令列出)。
DEBUG的的A命令可以汇编出简单的COM文件,所以DEBUG编写的程序一定要由地址100h(COM文件要求)开始才合法。
FOLLOWME,SETPBYSETP(步步回车):
输入A100;从DS:
100开始汇编
2.输入MOVDL,1;将数值01h装入DL寄存器
3.输入MOVAH,2;将数值02h装入DL寄存器
4.输入INT21;调用DOS21号中断2号功能,用来逐个显示装入DL的字符
5.输入INT20;调用DOS20号中断,终止程序,将控制权交回给DEBUG
6.请按Enter键
7.现在已将汇编语言程序放入内存中了,输入G(运行)
8.出现结果:
输出一个符号。
ㄖ←输出结果其实不是它,因WORD97无法显示原结果,故找一赝品将就着。
Programterminatednormally
我们可以用U命令将十六进制的机器码反汇编(Unassemble)成汇编指令。
你将发现每一行右边的汇编指令就是被汇编成相应的机器码,而8086实际上就是以机器码来执行程序。
1.输入U100,106
1FED:
0100B201MOVDL,01
1FED:
0102B402MOVAH,02
1FED:
0104CD21INT21
1FED:
0106CD20INT20
DEBUG可以用R命令来查看、改变寄存器内容。
CS:
IP寄存器,保存了将执行指令地址。
1.输入R
AX=0000BX=0000CX=0000DX=0000SP=FFEEBP=0000SI=0000DI=0000
DS=1FEDES=1FEDSS=1FEDCS=1FEDIP=0100NVUPEIPLNZNAPONC
1FED:
0100B201MOVDL,01
当程序由DS:
100开始执行,那么终止程序时,DEBUG会自动将IP内容重新设定为100。
当你要将此程序做成一个独立的可执行文件,则可以用N命令对该程序命名。
但一定要为COM文件,否则无法以DEBUG载入。
输入NSMILE.COM;我们得告诉DEBUG程序长度:
程序从100开始到106,故占用7
;字节。
我们利用BX存放长度值高位部分,而以CX存放低位部分。
2.输入RBX;查看BX寄存器的内容,本程序只有7个字节,故本步可省略
3.输入RCX ;查看CX寄存器的内容
4.输入7 ;程序的字节数
5.输入W;用W命令将该程序写入(Write)磁盘中
修行至此,我们便可以真正接触8086汇编指令了。
当我们写汇编语言程序的时候,通常不会直接将机器码放入内存中,而是打入一串助记符号(MnemonicSymbols),这些符号比十六进制机器码更容易记住,此之谓汇编指令。
助记符号,告诉CPU应执行何种运算。
也就是说,助忆符号所构成的汇编语言是为人设计的,而机器语言是对PC设计的。
现在,我们再来剖析一个可以将所有ASCII码显示出来的程序。
1.输入DEBUG
2.输入A100
3.输入MOVCX,0100;装入循环次数
MOVDL,00;装入第一个ASCII码,随后每次循环装入新码
MOVAH,02
INT21
INCDL;INC:
递增指令,每次将数据寄存器DL内的数值加1
LOOP0105;LOOP:
循环指令,每执行一次LOOP,CX值减1,并跳
;到循环的起始地址105,直到CX为0,循环停止
INT20
4.输入G即可显示所有ASCII码
当我们想任意显示字符串,如:
UNDERSTAND?
,则可以使用DOS21H号中断9H号功能。
输入下行程序,存盘并执行看看:
1.输入A100
MOVDX,109;DS:
DX=字符串的起始地址
MOVAH,9;DOS的09h功能调用
INT21;字符串输出
INT20
DB'UNDERSTAND?
$';定义字符串
在汇编语言中,有两种不同的指令:
1.正规指令:
如MOV等,是属于CPU的指令,用来告诉CPU在程序执行时应做些什么,所以它会以运算码(OP-code)的方式存入内存中;2.伪指令:
如DB等,是属于DEBUG等编译器的指令,用来告诉编译器在编译时应做些什么。
DB(DefineByte)指令用来告诉DEBUG将单引号内的所有ASCII码放入内存中。
使用9H功能的字符串必须以$结尾。
用D命令可用来查看DB伪指令将那些内容放入内存。
6.输入D100
1975:
0100BA0901B409CD21CD-20756E6465727374......!
.underst
1975:
0110616E64248B46F889-45048B4634006419and$.F..E..F4.d.
1975:
012089450233C05E5FC9-C300C80400005756.E.3.^_.......WV
1975:
01306BF80E81C7FE538B-DF8BC2E832FE0BC0k.....S.....2...
1975:
0140740533C099EB178B-450CE8D4978BF089t.3.....E.......
1975:
015056FE0BD074EC8B45-0803C68B56FE5E5FV...t..E....V.^_
1975:
0160C9C3C80200006BD8-0E81C3FE53895EFE......k.....S.^.
1975:
01708BC2E8FBFD0BC075-098B5EFE8B470CE8.......u..^..G..
现在,我们来剖析另一个程序:
由键盘输入任意字符串,然后显示出来。
db20指示DEBUG保留20h个未用的内存空间供缓冲区使用。
输入A100
MOVDX,0116;DS:
DX=缓冲区地址,由DB伪指令确定缓冲区地址
MOVAH,0A;0Ah号功能调用
INT21;键盘输入缓冲区
MOVDL,0A;由于功能Ah在每个字符串最后加一个归位码(0Dh由Enter
MOVAH,02;产生),使光标自动回到输入行的最前端,为了使新输出的
INT21;字符串不会盖掉原来输入的字符串,所以利用功能2h加一
;个换行码(OAh),使得光标移到下一行的的最前端。
MOVDX,0118;装入字符串的起始位置
MOVAH,09;9h功能遇到$符号才会停止输出,故字符串最后必须加上
INT21;$,否则9h功能会继续将内存中的无用数据胡乱显示出来
INT20
DB20;定义缓冲区
送你一句话:
学汇编切忌心浮气燥。
客套话就不讲了。
工欲善其事,必先利其器。
与其说DEBUG是编译器,倒不如说它是“直译器”,DEBUG的A命令只可将一行汇编指令转成机器语言,且立刻执行。
真正编译器(MASM)的运作是利用文本编辑器(EDIT等)将汇编指令建成一个独立且附加名为.ASM的文本文件,称源程序。
它是MASM程序的输入部分。
MASM将输入的ASM文件,编译成.OBJ文件,称为目标程序。
OBJ文件仅包含有关程序各部份要载入何处及如何与其他程序合并的信息,无法直接载入内存执行。
链结程序LINK则可将OBJ文件转换成可载入内存执行(EXEcute)的EXE文件。
还可以用EXE2BIN,将符合条件的EXE文件转成COM文件(COM文件不但占用的内存最少,而且运行速度最快)。
下面我们用MASM写一个与用DEBUG写的第一个程序功能一样的程序。
用EDIT编辑一个SMILE.ASM的源程序文件。
源程序DEBUG程序
prognamsegment
assumecs:
prognam
org100hA100
movdl,1movdl,1
movah,2movah,2
int21hint21
int20hint20
prognamends
end
比较一下:
1.因为MASM会将所有的数值假设为十进制,而DEBUG则只使用十六进制,所以在源程序中,我们必须在有关数字后加上代表进制的字母,如H代表十六进制,D代表十进制。
若是以字母开头的十六进制数字,还必须在字母前加个0,以表示它是数,如0AH。
2.源程序增加五行叙述:
prognamsegment与prognamends是成对的,用来告诉MASM及LINK,此程序将放在一个称为PROGNAM(PROGramNAMe)的程序段内,其中段名(PROGNAM)可以任取,但其位置必须固定。
assumecs:
prognam必须在程序的开头,用来告诉编译器此程序所在段的位置放在CS寄存器中。
end用来告诉MASM,程序到此结束,ORG100H作用相当于DEBUG的A100,从偏移量100开始汇编。
COM文件的所有源程序都必须包含这五行,且必须依相同的次序及位置出现,这点东西记下就行,千篇一律。
接着,我们用MASM编译SMILE.ASM。
输入MASMSMILE←不用打入附加名.ASM。
Microsoft(R)MacroAssemblerVersion5.10
Copyright(C)MicrosoftCorp1981,1988.Allrightsreserved.
Objectfilename[SMILE.OBJ]:
←是否改动输出OBJ文件名,如不改就ENTER
Sourcelisting[NUL.LST]:
←是否需要列表文件(LST),不需要就ENTER
Cross-reference[NUL.CRF]:
←是否需要对照文件(CRF),不需要则ENTER
50162+403867Bytessymbolspacefree
0WarningErrors←警告错误,表示编译器对某些语句不理解,通常是输入错误。
0SevereErrors←严重错误,会造成程序无法执行,通常是语法结构错误。
如果没有一个错误存在,即可生成OBJ文件。
OBJ中包含的是编译后的二进制结果,它还无法被DOS载入内存中加以执行,必须加以链结(Linking)。
以LINK将OBJ文件(SMILE.OBJ)链结成EXE文件(SMILE.EXE)时,。
1.输入LINKSMILE←不用附加名OBJ
Microsoft(R)OverlayLinkerVersion3.64
Copyright(C)MicrosoftCorp1981,1988.Allrightsreserved.
RunFile[SMILE.EXE]:
←是否改动输出EXE文件名,如不改就ENTER
ListFile[NUL.MAP]:
←是否需要列表文件(MAP),不需要则ENTER
Libraries[.LIB]:
←是否需要库文件,要就键入文件名,不要则ENTER
LINK:
warningL4021:
nostacksegment←由于COM文件不使用堆栈段,所以错误信息
←"nostacksegment"并不影响程序正常执行
至此已经生成EXE文件,我们还须使用EXE2BIN将EXE文件(SMILE.EXE),转换成COM文件(SMILE.COM)。
输入EXE2BINSMILE产生BIN文件(SMILE.BIN)。
其实BIN文件与COM文件是完全相同的,但由于DOS只认COM、EXE及BAT文件,所以BIN文件无法被正确执行,改名或直接输入EXE2BINSMILESMILE.COM即可。
现在,磁盘上应该有SMILE.COM文件了,你只要在提示符号C:
>下,直接输入文件名称SMILE,就可以执行这个程序了。
prognamsegment;定义段
assumecs:
prognam;把上面定义段的段基址放入CS
movcx,100h;装入循环次数
movdl,0;装入第一个ASCII码,随后每次循环装入新码
next:
movah,2
int21h
incdl;INC:
递增指令,每次将数据寄存器DL内的数值加1
loopnext;循环指令,执行一次,CX减1,直到CX为0,循环停止
int20h
prognamends;段终止
end;汇编终止
在汇编语言的源程序中,每一个程序行都包含三项元素:
start:
movdl,1;装入第一个ASCII码,随后每次循环装入新码
标识符表达式注解
在原始文件中加上注解可使程序更易理解,便于以后参考。
每行注解以“;”与程序行分离。
编译器对注解不予理会,注解的数据不会出现在OBJ、EXE或COM文件中。
由于我们在写源程序时,并不知道每一程序行的地址,所以必须以符号名称来代表相对地址,称为“标识符”。
我们通常在适当行的适当位置上,键入标识符。
标识符(label)最长可达31个字节,因此我们在程序中,尽量以简洁的文字做为标识符。
现在,你可以将此ASCII.ASM文件编译成ASCII.COM了。
1.MASMASCII,2.LINKASCII,3.EXE2BINASCIIASCII.COM。
注意:
当你以编译器汇编你设计的程序时,常会发生打字错误、标识符名称拼错、十六进制数少了h、逻辑错误等。
汇编老手常给新人的忠告是:
最好料到自己所写的程序一定会有些错误(别人告诉我的);如果第一次执行程序后,就得到期望的结果,你最好还是在检查一遍,因为它可能是错的。
原则上,只要大体的逻辑架构正确,查找程序中错误的过程,与写程序本身相比甚至更有意思。
写大程序时,最好能分成许多模块,如此可使程序本身的目的较单纯,易于撰写与查错,另外也可让程序中不同部份之间的界限较清楚,节省编译的时间。
如果读程序有读不懂的地方最好用纸笔记下有关寄存器、内存等内容,在纸上慢慢比划,就豁然开朗了。
下面我们将写一个能从键盘取得一个十进制的数值,并将其转换成十六进制数值而显示于屏幕上的“大程序”。
前言:
要让8086执行这样的功能,我们必须先将此问题分解成一连串的步骤,称为程序规划。
首先,以流程图的方式,来确保整个程序在逻辑上没有问题(不用说了吧!
什么语言都要有此步骤)。
这种模块化的规划方式,称之为“由上而下的程序规划”。
而在真正写程序时,却是从最小的单位模块(子程序)开始,当每个模块都完成之后,再合并成大程序;这种大处著眼,小处著手的方式称为“由下而上的程序设计”。
我们的第一个模块是BINIHEX,其主要用途是从8086的BX寄存器中取出二进制数,并以十六进制方式显示在屏幕上。
注意:
子程序如不能独立运行,实属正常。
binihexsegment
assumecs:
binihex
movch,4;记录转换后的十六进制位数(四位)
rotate:
movcl,4;利用CL当计数器,记录寄存器数位移动次数
rolbx,cl;循环寄存器BX的内容,以便依序处理4个十六进制数
moval,bl;把bx低八位bl内数据转移至al
andal,0fh;把无用位清零
addal,30h;把AL内数据加30H,并存入al
cmpal,3ah;与3ah比较
jlprintit;小于3ah则转移
addal,7h;把AL内数据加30H,并存入al
printit:
movdl,al;把ASCII码装入DL
movah,2
int21h
decch;ch减一,减到零时,零标志置1
jnzrotate;JNZ:
当零标志未置1,则跳到指定地址。
即:
不等,则转移
int20h;从子程序退回主程序
binihexends
end
利用循环左移指令ROL循环寄存器BX(BX内容将由第二个子程序提供)的内容,以便依序处理4个十六进制数:
1.利用CL当计数器,记录寄存器移位的次数。
2.将BX的第一个十六进制值移到最右边。
利用AND(逻辑“与”运算:
对应位都为1时,其结果为1,其余情况为零)把不要的部份清零,得到结果:
先将BL值存入AL中,再利用AND以0Fh(00001111)将AL的左边四位清零。
由于0到9的ASCII码为30h到39h,而A到F之ASCII码为41h到46h,间断了7h,所以得到结果:
若AL之内容小于3Ah,则AL值只加30h,否则AL再加7h。
ADD指令会将两个表达式相加,其结果存于左边表达式内。
标志寄存器(FlagRegister)是一个单独的十六位寄存器,有9个标志位,某些汇编指令(大部份是涉及比较、算术或逻辑运算的指令)执行时,会将相关标志位置1或清0,常碰到的标志位有零标志(ZF)、符号标志(SF)、溢出标志(OF)和进位标志(CF)。
标志位保存了某个指令执行后对它的影响,可用其他相关指令,查出标志的状态,根据状态产生动作。
CMP指令很像减法,是将两个表