简明x86汇编语言教程.docx
- 文档编号:27631630
- 上传时间:2023-07-03
- 格式:DOCX
- 页数:57
- 大小:62.13KB
简明x86汇编语言教程.docx
《简明x86汇编语言教程.docx》由会员分享,可在线阅读,更多相关《简明x86汇编语言教程.docx(57页珍藏版)》请在冰豆网上搜索。
简明x86汇编语言教程
第○章写在前面
我不想夸大或者贬低汇编语言。
但我想说,汇编语言改变了20世纪的历史。
与前辈相比,我们这一代编程人员足够的幸福,因为我们有各式各样的编程语言,我们可以操作键盘、坐在显示器面前,甚至使用鼠标、语音识别。
我们可以使用键盘、鼠标来驾驭“个人计算机”,而不是和一群人共享一台使用笨重的继电器、开关去操作的巨型机。
相比之下,我们的前辈不得不使用机器语言编写程序,他们甚至没有最简单的汇编程序来把助记符翻译成机器语言,而我们可以从上千种计算机语言中选择我们喜欢的一种,而汇编,虽然不是一种“常用”的具有“快速原型开发”能力的语言,却也是我们可以选择的语言中的一种。
每种计算机都有自己的汇编语言——没必要指望汇编语言的可移植性,选择汇编,意味着选择性能而不是可移植或便于调试。
这份文档中讲述的是x86汇编语言,此后的“汇编语言”一词,如果不明示则表示ia32上的x86汇编语言。
汇编语言是一种易学,却很难精通的语言。
回想当年,我从初学汇编到写出第一个可运行的程序,只用了不到4个小时;然而直到今天,我仍然不敢说自己精通它。
编写快速、高效、并且能够让处理器“很舒服地执行”的程序是一件很困难的事情,如果利用业余时间学习,通常需要2-3年的时间才能做到。
这份教材并不期待能够教给你大量的汇编语言技巧。
对于读者来说,x86汇编语言"就在这里"。
然而,不要僵化地局限于这份教材讲述的内容,因为它只能告诉你汇编语言是“这样一回事”。
学好汇编语言,更多的要靠一个人的创造力于悟性,我可以告诉你我所知道的技巧,但肯定这是不够的。
一位对我的编程生涯产生过重要影响的人曾经对我说过这么一句话:
写汇编语言程序不是汇编语言最难的部分,创新才是。
我想,愿意看这份文档的人恐怕不会问我“为什么要学习汇编语言”这样的问题;不过,我还是想说几句:
首先,汇编语言非常有用,我个人主张把它作为C语言的先修课程,因为通过学习汇编语言,你可以了解到如何有效地设计数据结构,让计算机处理得更快,并使用更少的存储空间;同时,学习汇编语言可以让你熟悉计算机内部运行机制,并且,有效地提高调试能力。
就我个人的经验而言,调试一个非结构化的程序的困难程度,要比调试一个结构化的程序的难度高很多,因为“结构化”是以牺牲运行效率来提高可读性与可调试性,这对于完成一般软件工程的编码阶段是非常必要的。
然而,在一些地方,比如,硬件驱动程序、操作系统底层,或者程序中经常需要执行的代码,结构化程序设计的这些优点有时就会被它的低效率所抹煞。
另外,如果你想真正地控制自己的程序,只知道源代码级的调试是远远不够的。
浮躁的人喜欢说,用C++写程序足够了,甚至说,他不仅仅掌握C++,而且精通STL、MFC。
我不赞成这个观点,掌握上面的那些是每一个编程人员都应该做到的,然而C++只是我们"常用"的一种语言,它不是编程的全部。
低层次的开发者喜欢说,嘿,C++是多么的强大,它可以做任何事情——这不是事实。
便于维护、调试,这些确实是我们的追求目标,但是,写程序不能仅仅追求这个目标,因为我们最终的目的是满足设计需求,而不是个人非理性的理想。
这份教材适合已经学习过某种结构化程序设计语言的读者。
其内容基于我在1995年给别人讲述汇编语言时所写的讲义。
当然,如大家所希望的,它包含了最新的处理器所支持的特性,以及相应的内容。
我假定读者已经知道了程序设计的一些基本概念,因为没有这些是无法理解汇编语言程序设计的;此外,我希望读者已经有了比较良好的程序设计基础,因为如果你缺乏对于结构化程序设计的认识,编写汇编语言程序很可能很快就破坏了你的结构化编程习惯,大大降低程序的可读性、可维护性,最终让你的程序陷于不得不废弃的代码堆之中。
基本上,这份文档撰写的目标是尽可能地便于自学。
不过,它对你也有一些要求,尽管不是很高,但我还是强调一下。
学习汇编语言,你需要
胆量。
不要害怕去接触那些计算机的内部工作机制。
知识。
了解计算机常用的数制,特别是二进制、十六进制、八进制,以及计算机保存数据的方法。
开放。
接受汇编语言与高级语言的差异,而不是去指责它如何的不好读。
经验。
要求你拥有任意其他编程语言的一点点编程经验。
头脑。
祝您编程愉快!
第一章汇编语言简介
先说一点和实际编程关系不太大的东西。
当然,如果你迫切的想看到更实质的内容,完全可以先跳过这一章。
那么,我想可能有一个问题对于初学汇编的人来说非常重要,那就是:
汇编语言到底是什么?
汇编语言是一种最接近计算机核心的编码语言。
不同于任何高级语言,汇编语言几乎可以完全和机器语言一一对应。
不错,我们可以用机器语言写程序,但现在除了没有汇编程序的那些电脑之外,直接用机器语言写超过1000条以上指令的人大概只能算作那些被我们成为“圣人”的牺牲者一类了。
毕竟,记忆一些短小的助记符、由机器去考虑那些琐碎的配位过程和检查错误,比记忆大量的随计算机而改变的十六进制代码、可能弄错而没有任何提示要强的多。
熟练的汇编语言编码员甚至可以直接从十六进制代码中读出汇编语言的大致意思。
当然,我们有更好的工具——汇编器和反汇编器。
简单地说,汇编语言就是机器语言的一种可以被人读懂的形式,只不过它更容易记忆。
至于宏汇编,则是包含了宏支持的汇编语言,这可以让你编程的时候更专注于程序本身,而不是忙于计算和重写代码。
汇编语言除了机器语言之外最接近计算机硬件的编程语言。
由于它如此的接近计算机硬件,因此,它可以最大限度地发挥计算机硬件的性能。
用汇编语言编写的程序的速度通常要比高级语言和C/C++快很多--几倍,几十倍,甚至成百上千倍。
当然,解释语言,如解释型LISP,没有采用JIT技术的Java虚机中运行的Java等等,其程序速度更无法与汇编语言程序同日而语。
永远不要忽视汇编语言的高速。
实际的应用系统中,我们往往会用汇编彻底重写某些经常调用的部分以期获得更高的性能。
应用汇编也许不能提高你的程序的稳定性,但至少,如果你非常小心的话,它也不会降低稳定性;与此同时,它可以大大地提高程序的运行速度。
我强烈建议所有的软件产品在最后Release之前对整个代码进行Profile,并适当地用汇编取代部分高级语言代码。
至少,汇编语言的知识可以告诉你一些有用的东西,比如,你有多少个寄存器可以用。
有时,手工的优化比编译器的优化更为有效,而且,你可以完全控制程序的实际行为。
我想我在罗嗦了。
总之,在我们结束这一章之前,我想说,不要在优化的时候把希望完全寄托在编译器上——现实一些,再好的编译器也不可能总是产生最优的代码。
第二章认识处理器
中央处理器(CPU)在微机系统处于“领导核心”的地位。
汇编语言被编译成机器语言之后,将由处理器来执行。
那么,首先让我们来了解一下处理器的主要作用,这将帮助你更好地驾驭它。
典型的处理器的主要任务包括从内存中获取机器语言指令,译码,执行根据指令代码管理它自己的寄存器根据指令或自己的的需要修改内存的内容响应其他硬件的中断请求
一般说来,处理器拥有对整个系统的所有总线的控制权。
对于Intel平台而言,处理器拥有对数据、内存和控制总线的控制权,根据指令控制整个计算机的运行。
在以后的章节中,我们还将讨论系统中同时存在多个处理器的情况。
处理器中有一些寄存器,这些寄存器可以保存特定长度的数据。
某些寄存器中保存的数据对于系统的运行有特殊的意义。
新的处理器往往拥有更多、具有更大字长的寄存器,提供更灵活的取指、寻址方式。
寄存器
如前所述,处理器中有一些可以保存数据的地方被称作寄存器。
寄存器可以被装入数据,你也可以在不同的寄存器之间移动这些数据,或者做类似的事情。
基本上,像四则运算、位运算等这些计算操作,都主要是针对寄存器进行的。
首先让我来介绍一下80386上最常用的4个通用寄存器。
先瞧瞧下面的图形,试着理解一下:
上图中,数字表示的是位。
我们可以看出,EAX是一个32-bit寄存器。
同时,它的低16-bit又可以通过AX这个名字来访问;AX又被分为高、低8bit两部分,分别由AH和AL来表示。
对于EAX、AX、AH、AL的改变同时也会影响与被修改的那些寄存器的值。
从而事实上只存在一个32-bit的寄存器EAX,而它可以通过4种不同的途径访问。
也许通过名字能够更容易地理解这些寄存器之间的关系。
EAX中的E的意思是“扩展的”,整个EAX的意思是扩展的AX。
X的意思Intel没有明示,我个人认为表示它是一个可变的量。
而AH、AL中的H和L分别代表高和低。
为什么要这么做呢?
主要由于历史原因。
早期的计算机是8位的,8086是第一个16位处理器,其通用寄存器的名字是AX,BX等等;80386是Intel推出的第一款IA-32系列处理器,所有的寄存器都被扩充为32位。
为了能够兼容以前的16位应用程序,80386不能将这些寄存器依旧命名为AX、BX,并且简单地将他们扩充为32位——这将增加处理器在处理指令方面的成本。
Intel微处理器的寄存器列表(在本章先只介绍80386的寄存器,MMX寄存器以及其他新一代处理器的新寄存器将在以后的章节介绍)
通用寄存器
下面介绍通用寄存器及其习惯用法。
顾名思义,通用寄存器是那些你可以根据自己的意愿使用的寄存器,修改他们的值通常不会对计算机的运行造成很大的影响。
通用寄存器最多的用途是计算。
EAX
32-bit宽
通用寄存器。
相对其他寄存器,在进行运算方面比较常用。
在保护模式中,也可以作为内存偏移指针(此时,DS作为段寄存器或选择器)
EBX
32-bit宽
通用寄存器。
通常作为内存偏移指针使用(相对于EAX、ECX、EDX),DS是默认的段寄存器或选择器。
在保护模式中,同样可以起这个作用。
ECX
32-bit宽
通用寄存器。
通常用于特定指令的计数。
在保护模式中,也可以作为内存偏移指针(此时,DS作为寄存器或段选择器)。
EDX
32-bit宽
通用寄存器。
在某些运算中作为EAX的溢出寄存器(例如乘、除)。
在保护模式中,也可以作为内存偏移指针(此时,DS作为段寄存器或选择器)。
上述寄存器同EAX一样包括对应的16-bit和8-bit分组。
用作内存指针的特殊寄存器
ESI
32-bit宽
通常在内存操作指令中作为“源地址指针”使用。
当然,ESI可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。
DS是默认段寄存器或选择器。
EDI
32-bit宽
通常在内存操作指令中作为“目的地址指针”使用。
当然,EDI也可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。
DS是默认段寄存器或选择器。
EBP
32-bit宽
这也是一个作为指针的寄存器。
通常,它被高级语言编译器用以建造‘堆栈帧’来保存函数或过程的局部变量,不过,还是那句话,你可以在其中保存你希望的任何数据。
SS是它的默认段寄存器或选择器。
注意,这三个寄存器没有对应的8-bit分组。
换言之,你可以通过SI、DI、BP作为别名访问他们的低16位,却没有办法直接访问他们的低8位。
段寄存器和选择器
实模式下的段寄存器到保护模式下摇身一变就成了选择器。
不同的是,实模式下的“段寄存器”是16-bit的,而保护模式下的选择器是32-bit的。
CS代码段,或代码选择器。
同IP寄存器(稍后介绍)一同指向当前正在执行的那个地址。
处理器执行时从这个寄存器指向的段(实模式)或内存(保护模式)中获取指令。
除了跳转或其他分支指令之外,你无法修改这个寄存器的内容。
DS数据段,或数据选择器。
这个寄存器的低16bit连同ESI一同指向的指令将要处理的内存。
同时,所有的内存操作指令默认情况下都用它指定操作段(实模式)或内存(作为选择器,在保护模式。
这个寄存器可以被装入任意数值,然而在这么做的时候需要小心一些。
方法是,首先把数据送给AX,然后再把它从AX传送给DS(当然,也可以通过堆栈来做).ES附加段,或附加选择器。
这个寄存器的低16bit连同EDI一同指向的指令将要处理的内存。
同样的,这个寄存器可以被装入任意数值,方法和DS类似。
FSF段或F选择器(推测F可能是Free?
)。
可以用这个寄存器作为默认段寄存器或选择器的一个替代品。
它可以被装入任何数值,方法和DS类似。
GSG段或G选择器(G的意义和F一样,没有在Intel的文档中解释)。
它和FS几乎完全一样。
SS堆栈段或堆栈选择器。
这个寄存器的低16bit连同ESP一同指向下一次堆栈操作(push和pop)所要使用的堆栈地址。
这个寄存器也可以被装入任意数值,你可以通过入栈和出栈操作来给他赋值,不过由于堆栈对于很多操作有很重要的意义,因此,不正确的修改有可能造成对堆栈的破坏。
*注意一定不要在初学汇编的阶段把这些寄存器弄混。
他们非常重要,而一旦你掌握了他们,你就可以对他们做任意的操作了。
段寄存器,或选择器,在没有指定的情况下都是使用默认的那个。
这句话在现在看来可能有点稀里糊涂,不过你很快就会在后面知道如何去做。
特殊寄存器(指向到特定段或内存的偏移量):
EIP这个寄存器非常的重要。
这是一个32位宽的寄存器,同CS一同指向即将执行的那条指令的地址。
不能够直接修改这个寄存器的值,修改它的唯一方法是跳转或分支指令。
(CS是默认的段或选择器)ESP这个32位寄存器指向堆栈中即将被操作的那个地址。
尽管可以修改它的值,然而并不提倡这样做,因为如果你不是非常明白自己在做什么,那么你可能造成堆栈的破坏。
对于绝大多数情况而言,这对程序是致命的。
(SS是默认的段或选择器)
IP:
InstructionPointer,指令指针
SP:
StackPointer,堆栈指针
好了,上面是最基本的寄存器。
下面是一些其他的寄存器,你甚至可能没有听说过它们。
(都是32位宽):
CR0,CR2,CR3(控制寄存器)。
举一个例子,CR0的作用是切换实模式和保护模式。
还有其他一些寄存器,D0,D1,D2,D3,D6和D7(调试寄存器)。
他们可以作为调试器的硬件支持来设置条件断点。
TR3,TR4,TR5,TR6和TR?
寄存器(测试寄存器)用于某些条件测试。
最后我们要说的是一个在程序设计中起着非常关键的作用的寄存器:
标志寄存器。
本节中部份表格来自DavidJurgens的HelpPC2.10快速参考手册。
在此谨表谢意。
2.2使用寄存器
在前一节中的x86基本寄存器的介绍,对于一个汇编语言编程人员来说是不可或缺的。
现在你知道,寄存器是处理器内部的一些保存数据的存储单元。
仅仅了解这些是不足以写出一个可用的汇编语言程序的,但你已经可以大致读懂一般汇编语言程序了(不必惊讶,因为汇编语言的祝记符和英文单词非常接近),因为你已经了解了关于基本寄存器的绝大多数知识。
在正式引入第一个汇编语言程序之前,我粗略地介绍一下汇编语言中不同进制整数的表示方法。
如果你不了解十进制以外的其他进制,请把鼠标移动到这里。
数字计算机内部只支持二进制数,因为这样计算机
只需要表示两种(某些情况是3种,这一内容超过了
这份教程的范围,如果您感兴趣,可以参考数字逻
辑电路的相关书籍)状态. 对于电路而言,这表现
为高、低电平,或者开、关,分别非常明显,因而
工作比较稳定;另一方面,由于只有两种状态,设
计起来也比较简单。
这样,使用二进制意味着低成
本、稳定,多数情况下,这也意味着快速。
与十进制类似,我们可以用下面的式子来换算出一
个任意形如am-1……a3a2a1a0的m位r进制数对应的
数值n:
程序设计中常用十六进制和八进制数字代替二进制
数,其原因在于,16和8是2的整次方幂,这样,一
位十六或八进制数可以表示整数个二进制位。
十六
进制中,使用字母A、B、C、D、E、F表示10-15,
而十六进制或八进制数制表示的的数字比二进制数
更短一些。
EAX的内容为000A3412h.
汇编语言中的整数常量表示十进制整数
这是汇编器默认的数制。
直接用我们熟悉的表示方式表示即可。
例如,1234表示十进制的1234。
不过,如果你指定了使用其他数制,或者有凡事都进行完整定义的小爱好,也可以写成[十进制数]d或[十进制数]D的形式。
十六进制数
这是汇编程序中最常用的数制,我个人比较偏爱使用十六进制表示数据,至于为什么,以后我会作说明。
十六进制数表示为0[十六进制数]h或0[十六进制数]H,其中,如果十六进制数的第一位是数字,则开头的0可以省略。
例如,7fffh,0ffffh,等等。
二进制数
这也是一种常用的数制。
二进制数表示为[二进制数]b或[二进制数]B。
一般程序中用二进制数表示掩码(maskcode)等数据非常的直观,但需要些很长的数据(4位二进制数相当于一位十六进制数)。
例如,1010110b。
八进制数
八进制数现在已经不是很常用了(确实还在用,一个典型的例子是Unix的文件属性)。
八进制数的形式是[八进制数]q、[八进制数]Q、[八进制数]o、[八进制数]O。
例如,777Q。
需要说明的是,这些方法是针对宏汇编器(例如,MASM、TASM、NASM)说的,调试器默认使用十六进制表示整数,并且不需要特别的声明(例如,在调试器中直接用FFFF表示十进制的65535,用10表示十进制的16)。
现在我们来写一小段汇编程序,修改EAX、EBX、ECX、EDX的数值。
我们假定程序执行之前,寄存器中的数值是全0:
?
XHLEAX00000000EBX00000000ECX00000000EDX00000000
正如前面提到的,EAX的高16bit是没有办法直接访问的,而AX对应它的低16bit,AH、AL分别对应AX的高、低8bit。
moveax,012345678h
movebx,0abcdeffeh
movecx,1
movedx,2;将012345678h送入eax
;将0abcdeffeh送入ebx
;将000000001h送入ecx
;将000000002h送入edx
则执行上述程序段之后,寄存器的内容变为:
?
XHLEAX12345678EBXabcdeffeECX00000001EDX00000002
那么,你已经了解了mov这个指令(mov是move的缩写)的一种用法。
它可以将数送到寄存器中。
我们来看看下面的代码:
moveax,ebx
movecx,edx;ebx内容送入eax
;edx内容送入ecx
则寄存器内容变为:
?
XHLEAXabcdeffeEBXabcdeffeECX00000002EDX00000002
我们可以看到,“move”之后,数据依然保存在原来的寄存器中。
不妨把mov指令理解为“送入”,或“装入”。
练习题
把寄存器恢复成都为全0的状态,然后执行下面的代码:
moveax,0a1234h
movbx,ax
movah,bl
moval,bh;将0a1234h送入eax
;将ax的内容送入bx
;将bl内容送入ah
;将bh内容送入al
思考:
此时,EAX的内容将是多少?
[答案]
下面我们将介绍一些指令。
在介绍指令之前,我们约定:
使用Intel文档中的寄存器表示方式
reg3232-bit寄存器(表示EAX、EBX等)reg1616-bit寄存器(在32位处理器中,这AX、BX等)reg8 8-bit寄存器(表示AL、BH等)imm3232-bit立即数(可以理解为常数)imm1616-bit立即数imm8 8-bit立即数
在寄存器中载入另一寄存器,或立即数的值:
movreg32,(reg32|imm8|imm16|imm32)
movreg32,(reg16|imm8|imm16)
movreg8,(reg8|imm8)
例如,moveax,010h表示,在eax中载入00000010h。
需要注意的是,如果你希望在寄存器中装入0,则有一种更快的方法,在后面我们将提到。
交换寄存器的内容:
xchgreg32,reg32
xchgreg16,reg16
xchgreg8,reg8
例如,xchgebx,ecx,则ebx与ecx的数值将被交换。
由于系统提供了这个指令,因此,采用其他方法交换时,速度将会较慢,并需要占用更多的存储空间,编程时要避免这种情况,即,尽量利用系统提供的指令,因为多数情况下,这意味着更小、更快的代码,同时也杜绝了错误(如果说Intel的CPU在交换寄存器内容的时候也会出错,那么它就不用卖CPU了。
而对于你来说,检查一行代码的正确性也显然比检查更多代码的正确性要容易)刚才的习题的程序用下面的代码将更有效:
moveax,0a1234h
movbx,ax
xchgah,al;将0a1234h送入eax
;将ax内容送入bx
;交换ah,al的内容
递增或递减寄存器的值:
increg(8,16,32)
decreg(8,16,32)
这两个指令往往用于循环中对指针的操作。
需要说明的是,某些时候我们有更好的方法来处理循环,例如使用loop指令,或rep前缀。
这些将在后面的章节中介绍。
将寄存器的数值与另一寄存器,或立即数的值相加,并存回此寄存器:
addreg32,reg32/imm(8,16,32)
addreg16,reg16/imm(8,16)
addreg8,reg8/imm(8)
例如,addeax,edx,将eax+edx的值存入eax。
减法指令和加法类似,只是将add换成sub。
需要说明的是,与高级语言不同,汇编语言中,如果要计算两数之和(差、积、商,或一般地说,运算结果),那么必然有一个寄存器被用来保存结果。
在PASCAL中,我们可以用nA:
=nB+nC来让nA保存nB+nC的结果,然而,汇编语言并不提供这种方法。
如果你希望保持寄存器中的结果,需要用另外的指令。
这也从另一个侧面反映了“寄存器”这个名字的意义。
数据只是“寄存”在那里。
如果你需要保存数据,那么需要将它放到内存或其他地方。
类似的指令还有and、or、xor(与,或,异或)等等。
它们进行的是逻辑运算。
我们称add、mov、sub、and等称为为指令助记符(这么叫是因为它比机器语言容易记忆,而起作用就是方便人记忆,某些资料中也称为指令、操作码、opcode[operationcode]等);后面的参数成为操作数,一个指令可以没有操作数,也可以有一两个操作数,通常有一个操作数的指令,这个操作数就是它的操作对象;而两个参数的指令,前一个操作数一般是保存操作结果的地方,而后一个是附加的参数。
我不打算在这份教程中用大量的篇幅介绍指令——很多人做得比我更好,而且指令本身并不是重点,如果你学会了如何组织语句,那么只要稍加学习就能轻易掌握其他指令。
更多的指令可以参考Intel提供的资料。
编写程序的时候,也可以参考一些在线参考手册。
Tech!
Help和HelpPC2.10尽管已经很旧,但足以应付绝大多数需要。
聪明的读者也许已经发现,使用subeax,eax,或者xoreax,eax,可以得到与moveax,0类似的效果。
在高级语言中,你大概不会选择用a=a-
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 简明 x86 汇编语言 教程