??xml version="1.0" encoding="utf-8" standalone="yes"?>
]]>
对在已连接套接字上接受接入数据来_ r e c v函数是最基本的方式。它的定义如下:
int recv(
SOCKET s,
char FAR * buf,
int len,
int flags
);
W一个参数sQ是准备接收数据的那个套接字。第二个参数b u fQ是卛_收到数据的字W缓Ԍ而l e n则是准备接收的字节数或b u f~冲的长度。最后, f l a g s参数可以是下面的| 0、M S G _ P E E K或M S G _ O O B。另外,q可对这些标志中的每一个进行按位和q算。当Ӟ 0表示无特D行为。M S G _ P E E K会有用的数据复制到所提供的接收端~冲内,但是没有从系l缓
冲中它删除。另外,q返回了待发字节数?br />消息取数不太好。它不仅D性能下降Q因为需要进行两ơ系l调用,一ơ是取数Q另一ơ是无M S G _ P E E K标志的真正删除数据的调用Q,在某些情况下q可能不可靠。返回的数据可能没有反射出真正有用的数量。与此同Ӟ把数据留在系l缓Ԍ可容Ux入数据的pȝI间׃来少。其l果便是Q系l减各发送端的T C PH口定w。由此,你的应用׃能获得最大的通。最好是把所有数据都复制到自q~冲中,q在那里计算数据。前面曾介绍qM S G _ O O B标志。有兌情,参见前面“带外数据”的内容?/p>
在面向消息或面向数据报的套接字上使用r e c vӞq几点应该注意。在待发数据大于所提供的缓冲这一事g中,~冲内会量地填充数据。这Ӟ r e c v调用׃产生W S A E M S G S I Z E错误。注意,消息镉K误是在用面向消息的协议时发生的。流协议把接入的数据~存下来Q?br />q尽量地q回应用所要求的数据,即待发数据的数量比~冲大。因此,Ҏ式传输协议来_׃会碰到W S A E M S G S I Z Eq个错误?br />W S A R e c v函数在r e c v的基上增加了一些新Ҏ。比如说重叠I / O和部分数据报通知?br />W S A R e c v的定义如下:
int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPWORD lpNumberOfBytesRecved,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE
);
参数sQ是已徏立连接的套接字。第二和W三个参数是接收数据的缓册Ӏl p B u ff e r s参数是一个W S A B U Fl构l成的数l,而d w B u ff e r C o u n t则表明前一个数l中W S A B U Fl构的数目?br />如果接收操作立即完成Q?l p N u m b e r O f B y t e s R e c e i v e d参数׃指向执行q个函数调用所收到的字节数。l p F l a g s参数可以是下面Q何一个| M S G _ P E E K、M S G _ O O B、M S G _ PA RT I A L或者对q些D行按位和q算之后的结果。M S G _ PA RT I A L标志使用和出现的地方不同Q其?br />义也不同。对面向消息的协议来_q个标志是W S A R e c v调用q回后设|的Q如果因为缓冲空间不够导致整条消息未能在q次调用中返回的话)。这Ӟ后面的W S A R e c v调用׃讄q个标志M A S G _ PA RT I A LQ直到整条消息返回,才把q个标志清除。如果这个标志当作一个输入参数投递,接收操作应该在一收到数据q束,即它收到的只是整条消息中的一部分?br />M S G _ PA RT I A L标志只随面向消息的协议一起用。每个协议的协议条目都包含一个标志,表明是否支持q一Ҏ。有兌情,参见W?章。l p O v e r l a p p e d和l p C o m p l e t i o n R O U T I N E参数用于重叠I / O操作
4. WSARecvDisconnect
q函CW S A S e n d D i s c o n n e c t函数对应Q其定义如下Q?br />int WSARecvDisconnect(
SOCKET s,
LPWSABUF lpOUTboundDisconnectData
);
和W S A S e n d D i s c o n n e c t函数的参CP该函数的参数也是已徏立连接的套接字句柄和
一个有效的W S A B U Fl构Q带有收到的数据Q。收到的数据可以只是断开数据。这个断开数据是另一端执行W S A S e n d D i s c o n n e c t调用发出的,它不能用于接收普通数据。另外,一旦收到这个数据, W S A R e c v D i s c o n n e c t函数׃取消接收q程通信方的数据Q其作用和调用带有S D _ R E C V的s h u t d o w n函数相同?br />5. WSARecvEx
W S A R e c v E x函数是微软专有的Winsock 1扩展Q除了f l a g s参数是按值引用外Q其余和r e c v函数是一L。它允许基层的提供者设|M S G _ PA RT I A L标志。该函数的原型如下:
int PASCAL FAR WSARecvEx(
SOCKET s,
char FAR * buf,
int len,
int * flags
);
如果收到的数据不是一条完整的消息Q?f l a g s参数中就会返回M S G _ PA RT I A L标志。对面向消息的协议(即非协议)来说Q这个标志比较有用(即非协议)。在M S G _ PA RT I A L标志被当作f l a g s参数的一部分投递,而且收到的消息又不完整时Q调用W S A R e c v E xQ就会立?br />q回收到的那个数据。如果提供的接收~冲容纳不下整条消息Q?W S A R e c v E x׃p|Qƈ出现W S A E M S G S I Z E 错误Q剩下的数据也会被截掉。注意, M S G _ PA RT I A L 标志和W S A E M S G S I Z E错误之间的确区别是:有了q个错误Q即使整条消息到达接收端Q但׃?br />供的数据~冲太少Q也不能对它q行接收。M S G _ P E E K 和M S G _ O O B标志q可以和W S A R e c v E x一起用?/p>
随后Q我们讲解了M U P如何调用一个网l提供者,从而揭C出怎样通过一个重定向器,在“服务器消息块”(Server Message Block, SMBQ协议的帮助下,在不同的计算Z间徏立数据通信?/p>
最后,我们探讨了网l安全方面的一些问题。用基本的文gI / O操作Q通过|络来访问文件时Q这些安全问题是必须考虑到的?/p>
2.1 通用命名规范
“U N C路径? 为网l文件及讑֤的访问徏立了一套统一的规范。它最大的特点便是不必指定或引用一个已映射到远E文件系l的本地驱动器字母?/p>
U N C名字完全解决了这些问题,它的格式如下Q?br />\ \ [服务器] \ [׃n名] \ [路径]
W一部分是\ \ [服务器]Q必M两个反斜杠开_紧跟着一个服务器名字?br />W二部分是\ [׃n名]Q它对应着q程服务器上的一个“共享入口”或者“共享位|”?br />。而第三部分\ [路径] 对应的是׃n位置下的某个具体目录Q或子目录)
W?章邮?/p>
一U简单的单向“进E间通信”(interprocess communication,I P CQ机制。这个机制的名字非常古怪,叫作“邮槽”(M a i l s l o tQ。用最单的话来_通过
邮槽Q客hq程可将消息传送或q播l一个或多个服务器进E。在同一台计机的不同进E之_或在跨越整个|络的不同计机的进E之_协助q行消息的传输。用邮槽来开发应用程序是一仉常简单的事情Q不要求对T C P / I P或I P Xq样的基层网l传送协议有着非常深入的了解。由于邮槽是围绕一个广播通信体系设计出来的,所以当然不能指望能通过它实现数据的“可靠”传输?/p>
邮槽最大的一个缺点便是只允许从客h到服务器Q徏立一U不可靠的单向数据通信?br />而另一斚wQ邮槽最大的一个优点在于,它们使客h应用能够非常Ҏ地将q播消息发送给一个或多个服务器应用?/p>
3.1 邮槽实施l节
邮槽是围lWi n d o w s文gpȝ接口设计出来的。客h和服务器应用需要用标准的Wi n 3 2文gpȝI / OQ输入/输出Q函敎ͼ比如R e a d F i l e和Wr i t e F i l e{等Q以便在邮槽上收发数据,同时利用Wi n 3 2文gpȝ的命名规则。邮槽必M赖Wi n d o w s重定向器Q通过一个“邮槽文件系l”(Mailslot File System, MSFSQ,来创建及标识邮槽?/p>
3.1.1 邮槽的名?br />寚w槽进行标识时Q需遵守下述命名规则Q?br />\ \ s e r v e r \ M a i l s l o t \ [ p a t h ] n a m e
请将上述字串分ؓ三段来看Q?\ \ s e r v e r、\ M a i l s l o t和\ [ p a t h ] n a m e。第一部分\ \ s e r v e r对应于服务器的名字,我们要在上面创徏邮槽Qƈ在在上面q行服务器程序。第二部分\ M a i l s l o t是一个“硬~码”的固定字串Q用于告诉系l这个文件名从属于M S F S。而第三部分\ [ p a t h ] n a m e?br />允许应用E序独一无二地定义及标识一个邮槽名。其中,“p a t h”代表\径,可指定多U目录?br />举个例子来说Q对一个邮槽进行标识时Q下面这些Ş式的名字都是合法的(注意M a i l s l o t不得变化Q必d文照输,亦即所谓的“硬~码”)Q?br />׃邮槽要依赖Wi n d o w s文gpȝ服务在网上来创徏和传输数据,所以接口是“与协议无关”的?br />要想保证各种Wi n d o w sq_之间能够完全正常地通信Q强烈徏议将消息长度限制? 2 4字节Q或者更短。如果进行面向连接的传输Q可考虑使用命名道Q而不是简单的邮槽?/p>
3.5 结
本章讲解了邮槽( M a i l s l o tQ网l编E技术。利用这一技术,应用E序可以在Wi n d o w s重定向器的帮助下Q实现简单的单向q程间数据通信。对邮槽来说Q它最有h值的一功能便是通过|络Q将一条消息广播给一台或多台计算机。然而,邮槽q未提供Ҏ据可靠传输的保障。假如希望用Wi n d o w s重定向器实现“可靠”的数据通信Q请考虑使用命名道Q这是下一章的主题Q?/p>
netBIOS名字
在Wi n 3 2环境中,针对每个可用的L A N A~号Q每
个进E都会ؓ其维持一张N e t B I O S名字表。若为LANA 0增添一个名字,意味着你的应用E序
只能在LANA 0上同客户机徏立连接。对每个L A N A来说Q能够添加的名字的最大数量是2 5 4Q?br />~号?? 5 4Q?? 5 5ql保留)
QN e t B I O S名字共有两个性质Q唯一名字和组?br />微Y|络中的机器命名是NetBIOS命名?br />“组名”的作用是将数据同时发给多个接收者;或者相反,接收发给多个
接收者的数据。组名ƈ非一定要“独一无二”,它主要用于多播(多点发送)数据通信
若有“windows互联|命名服务器”即winsQ则有它理Q若无则发广播探是否重名?/p>
1.1.3 NetBIOSҎ?/p>
N e t B I O S同时提供了“面向连接”服务以及“无q接”服务?/p>
1.2 NetBIOS~程基础
NetBIOS API的设|,只有一个函敎ͼ
UCHAR Netbios(PNCB pNCB);
用于N e t B I O S的所有函数声明、常数等{均是在头文件N b 3 0 . h内定义的。若惌?br />N e t B I O S应用Q唯一需要的库是N e t a p i 3 2 . l i b?br />调用N e t b i o s函数Ӟ可选择q行同步调用Q还是进行异步调用。所有N e t B I O S命o本n均是同步的。要惛_步调用一个命令,需要让N e t B I O S命o同A S Y N C H标志q行一ơ逻辑O RQ或Q运。如指定了A S Y N C H标志Q那么必dn c b _ p o s t字段中指定一个后例程QPost RoutineQ,或必dn c b _e v e n t字段中指定一个事件句柄。执行一个异步命令时Q从N e t b i o sq回的值是N R C _ G O O D R E T( 0 x 0 0 )Q但n c b _ c m d _ c p l t字段会设为N R C _ P E N D I N G ( 0 x F F )。除此以外, N e t b i o s函数q会N C Bl构的n c b _ c m d _ c p l t字段设ؓN R C _ P E N D I N GQ待冻IQ直到命令完成ؓ止。命令完成后Qn c b _ c m d _ c p l t字段会设命o的返回倹{N e t b i o s也会在完成后n c b _ r e t c o d e字段设ؓ命o的返回倹{?br />
1.4 数据报的工作原理
“数据报”(D a t a g r a mQ属于一U“无q接”的通信Ҏ。作为发送方Q只需用目?br />N e t B I O S名字为发出的每个包定址Q然后简单地送出了事。此Ӟ不会执行M查,以确
保数据的完整性、抵N序或者传输的可靠性等{?/p>
发出一个数据报共有三种方式?br />W一U是指挥数据报抵达一个特定的Q或唯一的)l名。这意味着只能有一个进E负责数据报的接收—亦x册了目标名字的那个进E?br />W二U是数据报发给一个组名。只有注册了指定l名的那些进E才有权接收消息?br />最后,W三U方式是数据报q播到整个网l?/p>
原代号ؓ“Royale”的MacromediaFlex软g把服务器Y件、开发指南和其他工具l合在一P使传l的|络应用开发h员能够用Macromedia公司的Flash格式创作软g单元。如从前报道的那P该品的重点是让那些使用Sun微系l公司的Java2企业?J2EE)的开发h员能够创作出更有吸引力、更ҎD的J2EE应用软g接口?o:p>
FlexJ2EE开发h员用标准的文本式开发工h制作Flash应用E序Q而不必用Macromedia公司以前出售的复杂的设计工具。Macromedia公司从今q年初开始,努力扩大Flash格式对于L开发商的吸引力Q其目标是扩大Flash的用途,使其成ؓ提供互联|应用和建立交互式网站的基础?o:p>
Macromedia公司计划?004q上半年推出Flex服务器YӞ该Y件的h目前q没有确定。它的初U版本将q行于J2EE中,q计划随后推出支持微软的.Net格式的版本。最初的支持者包括IBM公司Q它随自己的WebSphere软g一hqFlex的应用?
需要了解更多Flex技术的朋友可以讉KFlex的主:
http://www.macromedia.com/software/flex/
Thinlet
Thinlet是一个采用Applet解析XULq提供相应界面的解析器,在事件发生时Q调用用戯q事g处理E序(java E序)Q需要客L览器支持Applet。更多信息可以参考 http://www.thinlet.com/
具体通信机制资料误者参考网上内容和透明?004q?期《程序员》杂志中《王朝复辟还是ʎ火重生》一文?o:p>
Rich Client
开源开发^?/b>
Laszlo
Laszlo是一个开源的Rich client开发环境。用Laszloq_Ӟ开发者只需~写名ؓLZX的描q语aQ其中整合了XML和JavaScriptQ,q行在J2EE应用服务器上的Laszloq_会将其编译成FLASH文gq传输给客户端展C。单从运行原理来_Laszlo与XULQ?a class="Channel_KeyLink" >XML用户接口语言Q ?XML User interface LanguageQ、XAMLQ“Longhorn”)标记语言很类伹{但它的最大优势在于:它把描述语言~译成FLASHQ而FLASH是Q何浏览器都支持的展示形式Q从而一举解决了览器之间的UL问题。而且Q在未来的计划中QLaszloq可以将LZX~译成Java?NET本地代码Q从而大大提高运行效率?o:p>
具体请参考http://www.openlaszlo.org?o:p>
IBM AlphaWorks|站q日发布了用于开发Laszlo应用E序的集成开发环境(实际上是一个Eclipse插gQ,使J2EE开发者能够在他们熟悉的Eclipse环境中快速开发基于Laszlo的rich client应用E序。可以在下列地址下蝲该插Ӟ
http://alphaworks.ibm.com/tech/ide4laszlo
此外QAlphaWorks|站q提供了一个用Laszlo开发的CZ应用Q展CZ在Eclispe环境下开发Laszlo应用的过E。demo的地址如下Q?o:p>
http://dl.alphaworks.ibm.com/technologies/rcb/demo.html
FLEX
Flex是Macromedia公司开发的Q用于Rich client开发的环境Q其原理是将MXML(the Macromedia Flex Markup Language)文gQ编译成SWF文gQ然后显C在览器中,q利用Web Service技术和服务器通信。从而利用Flash的强大功能,带来更丰富的用户体验?/p>
CORBA和RMI有一个共同的~陷Q通常不会在系l?0端口提供服务Q所以这在具备网|防火墙的情况下昑־非常被动?o:p>
XML-RPC
Z解决在系l的80端口提供RPC的服务,而又不媄响正在执行的WEB服务Qh们想Z用HTTP协议传输RPC包的办法。对于几乎是专门用于传输文本的HTTP协议Q要在其上传输RPC包Q最方便的方法莫q于把RPC包~码成文本Ş式——例?a class="Channel_KeyLink" >XML文g?o:p>
XML-RPCQhttp://www.xml-rpc.comQ是q国UserLand公司指定的一个RPC协议。它RPC信息包~码?a class="Channel_KeyLink" >XMLQ然后通过HTTP传输包Q它是一个简单的RPC协议Q只支持一些基本数据类型,不支持对象模型,q势必掣肘在客户端和服务器端之间传输复杂的对象?o:p>
SOAP
SOAP即Simple Object Access Protocol(单对象访问协?是在分散或分布式的环境中交换信息的简单的协议Q是一个基?a class="Channel_KeyLink" >XML的协议。它允许所有的操作?0端口上进行,从而也l过了防火墙{问题?o:p>
SOAP规范中有三个基本l成部分QSOAP装Q编码规则,以及在请求和响应之间的交互方式?o:p>
目前已有的基于JAVA提供SOAP功能的品有QApache SOAP, IBM SOAP4J{?o:p>
要了解更多关于SOAP的信息,可以讉K http://www.w3.org/TR/SOAP
不得不说的是Q宽带网l的出现在某U意义促成了Rich Client的诞生。通过快捷的发布,特定的通讯协议标准QRich Client正以不可L的气势向Z重现着C/S模式下客LE序的优ѝ?o:p>
Rich Client的发?/b>
C/S架构下,客户端程序发布与l护一直比较困隑֒J琐。在版本更新以后Q需要对客户的客LE序q行逐个下蝲安装及配|更斎ͼq是一个体力活Q而这也一直是使用户大量选择WEBE序的因素之一?o:p>
在Rich Client时代Q由于宽带网l的便利Q在客户端尽需要从服务器端下蝲已经更新好的E序q行Q而不必理会繁琐的下蝲、安装和配置的过E?o:p>
q里不得不提Java的是WebStart技术?o:p>
WebStart是让用户只需在网上点击一个超U链接就能运行一个Java桌面应用的技术。对于一个拥有WebStart能力的Java应用来说Q用户用它和使用WEB应用一L单,但它所h的界面能力和本地处理能力却是WEB应用无法望其背的?o:p>
具体的应用的技术知识可以从http://java.sun.com中寻扄x档,q里不一一赘述?o:p>
Rich Client的通信机制
除了快捷方便的发布外QRich Clientq需要与服务器端建立一U快速、可靠、强大、易用的通信交互机制。但我们开发WEB应用Ӟ表现层和业务服务层常常只是同一个进E中的不同对象,它们之间的交互不q是Java的方法调用而已Q当表现层逻辑被分发到世界各地的计机上,客户端和服务器之间的交互成了一个大问题——从前的C/S被淘汎ͼ很大E度上归咎于socket通信的复杂性?o:p>
现在QŞ形色色的RPCQ远E过E调用,Remote Procedure CallQ技术以独特的优势扮演v了信使的角色。以下列丑ևURich Client可以采用的通信机制?o:p>
CORBA?/b>RMI
CORBAQ通用对象h代理体系l构QCommon Object Request Broker ArchitectureQ曾l红极一Ӟ它能够兼容各U操作系l^台的语言Q强大的的可扩展性所带来的负面媄响就是实现的复杂和繁琐。如果服务器端和客户端都采用Java开发,那么CORBA所需要的语言无关的IDL完全变成了画蛇添。当Ӟ对于需要集成大量企业内遗留的系l的EAIQ企业应用集成)目中,它一直是首选的技术?/p>
基本概念
Windows 2000 使用Z分页机制的虚拟内存。每个进E有4GB的虚拟地址I间。基于分|Ӟq?GB地址I间的一些部分被映射了物理内存,一些部分映硬盘上的交换文Ӟ一些部分什么也没有映射。程序中使用的都?GB地址I间中的虚拟地址。而访问物理内存,需要用物理地址?BR>
下面我们看看什么是物理地址Q什么是虚拟地址?BR>
物理地址 (physical address): 攑֜dȝ上的地址。放在寻址ȝ上,如果是读Q电路根据这个地址每位的值就相应地址的物理内存中的数据放到数据ȝ中传输。如果是写,电\Ҏq个地址每位的值就相应地址的物理内存中攑օ数据ȝ上的内容。物理内存是以字?8?为单位编址的?BR>
虚拟地址 (virtual address): 4G虚拟地址I间中的地址Q程序中使用的都是虚拟地址?BR>
如果CPU寄存器中的分|志位被设|,那么执行内存操作的机器指令时QCPU会自动根据页目录和页表中的信息,把虚拟地址转换成物理地址Q完成该指o。比?mov eax,004227b8h Q这是把地址004227b8h处的Dl寄存器的汇~代码,004227b8q个地址是虚拟址。CPU在执行这行代码时Q发现寄存器中的分页标志位已l被讑֮Q就自动完成虚拟地址到物理地址的{换,使用物理地址取出|完成指o。对于Intel CPU 来说Q分|志位是寄存器CR0的第31位,?表示使用分页Qؓ0表示不用分c对于初始化之后?Win2k 我们观察 CR0 Q发现第31位ؓ1。表明Win2k是用分늚?BR>
使用了分|制之后,4G的地址I间被分成了固定大小的页Q每一|者被映射到物理内存,或者被映射到硬盘上的交换文件中Q或者没有映Q何东ѝ对于一般程序来_4G的地址I间Q只有一部分映了物理内存Q大片大片的部分是没有映Q何东ѝ物理内存也被分,来映地址I间。对?2bit的Win2kQ页的大是4K字节。CPU用来把虚拟地址转换成物理地址的信息存攑֜叫做늛录和表的结构里?BR>
物理内存分页Q一个物理页的大ؓ4K字节Q第0个物理页从物理地址 0x00000000 处开始。由于页的大ؓ4KBQ就?x1000字节Q所以第1从物理地址 0x00001000 处开始。第2从物理地址 0x00002000 处开始。可以看到由于页的大是4KBQ所以只需?2bit的地址中高20bit来寻址物理c?/FONT>
表Q一个页表的大小?K字节Q放在一个物理页中。由1024?字节的页表项l成。页表项的大ؓ4个字?32bit)Q所以一个页表中?024个页表项。页表中的每一的内容Q每?个字?32bitQ高20bit用来放一个物理页的物理地址Q低12bit攄一些标志?BR>
늛录,一个页目录大小?K字节Q放在一个物理页中。由1024?字节的页目录组成。页目录的大小?个字?32bit)Q所以一个页目录中有1024个页目录V页目录中的每一的内容Q每?个字节)?0bit用来放一个页表(表攑֜一个物理页中)的物理地址Q低12bit攄一些标志?/FONT>
对于x86pȝQ页目录的物理地址攑֜CPU的CR3寄存器中?BR>
CPU把虚拟地址转换成物理地址Q?BR> 一个虚拟地址Q大?个字?32bit)Q包含着扑ֈ物理地址的信息,分ؓ3个部分:W?2位到W?1位这10位(最?0位)是页目录中的索引Q第12位到W?1位这10位是表中的索引Q第0位到W?1位这12位(?2位)是页内偏UR对于一个要转换成物理地址的虚拟地址QCPU首先ҎCR3中的|扑ֈ늛录所在的物理c然后根据虚拟地址的第22位到W?1位这10位(最高的10bit)的g为烦引,扑ֈ相应的页目录?PDE,page directory entry),늛录项中有q个虚拟地址所对应表的物理地址。有了页表的物理地址Q根据虚拟地址的第12位到W?1位这10位的g为烦引,扑ֈ该页表中相应的页表项(PTE,page table entry),表中有q个虚拟地址所对应物理늚物理地址。最后用虚拟地址的最?2位,也就是页内偏U,加上q个物理늚物理地址Q就得到了该虚拟地址所对应的物理地址?/FONT>
一个页目录?024,虚拟地址最高的10bit刚好可以索引1024(2?0ơ方{于1024Q。一个页表也?024,虚拟地址中间部分?0bitQ刚好烦?024V虚拟地址最低的12bitQ??2ơ方{于4096Q,作ؓ内偏移Q刚好可以烦?KBQ也是一个物理页中的每个字节?/FONT>
一个虚拟地址转换成物理地址的计过E就是,处理器通过CR3扑ֈ当前늛录所在物理页Q取虚拟地址的高10bit,然后把这10bit右移2bitQ因为每个页目录?个字节长Q右U?bit相当于乘4Q得到在该页中的地址Q取地址处PDEQ?个字节)Q就扑ֈ了该虚拟地址对应表所在物理页Q取虚拟地址W?2位到W?1位这10位,然后把这10bit右移2bitQ因为每个页表项4个字节长Q右U?bit相当于乘4Q得到在该页中的地址Q取地址处的PTEQ?个字节)Q就扑ֈ了该虚拟地址对应物理늚地址Q最后加?2bit的页内偏UdC物理地址?BR>
32bit的一个指针,可以d范围0x00000000-0xFFFFFFFF,4GB大小。也是说一?2bit的指针可以寻址整个4GB地址I间的每一个字节。一个页表项负责4K的地址I间和物理内存的映射Q一个页?024,也就是负?024*4k=4M的地址I间的映。一个页目录,对应一个页表。一个页目录?024,也就对应着1024个页表,每个表负责4M地址I间的映?024个页表负?024*4M=4G的地址I间映射。一个进E有一个页目录。所以以ؓ单位Q页目录和页表可以保?G的地址I间中的每页和物理内存的映射?BR>
每个q程都有自己?G地址I间Q从 0x00000000-0xFFFFFFFF 。通过每个q程自己的一套页目录和页表来实现。由于每个进E有自己的页目录和页表,所以每个进E的地址I间映射的物理内存是不一L。两个进E的同一个虚拟地址处(如果都有物理内存映射Q的g般是不同的,因ؓ他们往往对应不同的物理页?BR>
4G地址I间中低2GQ?x00000000-0x7FFFFFFF 是用户地址I间Q?G地址I间中高2GQ?BR>0x80000000-0xFFFFFFFF 是系l地址I间。访问系l地址I间需要程序有ring0的权限?/FONT>
作? JIURL
在前面的章节中,我们已经了解了寄存器的基本用方法。而正如结提到的那样Q仅仅用寄存器做一点运是没有什么太大意义的Q毕竟它们不能保存太多的数据Q因此,对编Eh员而言Q他肯定q切地希望访问内存,以保存更多的数据?/P>
我将分别介绍如何在保护模式和实模式操作内存,然而在此之前,我们先熟悉一下这两种模式中内存的l构?/P>
事实上,在实模式中,内存比保护模式中的结构更令h困惑。内存被分割成段Qƈ且,操作内存Ӟ需要指定段和偏U量。不q,理解q些概念是非常容易的事情。请看下面的图: D?寄存器这U格局是早期硬件电路限制留下的一个伤疤。地址ȝ在当时有20-bit?/P>
然?0-bit的地址不能攑ֈ16-bit的寄存器里,q意味着?-bit必须攑ֈ别的地方。因此,Z讉K所有的内存Q必M用两?6-bit寄存器?/P>
q一设计上的折衷ҎD了今天的D?偏移量格局。最初的设计中,其中一个寄存器只有4-bit有效Q然而ؓ了简化程序,两个寄存器都?6-bit有效Qƈ在执行时求出加权和来标识20-bit地址?/P>
偏移量是16-bit的,因此Q一个段?4KB。下面的囑֏以帮助你理解20-bit地址是如何Ş成的Q?/P>
D?偏移量标识的地址通常记做 D?偏移?的Ş式?/P>
׃q样的结构,一个内存有多个对应的地址。例如,0000:0010?001:0000指的是同一内存地址。又如, 0000:1234 = 0123:0004 = 0120:0034 = 0100:0234 作ؓ负面影响之一Q在D上?相当于在偏移量上?6Q而不是一个“全新”的Dc反之,在偏U量上加16也和在段上加1{h。某些时候,据此认ؓD늚“粒度”是16字节?/P>
l习?BR>试一下将下面的地址转化?0bit的地址Q?/P>
E高一些的要求是,写一个程序将DؓAX、偏U量为BX的地址转换?0bit的地址Qƈ保存于EAX中?/P>
[上面习题的答?/SPAN>] 我们现在可以写一个真正的E序了?/P>
l典E序QHello, world 那么Q我们需要解释很多东ѝ?/P>
首先Q作为汇~语a的抽象,C语言拥有“指针”这个数据类型。在汇编语言中,几乎所有对内存的操作都是由对给定地址的内存进行访问来完成的。这P在汇~语a中,l大多数操作都要和指针生或多或的联系?/P>
q里我想的是Q由于这一Ҏ,汇编语言中同样会出现CE序中常见的~冲区溢出问题。如果你正在设计一个与安全有关的系l,那么最好是仔细查你用到的每一个串Q例如,它们是否一定能够以你预期的方式l束Q以及(如果使用的话Q你的缓冲区是否能保证实际可能输入的数据不被写入到它以外的地斏V作Z个汇~语aE序员,你有义务查每一行代码的可用性?/P>
E序中的equ伪指令是宏汇~特有的Q它的意思接q于C或Pascal中的constQ常量)。多数情况下Qequ伪指令ƈ不ؓW号分配I间?/P>
此外Q汇~程序执行一Ҏ作是非常J琐的,通常Q在对与效率要求不高的地方,我们习惯使用pȝ提供的中断服务来完成d。例如本例中的中?1hQ它是DOS时代的中断服务,在Windows中,它也被认为是Windows API的一部分Q这一点可以在Microsoft的文档中查到Q。中断可以被理解为高U语a中的子程序,但又不完全一样——中断用系l栈来保存当前的机器状态,可以q件发P通过修改机器状态字来反馈信息,{等?/P>
那么Q最后一D通过DB存放的数据到底保存在哪里了呢Q答案是紧挨着代码存放。在汇编语言中,DB和普通的指o的地位是相同的。如果你的汇~程序ƈ不知道新的助记符Q例如,新的处理器上的CPUID指oQ,而你很清楚,那么可以用DB 机器码的方式写下指o。这意味着Q你可以越汇编器的能力撰写汇编E序Q然而,直接用机器码~程是几乎肯定是一件费力不讨好的事——汇~器厂商会经常更新它所支持的指令集以适应市场需要,而且Q你可以期待你的汇编其能够生正的代码Q因为机器查表是不会出错的。既然机器能够帮我们做将E序转换Z码这件事情,那么Z么不让它来做呢? l心的读者不隑֏玎ͼ在程序中我们没有对DSq行赋倹{那么,q是否意味着E序的结果将是不可预的呢?{案是否定的。DOSQ或Windows中的MS-DOS VMQ在加蝲.com文g的时候,会对寄存器进行很多初始化?com文g被限制ؓ于64KBQ这P它的代码Dc数据段都被装入同样的数|卻I初始状态下DS=CSQ?/P>
也许会有Q“嘿Q这听v来不太好Q一?4KB的程序能做得了什么呢Q还有,你吹得天׃ؕ坠的堆栈D在什么地方?”那么,我们来看看下面这个新的Hello worldE序Q它是一个EXE文gQ在DOS实模式下q行?/P>
; 采用“SMALL”内存模?BR>; 堆栈D?/FONT> ; 回R ; 定义数据D?/FONT> ; 定义昄?/FONT> ; 定义代码D?/FONT> ; 讄DX 561字节Q实现相同功能的E序大了q么多!Z么呢Q我们看刎ͼE序拥有了完整的堆栈Dc数据段、代码段Q其中堆栈段占掉?12字节Q其余的基本上没什么变化?/P>
分成多个D|什么好处呢Q首先,它让E序昑־更加清晰——你肯定更愿意看一个结构清楚的E序Q代码中hard-coded的字W串、数据让得费解。比如,mov dx, 0152h肯定不如mov dx, offset Message来的亲切。此外,通过分段你可以用更多的内存Q比如,代码D腾出的I间可以做更多的事情。exe文g另一个吸引h的地Ҏ它能够实现“重定位”。现在你不需要指定程序入口点的地址了,因ؓpȝ会找C的程序入口点Q而不是死板的100h?/P>
E序中的W号也会在系l加载的时候重新赋予新的地址。exeE序能够保证你的设计Ҏ地被实现Q不需要考虑太多的细节?/P>
当然Q我们的主要目的是将汇编语言作ؓ高语言的一个有用的补充。如我在开始提到的那样Q真正完全用汇编语言实现的程序不一定就好,因ؓ它不便于l护Q而且Q由于结构的原因Q你也不太容易确保它是正的Q汇~语a是一U非l构化的语言Q调试一个精心设计的汇编语言E序Q即使对于一个老手来说也不L一场恶梦,因ؓ你很可能掉到别h预设的“陷阱”中——这些技巧确实提高了代码性能Q然而你很可能不理解它,于是你把它改掉,接着发现程序彻底|掉了。用汇~语a加强高语言E序Ӟ你要做的通常只是使用汇编指oQ而不必搭建完整的汇编E序。绝大多敎ͼ也是目前我遇到的全部QC/C++~译器都支持内嵌汇编Q即在程序中使用汇编语言Q而不必撰写单独的汇编语言E序——这可以节省你的不少_֊Q因为前面讲q的那些伪指令,如equ{,都可以用你熟悉的高语言方式来编写,~译器会把它转换为适当的Ş式?/P>
需要说明的是,在高U语a中一定要注意~译l果。编译器会对你的汇编E序做一些修改,q不一定符合你的要求(附带说一句,有时~译器会很聪明地调整指o序来提高性能Q这U情况下最好测试一下哪U写法的效果更好Q,此时需要做一些更深入的修改,或者用db来强制编码?/P>
实模式的东西说得太多了,管我已l删掉了许多东西Qƈ把一些原则性的问题拿到了这一节讨论。这样做不是没有理由的——保护模式才是现在的E序Q除了操作系l的底层启动代码Q最常用的CPU模式。保护模式提供了很多令h耳目一新的功能Q包括内存保护(q是保护模式q个名字的来源)、进E支持、更大的内存支持Q等{?/P>
对于一个编Eh员来_能“偷懒”是一件o人愉快的事情。这里“偷懒”是说把“应该”由pȝ做的事情做的事情全都交给pȝ。ؓ什么呢Q这一个基本思想——hL犯错误的时候,然而规则不会,正确C解规则之后,你可以期待它像你所了解的那h行。对于CE序来说Q你自己用C语言写的实现相同功能的函数通常没有pȝ提供的函数性能好(除非你用了比函数库好很多的算法)Q因为系l的函数往往使用了更好的优化Q甚臛_能不是用C语言直接~写的?/P>
当然Q“偷懒”的意思是_把那些应该让机器做的事情交给计算机来做,因ؓ它做得更好。我们应该把_֊集中到设计算法,而不是编写源代码本n上,因ؓ~译器几乎只能做{h优化Q而实现相同功能,但用更好算法的E序实现Q则几乎只能׃h自己完成?/P>
举个例子Q这样一个函敎ͼ 在某U编译模式[DEBUG]下被~译?/P>
; 走着 ; 恢复现场 ; q回 而在另一U模式[RELEASE/MINSIZE]下却被编译ؓ ; a=0; 如果让我来写Q多半会写成 ; return 499500 Z么这样写呢?我们看到Qi是一个外界不能媄响、也无法L的内部状态量。作D늨序来_对它的计对于结果ƈ没有直接的媄响——它的存在不q是方便法描述而已。ƈ且我们看到的Q这D늨序实际上无论执行多少ơ,其结果都不会发生变化Q因此,直接q回计算l果可以了Q计是多余的(如果说一定要,那么应该是编译器在编译过E中完成它)?/P>
更进一步,我们甚至希望~译器能够直接把q个函数变成一个符号常量,q样q操作堆栈的q程也省掉了?/P>
W三U结果属于“等效”代码,而不是“等价”代码。作为用P很多时候是希望~译器这样做的,然而由于目前的技术尚不成熟,有时q种做法会造成一些问题(gcc和g++的顶U优化可以造成~译出的FreeBSD内核行ؓ异常Q这是我在FreeBSD上遇到的唯一一ơY件原因的kernel panicQ,因此Qƈ不是所有的~译器都q样做(另一斚w的原因是Q如果编译器在这斚w做的太过火,例如自动求解全部“固定”问题,那么如果你的E序是解军_定的问题“很大”,如求解迷宫,那么在编译过E中你就会找锤子来砸计算ZQ。然而,作ؓ~译器制造商Qؓ了提高自q产品的竞争力Q往往会用第三种代码来做函数库。正如前面所提到的那Pq种优化往往不是~译器本w的作用Q尽现代编译程序拥有编译执行、@环代码外提、无用代码去除等诸多优化功能Q但它都不能保证E序最优。最后一U代码恐怕很有~译器能够做刎ͼ不信你可以用自己常用的编译器加上各种优化选项试试:) 发现什么了吗?三种代码中,对于内存的访问一个比一个少。这样做的理由是Q尽可能地利用寄存器q减对内存的访问,可以提高代码性能。在某些情况下,使代码既又快是可能的?/P>
书归正传Q我们来说说保护模式的内存模型。保护模式的内存和实模式有很多共同之处?/P>
毫无疑问Q以'protected mode'(保护模式), 'global descriptor table'(全局描述W表), 'local descriptor table'(本地描述W表)?selector'(选择?搜烦Q你会得到完整介l它们的大量信息?/P>
保护模式与实模式的内存类|然而,它们之间最大的区别是保护模式的内存是“线性”的?/P>
新的计算ZQ?2-bit的寄存器已经不是什么新鲜事Q如果你哪天听说你的CPU的寄存器不是32-bit的,那么它——简直可以肯定地说——的字长要比32-bitq要多。新的个人机上已l开始逐步采用64-bit的CPU了)Q换a之,实际上段/偏移量这一格局已经不再需要了。尽如此,在l看保护模式内存l构Ӟ仍请CD?偏移量的概念。不妨把D寄存器看作对于保护模式中的选择器的一个模拟。选择器是全局描述W表(Global Descriptor Table, GDT)或本地描q符?Local Descriptor Table, LDT)的一个指针?/P>
如图所C,GDT和LDT的每一个项目都描述一块内存。例如,一个项目中包含了某块被描述的内存的物理的基地址、长度,以及其他一些相关信息?/P>
保护模式是一个非帔R要的概念Q同时也是目前撰写应用程序时Q最常用的CPU模式Q运行在新的计算Z的操作系l很有在实模式下运行的Q?/P>
Z么叫保护模式呢?它“保护”了什么?{案是进E的内存。保护模式的主要目的在于允许多个q程同时q行Qƈ保护它们的内存不受其他进E的늊。这有点cM于C++中的机制Q然而它的强制力要大得多。如果你的进E在保护模式下以不恰当的方式讉K了内存(例如Q写了“只诠Z内存,或读了不可读的内存,{等Q,那么CPU׃产生一个异常。这个异常将交给操作pȝ处理Q而这U处理,假如你的E序没有特别说明操作pȝ该如何处理的话,一般就是杀掉做错了事情的进E?/P>
我像q样的对话框大家一定非常熟悉(临时写了一个程序故意造成的错误)Q?/P>
好的Q只是一个程序崩溃了Q而操作系l的其他q程照常q行Q同LE序在DOS中几乎是板上钉钉的死机,因ؓNULL指针的位|恰好是中断向量表)Q你甚至q可以调试它?/P>
保护模式q有其他很多好处Q在此就不一一赘述了。实模式和保护模式之间的切换问题我打放在后面的“高U技巧”一章来Ԍ因ؓ多数E序q不涉及q个?/P>
了解了内存的格局Q我们就可以q入下一节——操作内存了?/P>
前两节中Q我们介l了实模式和保护模式中用的不同的内存格局。现在开始解释如何用这些知识?/P>
回忆一下前面我们说q的Q寄存器可以用作内存指针。现在,是他们发挥作用的时候了?/P>
可以内存想象ؓ一个顺序的字节。用指针,可以L地操作(dQ内存?/P>
现在我们需要一些其他的指o格式来描q对于内存的操作。操作内存时Q首先需要的是它的地址?/P>
让我们来看看下面的代码: Ҏ可C,里面的表辑ּ指定的不是立xQ而是偏移量。在实模式中QDS:0中的那个字(16-bit长)被装入AX?/P>
然?是一个常敎ͼ如果需要在q行的时候加以改变,需要一些特D的技巧,比如E序自修攏V汇~支持这个特性,然而我个hq不推荐q种Ҏ——自修改大大降低E序的可L,q且q降低稳定性,性能q不一定好。我们需要另外的技术?/P>
看v来舒服了一些,不是吗?BX寄存器的内容可以随时更改Q而不需要用冗长的代码去修改自nQ更不用担心由此带来的不E_问题?/P>
同样的,mov指o也可以把数据保存到内存中Q?/P>
在存储器与寄存器之间交换数据应该_清楚了?/P>
有些时候我们会需要操作符来描q内存数据的宽度Q?/P>
例如Q在DS:100h处保?234hQ以字存放: 于是我们mov指o扩展为: 需要说明的是,加减同样也可以在[]中用,例如Q?/P>
{等。我们看刎ͼ对于内存的操作,即使用MOV指oQ也有许多种可能的方式。下一节中Q我们将介绍如何操作丌Ӏ?/P>
感谢 |友 水杉 指出此答案中的一处错误?BR>感谢 Heallven 指出.COME序实例~译p|的问?/P> 2EA8:D678 -> 物理?3C0F8 ~程3.1 实模?/H3>
0001:1234 = 0124:0004 = 0120:0044 = 0100:0244
2EA8:D678 26CF:8D5F 453A:CFAD 2933:31A6 5924:DCCF
694E:175A 2B3C:D218 728F:6578 68E1:A7DC 57EC:AEEA
;;; 应该得到一?9字节?com文g
.MODEL TINY
.CODE
CR equ 13
LF equ 10
TERMINATOR equ '$'
ORG 100h
Main PROC
mov dx,offset sMessage
mov ah,9
int 21h
mov ax,4c00h
int 21h
Main ENDP
sMessage:
DB 'Hello, World!'
DB CR,LF,TERMINATOR
END Main
; .COM文g的内存模型是‘TINY?BR>; 代码D开?BR>
; 回R
; 换行
; DOS字符串结束符
; 代码起始地址为CS:0100h
; 令DS:DX指向Message
; int 21h(DOS中断)功能9 -
; 昄字符串到标准输出讑֤
; int 21h功能4ch -
; l止E序q返回AL的错误代?BR>
; E序l束的同时指定入口点为Main
;;; 应该得到一?61 字节的EXE文g
.MODEL SMALL
.STACK 200h
CR equ 13
LF equ 10
TERMINATOR equ '$'
.DATA
Message DB 'Hello, World !'
DB CR,LF,TERMINATOR
.CODE
Main PROC
mov ax, DGROUP
mov ds, ax
mov dx, offset Message
mov ah, 9
int 21h
mov ax, 4c00h
int 21h
Main ENDP
END main
; 换行
; DOS字符串结束符
; 数据段
; 加蝲到DS寄存?/FONT>
; 昄
; l止E序3.2 保护模式
int fun(){
int a=0;
register int i;
for(i=0; i<1000; i++) a+=i;
return a;
}
push ebp
mov ebp,esp
sub esp,48h
push ebx
push esi
push edi
lea edi,[ebp-48h]
mov ecx,12h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
mov dword ptr [ebp-4],0
mov dword ptr [ebp-8],0
jmp fun+31h
mov eax,dword ptr [ebp-8]
add eax,1
mov dword ptr [ebp-8],eax
cmp dword ptr [ebp-8],3E8h
jge fun+45h
mov ecx,dword ptr [ebp-4]
add ecx,dword ptr [ebp-8]
mov dword ptr [ebp-4],ecx
jmp fun+28h
mov eax,dword ptr [ebp-4]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret; 子程序入?/FONT>
; 保护现场
; 初始化变?调试版本Ҏ?BR>; 本质是在堆中挖一块地儿,存CCCCCCCC?BR>; 用串操作q行Q这发挥Intel处理器优?BR>; ‘a=0?BR>; ‘i=0?/FONT>
; i++
; i<1000?
; a+=i;
; return a;
xor eax,eax
xor ecx,ecx
add eax,ecx
inc ecx
cmp ecx,3E8h
jl fun+4
ret
; i=0;
; a+=i;
; i++;
; i<1000?
; ?>l箋l箋
; return a
mov eax, 079f2ch
ret
3.3 操作内存
mov ax,[0]
mov bx,0
mov ax,[bx]
mov [0],ax
操作W?/FONT>
意义
byte ptr
一个字?8-bit, 1 byte)
word ptr
一个字(16-bit)
dword ptr
一个双?32-bit)
mov word ptr [100h],01234h
mov reg(8,16,32), mem(8,16,32)
mov mem(8,16,32), reg(8,16,32)
mov mem(8,16,32), imm(8,16,32)
mov ax,[bx+10]
mov ax,[bx+si]
mov ax,es:[di+bp]
694E:175A -> 物理?6AC4A
26CF:8D5F -> 物理?2FA4F
2B3C:D218 -> 物理?385E8
453A:CFAD -> 物理?5235D
728F:6578 -> 物理?78E68
2933:31A6 -> 物理?2C4D6
68E1:A7DC -> 物理?735FC
shl eax,4
add eax,bx
在前一节中的x86基本寄存器的介绍Q对于一个汇~语a~程人员来说是不可或~的。现在你知道Q寄存器是处理器内部的一些保存数据的存储单元。仅仅了解这些是不以写Z个可用的汇编语言E序的,但你已经可以大致L一般汇~语aE序了(不必惊讶Q因为汇~语a的祝记符和英文单词非常接q)Q因Z已经了解了关于基本寄存器的绝大多数知识?/P>
在正式引入第一个汇~语aE序之前Q我_略Cl一下汇~语a中不同进制整数的表示Ҏ。如果你不了解十q制以外的其他进Ӟh鼠标Ud?SPAN class=tip id=oRadixes>q里?/P>
需要说明的是,q些Ҏ是针对宏汇编器(例如QMASM、TASM、NASMQ说的,调试器默认用十六进制表C整敎ͼq且不需要特别的声明Q例如,在调试器中直接用FFFF表示十进制的65535Q用10表示十进制的16Q?/P>
现在我们来写一段汇编E序Q修改EAX、EBX、ECX、EDX的数倹{?/P>
我们假定E序执行之前Q寄存器中的数值是?Q?/P>
正如前面提到的,EAX的高16bit是没有办法直接访问的Q而AX对应它的?6bitQAH、AL分别对应AX的高、低8bit?/P>
则执行上q程序段之后Q寄存器的内容变为: 那么Q你已经了解了movq个指oQmov是move的羃写)的一U用法。它可以数送到寄存器中。我们来看看下面的代码: 则寄存器内容变ؓQ?/P>
我们可以看到Q“move”之后,数据依然保存在原来的寄存器中。不妨把mov指o理解为“送入”,或“装入”?/P>
l习?/B> 把寄存器恢复成都为全0的状态,然后执行下面的代码: 思考:此时QEAX的内容将是多?[{案] 下面我们介l一些指令。在介绍指o之前Q我们约定: 在寄存器中蝲入另一寄存器,或立x的| mov reg32, (reg32 | imm8 | imm16 | imm32) 例如Qmov eax, 010h表示Q在eax中蝲?0000010h。需要注意的是,如果你希望在寄存器中装入0Q则有一U更快的ҎQ在后面我们提到?/P>
交换寄存器的内容Q?/P>
例如Qxchg ebx, ecxQ则ebx与ecx的数值将被交换。由于系l提供了q个指oQ因此,采用其他Ҏ交换Ӟ速度会较慢Qƈ需要占用更多的存储I间Q编E时要避免这U情况,卻I量利用pȝ提供的指令,因ؓ多数情况下,q意味着更小、更快的代码Q同时也杜绝了错误(如果说Intel的CPU在交换寄存器内容的时候也会出错,那么它就不用卖CPU了。而对于你来说Q检查一行代码的正确性也昄比检查更多代码的正确性要ҎQ刚才的习题的程序用下面的代码将更有效: 递增或递减寄存器的| q两个指令往往用于循环中对指针的操作。需要说明的是,某些时候我们有更好的方法来处理循环Q例如用loop指oQ或rep前缀。这些将在后面的章节中介l?/P>
寄存器的数g另一寄存器,或立x的值相加,q存回此寄存器: add reg32, reg32 / imm(8,16,32) 例如Qadd eax, edxQ将eax+edx的值存入eax。减法指令和加法cMQ只是将add换成sub?/P>
需要说明的是,与高U语a不同Q汇~语a中,如果要计两C和(差、积、商Q或一般地_q算l果Q,那么必然有一个寄存器被用来保存结果。在PASCAL中,我们可以用nA := nB + nC来让nA保存nB+nC的结果,然而,汇编语言q不提供q种Ҏ。如果你希望保持寄存器中的结果,需要用另外的指令。这也从另一个侧面反映了“寄存器”这个名字的意义。数据只是“寄存”在那里。如果你需要保存数据,那么需要将它放到内存或其他地方?/P>
cM的指令还有and、or、xorQ与Q或Q异或){等。它们进行的是逻辑q算?/P>
我们Uadd、mov、sub、and{称Zؓ指o助记W(q么叫是因ؓ它比机器语言Ҏ记忆Q而v作用是方便忆,某些资料中也UCؓ指o、操作码、opcode[operation code]{)Q后面的参数成ؓ操作敎ͼ一个指令可以没有操作数Q也可以有一两个操作敎ͼ通常有一个操作数的指令,q个操作数就是它的操作对象;而两个参数的指oQ前一个操作数一般是保存操作l果的地方,而后一个是附加的参数?/P>
我不打算在这份教E中用大量的幅介绍指o——很多h做得比我更好Q而且指o本nq不是重点,如果你学会了如何l织语句Q那么只要稍加学习就能轻易掌握其他指令。更多的指o可以参?A >Intel提供的资料。编写程序的时候,也可以参考一些在U参考手册。Tech!Help和HelpPC 2.10管已经很旧Q但以应付l大多数需要?/P>
聪明的读者也许已l发玎ͼ使用sub eax, eaxQ或者xor eax, eaxQ可以得Cmov eax, 0cM的效果。在高语言中,你大概不会选择用a=a-a来给a赋|因ؓ试会告诉你q么做更慢,直就是在自找ȝQ然而在汇编语言中,你会得到相反的结论,多数情况下,以由快到慢的速度排列Q这三条指o是xor eax, eax、sub eax, eax和mov eax, 0?/P>
Z么呢Q处理器在执行指令时Q需要经q几个不同的阶段Q取指、译码、取数、执行?/P>
我们反复Q寄存器是CPU的一部分。从寄存器取敎ͼ光度很显然要比从内存中取数快。那么,不难理解Qxor eax, eax要比mov eax, 0更快一些?/P>
那么Qؓ什么a=a-a通常要比a=0慢一些呢Q这和编译器的优化有一定关pR多数编译器会把a=a-a译成类g面的代码(通常Q高U语a通过ebp和偏U量来访问局部变量;E序中,x为a相对于本地堆的偏U量Q在只包含一?2-bit整Ş变量的程序中Q这个值通常?)Q?/P>
mov eax, dword ptr [ebp-x] 而把a=0译?/P>
mov dword ptr [ebp-x], 0 上面的翻译只是示意性的Q略M很多必要的步骤,如保护寄存器内容、恢复等{。如果你对与~译E序的实现过E感兴趣Q可以参考相应的书籍。多数编译器Q特别是C/C++~译器,如Microsoft Visual C++Q都提供了从源代码到宏汇~语aE序的附加编译输出选项。这U情况下Q你可以很方便地了解~译E序执行的输出结果;如果~译E序没有提供q样的功能也没有关系Q调试器会让你看到编译器的编译结果?/P>
如果你明地知道~译器编译出的结果不是最优的Q那可以着手用汇编语言来重写那D代码了。怎么认是否应该用汇~语a重写呢? 曄在一份杂志上看到q有人用U机器语a~写E序。不清楚到底q是不是~辑的失误,因ؓ一个头脑正常的人恐怕不会这么做E序Q即使它不长、也不复杂。首先,汇编器能够完成某些封包操作,即不行Q也可以用db伪指令来写指令;用汇~语a写程序可以防止很多错误的发生Q同Ӟ它还减轻了h的负担,很显Ӟ“完全用机器语言写程序”是完全没有必要的,因ؓ汇编语言可以做出完全一L事情Qƈ且你可以依赖它,因ؓ计算Z会出错,而hL出错的时候。此外,如前面所aQ如果用高语言实现E序的代价不大(例如Q这D代码在E序的整个执行过E中只执行一遍,q且Q这一遍的执行旉也小于一U)Q那么,Z么不用高U语a实现呢? 一些比较狂热的~程爱好者可能不太喜Ƣ我的这U观炏V比方说Q他们可能希望精益求_֜优化每一字节的代码。但多数情况下我们有更重要的事情Q例如,你的法是最优的吗?你已l把E序在高U语a许可的范围内优化到尽头了吗?q不是所有的人都有资D栯。汇~语a是这样一件东西,它够的强大Q能够控制计机Q完成它能够实现的Q何功能;同时Q因为它的强大,也会提高开发成本,q且Q难于维护。因此,我个人的是,如果在Y件开发中使用汇编语言Q则应在软g接近完成的时候用,q样可以减少很多不必要的投入?/P>
W二章中Q我介绍了x86pd处理器的基本寄存器。这些寄存器对于x86兼容处理器仍然是有效的,如果你偏爱AMD的CPUQ那么用这些寄存器的程序同样也可以正常q行?/P>
不过现在说用汇编语言q行优化qؓ时尚早——不可能写程序,而只操作q些寄存器,因ؓq样只能完成非常单的操作Q既然是单的操作Q那可能׃让h觉得乏味Q甚x一台够快的机器穷丑֮的所有结果(如果可以ID的话Q,q直接写E序调用Q因样通常会更快。但话说回来Q看完接下来的两章——内存和堆栈操作Q你可以独立完成几乎所有的d了,配合W五章中断、第六章子程序的知识Q你知道如何驾驭处理器Qƈ让它Z工作?/P> 与十q制cMQ我们可以用下面的式子来换算Z E序设计中常用十六进制和八进制数字代替二q制 EAX的内容ؓ000A3412h.
2.2 使用寄存?/H3>
汇编语言中的整数帔R表示
q也是一U常用的数制。二q制数表CZؓ[二进制数]b或[二进制数]B。一般程序中用二q制数表C掩码(mask codeQ等数据非常的直观,但需要些很长的数据(4位二q制数相当于一位十六进制数Q。例如,1010110b?
八进制数现在已经不是很常用了Q确实还在用Q一个典型的例子是Unix的文件属性)。八q制数的形式是[八进制数]q、[八进制数]Q、[八进制数]o、[八进制数]O。例如,777Q?/LI>
?
X
H
L
EAX
0000
00
00
EBX
0000
00
00
ECX
0000
00
00
EDX
0000
00
00
mov eax, 012345678h
mov ebx, 0abcdeffeh
mov ecx, 1
mov edx, 2; ?12345678h送入eax
; ?abcdeffeh送入ebx
; ?00000001h送入ecx
; ?00000002h送入edx
?
X
H
L
EAX
1234
56
78
EBX
abcd
ef
fe
ECX
0000
00
01
EDX
0000
00
02
mov eax, ebx
mov ecx, edx; ebx内容送入eax
; edx内容送入ecx
?
X
H
L
EAX
abcd
ef
fe
EBX
abcd
ef
fe
ECX
0000
00
02
EDX
0000
00
02
mov eax, 0a1234h
mov bx, ax
mov ah, bl
mov al, bh; ?a1234h送入eax
; ax的内定w入bx
; bl内容送入ah
; bh内容送入al
使用Intel文档中的寄存器表C方?/B>
mov reg32, (reg16 | imm8 | imm16)
mov reg8, (reg8 | imm8)
xchg reg32, reg32
xchg reg16, reg16
xchg reg8, reg8
mov eax, 0a1234h
mov bx, ax
xchg ah, al; ?a1234h送入eax
; ax内容送入bx
; 交换ah, al的内?/FONT>
inc reg(8,16,32)
dec reg(8,16,32)
add reg16, reg16 / imm(8,16)
add reg8, reg8 / imm(8)
sub eax, dword ptr [ebp-x]
mov dword ptr [ebp-x],eax
使用汇编语言重写代码之前需要确认的几g事情
只需要表CZU?某些情况?U,q一内容过?BR>q䆾教程的范_如果您感兴趣Q可以参考数字?BR>辑电路的相关书籍)状? 对于电\而言Q这表现
为高、低电^Q或者开、关Q分别非常明显,因?BR>工作比较E_Q另一斚wQ由于只有两U状态,?BR>计v来也比较单。这P使用二进制意味着低成
本、稳定,多数情况下,q也意味着快速?
个Q意Ş如am-1……a3a2a1a0 的m位rq制数对应的
数值nQ?/P>
敎ͼ其原因在于,16??的整ơ方q,q样Q一
位十六或八进制数可以表示整数个二q制位。十?BR>q制中, 使用字母A、B、C、D、E、F表示10-15Q?BR>而十六进制或八进制数制表C的的数字比二进制数
更短一些?/P>
1. 通用寄存?/FONT>:
EAX
(accumulator)"累加?,很多加法乘法指o的缺?FONT style="BACKGROUND-COLOR: #316ac5" color=#ffffff>寄存?/FONT>.
EBX
(base)"基地址"寄存?/FONT>, 在内存寻址时存攑֟地址.
ECX
(counter)计数? 是重?REP)前缀指o和LOOP指o的内定计?/P>
EDX
用来放整数除法生的余数.
?6?AX,BX,CX和DX
??:AL,BL,CL和DL
??:AH,BH,CH和DH
ESI
(source index)"源烦?FONT style="BACKGROUND-COLOR: #316ac5" color=#ffffff>寄存?/FONT>", DS:ESI指向源串,?字符串操作指令中,
EDI
(destination index)"目标索引寄存?/FONT>",ES:EDI指向目标?/P>
EBP (BASE POINTER)"基址指针",被用作高U语a函数调用?BR> ,ESP(q个虽然通用,?BR>很少被用做除了堆栈指针外的用? q些32位可以被用作多种4?FONT style="BACKGROUND-COLOR: #316ac5" color=#ffffff>寄存?/FONT>的又?/P>
函数的返回值经常被攑֜EAX? ESI/EDI分别叫做"?目标索引寄存?/FONT>"(source/destination index 中央处理?CPU)在微机系l处于“领导核心”的C。汇~语a被编译成机器语言之后Q将由处理器来执行。那么,首先让我们来了解一下处理器的主要作用,q将帮助你更好地N它?/P>
),因ؓ在很多字W串操作指o? DS:ESI指向源串,? EBP?基址指针"(BASE POINTER), 它最l常"
框架指针"(frame pointer). 在破解的时?l常可以看见一个标准的函数起始代码: push ebp ;保存当前ebp mov ebp,esp ;EBP设ؓ当前堆栈指针
sub esp, xxx ;预留xxx字节l函C时变? ... q样一?EBP 构成了该函数的一个框? 在EBP上方分别是原来的EBP, q回地址和参? EBP?BR>
方则是时变? 函数q回时作 mov esp,ebp/pop ebp/ret 卛_. ESP 专门用作堆栈指针. 2. D?FONT style="BACKGROUND-COLOR: #316ac5" color=#ffffff>寄存?/FONT>: CS(Code SegmentQ代码段) 指定当前?BR>
行的代码D? EIP (Instruction pointer, 指o指针)则指向该D中一个具体的指o. CS:EIP指向哪个指o, CPU 执行它. 一般只能用jmp, ret,
jnz, call {指令来改变E序程,而不能直接对它们赋? DS(DATA SEGMENT, 数据D? 指定一个数据段. 注意:在当前的计算机系l中, 代码和数
据没有本质差? 都是一串二q制? 区别只在于你如何用它. 例如, CS 制定的段L被用作代? 一般不能通过CS指定的地址M改该D? 然?BR>
,你可以ؓ同一个段甌一个数据段描述W?别名"而通过DS来访?修改. 自修改代码的E序常如此做. ES,FS,GS 是辅助的D?FONT style="BACKGROUND-COLOR: #316ac5" color=#ffffff>寄存?/FONT>, 指定附加的数
据段. SS(STACK SEGMENT)指定当前堆栈D? ESP 则指D中当前的堆栈顶. 所有push/pop pd指o都只对SS:ESP指出的地址q行操作. 3. 标志
寄存?/FONT>(EFLAGS): ?FONT style="BACKGROUND-COLOR: #316ac5" color=#ffffff>寄存?/FONT>?2?l合了各个系l标? EFLAGS一般不作ؓ整体讉K, 而只对单一的标志位感兴? 常用的标志有: q位标志C(
CARRY), 在加法生进位或减法有借位时置1, 否则?. 零标志Z(ZERO), 若运结果ؓ0则置1, 否则? W号位S(SIGN), 若运结果的最高位|?BR>
1, 则该位也|?. 溢出标志O(OVERFLOW), ?带符?q算l果出可表C? 则置1. JXX pd指o是Ҏq些标志来决定是否要跌{, 从?BR>
实现条g分枝. 要注?很多JXX 指o是等L, 对应相同的机器码. 例如, JE 和JZ 是一L,都是当Z=1是蟩? 只有JMP 是无条g跌{. JXX ?BR>
令分Zl? 分别用于无符h作和带符h? JXX 后面?XX" 有如下字? 无符h? 带符h? A = "ABOVE", 表示"高于" G = "
GREATER", 表示"大于" B = "BELOW", 表示"低于" L = "LESS", 表示"于" C = "CARRY", 表示"q位"?借位" O = "OVERFLOW", 表示"溢出" S
= "SIGN", 表示"? 通用W号: E = "EQUAL" 表示"{于", {h于Z (ZERO) N = "NOT" 表示"?, x志没有置? 如JNZ "如果Z没有|位则蟩
? Z = "ZERO", 与E? 如果仔细想一?׃发现 JA = JNBE, JAE = JNB, JBE = JNA, JG = JNLE, JGE= JNL, JL= JNGE, .... 4. 端口 端口
是直接和外部讑֤通讯的地斏V外设接入系l后Q系l就会把外设的数据接口映到特定的端口地址I间Q这P从该端口d数据是从外设读
入数据,而向外设写入数据是向端口写入数据。当然这一切都必须遵@外设的工作方式。端口的地址I间与内存地址I间无关Q系ld提供?BR>
64K?位端口的讉KQ编?-65535. 盔R?位端口可以组成成一?6位端口,盔R?6位端口可以组成一?2位端口。端口输入输出由指o
IN,OUT,INS和OUTS实现Q具体可参考汇~语a书籍?BR>
典型的处理器的主要Q务包?/B>
一般说来,处理器拥有对整个pȝ的所有ȝ的控制权。对于Intelq_而言Q处理器拥有Ҏ据、内存和控制ȝ的控制权Q根据指令控制整个计机的运行。在以后的章节中Q我们还讨论系l中同时存在多个处理器的情况?/P>
处理器中有一些寄存器Q这些寄存器可以保存特定长度的数据。某些寄存器中保存的数据对于pȝ的运行有Ҏ的意义?/P>
新的处理器往往拥有更多、具有更大字长的寄存器,提供更灵zȝ取指、寻址方式?/P>
寄存?/B>
如前所qͼ处理器中有一些可以保存数据的地方被称作寄存器?/P>
寄存器可以被装入数据Q你也可以在不同的寄存器之间Udq些数据Q或者做cM的事情。基本上Q像四则q算、位q算{这些计操作,都主要是针对寄存器进行的?/P>
首先让我来介l一?0386上最常用?个通用寄存器。先瞧瞧下面的图形,试着理解一下:
上图中,数字表示的是位。我们可以看出,EAX是一?2-bit寄存器。同Ӟ它的?6-bit又可以通过AXq个名字来访问;AX又被分ؓ高、低8bit两部分,分别由AH和AL来表C?/P>
对于EAX、AX、AH、AL的改变同时也会媄响与被修改的那些寄存器的倹{从而事实上只存在一?2-bit的寄存器EAXQ而它可以通过4U不同的途径讉K?/P>
也许通过名字能够更容易地理解q些寄存器之间的关系。EAX中的E的意思是“扩展的”,整个EAX的意思是扩展的AX。X的意思Intel没有明示Q我个h认ؓ表示它是一个可变的?。而AH、AL中的H和L分别代表高和??/P>
Z么要q么做呢Q主要由于历史原因。早期的计算机是8位的Q?086是第一?6位处理器Q其通用寄存器的名字是AXQBX{等Q?0386是Intel推出的第一ƾIA-32pd处理器,所有的寄存器都被扩充ؓ32位。ؓ了能够兼容以前的16位应用程序,80386不能这些寄存器依旧命名为AX、BXQƈ且简单地他们扩充ؓ32位——这增加处理器在处理指令方面的成本?/P>
Intel微处理器的寄存器列表Q在本章先只介绍80386的寄存器QMMX寄存器以及其他新一代处理器的新寄存器将在以后的章节介绍Q?/P>
通用寄存?/B>
下面介绍通用寄存器及其习惯用法。顾名思义Q通用寄存器是那些你可以根据自q意愿使用的寄存器Q修改他们的值通常不会对计机的运行造成很大的媄响。通用寄存器最多的用途是计算?/P>
EAX |
通用寄存器。相对其他寄存器Q在q行q算斚w比较常用。在保护模式中,也可以作为内存偏UL针(此时QDS作ؓD?寄存器或选择器) |
EBX |
通用寄存器。通常作ؓ内存偏移指针使用Q相对于EAX、ECX、EDXQ,DS是默认的D寄存器或选择器。在保护模式中,同样可以赯个作用?/TD> |
ECX |
通用寄存器。通常用于特定指o的计数。在保护模式中,也可以作为内存偏UL针(此时QDS作ؓ 寄存器或D选择器)?/TD> |
EDX |
通用寄存器。在某些q算中作为EAX的溢出寄存器Q例如乘、除Q。在保护模式中,也可以作为内存偏UL针(此时QDS作ؓD?寄存器或选择器)?/TD> |
上述寄存器同EAX一样包括对应的16-bit?-bit分组?/P>
用作内存指针的特D寄存器
ESI |
通常在内存操作指令中作ؓ“源地址指针”用。当ӞESI可以被装入Q意的数|但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器?/TD> |
EDI |
通常在内存操作指令中作ؓ“目的地址指针”用。当ӞEDI也可以被装入L的数|但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器?/TD> |
EBP |
q也是一个作为指针的寄存器。通常Q它被高U语a~译器用以徏造‘堆栈’来保存函数或过E的局部变量,不过Q还是那句话Q你可以在其中保存你希望的Q何数据。SS是它的默认段寄存器或选择器?/TD> |
注意Q这三个寄存器没有对应的8-bit分组。换a之,你可以通过SI、DI、BP作ؓ别名讉K他们的低16位,却没有办法直接访问他们的?位?/P>
D寄存器和选择?/B>
实模式下的段寄存器到保护模式下摇w一变就成了选择器。不同的是,实模式下的“段寄存器”是16-bit的,而保护模式下的选择器是32-bit的?/P>
CS | 代码D,或代码选择器。同IP寄存?E后介绍)一同指向当前正在执行的那个地址。处理器执行时从q个寄存器指向的D(实模式)或内存(保护模式Q中获取指o。除了蟩转或其他分支指o之外Q你无法修改q个寄存器的内容?/TD> |
DS | 数据D,或数据选择器。这个寄存器的低16 bitq同ESI一同指向的指o要处理的内存。同Ӟ所有的内存操作指o 默认情况下都用它指定操作D?实模?或内?作ؓ选择器,在保护模式。这个寄存器可以被装入Q意数|然而在q么做的时候需要小心一些。方法是Q首先把数据送给AXQ然后再把它从AX传送给DS(当然Q也可以通过堆栈来做). |
ES | 附加D,或附加选择器。这个寄存器的低16 bitq同EDI一同指向的指o要处理的内存。同LQ这个寄存器可以被装入Q意数|Ҏ和DScM?/TD> |
FS | FD|F选择?推测F可能是Free?)。可以用q个寄存器作为默认段寄存器或选择器的一个替代品。它可以被装入Q何数|Ҏ和DScM?/TD> |
GS | GD|G选择?G的意义和F一P没有在Intel的文档中解释)。它和FS几乎完全一栗?/TD> |
SS | 堆栈D|堆栈选择器。这个寄存器的低16 bitq同ESP一同指向下一ơ堆栈操?push和pop)所要用的堆栈地址。这个寄存器也可以被装入L数|你可以通过入栈和出栈操作来l他赋|不过׃堆栈对于很多操作有很重要的意义,因此Q不正确的修Ҏ可能造成对堆栈的破坏?/TD> |
* 注意 一定不要在初学汇编的阶D|q些寄存器弄淗他们非帔R要,而一旦你掌握了他们,你就可以对他们做L的操作了。段寄存器,或选择器,在没有指定的情况下都是用默认的那个。这句话在现在看来可能有点稀里糊涂,不过你很快就会在后面知道如何d?/P>
Ҏ寄存?指向到特定段或内存的偏移?Q?/P>
EIP | q个寄存器非常的重要。这是一?2位宽的寄存器 Q同CS一同指向即执行的那条指o的地址。不能够直接修改q个寄存器的|修改它的唯一Ҏ是蟩转或分支指o?CS是默认的D|选择? |
ESP | q个32位寄存器指向堆栈中即被操作的那个地址。尽可以修改它的|然而ƈ不提倡这样做Q因为如果你不是非常明白自己在做什么,那么你可能造成堆栈的破坏。对于绝大多数情况而言Q这对程序是致命的?SS是默认的D|选择? |
IP: Instruction Pointer, 指o指针
SP: Stack Pointer, 堆栈指针
好了Q上面是最基本的寄存器。下面是一些其他的寄存器,你甚臛_能没有听说过它们?都是32位宽)Q?/P>
CR0, CR2, CR3(控制寄存?。D一个例子,CR0的作用是切换实模式和保护模式?/P>
q有其他一些寄存器QD0, D1, D2, D3, D6和D7(调试寄存?。他们可以作试器的硬件支持来讄条g断点?/P>
TR3, TR4, TR5, TR6 ?TR? 寄存?试寄存?用于某些条g试?/P>
最后我们要说的是一个在E序设计中v着非常关键的作用的寄存器:标志寄存器?/P>