??xml version="1.0" encoding="utf-8" standalone="yes"?>
实现ҎQ在PHP讑֮(configure)时加入如下选项?
q样׃得你的PHPpȝ可以处理相关的IPC函数了?
IPC是什么?
IPC (Inter-process communication) 是一个Unix标准通讯机制Q它提供了得在同一CZ同进E之间可以互盔R讯的方法。基本的IPC处理机制?U:它们分别是共享内存、信号量和消息队列。本文中我们主要讨论׃n内存和信号量的用。关于消息队列,W者在不久的将来还会专门介l?
在PHP中用共享内存段
在不同的处理q程之间使用׃n内存是一个实C同进E之间相互通讯的好Ҏ。如果你在一个进E中向所׃n的内存写入一D信息,那么所有其他的q程也可以看到这D被写入的数据。非常方ѝ在PHP中有了共享内存的帮助Q你可以实现不同q程在运行同一DPHP脚本时返回不同的l果。或实现对PHP同时q行数量的实时查询等{?
׃n内存允许两个或者多个进E共享一l定的存储区。因为数据不需要在客户机和服务器之间复Ӟ所以这是最快的一UIPC。用共享内存的唯一H门是多个进E对一l定存储区的同步存取?
如何建立一个共享内存段呢?下面的代码可以帮你徏立共享内存?nbsp;
注意Q每个共享内存段都有一个唯一的ID, 在PHP中,shmop_open会把建立好的׃n内存D늚IDq回Q这里我们用$shm_id记录它。?key是一个我们逻辑上表C共享内存段的Key倹{不同进E只要选择同一个Key id可以共享同一D存储段。习惯上我们用一个串Q类似文件名一L东西Q的散列g为key id. $mode指明了共享内存段的用方式。这里由于是新徏Q因此gؓ’c’ –取create之意。如果你是访问已l徏立过的共享内存那么请?#8217;a’,-- 取Access之意?perm参数定义了访问的权限Q?q制Q关于权限定义请看UNIX文gpȝ帮助?size定义了共享内存的大小。尽有点象fopen(文g处理)你可不要当它同文件处理一栗后面的描述你将看到着一炏V?
例如Q?
q里我们打开了一个共享内存段 键?xff3 –rw-r—r—格式,大小?00字节?
如果需要访问已有的׃n内存D,你必d调用shmop_open中设W??个参Cؓ0?
IPC工作状态的查询
在Unix下,你可以用一个命令行E序ipcs查询pȝ所有的IPC资源状态。不q有些系l要求需要超U用h能执行。下图是一Dipcs的运行结果?/p>
上图中系l显CZ4个共享内存段Q注意其中第4个键gؓ0x00000ff3的就是我们刚刚运行过的PHPE序所创徏的。关于ipcs的用法请参考UNIX用户手册?
如何释放׃n内存?
释放׃n内存的办法是调用PHP指o:shmop_delete($id)
$id 是你调用shmop_open所存的shmop_op的返回倹{还有一个办法就是用UNIX的管理指?
ipcrm id, id是你用ipcs看到的ID.和你E序中的$id不一栗不q要心Q如果你用ipcrm直接删除׃n内存D那么有可能D其他不知道这一情况的进E在引用q个已经不复存在的共享内存器时出C些不可预的错误(往往l果不妙)?
如何使用(d)׃n内存?
使用如下所C函数向׃n内存写入数据
其中shmid是用shmop_openq回的句柄?Data变量存放了要存放的数据?offset描述了写入从׃n内存的开始第一个字节的位置Q以0开始)?
d操作是:
同样Q指?shmid,开始偏U量Q以0开始)、总读取数量。返回结果串。这P你就可以把共享内存段当作是一个字节数l。读几个再写几个Q想q嘛干嘛,十分方便?
多进E问题的考虑
现在Q在单独的一个PHPq程中读写、创建、删除共享内存方面上你应该没有问题了。但是,昄实际q行中不可能只是一个PHPq程在运行中。如果在多个q程的情况下你还是沿用单个进E的处理ҎQ你一定会到问题 ---- 著名的ƈ行和互斥问题。比如说?个进E同旉要对同一D内存进行读写。当两个q程同时执行写入操作Ӟ你将得到一个错误的数据Q因D内存将之可能是最后执行的q程的内容,甚至是由2个进E写入的数据轮流随机出现的一D|合的四不象。这昄是不能接受的。ؓ了解册个问题,我们必须引入互斥机制。互斥机制在很多操作pȝ的教材上都有专门讲述Q这里不多重复。实C斥机制的最单办法就是用信L。信号量是另外一U进E间通讯(IPC)的方式,它同其他IPC机构(道、FIFO、消息队?不同。它是一个记数器Q用于控制多q程对共享数据的存储。同L是你可以用ipcs和ipcrm实现对信L使用状态的查询和对其实现删除操作。在PHP中你可以用下列函数创Z个新的信号量q返回操作该信号量的句柄。如果该key指向的信号量已经存在Qsem_get直接q回操作该信号量的句柄?
$max_acquire 指明同时最多可以用几个q程q入该信可不必等待该信号被释放(也就是最大同时处理某一资源的进E数?一般该值均ZQ?perm指明了访问权限?
一旦你成功的拥有了一个信号量Q你对它所能做的只?U:h、释放。当你执行释放操作时, pȝ把该信号值减一。如果小?那就q设?。而当你执行请求操作时Q系l将把该信号值加一Q如果该值大于设定的最大值那么系l将挂v你的处理q程直到其他q程释放到小于最大gؓ止。一般情况下最大D?,q样一来当一个进E获得请求时其他后面的进E只能等待它退Z斥区后释放信号量才能q入该互斥区q同时设为独占方式。这L信号量常UCؓ双态信号量。当Ӟ如果初值是L一个正数就表明有多个׃n资源单位可供׃n应用?
甌、释放操作的PHP格式如下Q?
甌
释放
其中sem_identifier是调用sem_get的返回|句柄Q?nbsp;
一个简单的互斥协议例子
下面是一D很单的互斥操作规程?
正如你所看到的,互斥的实现很单:甌q入临界区,对界区资源q行操作Q比如修改共享内存)退Z界区q放信受这样一来就可以保证在同一个时间片中不可能有同?个进E对同一D共享内存进行操作。因Z号量机制保证一个时间片只能׃个进E进入,其他q程必须{待当前处理的进E完成后方能q入?
临界Z般是指那些不允许同时有多个进Eƈ发处理的代码Dc?
要注意的?在PHP中必ȝ同一个进E释攑֮所占用的信号量。在一般系l中允许q程释放别的q程占用的信受在~写临界Z码一定要心设计资源的分配,避免A{BQB{A的死锁情况发生?nbsp;
q?/strong>
IPC的运用是十分q泛的。比如,在不同进E间保存一个解释过的复杂的配置文g、或具体讄的用LQ以避免重复处理。我也曾l用׃n内存的技术把一大批PHP脚本必须引用的一个很大的文g攑օ׃n内存Qƈ由此显著提升了Web服务的速度、消除了部分瓉。关于它的用还有聊天室Q多路广播等{。IPC的威力取决于你的惌力的大小。如果本文对你有一点点启发Q那我不胜荣q。愿意很你讨令h入迷的电脑技术。Email: qwyaxm@163.net
优化是一仉帔R要的事情。作Z个程序设计者,你肯定希望自qE序既小又快。DOS时代的许多书中都提到Q“某某编译器能够生成非常紧凑的代码”,换言之,~译器会Z把代码尽可能地羃减,如果你能够正地使用它提供的功能的话。目前,Intel x86体系上流行的C/C++~译器,包括Intel C/C++ Compiler, GNU C/C++ CompilerQ以及最新的Microsoft和Borland~译器,都能够提供非常紧凑的代码。正地使用q些~译器,则可以得到性能_好的代码?/p>
但是Q机器目前还不能像h那样做富于创造性的事情。因而,有些时候我们可能会不得不手工来做一些事情?/p>
使用汇编语言优化代码是一件困难,而且技巧性很强的工作。很多编译器能够生成为处理器q行q特D优化处理的代码Q一旦进行修改,q些Ҏ优化可能׃被破坏而失效。因此,在你军_使用自己的汇~代码之前,一定要试一下,到底是编译器生成的那D代码更好,q是你的更好?/p>
本章中将讨论一些编译器在某些时候会做的事情(从某U意义上_本章内容更像是计机专业的基课中《编译程序设计原理》、《计机l成原理》、《计机体系l构》课E中的相兛_?。本章的许多内容和汇~语aE序设计本n关系q不是很紧密Q它们多数是在ؓ使用汇编语言q行优化做准备。编译器实做这些优化,但它q不Lq么做;此外Q就~译器的设计本质来说Q它实没有义务q么做——编译器做的是等义变换,而不是等效变换。考虑下面的代码:
// E序D? int gaussianSum(){ int i, j=0; for(i=0; i<100; i++) j+=i; return j; |
好的Q首先,l大多数~译器恐怕不会自作主张地把它“篡改”ؓ
// E序D?(改进1) int gaussianSum(){ int i, j=0; for(i=1; i<100; i++) j+=i; return j; |
多数Q但实不是全部Q编译器也不会把它改?/p>
// E序D?(改进2) |
q两个修改版本都不同于原先程序的语义。首先我们看刎ͼ让i?开始是没有必要的,因ؓj+=iӞi=0不会做Q何有用的事情Q然后是Q实际上没有必要每一ơ都计算1+...+100的和——它可以被预先计,q在需要的时候返回?/p>
q个例子也许q不恰当(估计没h会写出最初版本那L代码)Q但q种实践在程序设计中实可能出现。我们把改进2UCؓ~译时表辑ּ预先计算Q而把改进1成ؓ循环强度削减?/p>
然而,一些新的编译器的确会进行这两种优化。不q别慌,看看下面的代码:
// E序D? int GetFactorial(int k){ int i, j=1; if((k<0) || (k>=10)) return -1; if((k<=1)) return 1 for(i=1; i<k; i++) j*=i; return j; |
E序采用的是一个时间复杂度为O(n)的算法,不过Q我们可以把他轻易地改ؓO(1)的算法:
// E序D? (非规范改q? int GetFactorial(int k){ int i, j=1; static const int FractorialTable[]={1, 1, 2, 6, 24, if((k<0) || (k>=10)) return -1; return FractorialTable[k]; |
q是一个典型的以空间换旉的做法。通用的编译器不会q么做——因为它没有办法在编译时定你是不是要这么改。可以说Q如果编译器真的q样做的话,那将是一件可怕的事情Q因为那时候你很隄道编译器生成的代码和自己想的到底有多大的差距?/p>
当然Q这cM化超Z本文的范围——基本上Q我把它们归入“算法优化”,而不是“程序优化”一cR类似的优化q程需要程序设计h员对于程序逻辑非常深入C解和全盘的掌握,同时Q也需要有丰富的算法知识?/p>
自然Q如果你希望自己的程序性能有大q度的提升,那么首先应该做的是算法优化。例如,把一?i>O(n2)的算法替换ؓ一?i>O(n)的算法,则程序的性能提升远q超q对于个别语句的修改。此外,一个已l改写ؓ汇编语言的程序,如果要再在算法上作大q度的修改,其工作量和重写相当。因此,在决定用汇~语aq行优化之前Q必首先考虑法优化。但假如已经是最优的法Q程序运行速度q是不够快怎么办呢Q?/p>
好的Q现在,假定你已l用了已知最好的法Q决定把它交l编译器Q让我们来看看编译器会ؓ我们做什么,以及我们是否有机会插手此事,做得更好?/p>
比较新的~译器在~译时会自动把下面的代码Q?/p>
for(i=0; i<10; i++){ j = i; k = j + i; } |
臛_变换?/p>
for(i=0; i<10; i++); j=i; k=j+i; |
甚至
j=i=10; k=20; |
当然Q真正的~译器实际上是在中间代码层次作这件事情?/p>
原理如果数据的某个中间?E序执行q程中的计算l果)在用之前被另一中间D盖,则相兌不必进行?/p>
也许有h会问Q编译器不是都给׃做了吗,它做什么?注意Q这里说的只是编译系l中优化部分的基本设计。不仅在从源代码C间代码的q程中存在优化问题,而且~译器生成的最l的机器语言(汇编)代码同样存在cM的问题。目前,几乎所有的~译器在最l生成代码的q程中都有或多或的瑕疵Q这些瑕늛?font color="#ff0000">只能依靠手工修改代码来解冟?/p>
表达式预先计非常简单,是在编译时可能地计算E序中需要计的东西。例如,你可以毫不犹豫地写出下面的代码:
const unsigned long nGiga = 1024L * 1024L * 1024L; |
而不必担心程序每ơ执行这个语句时作两遍乘法,因ؓ~译器会自动地把它改?/p>
const unsigned long nGiga = 1073741824L; |
而不是傻乎乎地让计算机在执行到这个初始化赋D句的时候才计算。当Ӟ如果你愿意在上面的代码中Z一些变量的话,~译器同样会把常数部分先行计,q拿到结果?/p>
表达式预计算q不会让E序性能有飞跃性的提升Q但实减少了运行时的计强度。除此之外,l大多数~译器会把下面的代码Q?/p>
// [假设此时b, c, d, e, f, g, h都有一个确定的非零整数|q且Q?br />// a[]Z个包?个整数元素的数组Q其下标??] a[0] = b*c; |
优化?再次Q编译器实际上是在中间代码的层次Q而不是源代码层次做这件事情!)Q?/p>
// [假设此时b, c, d, e, f, g, h都有一个确定的非零整数|q且Q?br />// a[]Z个包?个整数元素的数组Q其下标??] a[0] = b*c; |
更进一步,在实际代码生成过E中Q一些编译器q会对上q语句的ơ序q行调整Q以使其q行效率更高。例如,语句调整ؓ下面的次序:
// [假设此时b, c, d, e, f, g, h都有一个确定的非零整数|q且Q?br />// a[]Z个包?个整数元素的数组Q其下标??] a[0] = b*c; |
在某些体pȝ构中Q刚刚计完的a[1]可以攑ֈ寄存器中Q以提高实际的计性能。上q?个计Q务之_只有1, 3, 4三个计算d必须串行地执行,因此Q在新的处理器上Q这样做甚至能够提高E序的ƈ行度Q从而ɽE序效率变得更高?/p>
[待修订内?/font>] 本章中,从这一节开始的所有优化都是在微观层面上的优化了。换a之,q些优化是不能用高U语a中的对应设施q行解释的。这一部分内容进行较大规模的修订?/i>
通常Q此cM化是q译器自动完成的。我个hq不推荐真的׃h来完成这些工作——这些工作多半是枯燥而重复性的Q编译器通常会比人做得更?没说的,肯定也更?。但话说回来Q用汇~语a的程序设计h员有责Q了解q些内容Q因为只有这h能更好地N处理器?/p>
在前面的几章中我已经提到q,寄存器的速度要比内存快。因此,在用寄存器斚wQ编译器一般会做一U称为全局寄存器优化的优化?/p>
例如Q在我们的程序中使用?个变量:i, j, k, l。它们都作ؓ循环变量使用Q?/p>
for(i=0; i<1000; i++){ for(j=0; j<1000; j++){ for(k=0; k<1000; k++){ for(l=0; l<1000; l++) do_something(i, j, k, l); } } } |
q段E序的优化就不那么简单了。显Ӟ按照通常的压栈方法,i, j, k, l应该按照某个序被压q堆栈,然后调用do_something()Q然后函数做了一些事情之后返回。问题在于,无论如何压栈Q这些东西大概都得进内存(不可否认某些机器可以用CPU的Cache做这件事情,但Cache是写通式的和回写式的又会造成一些性能上的差异)?/p>
聪明的读者马上就会指出,我们不是可以在定义do_something()的时候加上inline修饰W,让它在本地展开吗?没错Q本地展开以增加代码量Zh取性能Q但q只是问题的一半。编译器管完成了本地展开Q但它仍焉要做许多额外的工作。因为寄存器只有那么有限的几个,而我们却有这么多的@环变量?/p>
把四个变量按照它们在循环中用的频率排序Qƈ军_在do_something()块中的优先顺?攑օ寄存器中的优先顺?是一个解x案。很明显Q我们可以按照l, k, j, i的顺?从高CQ因为l被q行1000*1000*1000*1000ơ运!)来排列,但在实际的问题中Q事情往往没有q么单,因ؓ你不知道do_something()中做的到底是什么。而且Q凭什么就以for(l=0; l<1000; l++)作ؓ优化的分界点呢?如果do_something()中还有@环怎么办?
如此复杂的计问题交l计机来做通常会有比较满意的结果。一般说来,~译器能够对E序中变量的使用q行更全面地估计Q因此,它分配寄存器的结果有时虽然让解,但却是最优的(因ؓ计算够进行大量的重复计算Qƈ扑ֈ最好的ҎQ而h做这件事相对来讲比较困难)?/p>
~译器在许多时候能够作出相当让人满意的l果。考虑以下的代码:
int a=0;
for(int i=1; i<10; i++) |
让我们把它变为某UŞ式的中间代码Q?/p>
00: 0 -> a 01: 1 -> i 02: 1 -> j 03: i*j -> t 04: a+t -> a 05: j+1 -> j 06: evaluate j < 100 07: TRUE? goto 03 08: i+1 -> i 09: evaluate i < 10 10: TRUE? goto 02 11: [l箋执行E序的其余部分] |
E序中执行强度最大的无疑?3?5q一D,涉及的需要写入的变量包括a, jQ需要读出的变量是i。不q,最l的~译l果大大Z我们的意料。下面是某种优化模式下Visual C++ 6.0~译器生成的代码(我做了一些修?Q?/p>
xor eax, eax ; a=0(eax: a) mov edx, 1 ; i=1(edx: i) push esi ; 保存esi(最后要恢复Qesi作ؓ代替j的那个@环变? nexti: mov ecx, edx ; [t=i] mov esi, 999 ; esi=999: 此处修改了原E序的语义,但仍?000ơ@环?/font> nextj: add eax, ecx ; [a+=t] add ecx, edx ; [t+=i] dec esi ; j-- jne SHORT nextj ; jne {h?jnz. [如果q需要,则再ơ@环] inc edx ; i++ cmp edx, 10 ; i?0比较 jl SHORT nexti ; i < 10, 再次?/font>?br />pop esi ; 恢复esi |
q段代码可能有些令h费解。主要是因ؓ它不仅用了大量寄存器,而且q包括了5.2节中曾提到的子表辑ּ提取技术。表面上看,多引入的那个变量(t)增加了计时_但要注意Q这个t不仅不会降低E序的执行效率,相反q会让它变得更快Q因为同样得C计算l果(本质上,i*jxWjơ篏加i的?Q但q个l果不仅用到了上ơ运的l果Q而且q省M乘法(很显然计机计算加法要比计算乘法??/p>
q里可能会有人问Qؓ什么要?99循环?Q而不是按照程序中写的那样?循环?99呢?q个问题和汇~语a中的取址有关。在下两节中我将提到q方面的内容?/p>
考虑q样的问题,我和两个同伴现在在山里,q处有一口井Q我们带着一口锅QnҎ树林Qn上的饮用水已l喝光了Q此处允许砍柴和使用明火(当然我们不想引v火灾:)Q需要烧一锅水Q应该怎么样呢Q?/p>
一U方案是Q三个h一hӞ一L_一h_一h水烧开?/p>
另一U方案是Q一个h搭灶Q此时另一个hȝ_W三个h打水Q然后把水烧开?/p>
q两U方案画出图来是q样Q?/p>
仅仅q样很难说明两个ҎC孰劣Q因为我们ƈ不明三个h一h水、一L柴、一h灶的效率更高Q还是分别作效率更高(通常的想法,一起做也许效率会更?。但假如_三个Z个只会搭Ӟ一个只会砍_一个只会打?当然是说q三件事?Q那么,Ҏ2的效率就会搞一些了?/p>
在现实生zMQ某个h拥有专长是比较普遍的情况Q在设计计算机硬件的时候则更是如此。你不可能指望加法器不做M改动pdUM甚至整数乘法Q然而我们注意到Q串行执行的E序不可能在同一时刻同时用到处理器的所有功能,因此Q我?很自然地)会希望有一些指令ƈ行地执行Q以充分利用CPU的计资源?/p>
CPU执行一条指令的q程基本上可以分Z面几个阶D:取指令、取数据、计、保存数据。假设这4个阶D各需?个时钟周期,那么Q只要资源够用,q且4条指令之间不存在串行关系(换言之这些指令的执行先后ơ序不媄响最l结果,或者,更严格地_没有M一条指令依赖其他指令的q算l果)指o也可以像下面q样执行Q?/p>
指o1 | 取指?/td> | 取数?/td> | 计 ?/td> | 存数?/td> | |||
指o2 | 取指?/td> | 取数?/td> | 计 ?/td> | 存数?/td> | |||
指o3 | 取指?/td> | 取数?/td> | 计 ?/td> | 存数?/td> | |||
指o4 | 取指?/td> | 取数?/td> | 计 ?/td> | 存数?/td> |
q样Q原本需?6个时钟周期才能够完成的Q务就可以?个时钟周期内完成Q时间羃短了一半还多。如果考虑灰色的那些方?q些Ҏ可以?条指令以外的其他指o使用Q只要没有串行关pL冲突)Q那么,如此执行对于性能的提升将是相当可观的(此时QCPU的所有部仉得到了充分利??/p>
当然Q作为程序来_真正做到q样是相当理惛_的情c实际的E序中很隑ց到彻底的q行化。假设CPU能够支持4条指令同时执行,q且Q每条指令都是等周期长度?周期指oQ那么,E序需要保证同一时刻先后发射?条指令都能够q行执行Q相互之间没有关联,q通常是不太可能的?/p>
最新的Intel Pentium 4-XEON处理器,以及Intel Northwood Pentium 4都提供了一U被UCؓ线E?Hyper-Threading TM)的技术。该技术通过在一个处理器中封装两l执行机构来提高指oq行度,q依靠操作系l的调度来进一步提升系l的整体效率?/p>
׃U程机制是与操作pȝ密切相关的,因此Q在本文的这一部分中不可能做更为深入地探讨。在后箋的章节中Q我介lWin32、FreeBSD 5.x以及Linux中提供的内核U线E机?q三U操作系l都支持SMP及超U程技术,q且以线E作度单?在汇~语a中的使用Ҏ?/p>
关于U程的讨论就此打住,因ؓ它更多地依赖于操作系l,q且Q无论如何,操作pȝ的线E调度需要更大的开销q且Q到目前为止Q真正用支持超U程的CPUQƈ且用相应操作系l的人是非常的。因此,我们需要关心的实际上还是同一执行序列中的q发执行和指令封包。不q,令h遗憾的是Q实际上在这斚w~译器做的几乎是肯定要比人好Q因此,你需要做的只是开启相应的优化Q如果你的编译器不支持这LҎ,那么把它扔掉……据我所知,目前在Intelq_上指令封包方面做的最好的是Intel的C++~译器,l过Intel~译器编译的代码的性能令h惊异地高Q甚臛_AMD公司推出的兼容处理器上也是如此?/p>
从前一节的图中我们不难看出Q方?中,如果谁的动作慢,那么他就会成为性能的瓶颈。实际上QCPU也不会像我描q的那样四^八稳地运行,指o执行的不同阶D需要的旉(旉周期?是不同的Q因此,~短关键步骤(卻I造成瓉的那个步?是羃短执行时间的关键?/p>
臛_对于使用Intelpd的CPU来说Q取数据q个步骤需要消耗比较多的时间。此外,假如数据跨越了某U边???字节Q与CPU的字长有?Q则CPU需要启动两ơ甚x多次数的d存操作,q无疑对性能构成不利影响?/p>
Zq样的原因,我们可以得到下面的设计策略:
|
W一条规则比较简单。例如,需要求一l数据中的最大倹{最倹{^均数Q那么,最好是在一ơ@环中做完?/p>
“于是,q家伙又攒了一D代码”…?/p>
int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0}; int i; int avg, max, min; avg=max=min=a[0]; for(i=1; i<(sizeof(a)/sizeof(int)); i++){ avg /= i; |
Visual C++~译器把最开始一D赋D句翻译成了一D늮直可以说是匪h思的代码Q?/p>
; int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0}; mov edi, 2 ; 此时edi没有意义 mov esi, 3 ; esi也是Q时变量而已?/font> mov DWORD PTR _a$[esp+92], edi mov edx, 5 ; 黑名单加上edx mov eax, 7 ; eax也别?) mov DWORD PTR _a$[esp+132], edi mov ecx, 9 ; 差你了Qecx ; int i; ; int avg, max, min; ; avg=max=min=a[0]; mov edi, 1 ; edi摇n一变,现在它是min了?/font> mov DWORD PTR _a$[esp+96], esi mov DWORD PTR _a$[esp+104], edx mov DWORD PTR _a$[esp+112], eax mov DWORD PTR _a$[esp+136], esi mov DWORD PTR _a$[esp+144], edx mov DWORD PTR _a$[esp+152], eax mov DWORD PTR _a$[esp+88], 1 ; ~译器失? 此处edi应更?/font> mov DWORD PTR _a$[esp+100], 4 mov DWORD PTR _a$[esp+108], 6 mov DWORD PTR _a$[esp+116], 8 mov DWORD PTR _a$[esp+120], ecx mov DWORD PTR _a$[esp+124], 0 mov DWORD PTR _a$[esp+128], 1 mov DWORD PTR _a$[esp+140], 4 mov DWORD PTR _a$[esp+148], 6 mov DWORD PTR _a$[esp+156], 8 mov DWORD PTR _a$[esp+160], ecx mov DWORD PTR _a$[esp+164], 0 mov edx, edi ; edx是max?/font> mov eax, edi ; 期待已久的avg, 它被指定为eax |
q段代码是最优的吗?我个Z是。因为编译器完全可以在编译过E中直接把它们作为常量数据放入内存。此外,如果预先对a[0..9]10个元素赋|q利用串操作指o(rep movsdw)Q速度会更快一些?/p>
当然Q犯不上因ؓq些问题责怪编译器。要求编译器知道a[0..9]和[10..19]的内容一h免过于苛刅R我们看看下面的指oD:
; for(i=1; ... mov esi, edi for_loop: ; avg+=a[i]; mov ecx, DWORD PTR _a$[esp+esi*4+88] add eax, ecx ; if(max < a[i]) cmp edx, ecx jge SHORT elseif_min ; max = a[i]; mov edx, ecx ; else if(min > a[i]) jmp SHORT elseif_min elseif_min: cmp edi, ecx jle SHORT elseif_end ; min = a[i]; mov edi, ecx elseif_end: ; [for i=1]; i<20; i++){ inc esi cmp esi, 20 jl SHORT for_loop ; } ; avg /= i; cdq idiv esi | ; esi: i ; ecx: 暂存变量, =a[i] ; eax: avg ; edx: max ; 有趣的代?..q不是所有的时候都有用 ; 但是也别随便删除 ; edi: min ; i++ ; i?0比较 ; avg /= i |
上面的程序倒是没有什么惊Z处。唯一一个比较吓人的东西是那个jmp SHORT指oQ它是否有用取决于具体的问题。C/C++~译器有时会产生q样的代码,我过Ll错误地把所有的此类指o当作没用的代码而删掉,后来发现E序执行旉没有明显的变化。通过查阅文档才知道,q类指o实际上是“占位指令”,他们存在的意义在于占据那个地方,一来其他语句能够正确地按CPU觉得舒服的方式对齐,二来它可以占据CPU的某些周期,使得后箋的指令能够更好地q发执行Q避免冲H。另一个比较常见的、实现类似功能的指o是NOP?/p>
占位指o的去留主要是靠计时执行来判断。由于目前流行的操作pȝ基本上都是多d的,因此会对计时的精性有一定媄响。如果需要进行测试的话,需要保证以下几点:
|
对于l大多数E序来说Q计时测试是一个非帔R要的东西。我个h們于在q行优化后进行计时测试ƈ比较l果。目前,我基于经验进行的优化基本上都能够提高E序的执行性能Q但我还是不敢过于自信。优化确实会提高性能Q但人做的和~译器做的思\不同Q有Ӟ我们的确会做一些费力不讨好的事情?/p>
已经掌握了汇~语aQ没错,你现在已l可以去破译别h代码中的U密。然而,我们q有一仉要的东西没有提到Q那是自程序和中断。这两g东西是如此的重要Q以至于你的E序几乎不可能离开它们?/p>
在高U语a中我们经常要用到子程序。高U语a中,子程序是如此的神奇,我们能够定义和主E序Q或其他子程序一L变量名,而访问不同的变量Qƈ且,q不和程序的其他部分相冲H?/p>
然而遗憄是,q种“优劎쀝在汇编语言中是不存在的?/p>
汇编语言q不注重如何减轻E序员的负担Q相反,汇编语言依赖E序员的良好设计Q以期发挥CPU的最x能。汇~语a不是l构化的语言Q因此,它不提供直接的“局部变量”。如果需要“局部变量”,只能通过堆或栈自行实现?/p>
从这个意义上Ԍ汇编语言的子E序更像GWBASIC中的GOSUB调用的那些“子E序”。所有的“变量?本质上,属于q程的内存和寄存?为整个程序所׃nQ高U语a~译器所做的Q将局部变量放到堆或栈中的操作Q只能自行实现?/p>
参数的传递是靠寄存器和堆栈来完成的。高U语a中,子程?函数、过E,或类似概늚东西)依赖于堆和栈来传递?/p>
让我们来单地分析一下一般高U语a的子E序的执行过E。无论C、C++、BASIC、PascalQ这一部分基本都是一致的?/p>
|
毋庸|疑Q堆栈在整个q程中发挥着非常重要的作用。不q,本质上对子程序最重要的还是返回地址。如果子E序不知道这个地址Q那么系l将会崩溃?/p>
调用子程序的指o是CALLQ对应的q回指o是RET。此外,q有一l指令,即ENTER和LEAVEQ它们可以帮助进行堆栈的l护?/p>
CALL指o的参数是被调用子E序的地址。用宏汇编的时候,q通常是一个标受CALL和RETQ以及ENTER和LEAVE配对Q可以实现对于堆栈的自动操作Q而不需要程序员q行PUSH/POPQ以及蟩转的操作Q从而提高了效率?/p>
作ؓ一个编译器的实现实例,我用Visual C++~译了一DC++E序代码Q这D|~代码是使用特定的编译选项得到的结果,正常的RELEASE代码会比它精得多。包含源代码的部分反汇编l果如下(取自Visual C++调试器的q行l果Q我删除?0条int 3指oQƈ加上了一些注释,除此之外Q没有做M修改)Q?/p>
1: int myTransform(int nInput){ 00401000 push ebp ; 保护现场原先的EBP指针 00401001 mov ebp,esp 2: return (nInput*2 + 3) % 7; 00401003 mov eax,dword ptr [nInput] ; 取参?/font> 00401006 lea eax,[eax+eax+3] ; LEA比ADD加法更快 0040100A cdq ; DWORD->QWORD(扩展字长) 0040100B mov ecx,7 ; 除数 00401010 idiv eax,ecx ; ?/font> 00401012 mov eax,edx ; ?>eax(eax中保存返回? 3: } 00401014 pop ebp ; 恢复现场的ebp指针 00401015 ret ; q回 ; 此处删除10条int 3指oQ它们是方便调试用的Qƈ不媄响程序行为?/font> 4: 5: int main(int argc, char* argv[]) 6: { 00401020 push ebp ; 保护现场原先的EBP指针 00401021 mov ebp,esp 00401023 sub esp,10h ; 为取argc, argv修正堆栈指针?br />7:int a[3]; 8:for(register int i=0; i<3; i++){ 00401026 mov dword ptr [i],0 ; 0->i 0040102D jmp main+18h (00401038) ; 判断循环条g 0040102F mov eax,dword ptr [i] ; i->eax 00401032 add eax,1 ; eax ++ 00401035 mov dword ptr [i],eax ; eax->i 00401038 cmp dword ptr [i],3 ; 循环条g: i?比较 0040103C jge main+33h (00401053) ; 如果不符合条Ӟ则应l束循环 9: a[i] = myTransform(i); 0040103E mov ecx,dword ptr [i] ; i->ecx 00401041 push ecx ; ecx (i) -> 堆栈 00401042 call myTransform (00401000); 调用myTransform 00401047 add esp,4 ; esp+=4: 在堆中的新单?br /> ; 准备存放q回l果 0040104A mov edx,dword ptr [i] ; i->edx 0040104D mov dword ptr a[edx*4],eax ; eax(myTransformq回? ; 攑֛a[i] 10: } 00401051 jmp main+0Fh (0040102f) ; 计算i++Qƈl箋循环 11:return 0; 00401053 xor eax,eax ; q回值应该是0 12: } 00401055 mov esp,ebp ; 恢复堆栈指针 00401057 pop ebp ; 恢复BP 00401058 ret ; q回调用?C++q行环境) |
上述代码实做了一些无用功Q当Ӟq是因ؓ~译器没有对q段代码q行优化。让我们来关注一下这D代码中Q是如何调用子程序的。不考虑myTransformq个函数实际q行的数D,最让我感兴的是这一行代码:
00401003 mov eax,dword ptr [nInput] ; 取参?/font> |
q里nInput是一个简单单的变量符号吗QVisual C++的调试器昄不能告诉我们{案——它的设计目标是Z方便E序调试Q而不是向你揭C编译器生成的代码的实际构造。我用另外一个反汇编器得到的l果是:
00401003 mov eax,dword ptr [ebp+8] ; 取参?/font> |
q和我们在main()中看到的压栈序是完全吻合的(注意Q程序运行到q个地方的时候,EBP=ESP)。main()最l将i?b>?/b>通过堆栈传递给了myTransform()?/p>
剖析上面的程序只是说明了我前面所提到的子E序的一部分用法。对于汇~语a来说Q完全没有必要拘泥于l构化程序设计的框架(在今天,使用汇编的主要目的在于提高执行效率,而不是方便程序的l护和调试,因ؓ汇编不可能在q一点上做得比C++更好)。考虑下面的程序:
void myTransform1(int nCount, char* sBytes){ |
很容易看出,q两个函数包含了公共部分Q即
for(i=0; i<nCount; i++) sBytes[i] <<= 1; |
目前Q还没有~译器能够做到将q两部分合ƈ。依然沿用刚才的~译选项Q得到的反汇~结果是(同样地删除了int 3)Q?/p>
1:void myTransform1(int nCount, char* sBytes){ 00401000 push ebp 00401001 mov ebp,esp 00401003 push ecx 2:for(register int i=1; i<nCount; i++) 00401004 mov dword ptr [i],1 0040100B jmp myTransform1+16h (00401016) 0040100D mov eax,dword ptr [i] 00401010 add eax,1 00401013 mov dword ptr [i],eax 00401016 mov ecx,dword ptr [i] 00401019 cmp ecx,dword ptr [nCount] 0040101C jge myTransform1+3Dh (0040103d) 3: sBytes[i] += sBytes[i-1]; 0040101E mov edx,dword ptr [sBytes] 00401021 add edx,dword ptr [i] 00401024 movsx eax,byte ptr [edx-1] 00401028 mov ecx,dword ptr [sBytes] 0040102B add ecx,dword ptr [i] 0040102E movsx edx,byte ptr [ecx] 00401031 add edx,eax 00401033 mov eax,dword ptr [sBytes] 00401036 add eax,dword ptr [i] 00401039 mov byte ptr [eax],dl 0040103B jmp myTransform1+0Dh (0040100d) 4:for(i=0; i<nCount; i++) 0040103D mov dword ptr [i],0 00401044 jmp myTransform1+4Fh (0040104f) 00401046 mov ecx,dword ptr [i] 00401049 add ecx,1 0040104C mov dword ptr [i],ecx 0040104F mov edx,dword ptr [i] 00401052 cmp edx,dword ptr [nCount] 00401055 jge myTransform1+6Bh (0040106b) 5: sBytes[i] <<= 1; 00401057 mov eax,dword ptr [sBytes] 0040105A add eax,dword ptr [i] 0040105D mov cl,byte ptr [eax] 0040105F shl cl,1 00401061 mov edx,dword ptr [sBytes] 00401064 add edx,dword ptr [i] 00401067 mov byte ptr [edx],cl 00401069 jmp myTransform1+46h (00401046) 6: } 0040106B mov esp,ebp 0040106D pop ebp 0040106E ret 7: 8:void myTransform2(int nCount, char* sBytes){ 00401070 push ebp 00401071 mov ebp,esp 00401073 push ecx 9:for(register int i=0; i<nCount; i++) 00401074 mov dword ptr [i],0 0040107B jmp myTransform2+16h (00401086) 0040107D mov eax,dword ptr [i] 00401080 add eax,1 00401083 mov dword ptr [i],eax 00401086 mov ecx,dword ptr [i] 00401089 cmp ecx,dword ptr [nCount] 0040108C jge myTransform2+32h (004010a2) 10: sBytes[i] <<= 1; 0040108E mov edx,dword ptr [sBytes] 00401091 add edx,dword ptr [i] 00401094 mov al,byte ptr [edx] 00401096 shl al,1 00401098 mov ecx,dword ptr [sBytes] 0040109B add ecx,dword ptr [i] 0040109E mov byte ptr [ecx],al 004010A0 jmp myTransform2+0Dh (0040107d) 11: } 004010A2 mov esp,ebp 004010A4 pop ebp 004010A5 ret 12: 13:int main(int argc, char* argv[]) 14: { 004010B0 push ebp 004010B1 mov ebp,esp 004010B3 sub esp,0CCh 15:char a[200]; 16:for(register int i=0; i<200; i++)a[i]=i; 004010B9 mov dword ptr [i],0 004010C3 jmp main+24h (004010d4) 004010C5 mov eax,dword ptr [i] 004010CB add eax,1 004010CE mov dword ptr [i],eax 004010D4 cmp dword ptr [i],0C8h 004010DE jge main+45h (004010f5) 004010E0 mov ecx,dword ptr [i] 004010E6 mov dl,byte ptr [i] 004010EC mov byte ptr a[ecx],dl 004010F3 jmp main+15h (004010c5) 17: myTransform1(200, a); 004010F5 lea eax,[a] 004010FB push eax 004010FC push 0C8h 00401101 call myTransform1 (00401000) 00401106 add esp,8 18: myTransform2(200, a); 00401109 lea ecx,[a] 0040110F push ecx 00401110 push 0C8h 00401115 call myTransform2 (00401070) 0040111A add esp,8 19:return 0; 0040111D xor eax,eax 20: } 0040111F mov esp,ebp 00401121 pop ebp 00401122 ret |
非常明显圎ͼ0040103d-0040106e?0401074-004010a5q两D代码存在少量的差别Q但很显然只是对寄存器的偏好不同(~译器在优化Ӟq可能会减少堆栈操作Q从而提高性能Q但在这里只是用了不同的寄存器而已)
对代码进行合q的好处是非常明昄。新的操作系l往往使用式内存理。当内存不ӞE序往往会频J引发页面失?Page faults)Q从而引发操作系l从盘中读取一些东ѝ磁盘的速度赶不上内存的速度Q因此,q一行ؓ导致性能的下降。通过合ƈ一部分代码Q可以减程序的大小Q这意味着减少面失效的可能性,从而Y件的性能会有所提高?/p>
当然Q这样做的代价也不算低——你的程序将变得难懂Qƈ且难于维护。因此,再进行这L优化之前Q一定要注意Q?/p>
|
中断应该说是一个陈旧的话题。在新的pȝ中,它的作用正在逐渐被削弱,而变成操作系l专用的东西。ƈ不是所有的计算机系l都提供中断Q然而在x86pȝ中,它的作用是不可替代的?/p>
中断实际上是一cȝD的子程序。它通常ql调用,以响应突发事件?/p>
例如Q进行磁盘操作时Qؓ了提高性能Q可能会使用DMA方式q行操作。CPU向DMA控制器发出指令,要求外设和内存直接交换数据,而不通过CPU。然后,CPU转去q行起他的操作;当数据交换结束时QCPU可能需要进行一些后l操作,但此时它如何才能知道DMA已经完成了操作呢Q?/p>
很显然不是依靠CPUL询状态——这样DMA的优势就不明显了。ؓ了尽可能地利用DMA的优势,在完成DMA操作的时候,DMA会告诉CPU“这事儿我办完了”,然后CPU会根据需要进行处理?/p>
q种处理可能很复杂,需要若q条指o来完成。子E序是一个不错的LQ不q,CALL指o需要指定地址Q让外设CPU执行一条CALL指o也违背了CPU作ؓ核心控制单元的设计初街考虑到这些,在x86pȝ中引入了中断向量的概c?/p>
中断向量表是保存在系l数据区(实模式下Q是0:0开始的一D区?的一l指针。这l指针指向每一个中断服务程序的地址。整个中断向量表的结构是一个线性表?/p>
每一个中断服务有自己的唯一的编P我们通常UCZ断号。每一个中断号对应中断向量表中的一,也就是一个中断向量。外讑CPU发出中断hQ而CPU自己根据当前的E序状态决定是否中断当前程序ƈ调用相应的中断服务?/p>
不难Ҏ造成中断的原因将中断分ؓ两类Q硬件中断和软g中断。硬件中断有很多分类ҎQ如Ҏ是否可以屏蔽分类、根据优先高低分类Q等{。考虑到这些分cdƈ不一定科学,q且对于我们介绍中断的用没有太大的帮助Q因此我q不打算太详l地介绍?在本教程的高U篇中,关于加密解密的部分会提到某些g中断的利用,但那是后??/p>
在设计操作系l时Q中断向量的概念曄带来q很大的便利。操作系l随时可能升U,q样Q通过CALL来调用操作系l的服务(如果说每个程序都包含对于文gpȝ、进E表q些应该由操作系l管理的数据的直接操作的话,不仅会造成E序的臃肿,而且不利于系l的安全)显得不太合适了——没知道Q以后的操作pȝ的服务程序入口点会不会是那儿。Y件中断的存在册个问题提供了方便?/p>
对于一台包含了BIOS的计机来说Q启动的时候系l已l提供了一部分服务Q例如显C服务。无Z的BIOS、显C卡有多么的“个性”,只要他们和IBM PC兼容Q那么此时你肯定可以通过调用16(10h)号中断来使用昄服务。调用中断的指o?/p>
|
q将引发CPU去调用一个中断。CPU保存当前的E序状态字Q清除Trap和Interrupt两个标志Q将卛_执行的指令地址压入堆栈Qƈ调用中断服务(Ҏ中断向量??/p>
~写中断服务E序不是一件容易的事情。很多时候,中断服务E序必须写成可重入代?/b>(或纯代码Qpure code)。所谓可重入代码是指Q程序的q行q程中可以被打断Qƈ由开始处再次执行Qƈ且在合理的范围内(多次重入Q而不造成堆栈溢出{其他问?Q程序可以在被打断处l箋执行Qƈ且执行结果不受媄响?/p>
׃在多U程环境中等其他一些地方进行程序设计时也需要考虑q个因素Q因此这里着重讲一下可重入代码的编写?/p>
可重入代码最主要的要求就是,E序不应使用某个指定的内存地址的内?对于高语言来说Q这通常是全局变量Q或对象的成?。如果可能的话,应用寄存器Q或其他方式来解冟뀂如果不能做到这一点,则必d开始、结束的时候分别禁止和启用中断Qƈ且,q行旉不能太长?/p>
下面用C语言分别举一个可重入函数Q和两个非可重入函数的例?? q些例子应该是在某本多线E或操作pȝ的书上看到的Q遗憄是我想不h是哪本书了,在这里先感谢那位作者提供的范例)Q?/p>
可重入函敎ͼ
void strcpy(char* lpszDest, char* lpszSrc){ while(*dest++=*src++); *dest=0; } |
非可重入函数
char cTemp; // 全局变量 void SwapChar(char* lpcX, char* lpcY){ cTemp = *lpcX; *lpcX = *lpcY; lpcY = cTemp; // 引用了全局变量Q在分n内存的多个线E中可能造成问题 } |
非可重入函数
void SwapChar2(char* lpcX, char* lpcY){ static char cTemp; // 静态变?/font> cTemp = *lpcX; *lpcX = *lpcY; lpcY = cTemp; // 引用了静态变量,在分享内存的多个U程中可能造成问题 } |
中断利用的是pȝ的栈。栈操作是可重入?因ؓ栈可以保证“先q后出?Q因此,我们q不需要考虑栈操作的重入问题。用宏汇编器写出可重入的汇~代码需要注意一些问题。简单地_q脆不要用标号作为变量是一个不错的L?/p>
使用高语言~写可重入程序相Ҏ讲轻松一些。把持住不访问那些全局(或当前对象的)变量Q不使用静态局部变量,坚持只适用局部变量,写出的程序就是可重入的?/p>
书归正传Q调用Y件中断时Q通常都是通过寄存器传q、传出参数。这意味着你的int指o周围也许会存在一些“帮手”,比如下面的代码:
mov ax, 4c00h int 21h |
是通过调用DOS中断服务q回父进E,q带回错误反馈码0。其中,ax中的数据4c00h是传递给DOS中断服务的参数?/p>
到这里,x86汇编语言的基部分基本上讲完了,《简明x86汇编语言教程》的初——汇~语a基础也就到此告一D落。当Ӟ目前为止Q我只是蜻蜓Ҏ一般提C一些学习x86汇编语言中我认ؓ需要注意的重要概念。许多东西,包括全部汇编语句的时序特?指o执行周期敎ͼ以及指o周期中各个阶D늚节拍数等)、功能、参数等{,限于个h水^和篇q我都没有作详细介绍。如果您对这些内Ҏ兴趣Q请参考Intel和AMD两大CPU供应商网站上提供的开发h员参考?/p>
在以后的明x86汇编语言教程中和高中Q我着重介l汇~语a的调试技术、优化,以及一些具体的应用技巧,包括反跟t、反反跟t、加密解密、病毒与反病毒等{?/p>
我们前面已经提到Q内存可以和寄存器交换数据,也可以被赋予立即数。问题是Q如果我们需要把内存的某部分内容复制到另一个地址Q又怎么做呢Q?/p>
设想DS:SI处的q箋512字节内容复制到ES:DIQ先不考虑可能的重叠)。也怼有h写出q样的代码:
NextByte: | mov cx,512 mov al,ds:[si] mov es:[di],al inc si inc di loop NextByte | ; 循环ơ数 |
我不喜欢上面的代码。它的确能达C用,但是Q效率不好。如果你是在做优化,那么写出q样的代码意味着赔了夫h又折c?/p>
Intel的CPU的强Ҏ串操作。所谓串操作是由CPUd成某一数量的、重复的内存操作。需要说明的是,我们常用的KMP法Q用于匹配字W串中的模式Q的改进——Boyer法Q由于没有利用串操作Q因此在Intel的CPU上的效率q最优。好的编译器往往可以利用Intel CPU的这一Ҏ优化代码,然而,q所有的时候它都能产生最好的代码?/p>
某些指o可以加上REP前缀Qrepeat, 反复之意Q,q些指o通常被叫做串操作指o?/p>
举例来说QSTOSD指oEAX的内容保存到ES:DIQ同时在DI上加或减四。类似的QSTOSB和STOSW分别?字节?字的上述操作Q在DI上加或减的数???/p>
计算a通常是不允许二义性的。ؓ什么我要说“加或减”呢Q没错,孤立地看STOS?指oQƈ不能知道到底是加q是减,因ؓq取决于“方向”标?DF, Direction Flag)。如果DF被复位,则加Q反之则减?/p>
|位、复位的指o分别是STD和CLD?/p>
当然QREP只是几种可用前缀之一。常用的q包括REPNEQ这个前~通常被用来比较两个串Q或搜烦某个特定字符Q字、双字)。REPZ、REPE、REPNZ也是非常常用的指令前~Q分别代表ZF(Zero Flag)在不同状态时重复执行?/p>
下面说三个可以复制数据的指oQ?/p>
助记W?/font> | 意义 |
movsb | DS:SI的一字节复制到ES:DIQ之后SI++、DI++ |
movsw | DS:SI的一字节复制到ES:DIQ之后SI+=2、DI+=2 |
movsd | DS:SI的一字节复制到ES:DIQ之后SI+=4、DI+=4 |
于是上面的程序改写ؓ
cld mov cx, 128 rep movsd | ; 复位DF ; 512/4 = 128Q共128个双?br />; 行动Q?/font> |
W一句cld很多时候是多余的,因ؓ实际写程序时Q很会出现|DF的情c不q在正式军_删掉它之前,你仔l地调试自己的程序,q确认每一个能够走到这里的路径中都不会DF|位?/p>
错误Q非预期的)的DF是危险的。它很可能断送掉你的E序Q因直接造成~冲区溢?/b>问题?/p>
什么是~冲区溢出呢Q缓冲区溢出分ؓ两类Q一cL写入~冲Z外的内容Q一cLd~冲Z外的内容。后一U往往更隐蔽,但随便哪一个都有可能断送掉你的E序?/p>
~冲区溢出对于一个网l服务来说很可能更加危险。怀有恶意的用户能够利用它执行自己希望的指o。服务通常拥有更高的特权,而这很可能会造成Ҏ提升Q即使不能提升攻击者拥有的ҎQ他也可以利用这U问题服务崩溃Q从而Ş成一ơ成功的DoSQ拒l服务)d。每qCERT的安全公告中Q都?成左右的问题是由于缓冲区溢出造成的?/p>
在用汇~语aQ或C语言~写E序Ӟ很容易在无意中引入缓冲区溢出。然而ƈ不是所有的语言都会引入~冲区溢出问题,Java和C#Q由于没有指针,q且~冲区采取动态分配的方式Q有效地消除了造成~冲区溢出的土壤?/p>
汇编语言中,׃REP*前缀都用CX作ؓ计数器,因此情况会好一些(当然Q有时也会更p糕Q因为由于CX的限Ӟ很可能原本可能改变E序行ؓ的缓冲区溢出的范围羃,从而更为隐蔽)。避免缓冲区溢出的一个主要方法就是仔l检查,q包括两斚wQ设|合理的~冲区大,和根据大编写程序。除此之外,非常重要的一点就是,在汇~语aq个U别写程序,你肯定希望去掉所有的无用指oQ然而再L之前Q一定要q行严格的测试;更进一步,如果能加上注释,q过善用宏来做调试模式检查,往往能够辑ֈ更好的效果?/p>
正如3.2节提到到的那P保护模式中,你可以?2位的U性地址Q这意味着直接讉K4GB的内存。由于这个原因,选择器不用像实模式中D寄存器那样频繁C攏V顺便提一句,q䆾教程中所说的保护模式指的?86以上的保护模式,或者,Microsoft通常UCؓ“增强模式”的那种?/p>
在ؓ选择器装入数值的时候一定要非常心。错误的数值往往会导致无效页面错?在Windows中经常出?)。同Ӟ也不要忘C的地址?2位的Q这也是保护模式的主要优势之一?/p>
现在假设存在一个描q符描述从物理的0:0开始的全部内存Qƈ已经加蝲qDS(数据选择?Q则我们可以通过下面的程序来操作VGA的VRAMQ?/p>
mov edi,0a0000h mov byte ptr [edi],0fh | ; VGA昑֭的偏U量 ; 第一字节改ؓ0fh |
很明显,q比实模式下的程?/p>
mov ax,0a000h mov ds,ax mov di,0 mov [di],0fh | ; AX -> VGAD地址 ; AXD入DS ; DI清零 ; 修改W一字节 |
看上去要舒服一些?/p>
到目前ؓ止,您已l了解了基本的寄存器以及内存的操作知识。事实上Q您现在已经可以写出很多的底层数据处理程序了?/p>
下面我来说说堆栈。堆栈实在不是一个让人陌生的数据l构Q它是一?span class="tip" id="oFILO" title="">先进后出(FILO)Q?b>先进后出(FILO)是这样一个概念:最?/b>放进表中的数据在取出?b>最?/b>出来?b>先进后出(FILO)?b>先进先出(FIFO, 和先q后出的规则相反)Q以?b>随机存取是最主要的三U存储器讉K方式。对于堆栈而言Q最后放入的数据在取出时最先出现。对于子E序调用Q特别是递归调用来说Q这是一个非常有用的Ҏ。)的线性表Q能够帮助你完成很多很好的工作?/p>
一个铁杆的汇编语言E序员有时会发现pȝ提供的寄存器不够。很昄Q你可以使用普通的内存操作来完成这个工作,像C/C++中所做的那样?没错Q没错,可是Q如果数据段Q数据选择器)以及偏移量发生变化怎么办?更进一步,如果希望保存某些在这U操作中可能受到影响的寄存器的时候怎么办?实Q你可以把他们也存到自己的那片内存中Q自己实现堆栈?/p>
太麻烦了…?/p>
既然pȝ提供了堆栈,q且性能比自己写一份更好,那么Z么不直接加以利用呢?
pȝ堆栈不仅仅是一D内存。由于CPU对它实施理Q因此你不需要考虑堆栈指针的修正问题。可以把寄存器内容,甚至一个立x直接攑ֈ堆栈里,q在需要的时候将其取出。同Ӟpȝq不要求取出的数据仍然回到原来的位置?/p>
除了昑ּ地操作堆栈(使用PUSH和POP指oQ之外,很多指o也需要用堆栈,如INT、CALL、LEAVE、RET、RETF、IRET{等。配对用上q指令ƈ不会造成什么问题,然而,如果你打用LEAVE、RET、RETF、IRETq样的指令实现蟩?比JMP更ؓȝQ然而有Ӟ例如在加密Y件中Q或者需要修改调用者状态时Q这是必要的)的话Q那么我的徏议是Q先搞清楚它们做的到底是什么,q且Q精地了解自己要做什么?/p>
正如前面所说的Q有两个昑ּ地操作堆栈的指oQ?/p>
助记W?/font> | 功能 |
PUSH | 操作数存入堆栈Q同时修正堆栈指?/td> |
POP | 栈内容取出ƈ存到目的操作CQ同时修正堆栈指?/td> |
我们现在来看看堆栈的操作?/p>
执行之前
执行代码
mov ax,1234h mov bx,10 push ax push bx |
之后Q堆栈的状态ؓ
之后Q再执行
pop dx pop cx |
堆栈的状态成?/p>
当然Qdx、cx中的内容分别是000ah?234h?/p>
注意Q最后这张图中,我没有抹?234h?00ahQ因为POP指oq不从内存中抹去数倹{不q尽如此,我个Z焉常反对l用这两个敎ͼ你可以通过修改SP来再ơPOP它们Q,然而这很容易导致错误?/p>
一定要保证堆栈D|_的空间来执行中断Q以及其他一些隐式的堆栈操作。仅仅统计PUSH的数量ƈ据此计算堆栈所需的大很可能造成问题?/p>
CALL指o返回地址攑ֈ堆栈中。绝大多数C/C++~译器提供了“堆栈检查”这个编译选项Q其作用在于保证CE序D中没有忘记对堆栈中多余的数据进行清理,从而保证返回地址有效?/p>
本章中介l了内存的操作的一些入门知识。限于篇q,我不打算展开l讲指oQ如cmps*Qlods*Qstos*Q等{。这些指令的用法和前面介l的movs*基本一P只是有不同的作用而已?/p>