[/ Learning OpCode Document ] [doc 学习OpCode] [/ 2004-01-09 ] [def :) [$theme/icon_smile.gif]] [def ;) [$theme/icon_wink.gif]] [def Spirit [@http://spirit.sourceforge.net Spirit]] [page 历史记录] [:['"Rome was not built in a day."]] 正如这句著名的英国谚语所言——罗马不是一天建成的。这份教程也不可能是在一朝一夕之间写好的。在这里,我记录下更新的历史,以便今后回忆: * 2004-02-25 完成 [@5._ModRM_-_Part_I.htm 第五章] * 2004-02-02 完成 [@4._Prefixes_Part_II.htm 第四章] * 2004-01-31 完成 [@3._Prefixes_Part_I.htm 第三章] * 2004-01-17 完成 [@2._从哪里开始,到哪里结束.htm 第二章] * 2004-01-15 对 [@1._什么是OpCode?.htm 第一章] 进行了扩充,增加了约一倍的内容。 * 2004-01-12 完成 [@1._什么是OpCode?.htm 第一章] * 2003-12-24 完成 [@前言.htm 前言] [page 前言] [:['“不管现在流行什么语言,你都可以肯定十年二十年之后它不再风光。我总是在自己的书中写些不时髦的东西,但这些东西却值得后代子孙记取。”[br][br]] -- Donald E. Knuth] [h2 为什么要学习OpCode?] 随着学习的深入和知识的积累,各位编程爱好者都一定不再满足于语言和表层知识的学习,而开始对系统的一些底层知识感到好奇,进而逐渐往深层次探究。正当我们为微软一轮轮的技术革新而强迫自己努力学习的时候,我们可曾花时间去考虑一些深层次的东西? 在编译器后端设计、反汇编器、病毒、破解等底层领域中,处处可见OpCode的影子,奇怪的是,很少有人真正深入地去研究这个领域的东西。一种技术的发展和流行,关键是要有好的书籍/教程作为指引,目前比较规范的中文的OpCode教程好像还没有,于是很多人在这个问题上,都只是处于摸索阶段,没有抓住问题的核心。在这里,希望能与大家分享一些我的经验和教训。:) [h2 版权声明] 本教程是完全免费和自由的,你可以自由拷贝,但请注明转载的出处。内容的大部分是翻译自国外的一些OpCode教程和资料,以及Intel的<>,少部分是我的经验总结。参考资料将在下面给出。 [h2 准备工作] 在开始之前,请先做好以下的准备: * 下载:<<[@http://www.intel.com/design/Pentium4/manuals/245471.htm IA-32 Intel Architecture Software Developer's Manual Volume 2: Instruction Set Reference]>> * 下载:<<[@http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/24594.pdf AMD64 Architecture Programmer's Manual Volume 3: General-Purpose and System Instructions]>> [h2 参考资料] * 最权威的 <<[@http://www.intel.com/design/Pentium4/manuals/245471.htm IA-32 Intel Architecture Software Developer's Manual Volume 2: Instruction Set Reference]>> * 同样权威的 <<[@http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/24594.pdf AMD64 Architecture Programmer's Manual Volume 3: General-Purpose and System Instructions]>> * 俄罗斯大哥The Svin[br]的OpCode系列教程使我入了门。原文可以在[@http://board.win32asmcommunity.net board.win32asmcommunity.net]通过搜索关键字“[*OpCode]”找到。 * [@http://aod.anticrack.de AoD]也是一份英文OpCode教程。 [h2 关于作者] 罗聪,研究兴趣为算法和数据结构、编译原理等,可以通过电子邮件地址 lcother@163.net 与我联系。个人主页是:[@http://www.luocong.com 老罗的缤纷天地]。 [page 1. 什么是OpCode?] [:['Things have changed in the past two decades.[br][br]] -- Bill Gates(1995)] [h2 什么是OpCode?] 不管计算机技术的发展如何日新月异,其最基本的东西是不会突然改变的。OpCode就是这其中的一样东西——因此,Bill Gates的这句话用在这里并不太合适。 在开始回答什么是OpCode之前,请让我先来提几个小问题。;) # 计算机只认识0和1吗? # 如果上面的回答是“是”,那么我们平时写的程序源代码是0和1吗? # 如果上面的回答是“不是”,那么计算机是怎么“知道”我们的程序的意思的? 按顺序作答,依次是: # 是 # 不是 # ??? 最后一个问题的答案是……?我们来举个例子,在汇编语言中: [pre NOP] 这条指令很简单,是吧? 在编译的时候,Assembler会扫描整个源代码。 在前面我们已经知道了,由于计算机只认识0和1,所以,源代码“NOP”是无法直接运行的。当Assembler遇到“NOP”的时候,为了生成让计算机能运行的“东西”(暂且这样称呼吧),就会以十六进制数“0x90”来代替它。 在这里,“0x90”就是“[^OpCode]”,而“NOP”则是“助记符([^mnemonic])”。 [blurb [$theme/lens.gif] [* OpCode的全称][br][br] OpCode就是Operation Code,意即操作码的意思。 ] [h2 一个OpCode只对应一个助记符吗?] [table 示例:OpCode && mnemonic [[*OpCode]] [[*mnemonic]] [0x90] [NOP] [0x90] [XCHG AX, AX] [0x90] [XCHG EAX, EAX] ] 从上表中可以看出,同一个[^OpCode]可以对应N个[^mnemonic]。为什么会这样呢?原因现在不必深究,以后自然会明白的。 [h2 一个助记符只对应一个OpCode吗?] [table 示例:OpCode && mnemonic [[*mnemonic]] [[*OpCode]] [ADD EAX, 1] [0x83C001] [ADD EAX, 1] [0x0501000000] [ADD EAX, 1] [0x81C001000000] ] 从上表中也可以看出,同一个[^mnemonic]可以对应多个[^OpCode]。原因同样留待以后再说。 [blurb [$theme/note.gif] [* OpCode与mnemonic的关系][br][br] [$theme/bullet.gif] 一个[^OpCode]不只对应一个[^mnemonic]。[br] [$theme/bullet.gif] 一个[^mnemonic]不只对应一个[^OpCode]。 ] [h2 OpCode管中窥豹] 有6个域是OpCode可能会用到的,或者说OpCode是由这6个域组成的——不过请注意:它们的名字是什么,这并不重要——重要的是它们的排列顺序。 它们是: # Prefixes # code # ModR/M # SIB # Displacement # Immediate OpCode的这6个域的详细介绍留待以后再说,现在首先要知道: [blurb [$theme/alert.gif] 在实际的使用中,并不是这所有的6个域都会被用到的,但是有一项却是一定会有的,那就是第2项:[^code],有些指令甚至只会用到[^code]这一项。 ] 例如: [table OpCode && mnemonic [[*OpCode]] [[*mnemonic]] [0xC3] [RETN] [0x2F] [DAS] [0x90] [NOP] [0xAC] [LODSB] ] 上表中的几个OpCode都只用到了[^code]这一项。其中的最后一项:[^0xAC],让我们来看看能不能给它加上一些额外的“东西”: [pre 0x[*F3AC] REP LODSB] 可以看到:rep lodsb 为什么会多了个“[*rep]”呢?是不是由额外的“[*F3]”造成的呢? Yes,猜对了,我们来看看它的OpCode格式描述,如下:(注:用{}包围起来的是域的名称) [pre [^AC] -- {[^code]} [^F3 AC] -- {[^Prefix]}{[^code]} 因此,[^F3] 就是域 [_Prefix] ] 在稍后的章节中我们会知道,[^F3]表示的是[_[^Rep Prefix]],它也能与[^movsb],[^stosb]等指令联用,但是,具体细节在这里暂不深究。 让我再来强调一次:[*OpCode中的6个域是可选的(除了域[^code]之外),不必都用上,但是[^code]是一定会有的。] 知道了这一点,我们再来看一些例子: [table OpCode && mnemonic [[*OpCode]] [[*mnemonic]] [27] [DAA] [2F] [DAS] [3F] [AAS] [37] [AAA] [D40A] [AAM] [D50A] [AAD] ] 在Intel的文档中,上表中的所有指令都是1字节的,但是,我们能够看到[^AAM]和[^AAD]是2字节的,到底有什么不同呢?[br] 先不要看下面的答案,试着自己想一想…… .[br] .[br] .[br] .[br] .[br] .[br] .[br] .[br] .[br] We can see: # [^AAM]和[^AAD]都是2字节的,然而其余的4个指令都是1字节的。 # [^AAM]和[^AAD]的OpCode的第2个字节都是[*0Ah]。 如果你还没把大学里的汇编知识彻底还给老师的话:),应该还记得[^AAM]和[^AAD]的描述: [pre [^AAM] : divide al by 10 商 放在AH里 余数 放在AL里 ] [pre [^AAD] : AL = AH * 10 + AL ] 注意到了吗?两者的操作都与10有关。而且两者的OpCode的第二个字节都是10([*0Ah])。 人类与动物的其中一个区别是具有思维的联想性;)。聪明的你是不是猜到了什么? 嗯……[*0Ah]会不会是偶然的呢?它会不会是操作数的一部分?进一步地,[^AAM]与[^AAD]的指令格式会不会[*不是]: [pre [*D40A] for [^AAM] [*D50A] for [^AAD] ] 而是: [pre [*D4]:[^imm8] for [^AAM] [*D5]:[^imm8] for [^AAD] ] 以及,[^imm8]可以是任何别的数字呢?(注:[^imm8]表示8位的立即数) 答案是肯定的! 事实上,我们可以通过反汇编器得知,[*D407]表示的是[^AAM] [*7],[*D508]表示的是[^AAD] [*8],以此类推。 现在,我们又知道了一种新的指令格式: [pre {[^code]}{[^Immediate]}(域2和域6) ] ;) 还有别的,以后再说。 [h2 There's Something We Should Know...] 最后再强调一点: * 虽然并不是6个域都是必要的,但是,它们的排列顺序绝对不能乱,必须严格按照上面的顺序进行。有些域也许不会出现,但是只要出现了,编号小的域就绝对不允许出现在编号大的域的后面,反之亦然。 例如,{[^Prefix]}{[^code]}的顺序绝对不允许变成{[^code]}{[^Prefix]}。 不相信?举个例子:[*4004]和[*0440](假设在32位条件下) [table OpCode && mnemonic [[*OpCode]] [[*mnemonic]] [4004] [INC EAX] [0440] [ADD AL, 40h] ] 明白了吗? 理解了OpCode的规则,将有助于底层程序员明白一些鲜为人知的事情。在接下来的章节中,我们将学习OpCode的6个域的详细信息。 [page 2. 从哪里开始,到哪里结束] [:['Everything that has a beginning... Has an end.[br][br]] -- Neo, The Movie - "The Matrix: Revolutions"(2003)] [h2 开始] “从哪里开始,到哪里结束”,这是几乎所有的OpCode初学者都会问到的问题,具体来说,就是给定一串OpCode,计算机是如何知道哪里是某一条指令的开始,到哪里才是它的结束呢? 让我们照例从实例开始说明: [pre [*EB]:[^imm8] ] 这是一条近跳转指令,翻译成mnemonic就是[^jmp],[^imm8]表示一个8位的立即数,整条指令的意思是[^jmp]到[^imm8]的偏移后的地址去。 从上一章的内容中我们可以知道,这条OpCode的域格式是这样的: [pre {[^code]}{[^Immediate]} ] 一共是2个字节。 问题来了,我们用肉眼一看就知道,这代表2个字节,我们也知道这条指令应该是从[^EB]开始,总长度是2个字节,到[^imm8]为结束,可是计算机是怎么知道这一点的呢?假如有一串OpCode发送给处理器,例如“[^90EB0090]”,让它从中找到这个[^jmp]指令,它会不会认不出来呢? 又或者,传送一串OpCode给处理器,例如“[^EB1234]”,它会不会把后面的[^34]也算进了[^jmp]的跳转范围呢? 答案是,不会的。 CPU有个寄存器叫做[^EIP],它储存了内存中的某个地址,这个地址会告诉CPU,哪里是当前指令的开始;但是,在CPU没有对OpCode进行解码之前,它并不会知道哪里才是指令的结束。 让我们来举个例子: [pre 00401000 [*90] NOP ] 第一列表示的是内存中的地址,在这里是[^00401000],它同时也是[^EIP]的值,此时[^EIP] = [^00401000][br] 第二列表示的是OpCode,第三列表示的是mnemonic,相信不必多说,读者也能明白它们的意思:[^90]对应[^NOP] 由于[^EIP] = [^00401000],所以CPU会知道当前的指令应该是从内存单元中的[^00401000]开始,在这里,储存了一个OpCode:[^90],接下来CPU会对[^90]进行解码: [pre OpCode:[^90] 域格式:{[^code]} ] 只有1个字节。所以CPU就会知道,OpCode“[^90]”是从内存地址“[^00401000]”开始,到“[^00401001]”结束。 明白了吗?不过还有一种特殊情况:如果CPU遇到了无效的指令,它就会无法解析,例如OpCode“[^FFFF]”,在运行的时候,会产生一个异常。 再来看本章开头的: [pre [*EB]:[^imm8] ] [^EB]是域{[^code]},当[^EIP]遇到内存中的[^EB]的地址时,CPU就会知道第1个字节后面会跟着一个[^imm8]立即数,总长度是2个字节。 至此,我们可以给出: [blurb [$theme/note.gif] [* 初步的结论][br][br] 1. [_开始]:处理器认为当前[^EIP]指向的内存单元中的第一个字节就是指令的开始。[br] 2. [_结束]:处理器通过对OpCode进行解码(大多数情况下是根据{[^code]}域),从而知道哪里是结束。 ] 不过,不得不提的一点是: * [*在运行完一条指令后,[^EIP]并不总是指向下一条指令的开始!] 举个例子: [pre 00401000 [*EB 00] JMP 00000002 00401002 [*90] NOP ] 此时[^EIP] = [^00401000],[^EB00]翻译成mnemonic就是[^JMP 00000002]。为什么呢?因为[^EB:imm8]是2个字节的OpCode,在这里[^imm8]的值是[^00],所以[^00(imm8) + 02(本条OpCode的长度) = 02(应该跳转的地址)],也就是跳转到相对偏移为[^02]的地方去。 因此,[^EB00]运行完后,[^EIP]的值应该是[^00401002],也就是指向[^90]的地址,下一步处理器将会执行指令“[^NOP]”。 好,再看: [pre 00401000 [*EB FE] JMP 00000000 00401002 [*90] NOP ] 此时[^EIP] = [^00401000],但是为什么[^EBFE]会是[^JMP 00000000]呢?想想看? 答案:[br] FE + 02 = 100 由于[^imm8]的关系(8位只表示一个字节),[^100]其实只取[^00]([^100]其实是2个字节了——其高位为0,即[^0100])[br] 所以这条指令运行后,[^EIP]应该还是[^00401000],没有改变!原因是这条指令的跳转地址是它本身!后面[^00401002]处的“[^90]”永远都不会执行! 真正的底层程序员应该会理解指令的本质,而不仅仅是从指令的字面上去理解它的意思。例如,[^cmp],从字面上来看,表示[^compare]一些东西。但是真正的底层程序员不会这样说,他会说,[^cmp]表示的是用第一个操作数[_减去]第二个操作数,由此来设置相应的标志位。同时,我们关心的只是标志位,并不关心减操作后的结果,所以不需要把减操作的结果储存到第一个操作数中。 让我们回到正题。再来看一些应用: OpCode:[^04 AC] [pre 00401000 [*04 AC] ADD AL, 0AC ] 我们已经知道,[^AC]是助记符[^lodsb]的OpCode,[^00401000]是OpCode [^04AC]的开始地址,而[^00401002]将会是它的结束(这个指令只有2个字节的关系)。但是,我们一直以来都没讨论的是:如果把这条OpCode从中间截断!即从[^00401001]地址处开始的指令会是什么呢? 如果我们把寄存器[^EIP]的内容设置成[^00401001],我们就会发现:[br] 处理器会把[^AC]看作[^lodsb],而不是:[br] ADD AL, 0AC[br] [*04]:[^imm8](AL+imm8)中的[^imm8] 应用这个原理,我们来看一个小例子,假设要实现下面的算法: [pre IF zf = 0 lodsb ELSE add al, 0AC ] 试试写成助记符?不知道读者朋友们会怎么写——我会写成这样: [pre jnz $+1 add al, 0AC ] 解释如下: [pre 如果标志位[*zf]等于0,则[^EIP]会指向[^add al, 0AC]的第2个字节,也就是[^AC]——我们知道[^AC]表示助记符[^lodsb] 明白了吗?使人惊奇的是,整个算法的实现只用了区区4个字节! ] 这个算法的OpCode: [pre 00401000 [*75 01] JNZ SHORT 3 00401002 [*04 AC] ADD AL, 0AC ] 让我们来看看每个字节表示什么意思: [*75]:[^imm8] 是 [^7501] 的域格式[br] [*75]是[^JNZ]的OpCode,[^imm8]在这里是[^01],会加到[^EIP]里面去,整个[^7501]表示如果这条指令被执行了,则[^EIP]会指向下一条指令的第2个字节的地址。 [^04AC]的域格式:[br] [*04]:[^imm8] 其中:[br] [*04] - {[^code]}[br] [*AC] - {[^Immediate]} 整个算法实现的思路如下: 如果[_zf=0],[^7501]这条指令就会把下一条指令的起始地址+1([^75]后面的操作数就是需要跳的字节数:0不跳,1跳一个,n就跳n个……但是字节是有符号的,负的就往后跳……所以[^jnz short xxx]是有最大的跳跃限制的),然后把跳跃后的地址赋值给[^EIP]——也就是[^00401003],从而迫使处理器认为[^AC]所在的地址才是下一条指令的开始(跳过了OpCode [^04]),这时,[^AC]会被当成{[^code]}。 否则,[^EIP]会指向[^04AC]所在的地址[^00401002],所以下一条指令的开始就会从[^04]开始算起,处理器会认出域格式:[br] [*04]:[^imm8](add al, imm8)[br] 这时,[^AC]会被当成{[^Immediate]},而不是{[^code]}。 呵呵,是不是有点儿迷糊了?;) 为了加深理解,最后再给大家看一个算法及其实现: [pre IF zf = 0 inc eax ELSE mov al, 40 ] 答案: [pre 00401000 [*75 01] JNZ SHORT 3 00401002 [*B0 40] MOV AL, 40 ] 嗯……提示一下:[^40]表示的是[^inc eax]……聪明的你,明白了吗? [h2 结束] 本章到这里已经结束了,但是……OpCode的学习只是刚刚开始而已,请大家打好精神,为后面的旅程作好准备!;) [page 3. Prefixes - Part I] [:['To be, or not to be: that is the question.[br][br]] -- William Shakespeare, "HAMLET".] [h2 Hello, Prefixes!] 就像经典的“Hello World!”程序一样,让我们也从最简单的一个实例看起: [table OpCode && mnemonic [[*OpCode]] [[*mnemonic]] [40] [INC EAX] [66 40] [INC AX] ] 假设默认的操作数是32位,我们就可以得到上表的结果。(为什么默认是32位?看到后面就会明白的) 我们可以看到,[^40]表示的是[^INC EAX],[^66 40]表示的是[^INC AX],两者的分别在于:前者的操作数是32位的([^EAX]),而后者是16位的([^AX])。 从OpCode的角度来看,后者比前者多了一个[^66],就导致了不同的结果,唔……Intel x86规定: [*66]是一个[^Prefix],我们把[^Prefix]翻译为[^前缀],所谓[^前缀],就是与[^code]进行组合用以产生出某些变化形式的“东西”。唔……好拗口啊,真不好解释,请看晕了的朋友不要抛砖头,继续往下阅读吧。 :) [h2 认识 Prefixes] 回忆一下第一章中介绍的OpCode的6个域: # [*Prefixes] # code # ModR/M # SIB # Displacement # Immediate 记住: * 在实际的使用中,并不是这所有的6个域都会被用到的,但是有一项却是一定会有的,那就是第2项:[^code],有些指令甚至只会用到[^code]这一项。 * 这6个域的排列顺序绝对不能乱,必须严格按照上面的顺序进行。有些域也许不会出现,但是只要出现了,编号小的域就绝对不允许出现在编号大的域的后面,反之亦然。 [^Prefixes]是所有的域中最容易理解的一个,请先明了它的一些特性: [blurb [$theme/note.gif] [* Prefixes的几个特性][br][br] 1. 它是唯一的一个可能出现在[^code][*之前]的域。[br] 2. 所有的[^Prefixes]都只有[*1]个字节。[br] 3. 在一个OpCode中可能会有多个[^Prefixes]。 ] 看看刚才提到过的prefix [^66],这个prefix的意思是“切换默认的操作数的大小”。例如在有的系统中有2种默认的操作数大小:[*16]位和[*32]位。操作数有可能会被写成[*16]位或者[*32]位,唯一的区分方法是看它有没有prefix [^66]。 唔……是不是讲得不够清楚呢?我们来看看: [table OpCode && mnemonic [[*OpCode]] [[*mnemonic]] [66 AD] [LODSW] [AD] [LODSD] ] 依然假设默认的操作数是32位的,有没有发现什么不寻常的地方? [^LODSW]和[^LODSD]的[^code]域是一样的——都是[^AD]!其实,[^LODSW]和[^LODSD]这两个指令是同一个指令,只不过它们的操作数大小不一样——[^LODSW]使用了2个字节([*16]位)的[^WORD]作为操作数,而[^LODSD]则使用了4个字节([*32]位)的[^DWORD]作为操作数。 看到这里,读者应该能够明白了:prefix [^66]的作用是[*[_切换]]默认的操作数大小。请注意我们并没有说“指定”,而是“切换”!反映到这个例子中,就是“切换默认的[*32]位操作数到[*16]位”,而不是“指定操作数的大小为[*16]位”。 这点非常重要!!!绝对不是在玩文字游戏!! 如果默认的操作数大小是[^WORD]([*16]位),那么切换后就是[^DWORD]([*32]位);反之,如果默认的操作数大小是[^DWORD]([*32]位),那么切换后就是[^WORD]([*16]位)。 [* 切记!Prefixes [^66]就像一个触发器一样,起的作用就是进行切换。] 让我们再来看一个特例: [pre [*B0 FF] MOV AL, 0FF [*8A C1] MOV AL, CL ] 看清楚了吗?现在的操作数是[^AL]和[^CL],加上prefix [^66]后会如何? [pre [*66 B0 FF] MOV AL, 0FF [*66 8A C1] MOV AL, CL ] Faint!没有任何变化! 为什么呢?我们可以猜测一下:也许并不是所有情况下的操作数大小都可以随意改变的。假如这个改变是不允许的,那么它就会被忽略。 为了证实这个猜想,让我们来看看下一个更有趣的例子: prefix [^F3](rep)的作用是让CPU对随后的指令循环执行[^ecx(cx)]次,指令[^INC EAX]的OpCode是[^40],好,如果我们想连续执行3次[^INC EAX]的话,应该怎么样呢? 也许你会想当然地认为应该这样写: [pre xor eax, eax mov ecx, 3 [*rep] inc eax ] 实际上!并不是这样!这样的程序的运行结果是: # 没有任何异常(exception)产生。 # 最后eax = 1,这意味着prefix [^F3]并没有起作用——它被忽略了。 现在我们可以证实之前的想法: [*[_如果Prefixes不能对随它之后的OpCode起作用,那么它就会被忽略。]] 再回忆一下之前提到的三个特性: # [^Prefixes]是唯一的一个可能出现在[^code][*之前]的域。 # 所有的[^Prefixes]都只有[*1]个字节。 # 在一个OpCode中可能会有多个[^Prefixes]。 前面两点应该比较容易理解,让我们来看看第3点是什么意思。 如果想得到下面的指令: [pre REP LODSW ] 它的OpCode将会是: [pre [*66 F3 AD] ] 解释如下: [pre [*66 AD]:LODSW [*F3]: REP ] 都是前面讲过的内容,不难吧?只是组合起来使用罢了。 不过……细心的读者可能会问:为什么要把[^66]放在第一位,把[^F3]放在第二位呢?把它们的位置调换一下行不行?答案是:行!事实上它也可以写成: [pre [*F3 66 AD] ] 效果是一样的! [blurb [$theme/note.gif] [* Prefixes的特性][br][br] [$theme/bullet.gif] 一个OpCode中可以有多个Prefixes。[br] [$theme/bullet.gif] 如果有多个Prefixes,那么它们的顺序可以打乱,不会有任何问题。 ] 最后,我们还可以得出一个推论: 由于每个Prefixes会多占用1个字节,所以也必定会导致处理器多使用一个指令周期进行解码——无论在时间还是空间上都会造成浪费。因此,我们应该权衡在哪些场合才使用Prefixes,如非必要,应该减少对它的使用。 [h2 Is it ALL?] Of course not! 由于章节的篇幅问题,Prefixes的进一步讲解会放在后面的章节中继续进行,我们会看到更多的有关Prefixes的信息。 [page 4. Prefixes - Part II] [:['I'll be back.[br][br]] -- Arnold Schwarzenegger, The Movie - "Terminator"(1984)] [h2 Prefixes合集] 在前一章中我们已经知道: # 所有Prefixes的长度都是1个字节。 # 一个OpCode可能会有几个Prefixes。 # 如果有多个Prefixes,那么它们的顺序可以打乱。 # 如果Prefixes不能对随它之后的OpCode起作用,那么它就会被忽略。 现在我们将要学习剩下的几个Prefixes,它们可以被划分为5个集合,分别是: # Change DEFAULT operand size. ([*66]) # Change DEFAULT address size. ([*67]) # Repeat prefixes. ([*F2, F3]) # Segment override prefixes(change DEFAULT segment). ([*2E, 36, 3E, 26, 64, 65]) # LOCK prefix. ([*F0]) [h3 [_Prefix 66]] 在前面我们已经学习过它了,而且也够详细的了,对吗?;) [h3 [_Prefix 67]] 改变默认的[*地址]大小。 请注意:[^67]与[^66]的分别在于,[^66]改变的是默认的[*操作数]大小,而[^67]则是[*地址]的大小。两者有什么差异呢? [pre [*8A 00] MOV AL, [~EAX] ] 现在把它的OpCode改成以[^67]开头的: [pre [*67 8A 00] MOV AL, [~BX+SI] ] 我们可以看到: # [*地址]由原来的[*32]位的[~EAX]变成了[*16]位的[~BX+SI]。 # 疑问来了:为什么不是[~AX],而是[~BX+SI]呢? 第2个问题我们将会在以后的{[^ModR/M]}和{[^SIB]}的格式讲解中回答。现在我们可以暂时认为,在[*16]位的地址模式中无法完全使用[*32]位中的对应的地址模式,两种模式中的寄存器有着一定的区别。(看不明白?没关系,后面的章节中会详细解释) 强调一点:Prefix 67同样也是一个“触发器”,它起的作用是“[*切换]”,而不是“[*指定]”。 [h3 [_Repeat Prefixes (F2, F3)]] Repeat Prefixes通常是与[^movs]、[^scas]、[^cmps]等串指令搭配使用的,它们有: [pre [*[^F2]]: REPNE [*[^F3]]: REP / REPE ] Repeat Prefixes作为一个串操作指令的前缀,它重复执行其后的串操作指令。每一次重复都先判断(E)CX是否为0,如为0就结束重复,否则(E)CX的值减1,然后再重复其后的串操作指令。所以当(E)CX的值为0时,就不再执行其后的操作指令。 它类似于LOOP指令,但LOOP指令是先把(E)CX的值减1,后再判断是否为0。 举例: [pre CLD MOV ECX, 3 REP MOVSB ] 运行的结果是把DS:(E)SI的3个字节(byte)移动到ES:(E)DI去。 有两点规则: # 你可以看到有3种Repeat Prefixes的助记符:[^rep]/[^repe]/[^repene],但是只有2个OpCode:[^F2]、[^F3]。 # 如果某些指令[*[_只使用]]前缀[^rep],那么这里的[^rep]可以用[^repe]或者[^repne]来代替。 第2条规则比较难以理解,对吗?我们来举个例子: [pre REP LODSB REPE LODSB REPNE LODSB ] 这3条助记符的运行结果都是一样的:它会重复运行指令LODSB一共(E)CX次,而不管它的Repeat Prefixes是[^rep]/[^repe][~[^F3]]还是[^repne][~[^F2]]。 但是请注意:第2条规则的适用范围仅仅是[*[_只使用]]“[^rep]”的指令,意即无论是[^F2]还是[^F3],对指令的执行结果都无影响,而这样的指令非常的少! 从OpCode的角度来看rep/repe[~[^F3]]和repne[~[^F2]]的区别: 我们知道,重复串指令时可能会改变某些标志位(例如[^ZF]),在这种情况下,有些指令与重复前缀搭配使用时,[^F2]和[^F3]会把[_最后一位]与标志位[^ZF]进行比较,如果它们不相同,则重复串指令的操作将会结束。而有些指令不用进行这个比较的操作,因此标志位[^ZF]对这些指令的运行结果无影响。 讲得不够清楚?呵呵,把[^F2]和[^F3]转换成二进制就能明白了。;) [pre 1111 001[*0] [^F2] 1111 001[*1] [^F3] ] 最后我们再来看看Repeat Prefixes的结束条件: [table Repeat Prefiex的结束条件 [[*Repeat Prefix]] [[*结束条件1]] [[*结束条件2]] [REP] [ECX=0] [None] [REPE] [ECX=0] [ZF=0] [REPNE] [ECX=0] [ZF=1] ] 从上表中可以看出:[^repe]和[^repne]的结束必须同时满足两个结束条件,而[^rep]只管ECX等不等于0。 看到这里,再结合上面的第2条规则,我们就能更清楚了:由于[^rep]并不与标志位[^ZF]进行比较,所以它可以被替换成[^repe]或者[^repne],对执行结果无影响! [h3 [_Segment Override Prefixes (2E, 36, 3E, 26, 64, 65)]] 我们先来看看这些Prefixes是什么: [table Prefixes && Explanation [[*Prefix]] [[*Explanation]] [2E] [[*CS] segment override prefix] [36] [[*SS] segment override prefix] [3E] [[*DS] segment override prefix] [26] [[*ES] segment override prefix] [64] [[*FS] segment override prefix] [65] [[*GS] segment override prefix] ] 再来看一个例子: [pre [*8B 03] MOV EAX, [~DWORD [^DS]:EBX] [*65 8B 03] MOV EAX, [~DWORD [^GS]:EBX] ] [^65]就是一个Segment override prefix,用来改变[*默认]的段,从上表中我们可以看出:[^65]代表的是段[^GS]。注意!这里也是用[*默认]的概念。 读者在这里也许会存在一个疑问:[*默认]?我怎么知道当前默认的是哪个段呢?以及为什么要用[*默认]的概念呢? 答案是这样的:在使用内存中的数据时,处理器必须首先知道它的段地址([^Segment])和偏移量([^Offset]),但是如果在每个地方都要显式地直接指出段地址,那么在OpCode格式中就必须增加一个新的域,这将会比现有的OpCode体系多占用大量的字节,而且处理器也必须多花费额外的时钟周期来进行解码——无论在空间还是时间上,都不值得! 因此,为了解决这个问题,一个方案诞生了: 指令由不同的定义被划分为不同的组,每个组各自有一个[*默认]的段: [pre [^CS]: for [^EIP] pointer [^ES]: 目的操作数是内存单元的串指令(movs, cmps等),在这里源操作数是储存在段DS里面。 [^SS]: 堆栈操作(push, pop等) [^DS]: 剩下的数据操作指令。 ] 有了这个规则,处理器识别当前应该用哪个段将会变得非常简单而直接: # 如果有“Segment override prefix”,那么就使用这个prefix所指定的段。 # 否则就使用[*默认]的段。 看看: [pre [*AC] LODS [~BYTE [^DS]:ESI] [*3E AC] LODS [~BYTE [^DS]:ESI] ] 从上面的表中可以查出,[^3E]是表示段[^DS],但是实际上在这里即使不直接指明[^3E],处理器也是会使用[^DS]的,因为[^DS]是指令[^LODS]的默认段。 最后值得一提的是[^64],它表示的是段[^FS],也许读者会对[^FS]不太熟悉,平时好像很少会用到。没关系,我们来简单介绍一下:[^FS]一般是由SEH(结构化异常处理)所使用,但是由于SEH不属于OpCode格式的范畴,所以我们在这里不必深究,知道有这个概念就行了。 [h3 [_LOCK Prefix (F0)]] 对于这个Prefix,Intel的文档已经解释得很清楚了,不过它的具体意义对OpCode的格式学习并无任何帮助,有兴趣的读者可以在<>的3-387页看到关于它的详细解释。在OpCode的格式学习中,我们只需要知道[^F0]表示的是助记符[^LOCK]就足够了。 [h2 The End Of Prefixes] 结束了吗? 是的!关于Prefixes的格式学习到此就告一段落了。在接下来的章节中,我们将会学习最激动人心的一部分:如何对OpCode进行手工解码。 [page 5. ModRM - Part I] [:['道,可道,非常道。[br][br]] -- 老子,《道德经》] [h2 基本概念] 让我们从最经常用到的域开始学起——[^ModR/M]。 在开始之前,先来讲一些最基础的概念,扫扫盲。;) 一个字节如果被转换成二进制,则是由8位(bit)来表示(不足8位的话则高位用0来补足),例如: [pre 16进制 2进制 B7 1011 0111 3A 0011 1010 ] 示例中的B7的二进制是1011 0111,这是典型的4:4表示格式——1011表示的是B,0111表示的是7,这样,1011 0111表示的就是B7了。 很容易理解吧?呵呵,那么我们可不可以用另外一种方式来表示一个字节呢? 答案是肯定的: [pre 16进制 2进制的[*4:4]格式 2进制的[*2:3:3]格式 B7 1011 0111 10 110 111 3A 0011 1010 00 111 010 ] 请看,我们在这里引进了一种新的表示格式:[*2:3:3] 它的特点是把一个字节的8位二进制分成3个部分:最高的2位表示的是一个东西,接下来的3位表示的是另外一个东西,以及最后的3位表示的是另另外一个东西。 好了,明白了这一点后,我们来开始吧! 首先回忆一下OpCode的组成格式: # Prefixes # code # [*ModR/M] # SIB # Displacement # Immediate 请注意第三项:[*ModR/M],它占一个字节,其格式为: [pre 7 6 5 3 2 0 Mod Reg/Opcode R/M ] 可见,[*ModR/M]是由[^Mod]、[^Reg/Opcode]和[^R/M]三个部分组成的。每个部分所占的bit大小为: [pre [^Mod]: 占最高位的6~7共2个bit [^Reg/Opcode]: 占中间位的3~5共3个bit [^R/M]: 占最低位的0~2共3个bit ] 呵呵,正好是[*2:3:3]的格式!:) [*ModR/M]的具体描述如下面两个表,第一个是16位的,第二个是32位的:[br] (截图自《IA-32 Intel Architecture Software Developer's Manual Volume 2: Instruction Set Reference》,页码2-5,2-6,希望Intel不要因为版权问题找我的麻烦) [*- 16位 -][br] [$images/16bitmodrm.gif][br] [*- 32位 -][br] [$images/32bitmodrm.gif][br] 哇,这两个图好复杂呀!你是不是已经有了这个感慨了呢? 呵呵不要紧,让我们来举个例子看看[*ModR/M]到底是怎么来看的: [pre mov edi, ecx 8B [*F9] sub edi, ecx 2B [*F9] ] 注意这两个OpCode的第二个字节——都是[*F9],再来看看OpCode的格式: [pre Prefixes Code [*ModR/M] SIB Displacement Immediate ] 我们在前面说过,在OpCode的格式中,只有[^Code]是必须有的,别的都是可选的。所以,在8B F9和2B F9这两组OpCode中,8B和2B就是[^Code],(这里没有Prefixes,因为Prefixes只有在前面的章节中所介绍过的那几个:66、67、F2、F3、2E、36、3E、26、64、65、F0)。 紧接着在[^Code]后面的就是[^ModR/M]了,所以在这两组OpCode中的[*F9]就是[^ModR/M]。(在这里也没有[^SIB]、[^Displacement]和[^Immediate]) [*F9]的4:4格式的二进制是[*1111 1001],我们把它分解成2:3:3的二进制看看: [pre 16进制 2进制的2:3:3格式 F9 11 111 001 ] 也就是: [pre [^Mod]: 11 [^Reg/Opcode]: 111 [^R/M]: 001 ] 晕头转向了?呵呵,让我们来对其分而治之吧! 假设是在32位模式下。从上面的第二个图中可以看到,[^Mod]总共分为00、01、10、11四种情况,每种情况又分别有8种情况。现在[^Mod]是11,所以我们应该看[^Mod]为11的那一栏。 OK,现在来讨论第二个:[^Reg/Opcode] [^Reg/Opcode]中间的那个“/”表示“或”,意思就是,这个地方可以表示为[^Reg]或者[^Opcode]——至于到底什么时候表示[^Reg],什么时候表示[^Opcode],这就要由[^Code]来决定了。目前我们不必去深究它,后面会讲明白的,我们只要知道,如果它是表示[^Opcode],则这个指令必定是2个字节的。 [^Reg]由3个bit的二进制组成,因此,它可以表示: [pre 2 ^ 3 = 8 ] 一共8种可能的值。我们知道,常用的通用寄存器恰好也有8个,因此,根据组合数学的常识,可以得到: [table REG && Register [[*REG]] [[*Register]] [000] [EAX] [001] [ECX] [010] [EDX] [011] [EBX] [100] [ESP] [101] [EBP] [110] [ESI] [111] [EDI] ] 这是在32位的模式下得到的。 在16位的模式下,[^Reg]则是表示另外一种“局部”的格式,它的低4位表示寄存器的低地址,高4位表示寄存器的高地址,如下表: [table REG && Register [[*REG]] [[*Register]] [000] [AL] [001] [CL] [010] [DL] [011] [BL] [100] [AH] [101] [CH] [110] [DH] [111] [BH] ] 好了,把目光返回到上面的32位[*ModR/M]图,看看最上面,在[^r32(/r)]那一栏中,[^REG=111]表示的就是寄存器[^EDI] 到目前为止,最后剩下还没讨论的就是[^R/M]。这一栏要与[^Mod]结合起来。我们来看[^Mod]为11的那一栏——[^R/M]为001对应的寄存器是ECX 好了!大功告成!整理如下: [pre [^Mod]: 11 表示应该查看Mod为11的那一栏 [^Reg/Opcode]: 111 表示的是寄存器EDI [^R/M]: 001 表示的是ECX ] 因此,通过OpCode: [pre 8B [*F9] 2B [*F9] ] 不难得到: [pre mov edi, ecx 8B [*F9] sub edi, ecx 2B [*F9] ] (注:8B是助记符“MOV”的[^Code],2B是助记符“SUB”的[^Code])