优化是一仉帔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>
在前面的章节中,我们已经了解了寄存器的基本用方法。而正如结提到的那样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
0001:1234 = 0124:0004 = 0120:0044 = 0100:0244
作ؓ负面影响之一Q在D上?相当于在偏移量上?6Q而不是一个“全新”的Dc反之,在偏U量上加16也和在段上加1{h。某些时候,据此认ؓD늚“粒度”是16字节?/p>
l习?br />试一下将下面的地址转化?0bit的地址Q?/p>
2EA8:D678 26CF:8D5F 453A:CFAD 2933:31A6 5924:DCCF 694E:175A 2B3C:D218 728F:6578 68E1:A7DC 57EC:AEEA |
E高一些的要求是,写一个程序将DؓAX、偏U量为BX的地址转换?0bit的地址Qƈ保存于EAX中?/p>
[上面习题的答?/span>]
地址转换
2EA8:D678 -> 物理?3C0F8
694E:175A -> 物理?6AC4A
26CF:8D5F -> 物理?2FA4F
2B3C:D218 -> 物理?385E8
453A:CFAD -> 物理?5235D
728F:6578 -> 物理?78E68
2933:31A6 -> 物理?2C4D6
68E1:A7DC -> 物理?735FC<
~程 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>
shl eax,4
add eax,bx
注意 ~程问题{案q不唯一Q但l出的这份参考答案应该已l是“优化到头”了?/font>
我们现在可以写一个真正的E序了?/p>;;; 应该得到一?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 amov eax, 079f2ch
ret3.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]
在前一节中的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" title="">q里?/font>
数字计算机内部只支持二进制数Q因栯机只需要表CZU?某些情况?U,q一内容过了这份教E的范围Q如果您感兴,可以参考数字逻辑电\的相关书c?状? 对于电\而言Q这表现为高、低电^Q或者开、关Q分别非常明显,因而工作比较稳定;另一斚wQ由于只有两U状态,设计h也比较简单。这P使用二进制意味着低成本、稳定,多数情况下,q也意味着快速?
与十q制cMQ我们可以用下面的式子来换算Z个Q意Ş如am-1……a3a2a1a0 的m位rq制数对应的数值nQ?br />
E序设计中常用十六进制和八进制数字代替二q制敎ͼ其原因在于,16??的整ơ方q,q样Q一位十六或八进制数可以表示整数个二q制位。十六进制中Q?使用字母A、B、C、D、E、F表示10-15Q而十六进制或八进制数制表C的的数字比二进制数更短一些?br />
汇编语言中的整数帔R表示
|
需要说明的是,q些Ҏ是针对宏汇编器(例如QMASM、TASM、NASMQ说的,调试器默认用十六进制表C整敎ͼq且不需要特别的声明Q例如,在调试器中直接用FFFF表示十进制的65535Q用10表示十进制的16Q?/p>
现在我们来写一段汇编E序Q修改EAX、EBX、ECX、EDX的数倹{?/p>
我们假定E序执行之前Q寄存器中的数值是?Q?/p>
? | X | ||
H | L | ||
EAX | 0000 | 00 | 00 |
EBX | 0000 | 00 | 00 |
ECX | 0000 | 00 | 00 |
EDX | 0000 | 00 | 00 |
正如前面提到的,EAX的高16bit是没有办法直接访问的Q而AX对应它的?6bitQAH、AL分别对应AX的高、低8bit?/p>
mov eax, 012345678h mov ebx, 0abcdeffeh mov ecx, 1 mov edx, 2 | ; ?12345678h送入eax ; ?abcdeffeh送入ebx ; ?00000001h送入ecx ; ?00000002h送入edx |
则执行上q程序段之后Q寄存器的内容变为:
? | X | ||
H | L | ||
EAX | 1234 | 56 | 78 |
EBX | abcd | ef | fe |
ECX | 0000 | 00 | 01 |
EDX | 0000 | 00 | 02 |
那么Q你已经了解了movq个指oQmov是move的羃写)的一U用法。它可以数送到寄存器中。我们来看看下面的代码:
mov eax, ebx mov ecx, edx | ; ebx内容送入eax ; edx内容送入ecx |
则寄存器内容变ؓQ?/p>
? | X | ||
H | L | ||
EAX | abcd | ef | fe |
EBX | abcd | ef | fe |
ECX | 0000 | 00 | 02 |
EDX | 0000 | 00 | 02 |
我们可以看到Q“move”之后,数据依然保存在原来的寄存器中。不妨把mov指o理解为“送入”,或“装入”?/p>
l习?/b>
把寄存器恢复成都为全0的状态,然后执行下面的代码:
mov eax, 0a1234h mov bx, ax mov ah, bl mov al, bh | ; ?a1234h送入eax ; ax的内定w入bx ; bl内容送入ah ; bh内容送入al |
思考:此时QEAX的内容将是多?[{案]
EAX的内容ؓ000A3412h.
下面我们介l一些指令。在介绍指o之前Q我们约定:
|
在寄存器中蝲入另一寄存器,或立x的|
mov reg32, (reg32 | imm8 | imm16 | imm32) |
例如Qmov eax, 010h表示Q在eax中蝲?0000010h。需要注意的是,如果你希望在寄存器中装入0Q则有一U更快的ҎQ在后面我们提到?/p>
交换寄存器的内容Q?/p>
xchg reg32, reg32 xchg reg16, reg16 xchg reg8, reg8 |
例如Qxchg ebx, ecxQ则ebx与ecx的数值将被交换。由于系l提供了q个指oQ因此,采用其他Ҏ交换Ӟ速度会较慢Qƈ需要占用更多的存储I间Q编E时要避免这U情况,卻I量利用pȝ提供的指令,因ؓ多数情况下,q意味着更小、更快的代码Q同时也杜绝了错误(如果说Intel的CPU在交换寄存器内容的时候也会出错,那么它就不用卖CPU了。而对于你来说Q检查一行代码的正确性也昄比检查更多代码的正确性要ҎQ刚才的习题的程序用下面的代码将更有效:
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) |
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重写呢?
使用汇编语言重写代码之前需要确认的几g事情
|
曄在一份杂志上看到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>
中央处理?CPU)在微机系l处于“领导核心”的C。汇~语a被编译成机器语言之后Q将由处理器来执行。那么,首先让我们来了解一下处理器的主要作用,q将帮助你更好地N它?/p>
典型的处理器的主要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> EAX EBX ECX EDX
下面介绍通用寄存器及其习惯用法。顾名思义Q通用寄存器是那些你可以根据自q意愿使用的寄存器Q修改他们的值通常不会对计机的运行造成很大的媄响。通用寄存器最多的用途是计算?/p>
32-bit?/p>通用寄存器。相对其他寄存器Q在q行q算斚w比较常用。在保护模式中,也可以作为内存偏UL针(此时QDS作ؓD?寄存器或选择器)
32-bit?/p>通用寄存器。通常作ؓ内存偏移指针使用Q相对于EAX、ECX、EDXQ,DS是默认的D寄存器或选择器。在保护模式中,同样可以赯个作用?/td>
32-bit?/p>通用寄存器。通常用于特定指o的计数。在保护模式中,也可以作为内存偏UL针(此时QDS作ؓ 寄存器或D选择器)?/td>
32-bit?/p>通用寄存器。在某些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>
本节中部份表格来自David Jurgens的HelpPC 2.10快速参考手册。在此}表谢意?/p>
我不惛_大或者贬低汇~语a。但我想_汇编语言改变?0世纪的历双Ӏ与前辈相比Q我们这一代编Eh员够的q福Q因为我们有各式各样的编E语aQ我们可以操作键盘、坐在显C器面前Q甚至用鼠标、语韌别。我们可以用键盘、鼠标来N“个机”,而不是和一h׃n一C用笨重的l电器、开兛_操作的巨型机。相比之下,我们的前辈不得不使用机器语言~写E序Q他们甚x有最单的汇编E序来把助记W翻译成机器语言Q而我们可以从上千U计机语言中选择我们喜欢的一U,而汇~,虽然不是一U“常用”的h“快速原型开发”能力的语言Q却也是我们可以选择的语a中的一U?/p>
每种计算机都有自q汇编语言——没必要指望汇编语言的可UL性,选择汇编Q意味着选择性能而不是可UL或便于调试。这份文档中讲述的是x86汇编语言Q此后的“汇~语a”一词,如果不明C则表示ia32上的x86汇编语言?/p>
汇编语言是一U易学,却很隄通的语言。回惛_q_我从初学汇编到写?span class="tip" id="oFirstASM" title="">W一个可q行的程?font color="#808080">Q?/font>当时我学qBASIC, Fortran和PascalQ写的是一个对一个包?00?2bit整数的数l进行快速排序,q且输出出来的小E序?strong>Q?/strong>Q只用了不到4个小Ӟ然而直C天,我仍然不敢说自己_N它。编写快速、高效、ƈ且能够让处理器“很舒服地执行”的E序是一件很困难的事情,如果利用业余旉学习Q通常需?-3q的旉才能做到。这份教材ƈ不期待能够教l你大量的汇~语a技巧。对于读者来_x86汇编语言"在q里"。然而,不要僵化地局限于q䆾教材讲述的内容,因ؓ它只能告诉你汇编语言是“这样一回事”。学好汇~语aQ更多的要靠一个h的创造力于悟性,我可以告诉你我所知道的技巧,但肯定这是不够的。一位对我的~程生产生q重要媄响的人曾l对我说q这么一句话Q?/p>
写汇~语aE序不是汇编语言最隄部分Q创新才是?/p> |
我想Q愿意看q䆾文档的h恐怕不会问我“ؓ什么要学习汇编语言”这L问题Q不q,我还是想说几句:首先Q汇~语a非常有用Q我个hd把它作ؓC语言的先修课E,因ؓ通过学习汇编语言Q你可以了解到如何有效地设计数据l构Q让计算机处理得更快Qƈ使用更少的存储空_同时Q学习汇~语a可以让你熟悉计算机内部运行机Ӟq且Q有效地提高调试能力。就我个人的l验而言Q调试一个非l构化的E序的困隄度,要比调试一个结构化的程序的隑ֺ高很多,因ؓ“结构化”是以牺牲运行效率来提高可读性与可调试性,q对于完成一般Y件工E的~码阶段是非常必要的。然而,在一些地方,比如Q硬仉动程序、操作系l底层,或者程序中l常需要执行的代码Q结构化E序设计的这些优Ҏ时就会被它的低效率所抹煞。另外,如果你想真正地控制自qE序Q只知道源代码的调试是q远不够的?/p>
躁的h喜欢_用C++写程序够了Q甚臌Q他不仅仅掌握C++Q而且_NSTL、MFC。我不赞成这个观点,掌握上面的那些是每一个编Eh员都应该做到的,然而C++只是我们"常用"的一U语aQ它不是~程的全部。低层次的开发者喜Ƣ说Q嘿QC++是多么的强大Q它可以做Q何事情——这不是事实。便于维护、调试,q些实是我们的q求目标Q但是,写程序不能仅仅追求这个目标,因ؓ我们最l的目的是满计需求,而不是个人非理性的理想?/p>
q䆾教材适合已经学习q某U结构化E序设计语言的读者。其内容Z我在1995q给别h讲述汇编语言时所写的讲义。当Ӟ如大家所希望的,它包含了最新的处理器所支持的特性,以及相应的内宏V我假定读者已l知道了E序设计的一些基本概念,因ؓ没有q些是无法理解汇~语aE序设计的;此外Q我希望读者已l有了比较良好的E序设计基础Q因为如果你~Z对于l构化程序设计的认识Q编写汇~语aE序很可能很快就破坏了你的结构化~程习惯Q大大降低程序的可读性、可l护性,最l让你的E序陷于不得不废弃的代码堆之中?/p>
基本上,q䆾文档撰写的目标是可能地便于自学。不q,它对你也有一些要求,管不是很高Q但我还是强调一下?/p>
|
您~程愉快Q?/p>
先说一点和实际~程关系不太大的东西。当Ӟ如果你迫切的想看到更实质的内容,完全可以先蟩q这一章?/p>
那么Q我惛_能有一个问题对于初学汇~的人来说非帔R要,那就是:
汇编语言到底是什么?
汇编语言是一U最接近计算机核心的~码语言。不同于M高语言Q汇~语a几乎可以完全和机器语a一一对应。不错,我们可以用机器语a写程序,但现在除了没有汇~程序的那些电脑之外Q直接用机器语言写超q?000条以上指令的人大概只能算作那些被我们成ؓ“圣人”的牺牲者一cM。毕竟,记忆一些短的助记W、由机器去考虑那些琐碎的配位过E和查错误,比记忆大量的随计机而改变的十六q制代码、可能弄错而没有Q何提C强的多。熟l的汇编语言~码员甚臛_以直接从十六q制代码中读出汇~语a的大致意思。当Ӟ我们有更好的工具——汇~器和反汇编器?/p>
单地_汇编语言是机器语言的一U?span class="tip" id="oDiffOPCASM" title="">可以被hL的Ş?font style="BACKGROUND-COLOR: #d3d3d3">Q?/font>实际上用汇编器写出的机器码与在调试器中用它附带的汇编E序写出的机器码q是有一些细微差别的Q前者更大,然而却可能更高效,因ؓ汇编?strong>能够代码放|到适合处理器的地方Q?/strong>Q只不过它更Ҏ记忆。至于宏汇编Q则是包含了宏支持的汇编语言Q这可以让你~程的时候更专注于程序本w,而不是忙于计和重写代码?/p>
汇编语言除了机器语言之外最接近计算机硬件的~程语言。由于它如此的接q计机gQ因此,它可以最大限度地发挥计算机硬件的性能。用汇编语言~写的程序的速度通常要比高语言和C/C++快很?-几倍,几十倍,甚至成百上千倍。当Ӟ解释语言Q如解释型LISPQ没有采用JIT技术的Java虚机中运行的Java{等Q其E序速度?span class="tip" id="oOptimizeASM" title="">无法Q?/font>q句话假?strong>两个E序q行了同{程度的优化Q一个写的不好的汇编E序和一个写的很好的CE序相比Q汇~程?/font>不一?/strong>更快?/font>Q?/strong>与汇~语aE序同日而语 ?/p>
永远不要忽视汇编语言的高速。实际的应用pȝ中,我们往往会用汇编d重写某些l常调用的部分以期获得更高的性能。应用汇~也怸能提高你的程序的E_性,但至,如果你非常小心的话,它也不会降低E_性;与此同时Q它可以大大地提高程序的q行速度。我强烈所有的软g产品在最后Release之前Ҏ个代码进行ProfileQƈ适当地用汇编取代部分高语言代码。至,汇编语言的知识可以告诉你一些有用的东西Q比如,你有多少个寄存器可以用。有Ӟ手工的优化比~译器的优化更ؓ有效Q而且Q你可以完全控制E序的实际行为?/p>
我想我在|嗦了。MQ在我们l束q一章之前,我想_不要在优化的时候把希望完全寄托在编译器上——现实一些,再好的编译器也不可能L产生最优的代码?/p>
]]>