整理资料,发现一篇老文。如果版主觉得过于基础,可以移到初学者那边。现在不知道基础的东西应该发到哪个版块了。 谈谈IA-32的指令格式 废话不多说,作为CISC的代表,x86指令集的解码工作较为复杂。这种工作主要是为了照顾兼容性,实际上现在把一堆东西挤在几个比特里面完全没有必要,看看MIPS的汇编器,除了伪指令外,其他就是一一对应好嘛。 这部分的参考资料主要是intel手册2的第2章和第三章的3.1,当然还有手册2的附录。intel手册分为三册,手册1是给汇编程序员用的,主要指导你怎么写汇编程序;手册2是给编译器设计者用的,主要指导你怎么给你的编译器写一个x86后端--这也是我从事工作的一部分;手册3是给系统程序员用的,主要是指导操作系统编写者如何利用CPU。 先看看x86指令(IA-32)基本编码格式: 这部分没有明确的一致的术语,中文翻译确实有点不伦不类。 一、前缀:(prefix) 前缀是可选字段,也就是说它可以不出现。它分为四类(组),intel的术语叫四个group。在一条指令里,每一类前缀最多只能出现一次。这四类是: 1.锁总线和重复前缀 F0H:LOCK 锁总线前缀 F2H:REPNE/REPNZ F3H:REP REPE/REPZ 注意这三小类占用的是一个group,也就是说,前缀中出现了LOCK就不能再出现REP之类的重复前缀。 其实应该发现一个奇怪的问题,F1H不见了,它是否是一种前缀呢?事实上,inte和amd手册在这里存在着差异。在intel手册中,F1H的指令槽是空出来的,但是AMD手册里面把F1H给了INT1。这实际上是一个undocumented opcode。 2.段超越和分支提示前缀 CS/SS/DS/ES/FS/GS 六个段超越前缀 对应的编码是 2E/36/3E/26/64/65 分支不被接受:2E 分支被接受:3E 注意到,2E和3E是被复用的。如果我们写一个反汇编引擎的时候,仅仅得到2E和3E是不清楚到底是CS/DS的超越前缀还是分支预测。需要往后看opcode,这是解析前缀指令需要注意的一点。请注意段超越前缀不能用于分支跳转类指令,而分支预测前缀只能用于Jcc(条件跳转指令),它们是不存在交集的。 3.操作数超越前缀 实话说,此处“超越”翻译的不咋样。实际上,是可以改变操作数的宽度。 66H 4.地址超越前缀 67H 这些前缀有些需要说明: 1)不少前缀需要等待opcode才能确定。所以编程时候需要往后看opcode才能决定prefix解释成什么。 2)第一类的REP和REPE有什么区别?REPE和REPZ有什么区别? REP和REPE的区别是opcode不同。opcode如果修改标志位,则F3H是REPE,否则就是REP。这也是需要看标志位的prefix之一,实际上是两类指令前缀复用了这个指令槽。 REPE和REPZ没什么区别,助记符的两种写法。 3)什么叫地址超越?什么叫操作数超越? 超越是对英文override的翻译,这个翻译很奇特,也不符合override的真实意义。但是国内大部分介绍8086的书籍都采用这种翻译。实际上override在此处意思是“重写”。 重写了什么呢?操作数默认的size(宽度)可能是16位或者32位。而操作数超越前缀就是允许此指令使用非默认的宽度。 同样地址超越作用类似,允许指令使用非默认的地址宽度(16位或者32位)。 那么默认的操作数宽度、默认的地址宽度在哪儿呢?代码段选择子有一个位(第22位 CS.D)指明了默认值。被置位,上面两个默认宽度都是32位;被清零,上面两个默认宽度都是16位。 请注意,给指令施加这两类超越前缀不会改变默认宽度,仅仅是改变该指令的执行使用的宽度。 二、opcode 实际上opcode分为两个部分,一部分就是opcode本身,另一部分是个别指令会占用modr/m的reg/opcode位域。 学MIPS或者其他RISC指令集的见到intel的opcode编码可能会吓一跳。相比较MIPS那种I J R三类指令,x86的指令opcode编码实在是太复杂了。x86的opcode最短是1个字节,最长是3个字节。这是其一。 和MIPS解决方案不同,x86对于源操作数和目的操作数是暗含在opcode里面的。对于念MIPS教科书过来的朋友,几乎难以理解。这话意思是: 比如说mov指令: 我框出来的这两条指令没有任何的本质区别,就是源操作数和目的操作数反过来了。也就是说谁是源操作数谁是目的操作数就隐藏在opcode中。这和MIPS的想法不同。 比方说: 88 D9 对应 MOV CL,BL 而 8A D9 对应 MOV BL,CL D9还是那个D9,但是谁是源谁是目的不一样了(也就是说由于opcode不同,对于D9的解释不一样)。这一点对于modr/m字节理解有帮助。 单字节opcode比较好理解。 二字节和三字节opcode比较特殊: 二字节通用opcode是0fh+一字节的编码;但是二字节的SIMD opcode是一个强制前缀+0fh+一字节的操作码。 不知道大家看懂了没。intel的二字节的SIMD opcode实际上是三个字节,有一个字节(“强制前缀”)被计入了前缀部分。对于二字节的SIMD opcode,这个前缀不是可选部分。 同样的,三字节的通用opcode,是0fh+二字节的编码。SIMD opcode格式是强制前缀+0FH+二字节编码。 问: 1)此处强制前缀,是否计入“前缀”的四个字节限制? 不计入。此处强制前缀,是为了引导出来此处是SIMD指令,类似某种转义码。实际上,此处强制前缀,应该纳入opcode的范畴,所以此处SIMD所谓二字节、三字节名不副实,应该是三字节、四字节。 个别的SIMD指令不需要强制前缀来引导,比如addps(0FH+58H) 2)0FH是什么? x86常用的转义码。此处用来提示是多字节opcode。 3)强制前缀有哪些? 和前面的prefix编码有重叠。是66h f3h F2h。 4 ) 二字节和三字节都用0FH作为转义码,那么解码时候如何知道是二字节还是三字节呢? 问的好!事实上三字节指令的第二个字节(它和0FH一起构成三字节的转义码)是在二字节指令槽里面的。就像0FH在一字节指令槽里面一样。 三、modr/m字节 modr/m是一个字节。被切分成三个位域(23),它用来确定寻址方式,并可能对opcode做一定补充。 mod:提供寻址模式,11=寄存器寻址 其余都是内存寻址 reg/opcode: 两种作用,第一种是提供寄存器寻址;另一种为某些opcode提供补充说明。 R/M: 结合MOD位域,提供内存/寄存器寻址。 1.这里先研究reg/opcode位域: 1)请问此处提供的是源操作数还是目的操作数的寄存器寻址?换而言之,比如我要编码mov eax,ebx ,此处reg/opcode负责编码eax还是ebx? 都可以。取决于你使用什么opcode。这个问题我们在opcode那里已经解释了一部分,这里着重解释一下。 请注意画红框的两条opcode,都可以用来编码mov eax,ebx。如果采用89编码,则eax为r/m位域,编码是: 89H 11 011 000 opcode reg寻址 ebx eax 请注意89编码时候,目的地址是在r/m字段,也就是说,此处的eax是目的地址。这样编码就是89 D8。 而采用8B编码,则eax为reg/opcode位域: 8BH 11 000 011 opcode reg寻址 eax ebx 最终编码是8B C3。 这也就是我上面说的,x86把源和目的操作数隐藏在opcode里面了。 2)什么类型的opcode需要这个位域来补充?为何不干脆再用一个字节编码opcode,要占用这个位域? 这个问题你得问设计这些指令槽的设计师。实际上再设计一个字节编码也毫无问题。这部分编码类似元素周期表中镧系元素和锕系元素。 3)这会给反汇编引擎带来什么问题? 导致有些指令(被称为组编码指令--在原始的opcode表里面一组占用一个格子,用阴影区分)需要读到ModR/m才能确定其作用。 2.下面谈mod 位域和R/M位域 这个位域配合r/m一起来确定寻址方式,mod有4种,每一种对应一种寻址方式。请注意,16位下的mod r/m编码对应寻址方式和32位下的完全不同--也就是说,它们没有任何可比性。 16位下面是没有SIB字节的,那么16位怎么实现基址变址寻址呢?只好通过modr/m来实现。为此,16位牺牲了通用的寄存器间接寻址(只提供BP BX SI DI作为间接寻址的寄存器,并且BP实现稍微特殊)。也就是说16位下面没有mov [al],bl这种东西。 我们稍微来研究一下这张32位的modr/m编码表。mod=11时候,是寄存器寻址,这很简单。请注意红杠标注的那些个行。我们注意到: 1)[ESP]这种寻址方式在modR/M里面没有。它被用来引导SIB(可以理解R/M=100作为SIB的转义码)。表示[ESP]的编码方法要在SIB里面。 2)[EBP]这种寻址方式类似16位里面[BP]的编码方式。在mod=00时候,反倒没有[EBP]。[EBP]是采用[EBP]+disp8或者[EBP]+disp32来编码的。 稍后我们会看到,在SIB里面,也有类似的特殊设置。 四、SIB字节 同modr/m类似,SIB字节也是采用23切分成三个位域,名字分别叫Scale、Index、Base。SIB的名字也来自这三个位域名字的首字母缩写。 SIB字节由R/M=100 MOD≠11引导出来。 SIB确定的寻址方式是[base+Index* Scale +disp] disp意思是后面尾随的若干个displacement字节。 这个图,可以补充几点: 1)base中的ebp哪去了? SIB.base=ebp的时候,要看mod决定是何种寻址方式。上图的附注里面已经说明了。这是解析SIB字节,需要反过来查modr/m的情况。 2)index里面的esp哪去了? esp作为index时候,index自动被忽略--此时scale因子视为0.(请注意scale编码为00时候,因子视为1)上表里面none真是混淆是非的典范。也就是说esp作为index,计算方法是[base+disp],不管你的scale是多少。 这种表示有一个重要的作用,就是我们可以表示[ESP]这种寻址方式。如果你还记得modr/m那边把本该属于[esp]的编码当做转义码来引导SIB字节了,导致modr/m无法表示[ESP]。只好用sib来表示。 前面说过sib表示的是[base+ Index*Scale+disp],要想表示[esp],只好把base设置成esp,scale因子设置成0,disp设置成0.但是我们发现,scale没有0这个编码(00、01、10、11分别代表1 2 4 8 没有0)。只好用替代的方法,如果index是100(esp),则scale*index=0。 当然这个规则是一致的,就算你的base不是esp,而是eax等寄存器,只要把index设置成100,scale*index一样视为0。 五、displacement和immediate 问: 1)这两者有什么区别? 要说核心区别,那就是displacement大部分用在寻址上,而immediate用于操作数的编码上。 举个例子: mov ecx,1 可以编码成 OPCODE IMME B9 01 00 00 00H 请注意目的操作的地址ecx(一个寄存器)已经蕴含在操作码B9里面了 而mov ecx,[0x1] 编码成: opcode mod reg r/m displacement 8B 00 001 101 01 00 00 00H ecx 即 8B 0D 01 00 00 00H 其中mod和r/m在一起,表示disp32的寻址方式。 当然了,你也可以额外使用一个SIB字节来编码mov ecx,[0x1] opcode mod reg r/m scale index base displacement 8B 00 001 100 01 100 101 01 00 00 00H 引导 scale随便写 即:8B 0C 65 01 00 00 00 其中scale是可以随便写的,00 01 10 11都行,因为index=100,不管你写什么,都视为index*scale=0. 2)嵌入到指令编码里面所有的立即数不是disp就是imme吗? 个别指令提供了moffs*的立即数寻址,比如mov指令: 此处moffs32等就是moffs*的立即数,可以直接编码在opcode后面,你可以把他们归为一类disp,但是这类disp不需要modr/m字节来引导出来。 比如mov eax,[0x1]就可以编码为以下形式: a)采用moffs32编码 opcode moffs32 A1 01 00 00 00 A1 01 00 00 00H b)采用MODR/M引导的disp opcode mod reg r/m displacement 8B 00 000 101 01 00 00 00H 8B 05 01 00 00 00H c)采用SIB引导的disp opcode mod reg r/m scale index base displacement 8B 00 000 100 01 100 101 01 00 00 00H 引导 scale随便写 8B 04 65 01 00 00 00H
[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!