第一讲 学习汇编前你应该知道的知识 1 汇编需要什么工具和程序,到哪里下载? 目前阶段,汇编程序仅需要两个程序就够了。 masm.exe,link.exe。二者可由http://www.20cn.org/~unique/Download/Tool/masm.rar下载,前者是编译程序,后者是链接程序。 另外,为了验证和调试程序,还需要一个程序debug.exe,该程序由windows本身就提供,所以就不提供下载地址了。 将二者下载后,放到某一个目录中(任意目录都可以),考虑到很多命令需要通过键盘敲入,所以建议你不要把文件放入到长文件名目录、中文目录或很深的目录中。比如你可以建一个“D:/Masm”目录,并建议此后的程序都放这个目录,此后称这个目录为汇编目录。 2 学习汇编需要有哪些编程方面的知识。 没有任何编程方面的知识,学习此语言等于缘木求鱼,所以请放弃学习的想法。一般来说至少要知道如下几点: *)程序的运行逻辑结构有顺序(按语句依次执行)、分支结构(IF...THEN...ELSE...),循环结构(FOR...NEXT)三种结构。 *)知道什么是子程序,什么是调用。 *)汇编程序员的视角。不同编程视角编程要求是不一样的。比如删除文件, >>用户的视角是找到“删除”按钮或菜单,然后单击一下即可。 >>高级程序员的视角是知道删除的文件,并发出删除命令。这些通过API实现。 >>汇编程员的视角是得到要删除的文件名,找到该文件所在位置,通过调用删除“中断命令”进行删除。 >>操作系统开发人员的视角则是接到删除命令后,先找到系统根目录区,由根目录区的链接依次找到子目录区,直到找到要删除的文件,然后按照操作系统删除文件的规则对该文件名进行修改。比如DOS,只把第一个字符改成"?"。 按程序语句等价的角度看,一行VB的打印语句,用汇编实现大约需要一百二十多行。知道汇编语言的视角后就要知道,前面的道路是坎坷的,没有耐心是不行的。想通过几分钟几行程序就完成很复杂的操作不是件容易的事。 3 学汇编有什么用? 汇编产生于DOS时代或更早,而现在是Windows时代,所以可能遗憾地说:尽管还有批牛人在用汇编开发核心级程序,但我们几乎没什么用,除了必要时间能拿来分析一两个程序的部分代码之外,别的也就没干什么用了。并且并不是所有的汇编命令都能在windows下使用。而泛泛地追求“时髦”而学本语言,最后的结果是损了夫人又折兵。所以学之前你要考虑好。我劝那些为了当“黑客”而学汇编的人就此止步。 第零讲 预备知识 1 一个汇编程序的编译过程是怎么样的。 1)首先你需要找一个编辑器,编辑器用任何“纯文本”编辑器都可以。比如记事本。编好以后保存到汇编目录中。扩展名为asm,比如myfirst.asm。但这里建议你找一个能显示出当前行的编译器。这样出错后排错很容易。 2)然后在DOS下进入D:/Masm目录中,输入“masm myfirst.asm",如果有错系统会提示出错的行位置和出错原因。 3)然后再输入“link myfirst.obj”,即可看到当前目录下有一个myfirst.exe程序。 2 宏汇编和汇编有什么区别吗? 二者的区别在于前者提供宏,后者不提供。后者已找不到了,所以你可以认为二者没有区别。 3 机器语言、汇编语言、高级语言的关系 最早的计算机采用机器语言,这种语言直接用二进制数表示,通过直接输入二进制数,插拔电路板等实现,这种“编程”很容易出错,每个命令都是通过查命令表实现,既然是通过“查表”实现的,那当然也可以让计算机来代替人查表实现了。于是就产生了汇编语言,所以不管别人怎么定义机、汇语言,我就认为,二者是等价。后来人们发现,用汇编语言编某一功能的时候,连续一段代码都是相同或相似,于是就考虑用一句语言来代替这一段汇编语言,于是就产生了高级语言。因此,所有高级语言都能转化成汇编语言,而所以汇编语言又可转化成机器语言。反之,所有机器语言可以转成汇编语言(因为二者等价)。但并不是所以汇编语言都能转成高级语言。 4 计算机的组成 通常都把计算机定义成五部分:运算器、控制器、存储器、输入系统、输出系统。 为了简单其间,我们如此理解:运算器+控制器=CPU。存储器=内存(暂不包括外存,永不包括CACHE)。输入系统=键盘(不包括鼠标),输入系统=显示器(不包括打印机,绘图仪)。 5 寄存器和内存的区别 寄存器在CPU中。内存在内存条中。前者的速度比后者快100倍左右。后面的程序要求每条指定要么没有内存数据,要么在有一个寄存器的参与下有一个内存数据。(也就是说,不存在只访问内存的指令)。 6 汇编语言的计数 与生活中的计数不一样,汇编中的计数是从0开始的。比如16个计数,则是从0~15,而不是生活中的1~16。这一点看起来简单,真运算起来就不是件容易的事了,不信等着瞧。 7 进制问题 又与生活中不一样的地方是进制。切记下面的常识: *)计算机内部存储都用二进制。 *)我们的汇编源程序默认都用十进制。(除非你指明类型) *)我们用的调试程序debug默认的都是十六进制。(无法指明其他类型) 其中十六进制的十六个个位数依次是:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F。 8 进制转换 一个比较简单的方法是查表法。 十进制 十六进制 二进制 0 0 0000 1 1 0001 2 2 0010 3 3 0011 4 4 0100 5 5 0101 6 6 0110 7 7 0111 8 8 1000 9 9 1001 10 A 1010 11 B 1011 12 C 1100 13 D 1101 14 E 1110 15 F 1111 好了,结合6,7,8三条。大家来算一个“题”。某一组数据显示时,每个数据占了四个位置, 每行共十六个。问:十六进制的13位置在哪里(第几行,第几列)。 格式如下:m m m m n n n n o o o o p p p p '注:之所以没用ABC是怕与上面十六进制弄混。 r r r r s s s s t t t t u u u u 第一讲 基础知识 1 访问内存 程序在内存中,访问内存是几乎每一程序都要进行的操作,计算机对内存编址是线性的,也就是说是一维的,比如256M的内存,地址就应该是从0~(256M-1),这个地址称为物理地址或绝对地址。 1.1 地址表示 但从汇编程序员的角度看,内存却是二维的,要说明一个地址,需要给出两个值,就象你在平面上指定一点需要说出(X,Y)坐标一样,汇编程序员的内存视角也需要两个“坐标”,前一个称为段地址(Segment),后一个称为偏移地址(Offset),该地址称为逻辑地址。 比如“1234:3DF5”就是一个地址。“1F3F:”不是一个地址,因为他只有段地址,没有编移地址。注意此后的地址都用十六进制表示。 1.2 地址计算 前面提到,计算机编址是一维的,汇编程序员是二维的,那么二者怎么换算呢?由后者到前者的换算方法是,“段地址串”后面加个“0”,然后再加上偏移地址。 比如“1234:3DF5”(十六进制的加减运算参见相关资料) 12340 ‘串后加了一个0 3DF5 ----- 16135 ’注意此串仍然是十六进制。 所以,汇编程序员眼中的地址“1234:3DF5”就是物理地址(计算机编址):16135。 知道了由后者向前者的转换,那么由前者向后者的转换呢? “不知道”,为什么不知道,继续往下看。 1.3 到底哪个地址对。 知道了1.2的地址算法后,我又发现一个问题: “1000:6135”的物理地址是多少呢? 10000+6135=16135。 “1001:6125”的物理地址呢? 10010+6125=16135。 ...... 那么到底哪个对呢?问题的回答是这样的:假设我现在让你按一下“L”键,我可以告诉你如下几种方法中的一种或几种。1 请按一下“L”键; 2请按一下键盘上第四行第十个键;3 请按一下第十列中的第四个键;4 请按一下“K”右边的键;5 按标准指法单击一下右手无名指。 举上面的例子也就是说,同一个地址有很多种表示方式,具体用哪一种,要看实际使用时的情况。但无论用哪种方式,只要能达到目的即可。(实际中该问题一般不会受此问题困扰,但初学时突然想不通)。 1.4 有多少内存可以访问 无论是段地址还是偏移地址都是四位十六进制(如果不够四位,前面补0)。也就是说:总共可以访问的地址说是:0000:0000~FFFF:FFFF。 总共FFFF0+FFFF+1=10FFF0个地址。也就是不到1M的空间。 记住如下结论: *)不管你实际内存有多少,目前我们只能访问不到1M的空间。 *)而实际上连这1M也用不完。其中上端的384K的址只能读不能写,只能读,一般称为ROM。 *)低端的640K可以读写。但这640K的低端100多K也不能随便写,因此DOS系统使用该区。 *)原来1024M的内存,汇编程序只能使用其中400多K。这段内存的容易相当于一个普通文档的大小。不过这就足够了。 2 DEBUG的使用 先记住以下两个命令:D命令和Q命令。前者是显示内存内容,后者是退出DEBUG命令。 -------------以下为抄别的人内容--------------- DEBUG.EXE程序是专门为分析、研制和开发汇编语言程序而设计的一种调试工具,具有跟踪程序执行、观察中间运行结果、显示和修改寄存器或存储单元内容等多种功能。它能使程序设计人员或用户触及到机器内部,因此可以说它是80X86CPU的心灵窗口,也是我们学习汇编语言必须掌握的调试工具。 1)DEBUG程序使用 在DOS提示符下键入命令: C>DEBUG [盘符:][路径][文件名.EXE][参数1][参数2] 这时屏幕上出现DEBUG的提示符“-”,表示系统在DEBUG管理之下,此时可以用DEBUG进行程序调试。若所有选项省略,仅把DEBUG装入内存,可对当前内存中的内容进行调试,或者再用N和L命令,从指定盘上装入要调试的程序;若命令行中有文件名,则DOS把DEBUG程序调入内存后,再由DEBUG将指定的文件名装入内存。 2)DEBUG的常用命令 (1)退出命令 Q 格式:Q 功能:退出DEBUG,返回到操作系统。 (2)显示存储单元命令 D 格式1:D[起始地址] 格式2:D[起始地址][结束地址|字节数] 功能:格式1从起始地址开始按十六进制显示80H个单元的内容,每行16个单元,共8行,每行右边显示16个单元的ASCII码,不可显示的ASCII码则显示“·”。格式2显示指定范围内存储单元的内容,其他显示方式与格式1一样。如果缺省起始地址或地址范围,则从当前的地址开始按格式1显示。 例如: -D 200 ;表示从DS:0200H开始显示128个单元内容 -D 100 120 ;表示显示DS:0100-DS:0120单元的内容 说明:在DEBUG中,地址表示方式有如下形式: 段寄存器名:相对地址,如:DS:100 段基值:偏移地址(相对地址),如:23A0:1500 --------------------------小抄结束-------------------------------- 3 验证第一节里的内容 运行“开始/程序/附件/MS-DOS命令提示符”(这是win2000,win98下自己找吧) 在“_”下输入D,显示 -d 1398:0100 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1398:0110 00 00 00 00 00 00 00 00-00 00 00 00 34 00 87 13 ............4... 1398:0120 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1398:0130 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1398:0140 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1398:0150 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1398:0160 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1398:0170 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ - 我们记下:1398:011C的值是个34。1389:011C的物理地址应该是:13A9C。 那么1000:3A9C的物理地址也应该是13A9C,他的内存也应该是34,(因为本来就是一个地址吗,就象第三行第十列和第十列第三行当然应该是同一个位置)。 -d 1000:3A9C 1000:3A90 34 00 87 13 4... 1000:3AA0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1000:3AB0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1000:3AC0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1000:3AD0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1000:3AE0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1000:3AF0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1000:3B00 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 1000:3B10 00 00 00 00 00 00 00 00-00 00 00 00 ............ - 果然如此,同样你可以验证:13A9:000C也肯定是指这一个地址,不信试试。 4 DEBUG命令 -------------------继续小抄---------------- 前面已学过:显示存储单元命令 D 再学一个命令 (1)修改存储单元命令 E 格式1:E[起始地址] [内容表] 格式2:E[地址] 功能:格式1按内容表的内容修改从起始地址开始的多个存储单元内容,即用内容表指定的内容来代替存储单元当前内容。 例如:—E DS:0100 'VAR' 12 34 表示从DS:0100 为起始单元的连续五个字节单元内容依次被修改为 'V'、'A'、'R'、12H、34H。 格式2是逐个修改指定地址单元的当前内容。 如:—E DS:0010 156F:0010 41.5F 其中156F:0010单元原来的值是41H,5FH为输入的修改值。若只修改一个单元的内容,这时按回车键即可;若还想继续修改下一个单元内容,此时应按空格键,就显示下一个单元的内容,需修改就键入新的内容,不修改再按空格跳过,如此重复直到修改完毕,按回车键返回DEBUG“-”提示符。如果在修改过程中,将空格键换成按“-”键,则表示可以修改前一个单元的内容。 -------------------小抄结束---------------- 5 使用DOS时,汇编用户可以从DOS操作系统中得到什么? 现在编程,通常很多功能都是通过调用系统API。很多高级语言都直接把这些API包装起来,以系统接口或函数的方式提供给用户,那么汇编函数都能得到什么呢? 首先,汇编用户有很多东西可以调用。他们主要是: 5.1 BIOS提供的接口。现在硬件与软件的区分已越来越不明显,很多硬件不仅仅是电路,而还要提供一些固化写入硬件的一部分“程序”,这些程序以ROM的方式出现,汇编用户最大的好处就是可以直接使用这些“程序”,这些使用不仅功能强大,而且效率非常高。 5.2 DOS功能调用,作为操作系统也象BIOS一样向用户提供了相应的“程序”。这些程序在很大程序上扩充了BIOS。与BIOS不同的是,这部分程序放在内存中,它可以被修改。而BIOS中不能再修改。 ========================================================== 以上两种接口都通过一种相同的格式调用,这些程序统称为“中断”,现在先不要理解中断的本意,你现在可以认为是系统提供给你的函数。 ============================================================ 5.3 系统共享数据区。编过程序的人都知道全局变量的好处,全局变量方便之外在于任何函数、过程都可以调用、读取、修改。全局变量不足之处是危险性,有一个过程改了这个变量值,其它的也得跟着改变了。DOS操作系统同样也提供了这样的共享数据区,该区是整个系统的共享区,任何程序都可以查找、修改。当然,修改某处必然会对其它程序造成影响。 6 再谈中断 前面5.2已提到中断了,现在问题是不同硬件不一样,即使相同硬件的ROM,不同版本,各个BIOS中断程序所处的位置也不一样,DOS中断也一样,不同版本、不同配置,在内存位置也不一样。那么你使用某一个中断,系统怎么知道你使用的那个中断程序在哪呢? 为了解决这一问题,DOS会在启动的时候,把所有这些(BIOS和DOS)中断的首地址保存到一个地址。这个地址很容易记,这段地址是内存的绝对零地址(0000:0000)。前面已讲过,每个地址在汇编程序员角度来看是二维的,也就是分为段地址和偏移地址。每个地址各占两个字节,所以要表示这个二维地址需要4个字节。所以每个中断首地址由4个字节表示。一共256个中断,占用了1024个字节的位置。 另外需要注意的是,这4个表示地址的字节,数据是由低向高的。比如12 34 56 78所表示的地址是:7856:3412。 一般用INT M表示中断M,如果M是十六进制,则在后面加上一个H。比如19号中断,十六进制应该是13H。所以该中断就是INT 13H。 7 再谈系统共享数据区 该共享数据区在绝对地址:0040:0000开始。 8 验证我上面说的内容 8.1 找中断 运行DEBUG后。输入D 0000:0000。显示绝对零地址的内容。 C:/>debug -d 0:0 0000:0000 68 10 A7 00 8B 01 70 00-16 00 9B 03 8B 01 70 00 h.....p.......p. 0000:0010 8B 01 70 00 B9 06 0E 02-40 07 0E 02 FF 03 0E 02 ..p.....@....... 0000:0020 46 07 0E 02 0A 04 0E 02-3A 00 9B 03 54 00 9B 03 F.......:...T... 0000:0030 6E 00 9B 03 88 00 9B 03-A2 00 9B 03 FF 03 0E 02 n............... 0000:0040 A9 08 0E 02 99 09 0E 02-9F 09 0E 02 5D 04 0E 02 ............]... 0000:0050 A5 09 0E 02 0D 02 DC 02-B8 09 0E 02 8B 05 0E 02 ................ 0000:0060 02 0C 0E 02 08 0C 0E 02-13 0C 0E 02 AD 06 0E 02 ................ 0000:0070 AD 06 0E 02 A4 F0 00 F0-37 05 0E 02 71 84 00 C0 ........7...q... -u 0070:018B 0070:018B 1E PUSH DS 0070:018C 50 PUSH AX 0070:018D B84000 MOV AX,0040 0070:0190 8ED8 MOV DS,AX 0070:0192 F70614030024 TEST WORD PTR [0314],2400 0070:0198 754F JNZ 01E9 0070:019A 55 PUSH BP 0070:019B 8BEC MOV BP,SP 0070:019D 8B460A MOV AX,[BP+0A] 0070:01A0 5D POP BP 0070:01A1 A90001 TEST AX,0100 0070:01A4 7543 JNZ 01E9 0070:01A6 A90002 TEST AX,0200 0070:01A9 7422 JZ 01CD 首先,D命令把中断首地址显示出来。每4个表示一个地址。其中INT 0的中断首地址为:00A7:1068,INT 1的中断地址为:0070:018B.......0070:018B是中断3的首地址。后面那个U命令就表示显示该地址的“中断程序”的内存。 你们可以试着找找INT 13H的位置在哪。 8.2 验证系统共享数据区 系统共享数据区内容极为丰富,我实在记不住哪么多了。我曾记在一个本上,可惜那个本早在N年前(3
<6)就丢了。兄弟们谁找到这个地址的内容,一定要贴上来,这里有东西可以让大家眼界大开。 前几年,我用的286计算机是黑白显示器(555555~~~~~~~~~,别嫌我老、旧、慢呀),可当时有个游戏非要彩显,不是彩显不让运行。我就是改了这个区的某一个位,让哪游戏“以为”我用的是彩显,于是游戏能用了。虽然不好看,但总能用。 在DOS下,你每按一个键,系统都会记下来,下面我们一起找找这个键盘缓冲区的地址。知道这个地址,你就可以作一个“虚拟”键盘,通过发命令来模拟某个人在按键。这个地址位于:0040:001E。 其中每个键有两个字节,一个字节是ASCII码,一个是扫描码。共16个。 C:/>debug -d 40:0 0040:0000 F8 03 F8 02 E8 03 E8 02-BC 03 78 03 78 02 80 9F ..........x.x... 0040:0010 22 C8 00 80 02 28 00 00-00 00 2A 00 2A 00 20 39 "....(....*.*. 9 0040:0020 34 05 30 0B 3A 27 30 0B-0D 1C 64 20 20 39 34 05 4.0.:'0...d 94. 0040:0030 30 0B 3A 27 30 0B 0D 1C-71 10 0D 1C 64 20 00 00 0.:'0...q...d .. 0040:0040 A2 00 C3 00 A2 AF 09 E1-C8 03 50 00 00 10 00 00 ..........P..... 0040:0050 00 18 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0040:0060 0F 0C 00 D4 03 29 30 7F-03 00 C0 00 A1 B7 11 00 .....)0......... 0040:0070 00 00 00 00 00 00 00 00-14 14 14 00 01 01 01 01 ................ -d 0040:0000 0040:0000 F8 03 F8 02 E8 03 E8 02-BC 03 78 03 78 02 80 9F ..........x.x... 0040:0010 22 C8 00 80 02 28 00 00-00 00 2A 00 2A 00 3A 27 "....(....*.*.:' 0040:0020 30 0B 30 0B 30 0B 30 0B-0D 1C 64 20 20 39 30 0B 0.0.0.0...d 90. 0040:0030 30 0B 30 0B 30 0B 08 0E-08 0E 34 05 30 0B 00 00 0.0.0.....4.0... 0040:0040 1F 00 C3 00 A2 AF 09 E1-C8 03 50 00 00 10 00 00 ..........P..... 0040:0050 00 18 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0040:0060 0F 0C 00 D4 03 29 30 7F-03 00 C0 00 24 B8 11 00 .....)0.....$... 0040:0070 00 00 00 00 00 00 00 00-14 14 14 00 01 01 01 01 ................ - 既然是键盘缓冲区,每个输入的键都会显示在该区中,第一次我只输入了“d 40:0”,所以你可以在此后显示数据右边字符中找到这些字符,注意是间隔开的。 第二次我输入“d 0040:0000”,则右边显示的是“d 0040:0000”的内容。你可以找找。 第二讲 内存映象 之所以把这个内存单独放一章,是为了说明它的重要性,后面的几乎很多程序都需要你对这一章的理解。这里的内存映象就是指当你把一个可执行文件(EXE或COM文件)放到内存后,整个内存“看”起来是什么样子的。 前面讲过,这里汇编程序只能访问1M的内存空间,所以下面就以1M内存为例。并且以DOS操作系统作为讲解对象,所以所编出来的程序也仅是DOS程序。事实上,通过winasm可以访问远远超过1M的空间,并且可以编出FOR windows的程序。但那是另外的话题。我们暂且不说那些。 2.1 内存映象 首先,这1M内存如果我们不再以二维的方式看,而是一维的,线性地看(二维和一维的转化方式参见前面章节)。但描述还是以二维的方式描述,从最底端到最高端依次是: 1 中断向量区:该区由0000:0000~0000:03FF。这里存着系统的所有中断的中断向量表,对于中断向量表,你现在先理解为一些程序的首地址。由这个地址你就能找到该程序。 2 系统数据区:该区由0040:0000~0040:XXXX(不好意思,忘了),这里存着整个系统中,DOS操作系统要用的数据,由于这个区的数据对用户是开放的,所以用户当然也可以从这里读出来用。 3 DOS操作系统区:操作系统常驻内存,你向计算机发的每个命令其实都是操作系统执行的。这个区的大小主要是由操作系统的版本和用户的配置大小决定,如果是驱动程序配置,就放到根目录下的config.sys里,如果是程序,就放到autoexec.bat里。这里设置在现在的windows 95/98/nt/me/2000/xp/2003中仍然有,所以我就不多说了。 4 用户程序,这个当然就是你执行的程序了,这种程序分两种,一种是扩展名为com文件,一种是exe文件,从程序内部看,前者程序的四个段重合(后面要讲这四个段),所以最大长度只等于一个段,用前面段地址的理解就是com文件最大只能是64K,所以com文件只适合小的程序。而exe,四个段可任何分配,并可扩充段,而且每个段的段地址可以任何改动,因此exe的访问内存能力大多了。这种格式访问能力只受地址结构的限制了。 用户程序所占的内存大小完全由程序本身决定,但最大,只能到640K。这一点,怪不得别人,只能怪当前计算机软硬件设置高手高手高高手们(包括比尔盖茨)们的失误了,60年代的超级计算机只有36K的内存,所以他们就在80年代得到一个结论:640K的内存足够了。 如果用户程序大于由操作系统所占内存的顶底到640K之间的内存量,就会显示:内存不够,因而程序不能执行。这种现象对于一开始就用windows的人来说,几乎没见过,但对于一开始用DOS并打汉字的人来说,再正常不过。如果小于这段内存,多余部分就空着。 5 从640K到1M-64K,这段内存就很难说清了。这段内存中有一部分被硬件占有,有一部分是显示缓冲区点有,还有一部分是系统ROM占有。 6 从1M-64K到1M之间的这段64K的内存叫作HMA。这段内存是小孩没娘,说来话长,我们先不说他。 2.2 验证上面的理论 2.2.1 中断向量表 中断向量表就是所有中断向量首地址表,这里保存着每个中断程序的首地址,几乎所有的汇编书都把中断后面后面的章节中,并且对中断的解释也仅从字面意思解释,所以导致大学对中断的不重要和误解。没耐心的没到这个章节就不学汇编了,有耐心的到这里才豁然开朗。我现在不讲中断的原意。我直接告诉你,你把中断当成API也许更合适。也就是说,别人把很多已作好的功能放到了内存中。并且把调用这一功能的号告诉了你,你只要调用这些功能号,系统就自动从这个中断向量表中找到对应的中断,然后执行你的功能。 首先让你感受一下中断的魅力一下吧。比如中断21H的2A功能调用是读取系统的日期,这个调用的规则是,调用前AH寄存器置为2A。调用后年在CX中,月在DH中,DL在日中,星期在AL中。 -a 139D:0100 mov ah,2a 139D:0102 int 21 139D:0104 int 3 139D:0105 -g=100 AX=2A05 BX=0000 CX=07D4 DX=0C18 SP=FFEE BP=0000 SI=0000 DI=0000 DS=139D ES=139D SS=139D CS=139D IP=0104 NV UP EI PL NZ NA PO NC 139D:0104 CC INT 3 - 可能上面的程序你目前还看不懂。不过没关系,“mov ah,2a”表示调用功能号是2a号。“int 21”表示调用十六进制21号中断,“int 3”表示3号中断,表示程序运行到这一句时停一下。“g=100”表示从“139D:0100 ”开始执行。 AX=2A05 BX=0000 CX=07D4 DX=0C18 SP=FFEE BP=0000 SI=0000 DI=0000 DS=139D ES=139D SS=139D CS=139D IP=0104 NV UP EI PL NZ NA PO NC 表示执行的结果。其中CX是年,这个年是由CX中存。07D4十进制就是2004年。DH+DL=DX,所以DH=0C,DL=18。二者转化为十进制就是DH=12,DL=24,也就是今天了。AX=AH+AL=2A05,所以AL=05。那就是今天是星期五。 上面可能你们现在还看不懂,不过通过解说你应该可以知道,仅仅两行命令,就读到了现在的值。现在需要作的就是把这些值提取出来用作他用了。 从中断的作来与中断向量表又有什么关系呢?原来你在汇编里运行int 21时,系统就在上面的中断向量表中找到int 21的中断地址,该中断的地址应该位于:0000:0084~0000:0087,具体算法前面已说明了。 -d 0000:0084 0087 0000:0080 7C 10 A7 00 |... - 找到内容是:00A7:107C。然后系统就转到这个地址执行int 21。 2.2.2 系统数据区前面都已说明过。不再多说。系统区,很多DOS中断程序实现部分就在这个区。程序运行区依不同的程序而不用。 2.2.3 640K~1M之间,这期间有些地方是ROM,有些地方是硬件的BIOS区。我仅以两个例子说明这一区。 ROM区:ROM区就是只读内存,也就是说这个区的数据只能读不能写。比如F000:0000开始的内存是ROM。我们来写一下,然后再看看效果。 -d f000:0000 0005 '显示由F000:0000到F000:0005的六个字节值。 F000:0000 04 E8 A2 FF F9 C3 ...... -e f000:0000 '修改命令 F000:0000 04.00 E8.00 A2.00 FF.00 F9.00 C3.00'注意,.后面的是我改的,把这几个值都改成0了。 -d f000:0000 0005 '再次显示这个区的数据。 F000:0000 04 E8 A2 FF F9 C3 ...... - 通过上面测试,发现该区数据仍然未改变。但你要是试别的RAM区的,肯定会变。如果想试你自己试试吧。 显示缓冲区:在文本方式下,B800:0000开始的地址保存着屏幕上每个字符位置的值。在文本方式下,屏幕被分为80 X 25。每个位置有两个值,一个值是ASCII字符,一个值是该ASCII的属性值(主要是颜色)。所以一个屏幕共有80X25X2=400个字符。 我们来改: -d b800:0000 0010 '显示屏幕缓冲区的内容,注意此时本行最左边的“-”是屏幕左上角。 B800:0000 2D 07 64 07 20 07 62 07-38 07 30 07 30 07 3A 07 -.d. .b.8.0.0.:. B800:0010 30 0 - 看上面的命令,屏幕最上边一行是“-d b800:0000 0010”,所以他的内容就是“2D 07 64 07 20 07 62 07-38 07 30 07 30 07 3A 07”其中,2D是“-”的ASCII值,07是“-”的属性值。64是“d”的ASCII值,07是“d”的属性值。。。。。 现在修改这些值。我把左上角的字改成黄颜色的“-”,那当然是改b800:0001的属性值了。 -e b800:0001 0e 是不是左上角的颜色变成黄色了吗? 好了,把第二个字符变成绿色的“-”吧? -e b800:0002 2d 0b 变了吗? 可执行文件内存映象 DOS下可执行文件有两种(BAT是批处理文件,他只是简单调用DOS内部命令或其它程序,所以此处不认为它是可执行文件),一种是COM文件,一种是EXE文件,前面提到,COM文件一般小于64K。EXE文件则可以任意大。为什么呢? 说到这里,还要提到段。每个段64K。段的作用就是数据组织单位。段的类型有三种:代码段(Code Segment,简称CS)、数据段(Data Segment,简称DS)、栈段(Stack Segment,简称SS),另外还有一个附加数据段(Extra Segment,简称ES),它的用与数据段DS可以认为完全一样,当数据段的64K不够用,或你就需要把数据放到两个段中以便移动、复制、比较时,才用到附加数据段ES。(当然,移动、复制、比较操作在一个段中也可以完成)。 1 段的作用。 1.1 代码段(CS),程序装入内存中,DOS怎么知道是从哪里执行呢?答案就是系统自动从代码段指定位置开始执行,并且始终在代码段中执行。内此代码段CS的作用就是保存所有的指令。这里所说的代码也就是汇编指令了。所以编写汇编程序也就主要是编写代码段中的代码。 1.2 数据段(DS)、附加段(ES),顾名思义,数据段中存的就是数据,这些数据供代码段的程序调用。附加段就是附加数据段。作用与数据段相同。 1.3 栈段(SS),这个段非常重要,但实际上,你在使用中,似乎用不着这个段,但实际上,这是黑客编程中最重要的一部分,而且系统会不停地“偷偷地”使用这个段,正是这个偷偷地用,使得系统的很多动作被记录到这个段中。还有两点,你必须记住:一是如果你使用了这个栈,比如你把数据存到这个栈中,则必须有相应的出栈命令,并且入几个数据,就得出几个数据,多一个或少一个,你的程序就可能导致死机或异常;二是你要把握操作时机,比如你不能在系统使用栈的前后使用栈,比如你在调用子程序之前入栈,而在子程序中出栈,而在系统调用子程序时,系统也要使用栈,这种也将导致出错。 栈就是一种先入后出(也有称为后入先出)的结构,有地址由小到大的增加栈,有地址由大到小的逆向减栈。 2 段重叠 从上面,我们可以看到,CS,DS,SS三者作用各不相同,内存就是象录音磁带,录新歌,则旧歌被删,带子上存的始终是最后录的那段音乐。因此,如果重叠则必然相互冲突。那还能重叠吗? 这里所说的重叠不是指内容重叠,而是指概念上的重叠,即数据相互放到一个段中,但相互可以区分开。比如某一段既有数据也有代码,则代码在每要执行到数据之前加一个跳转指令跳过这段代码。这个跳转指令要求用户在编程的时候加上。 而栈段呢?栈段有自己的特殊性,特殊就在于系统也会自动地使用,而用户则又在不知道系统在使用的情况下使用。避免这种冲突的方法就是采用逆向的栈段。 2 COM文件内存映象。 COM文件被读到内存中后,该文件的前100H个字节被操作系统使用,操作系统使用这256个字节保存一些系统要使用的数据,汇编语言编程者不能在这里存自己的数据,但在知道这此数据的作用后可以使用其中的数据。从100H开始,就是程序的开始了。COM文件之所以最大只能有64K,其原因是COM文件的四个段是相互重叠的。也就是说,CS,DS,SS,ES四个段的地址都指向这个COM文件的100H处。程序代码、数据、栈都在由100H到64K的区域内。如何把三者分开呢?栈段采用逆向栈,这个栈由64K开始,随着数据入栈,则地址就减小。这样作的好处是,栈段由高端向低端进展,可以详细与数据、代码分开;坏处也不言而喻,假如一个COM程序大量用到栈(比如是个递归程序)因此栈就不停地降低,而程序代码本身也很多,甚至不停地申请新空间,这样数据和栈就会在中间碰头,导致程序被破坏。 区分开数据代码段与栈段后,下面讨论把数据段和代码段也分开。这个简单的多,只要逻辑上分开就可以。不过一般的方法就是:在100H处放一个跳转指令,随后放数据,然后再放置其它的代码。而100H处的跳转指令就跳到这里。 因此,COM文件内存映象就是: CS:0000 (由于COM的CS,DS,SS,ES三段重叠,因此此行前CS,写成DS,SS,ES都一样)。 CS:0100 一个跳转到YYYY地址的跳转指令。 CS:0101 本程序所需要用到的数据 CS:XXXX 数据结束处。 CS:YYYY 程序代码保存处。 CS:ZZZZ 程序代码结束处。 CS:FFFF 栈段开始处(注意栈是地址越来越小,所以这里是开始而不是结束处),也是程序的结束处。另外,此处FFFF与前面XXXX,YYYY,ZZZZ不一样,这里是十六进制的64K。 3. EXE文件 比起COM文件,EXE文件要复杂一些,他的复杂就在于COM文件前面规定了100H个字节用于系统使用,而EXE文件则有个文件头,文件头的大小看具体内容多少。文件头的内容使得EXE看起来复杂了,但也更灵活了。更重要的是,对于病毒设计者,这个文件头使他们如鱼得水。因为文件头处 EXE文件的内存映象为: XXXX:0000 文件头 XXXX:YYYY 文件头结束处。 CS:0000 代码段开始处 CS:ZZZZ 代码段结束处 DS:0000 数据码段开始处 DS:WWWW 数据码段结束处 SS:0000 栈段开始处 SS:UUUU 栈段结束处 ES:0000 附加段开始处 ES:VVVV 附加段结束处 说明: 1 上述ES可以没有,要看实际需要 2 CS,DS,ES,SS的顺序也是看编程者是怎么安排的,好在用户不必关心他的具体位置。 3 由上可见,CS,DS,ES,SS的段地址肯定保存到了文件头中。 4 由上可见,实际执行的只是CS,因此DS,ES,SS的首地址,CS肯定要想办法知道。:) 第三章 汇编指令 3.1 什么是机器语言 前面提到“最早的计算机采用机器语言,这种语言直接用二进制数表示,通过直接输入二进制数,插拔电路板等实现,这种“编程”很容易出错,每个命令都是通过查命令表实现”。 比如要执行21号中断,需要查表,得到21号中断的指令就是CD 21。这样不管你通过什么方式,在内存指令位置,写入两个字节,一个是CD(这可不是音乐光盘,而是二进制数,转成十进制就是205),另一个是21(同样是十六进制,十进制是33)。 上面就是机器语言。 3.2 什么是汇编语言 前面也提到“既然是通过“查表”实现的,那当然也可以让计算机来代替人查表实现了。于是就产生了汇编语言”,汇编语言产生的重要目的就是用容易记的符号来代替容易出错的二进制数(或十六进制数)。 比如前面的21号中断,机器语言是CD 21。而汇编语言就规定中断用int表示(interrupt的前三个字母),21号中断就成了int 21h。其中21后面的h表示是表示这个21是十六进制。由于大小写不敏感,所以int 21h写成下列方式都等价: int 33 Int 21h INT 21H 3.3 汇编指令集 一、数据传输指令 ─────────────────────────────────────── 它们在存贮器和寄存器、寄存器和输入输出端口之间传送数据. 1. 通用数据传送指令. MOV 传送字或字节. MOVSX 先符号扩展,再传送. MOVZX 先零扩展,再传送. PUSH 把字压入堆栈. POP 把字弹出堆栈. PUSHA 把AX,CX,DX,BX,SP,BP,SI,DI依次压入堆栈. POPA 把DI,SI,BP,SP,BX,DX,CX,AX依次弹出堆栈. PUSHAD 把EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI依次压入堆栈. POPAD 把EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX依次弹出堆栈. BSWAP 交换32位寄存器里字节的顺序 XCHG 交换字或字节.( 至少有一个操作数为寄存器,段寄存器不可作为操作数) CMPXCHG 比较并交换操作数.( 第二个操作数必须为累加器AL/AX/EAX ) XADD 先交换再累加.( 结果在第一个操作数里 ) XLAT 字节查表转换. ── BX 指向一张 256 字节的表的起点, AL 为表的索引值 (0-255,即 0-FFH); 返回 AL 为查表结果. ( [BX+AL]->AL ) 2. 输入输出端口传送指令. IN I/O端口输入. ( 语法: IN 累加器, {端口号│DX} ) OUT I/O端口输出. ( 语法: OUT {端口号│DX},累加器 ) 输入输出端口由立即方式指定时, 其范围是 0-255; 由寄存器 DX 指定时, 其范围是 0-65535. 3. 目的地址传送指令. LEA 装入有效地址. 例: LEA DX,string ;把偏移地址存到DX. LDS 传送目标指针,把指针内容装入DS. 例: LDS SI,string ;把段地址:偏移地址存到DS:SI. LES 传送目标指针,把指针内容装入ES. 例: LES DI,string ;把段地址:偏移地址存到ES:DI. LFS 传送目标指针,把指针内容装入FS. 例: LFS DI,string ;把段地址:偏移地址存到FS:DI. LGS 传送目标指针,把指针内容装入GS. 例: LGS DI,string ;把段地址:偏移地址存到GS:DI. LSS 传送目标指针,把指针内容装入SS. 例: LSS DI,string ;把段地址:偏移地址存到SS:DI. 4. 标志传送指令. LAHF 标志寄存器传送,把标志装入AH. SAHF 标志寄存器传送,把AH内容装入标志寄存器. PUSHF 标志入栈. POPF 标志出栈. PUSHD 32位标志入栈. POPD 32位标志出栈. 二、算术运算指令 ─────────────────────────────────────── ADD 加法. ADC 带进位加法. INC 加 1. AAA 加法的ASCII码调整. DAA 加法的十进制调整. SUB 减法. SBB 带借位减法. DEC 减 1. NEC 求反(以 0 减之). CMP 比较.(两操作数作减法,仅修改标志位,不回送结果). AAS 减法的ASCII码调整. DAS 减法的十进制调整. MUL 无符号乘法. IMUL 整数乘法. 以上两条,结果回送AH和AL(字节运算),或DX和AX(字运算), AAM 乘法的ASCII码调整. DIV 无符号除法. IDIV 整数除法. 以上两条,结果回送: 商回送AL,余数回送AH, (字节运算); 或 商回送AX,余数回送DX, (字运算). AAD 除法的ASCII码调整. CBW 字节转换为字. (把AL中字节的符号扩展到AH中去) CWD 字转换为双字. (把AX中的字的符号扩展到DX中去) CWDE 字转换为双字. (把AX中的字符号扩展到EAX中去) CDQ 双字扩展. (把EAX中的字的符号扩展到EDX中去) 三、逻辑运算指令 ─────────────────────────────────────── AND 与运算. OR 或运算. XOR 异或运算. NOT 取反. TEST 测试.(两操作数作与运算,仅修改标志位,不回送结果). SHL 逻辑左移. SAL 算术左移.(=SHL) SHR 逻辑右移. SAR 算术右移.(=SHR) ROL 循环左移. ROR 循环右移. RCL 通过进位的循环左移. RCR 通过进位的循环右移. 以上八种移位指令,其移位次数可达255次. 移位一次时, 可直接用操作码. 如 SHL AX,1. 移位>1次时, 则由寄存器CL给出移位次数. 如 MOV CL,04 SHL AX,CL 四、串指令 ─────────────────────────────────────── DS:SI 源串段寄存器 :源串变址. ES:DI 目标串段寄存器:目标串变址. CX 重复次数计数器. AL/AX 扫描值. D标志 0表示重复操作中SI和DI应自动增量; 1表示应自动减量. Z标志 用来控制扫描或比较操作的结束. MOVS 串传送. ( MOVSB 传送字符. MOVSW 传送字. MOVSD 传送双字. ) CMPS 串比较. ( CMPSB 比较字符. CMPSW 比较字. ) SCAS 串扫描. 把AL或AX的内容与目标串作比较,比较结果反映在标志位. LODS 装入串. 把源串中的元素(字或字节)逐一装入AL或AX中. ( LODSB 传送字符. LODSW 传送字. LODSD 传送双字. ) STOS 保存串. 是LODS的逆过程. REP 当CX/ECX<>0时重复. REPE/REPZ 当ZF=1或比较结果相等,且CX/ECX<>0时重复. REPNE/REPNZ 当ZF=0或比较结果不相等,且CX/ECX<>0时重复. REPC 当CF=1且CX/ECX<>0时重复. REPNC 当CF=0且CX/ECX<>0时重复. 五、程序转移指令 ─────────────────────────────────────── 1>无条件转移指令 (长转移) JMP 无条件转移指令 CALL 过程调用 RET/RETF过程返回. 2>条件转移指令 (短转移,-128到+127的距离内) ( 当且仅当(SF XOR OF)=1时,OP1
循环控制指令(短转移) LOOP CX不为零时循环. LOOPE/LOOPZ CX不为零且标志Z=1时循环. LOOPNE/LOOPNZ CX不为零且标志Z=0时循环. JCXZ CX为零时转移. JECXZ ECX为零时转移. 4>中断指令 INT 中断指令 INTO 溢出中断 IRET 中断返回 5>处理器控制指令 HLT 处理器暂停, 直到出现中断或复位信号才继续. WAIT 当芯片引线TEST为高电平时使CPU进入等待状态. ESC 转换到外处理器. LOCK 封锁总线. NOP 空操作. STC 置进位标志位. CLC 清进位标志位. CMC 进位标志取反. STD 置方向标志位. CLD 清方向标志位. STI 置中断允许位. CLI 清中断允许位. 六、伪指令 ─────────────────────────────────────── DW 定义字(2字节). PROC 定义过程. ENDP 过程结束. SEGMENT 定义段. ASSUME 建立段寄存器寻址. ENDS 段结束. END 程序结束. 3.4 再谈寄存器和内存的区别 第零讲说到“寄存器在CPU中。内存在内存条中。前者的速度比后者快100倍左右。后面的程序要求每条指定要么没有内存数据,要么在有一个寄存器的参与下有一个内存数据。(也就是说,不存在只访问内存的指令)。” 寄存器是在CPU中的存储器,而内存是在内存条中的存储器。CPU访问寄存器,只需要通过微指令直接就可以访问,而访问内存则要先经过总线,再由总线到达内存控制器,读到某单元的内存数据后放上总线,再传到CPU中,CPU才能使用。 8086系列计算机的寄存器,共有14个,每个都是十六位的。 AX,BX,CX,DX,SP,BP,SI,DI,CS,DS,SS,ES,IP,FLAGS。 其中前四位,每个可以单位再分成两个,AX=AH+AL,BX=BH+BL,CX=CH+CL,DX=DH+DL。这些分开的每个都是8位的。 这个分开不要理解成平时语言中的分开,你可以理解为AX是由AH和AL组合成的,你给AL付值,就意味着同时给AX的低半部付值。你给AX付值,就意味着同时改变AH和AL。这样作的好处是你可以更灵活地控制这个寄存器。 3.5 指令说明 看了3.3的指令集和3.4的寄存器,是不是已经晕了,或者了迷糊?不要急,上面的东西虽然多,我也没让你一下学会,(其实有些永远也不会似乎也不是什么大不了的事)。为了应付看的懂我后面所说的,我把其中的指令挑几个重点的,你必须要记住,其它的慢慢学吧。 1数据传输指令。 mov A,B 注意不是move,这个指令是把B中的数据复制给A,(B中仍保存原状)。这里的A和B可以是寄存器,可以是内存。但可以同时是寄存器,不能同时是内存。比如 mov ax,100 ;这是对的,注意100在这里叫立即数,但这个数在编译系统编译成exe的时候保存在内存中。如果学过别的高级语言,你就可以理解为这就是赋值语句 Let ax=100/ax:=100;/ax=100。 2 伪指令 伪指令就是不是真的指令,但他同时又是指令。之所以说这样矛盾的话,是因为伪指令不是机器语言的一部分,而是汇编语言的一部分,是你告诉汇编的编译系统如何去作。 string DB '这是我的第一个汇编语言程序$' 上面一行指令中,DB就是伪指令,他的作用就是告诉编译程序,把后面一些数据或字符串放到内存中。当然对于exe来说,已在内存中了,就不用“告诉”了。(这就是为什么叫伪指令)。string是你给这段内存起的名字,如果你不需要这段内存,不起名字也可以,但如果后面要用,当然要加上这个名字。'这是我的第一个汇编语言程序$'这个就是要处理的数据,当然你也可以换成别的内容,但需要注意的是,要以'$'结尾,这是汇编的约写,即:只是到了$,就认为字符串结束,否则就一直向下找,直到找到一个$为止。所以这就要求你的字符串中不能有'$',如果必须有,再换别的处理方式,后面再说。 3 地址传送指令 Lea A,string 前面已经定义了string,后面要把地址找到,就要用到lea指令。lea是把字符串的地址给A这个寄存器中,A当然可以上前面提到的任意寄存器。注意地址和内容的区别。如果是内容就是把string的字符串给A了。(当然这也不成立,一个字符串有很多字节,而一个寄存器只有两个字节)。 那么从上面也看到了,string代表一个地址,lea把这个地址给了A,那这个地址到底在哪里呢?事实上这不重要,就象你要把某书店买书,这个书店在哪并不是最重要的,有没有你要的书才是最重要的。所以你前面标出string,后面引用就行了,至于这个地址到底在哪是编译程序的事,不是你的事。 4 运算指令 ADD A,N 这个很容易理解吧,寄存器A加上N,把和仍存在A中。类似于高级语言中的let a=a+n/a:=a+n/a+=n。 5 串操作指令 记住串操作指令表面很复杂,其实很简单。 因为他就象一个复杂的数学公式一样简单,你所要记住的就是公式的格式,使用时具体套用即可。 从一个地址到另一个地址的复制需要注意的是: *把源串段地址给DS。 *把源串编址给SI。 *把目的串段址给ES。 *把目的串偏址给DI。 *把要复制的个数给CX,这里可不考虑$了。 *把FLAG中的方向标志标志你要的方向,一个是顺向,另一个是逆向。 *发送loop movs,scans等命令。 6 转移指令 记住:无条件转移指令 jmp。等于转 jz,不等于时转jnz 7 中断指令 int 中断号,注意进制,默认是十进制,所以十六进制就加h。 好了,上面的指令变成七八个了,这你不能嫌多了吧,如果再嫌多就不要继续向下看了。 第四讲 汇编程序 4.1 汇编程序框架 data SEGMENT '数据段,编程者可以把数据都放到这个段里 ....数据部分 '数据格式是: 标识符 db/dw 数据。 data ENDS'数据段结束处。 edata SEGMENT '附加数据段,编程者可以把数据都放到这个段里 ....附加数据部分 edata ENDS'附加数据段结束处。 code SEGMENT'代码段,实际的程序都是放这个段里。 ASSUME CS:code,DS:data,ES:edata '告诉编译程序,data段是数据段DS,code段是代码段CS start:MOV AX,data '前面的start表示一个标识位,后面用到该位,如果用不到,就可以不加 MOV DS,AX '这一句与上一行共同组成把data赋值给DS。段寄存器. MOV AX,edata MOV ES,AX '与前一句共同组成edata->ES .......程序部分 MOV AX,4C00h'程序退出,该句内存由下一行决定。退出时,要求ah必须是4c。 INT 21h code ENDS'代码段结束。 END start'整个程序结束,并且程序执行时由start那个位置开始执行。 上面就是一个程序的框架结构。在这个结构中,有三个段,DS,ES,CS。这三个段分别存数据,附加数据,代码段。 4.2 编写我们的Hello,world思路。 开始编写我们的第一个程序。 程序要求:显示一个“Hello,Mr.286.”怎么样? 思路: 1 要显示一个字符串,根据前面我让你们记的七八个指令够吗?答案是:不仅够,而且还用不完。 首先定义一下总可以吧。 hellostr db 'Hello,Mr.286.$' 最后的$不要忘了。 2 首先要考虑的问题就是找中断,找到合适的中断,该中断就能帮我们完成这个显示任务。我找到(在哪找到的,怎么找到的,别问我,到网上或书上都能找到): ------------------------------------------- 中断INT 21H功能09H 功能描述: 输出一个字符串到标准输出设备上。如果输出操作被重定向,那么,将无法判断磁盘已满 入口参数: AH=09H DS:DX=待输出字符的地址 说明:待显示的字符串以’$’作为其结束标志 出口参数: 无 ------------------------------------------- 由上面看到,我们所需要作的就是把DS指向数据段,DX指向字符串的地址,AH等于9H,调用21h中断。 mov ds,数据段地址 lea dx,hellostr 'hellostr已在前面1中定义了。 mov ah,9h int 21h。 由于只要在调用int 21h之前把准备的东西准备齐就行了,所以int 21h前面三行的顺序并不重要。 3 退出程序,运行完总要退出呀。再查中断手册 -------------------------------------------- 中断INT 21H功能4CH 功能描述: 终止程序的执行,并可返回一个代码 入口参数: AH=4CH AL=返回的代码 出口参数: 无 -------------------------------------------- mov ah,4Ch mov al,0 int 21h 或 mov ax,4c00h int 21h 这里需要说明的是返回代码有什么用,返回给谁?返回给操作系统,因为是操作系统DOS调用的这个程序,这个返回值可以通过批处理中的errorlevel得到,这里不多说明,实际上操作系统很少处理这一值,因此al你随便写什么值影响都不大。 4.3 程序实现 data SEGMENT msg DB 'Hello, Mr.286.$' data ENDS code SEGMENT ASSUME CS:code,DS:data start:MOV AX,data MOV DS,AX lea dx,msg mov ah,9h int 21h MOV AX,4C00h INT 21h code ENDS END start 4.4 编译运行。 把上面程序保存成hello286.asm后,就可以编译运行了。进入DOS,进入汇编目录,如果还没下载,到前面找下载地址。 ================================================= E:/Download/Masm>masm hello286.asm Microsoft (R) Macro Assembler Version 5.00 Copyright (C) Microsoft Corp 1981-1985, 1987. All rights reserved. Object filename [hello286.OBJ]: Source listing [NUL.LST]: Cross-reference [NUL.CRF]: 50408 + 415320 Bytes symbol space free 0 Warning Errors 0 Severe Errors 说明:上面连续三个回车,表示我要的都是默认值。下面是零个警告,零个严重错误,(当然了,我的程序还敢错吗?) E:/Download/Masm>link hello286 Microsoft (R) Overlay Linker Version 3.60 Copyright (C) Microsoft Corp 1983-1987. All rights reserved. Run File [HELLO286.EXE]: List File [NUL.MAP]: Libraries [.LIB]: LINK : warning L4021: no stack segment 说明:三个回车仍要默认,后面有个警告,没有栈段,这个没关系,没有的话系统会自动给一个。 E:/Download/Masm>hello286 Hello, Mr.286. 说明:运行成功。 E:/Download/Masm> 4.4 深度思考 4.4.1 是不是数据必须放数据段,代码必段放代码段呢? 答,代码必段放代码段,否则你怎么执行呀?但数据也可以放到代码段,只是程序要作修改。 code SEGMENT ASSUME CS:code,DS:data msg DB 'Hello, Mr.286.$' start:MOV AX,data MOV DS,AX lea dx,msg mov ah,9h int 21h MOV AX,4C00h INT 21h code ENDS END start 编译后仍然可以。 4.4.2 我编的程序在内存中是什么样子的呢? ------------------------------------------------------------------------ E:/Download/Masm>debug hello286.exe -u 1420:0000 B81F14 MOV AX,141F 1420:0003 8ED8 MOV DS,AX 1420:0005 8D160000 LEA DX,[0000] 1420:0009 B409 MOV AH,09 1420:000B CD21 INT 21 1420:000D B8004C MOV AX,4C00 1420:0010 CD21 INT 21 1420:0012 FF362421 PUSH [2124] 1420:0016 E87763 CALL 6390 1420:0019 83C406 ADD SP,+06 1420:001C FF362421 PUSH [2124] -d 141f:0000 L20 141F:0000 48 65 6C 6C 6F 2C 20 4D-72 2E 32 38 36 2E 24 00 Hello, Mr.286.$. 141F:0010 B8 1F 14 8E D8 8D 16 00-00 B4 09 CD 21 B8 00 4C ............!..L -q E:/Download/Masm> ------------------------------------------------------------------------------ 上面是什么呀?还记得前面说的吗? 1420:0000 B81F14 MOV AX,141F | | | | | 段址:偏址 机器语言 mov指令 把段地址的地址(141f)赋值给AX寄存器。 1420:0012后面的是垃圾数据,不用管它,把上面程序与源程序作一个比较,看有什么不用,差别在于把标号语言转成实际地址了。 程序前两行一执行,数据段地址就变成了141f,而那个字符串偏移地址在0000,由(LEA DX,[0000]看出),所以我用-d 141f:0000 L20(后面L20表示只显示20个字节),就能把段地址显示出来了。 所以刚才的程序在内存中就变成了: 141f:0000 Hello, Mr.286.$ ----->这是段地址里的内存 1420:0000 B81F14 MOV AX,141F ------>这是代码段里的内存。data变成了实际地址 1420:0003 8ED8 MOV DS,AX 1420:0005 8D160000 LEA DX,[0000] ------>偏址变成了0000,因为实际上msg也就是从头开始的。当然是0了。 1420:0009 B409 MOV AH,09 ------->注意Debug里,默认的是十六进制 1420:000B CD21 INT 21 1420:000D B8004C MOV AX,4C00 1420:0010 CD21 INT 21 刚刚学习了8086/8088汇编语言,发现寻址方式非常重要,于是做了一个小总结,请各位笑纳。 概念: 1.指令集:cpu能够执行的指令的集合。 2.指令:cpu所能够执行的操作。 3.操作数:参加指令运算的数据。 4.寻址方式:在指令中得到操作数的方式。 现在就重点讨论寻址方式,说白了也就是cpu怎么样从指令中得到操作数的问题。另外再强调一点操作数还分种类: 1)数据操作数:全都是在指令当中参加操作的数据。 1.立即操作数:它在指令中直接给出。 2.寄存器操作数:它被放到寄存器中。 3.存储器操作数:当然在存储器也就是内存中。 4.i/o操作数:它在你给出的i/o端口中。 2)转移地址操作数:在指令当中不是参加运算或被处理的数据了,而是转移地址。 还可以按照下面分类方式: 1)源操作数src 2)目的操作数dst 源操作数都是指令当中的第2个操作数,在执行完指令后操作数不变。而目的操作数是指令当中的第1个操作数,在执行完操作指令后被新的数据替代。 我们就围绕这几种操作数,也就是操作数所在的位置展开讨论。 先说数据操作数,它分3大类共7种。 1)立即数寻址方式:是针对立即操作数的寻址方式。在指令当中直接给出,它根本就不用寻址。 例1:mov ax,1234h mov [bx],5678h 在这里1234h和5678h都是立即操作数,在指令当中直接给出。 2)寄存器寻址方式:是针对寄存器操作数的寻址方式,它在寄存器中我们就用这中方式来找到它。 例2:mov bx,ax mov bp,[si] 在这里ax,bx,ds都算是寄存器寻址,例1中的ax也是寄存器寻址方式。 3)存储器寻址方式:针对在内存中的数据(存储器操作数)都用这种方式来寻找,一共有5种(这是我自己的说法,便于记忆)。 不得不提及以下的概念:由于8086/8088的字长是16bit,能够直接寻址2的16次方也就是64kb,而地址总线是20bit,能够直接寻址2的20次方也就是1M空间,所以把内存分为若干个段,每个段最小16byte(被称为小节),最大64kb,它们之间可以相互重叠,这样一来内存就被分成以16byte为单元的64k小节,cpu就以1小节为单位寻址:在段寄存器中给出段地址(16bit),在指令当中给出段内偏移地址(16bit),然后把段地址左移4bit再与偏移地址求和就得到数据在内存当中的实际物理地址了,因而可以找到数据。 1.存储器直接寻址方式:在指令当中以 [地址] 的方式直接给出数据所在内存段的偏移地址。 例3:mov ax,es:[1234h] mov dx,VALUE mov dx,[VALUE] 在这里[1234h]和VALUE就是在指令中直接给出的数据所在内存段的偏移地址(16bit)。 VALUE是符号地址,是用伪指令来定义的,它代表一个在内存中的数据(也就是它的名字)。es:是段前缀符,用来指出段地址,在这之前应该将段地址添入段中,本例中是es,默认是ds,也就是不需给出。应该注意 [地址] 与立即寻址的区别,在直接给出的数据两边加 [] 表示存储器直接寻址,以区别立即寻址。另外 VALUE=[VALUE]。 2.寄存器间接寻址:不是在指令中直接给出数据在内存中的偏移地址,而是把偏移地址放到了寄存器中。 例4:mov ax,[bx] 这里[bx]就是寄存器间接寻址,bx中应方入段内偏移地址。其中:若使用bx,si,di默认段地址为ds,若使用bp则默认段地址为ss,并且允许段跨越,也就是加段前缀符。注意:在寄存器两边加 [] 以与寄存器寻址区别。 3.寄存器间接相对寻址:偏移地址是bx,bp,si,di中的内容再与一个8bit或16bit 的位移量之和。 例5:mov ax,[bx]+12h mov ax,[si]+5678h mov ax,[bp]+1234h 在这里[bx]+12h,[si]+5678h,[bp]+1234h都是寄存器间接相对寻址。12h是8bit位移量,1234h和5678h是16bit位移量。若使用bx,si,di则默认段寄存器是ds,若使用bp则默认段寄存器是ss,并且允许段跨越。 4.基址变址寻址:偏移地址是一个基址寄存器和一个变址寄存器内容的和,既:bx或bp中的一个与si或di中的一个求和而得到。 例6:mov ax,[bx+si] mov ax,[bp+di] 上面[bx+si]和[bp+di]都是基址变址寻址。若使用bx做基址寄存器则默认段地址为ds,若使用bp为基址寄存器则默认段为ss,允许段跨越。 5.基址变址相对寻址:偏移量是一个基址寄存器一个变址寄存器只和再与一个8bit或一个16bit位移量只和得到。 例7:mov ax,[bx+si]+12h mov ax,[bp+di]+1234h [bx+si]+12h和[bp+di]+1234h就是基址变址相对寻址。若使用bx做基址寄存器则默认段是ds,若使用bp做基址寄存器则默认段为ss。允许段跨越。 下面是转移地址操作数的寻址方式: 1)段内直接转移 1.段内直接短转移:cs(代码段)内容不变,而ip(指令指针寄存器)内容由当前ip内容+(-127~127),在指令中直接给出。 例8:jmp short SHORT_NEW_ADDR 其中,short是段内短转移的操作符,用以指出是转移到当前位置前后不超过±127字节的地方。而NEW_ADDR是要转移到的符号地址,它的位置应该在当前ip指针所在偏移地址不超过 ±127的地方。否则语法出错。 2.段内直接近转移:cs内容不变,而ip内容由当前ip内容+(-32767~32767),在指令中直接给出。 例9:jmp near ptr NEAR_NEW_ADDR 其中near ptr是段内近转移的操作符,用以指出转移到当前位置前后不超过±32767的地方。NEAR_NEW_ADDR是要转移到的符号地址。 2)段内间接转移:cs的内容不变,而ip的内容放在寄存器中或者存储器中给出。 例10:jmp bx jmp word ptr [bx]+1234h 这种寻址方式是在寄存器或存储器中找到要转移到的地址,而地址是16bit的,因而寄存器必须为16bit,如:bx,我们用word ptr来指定存储器单元也是16bit的。注意:它是间接的给出,只能使用类似于数据操作数中的除立即寻址以外的6种寻址方式(就在上面)。 3)段间直接寻址:cs和ip的内容全都变化,由指令当中直接给出要转移到的某一个段内的某一个偏移地址处。 例11:jmp 1234h:5678h jmp far ptr NEW_ADDR 1234h送入cs中作为新的段地址,5678h送入ip中作为新的偏移地址。far ptr是段间直接转移操作符,NEW_ADDR是另外一个段内的偏移地址,在这个指令中把NEW_ADDR的段地址送入cs(不用你给出),把它的段内偏移地址送入ip中作为新的偏移地址。 4)段间间接寻址:cs和ip的内容全变化,由指令当中给出的一个4字节连续存储单元,其中低2字节送入ip作为偏移地址,高2字节送入cs作为段地址。 例12:jmp dword ptr [bx][si]+1234h jmp dword ptr [1234h] jmp dword ptr [si] dword ptr是双字(4个字节连续存储单元)操作符,用来指出下面的存储单元是4个字节的。由于它是4个字节的,所以只能使用类似于数据操作数中的存储器寻址方式(共5种,还记得吗?)。 另外作为特殊的寻址方式还有三种:I/O寻址,串寻址,隐含寻址。它们都分别针对I/O指令,串操作指令以及无操作数的指令,而且都比较简单,读者自行总结。 到此为止说明了8086/8088cpu中的所有寻址方式,我这里只是个总结,具体的细节还要大家自己钻研课本,才能理解。 写的有些仓促可能有些遗漏或错误,还请谅解。 ------------------------------------------------------------------------------------- 一定要记住寻址方式特别重要,这是我“毕生” 的总结,请笑纳,笑纳。
转载]学习汇编前你应该知道的知识
最新推荐文章于 2022-08-08 20:45:14 发布