Intel CPU相关内容学习目录:
一.概述
CPU可以处理的数据只有0,1数据,C/C++编译以后生成的可执行文件保存在磁盘中是都是以二进制的形式保存的。如下图是用Winhex打开一个可执行文件的内容,由于二进制难以查看,所以Winhex以16进制的形式展示出来。
那也就是说,在没有源码情况下,得到一个可执行文件的时候,只能通过这些指令编码来识别程序的行为。但这样的指令编码是难以读懂的,所以需要对这些指令编码进行转换。如下图就是OD将左边的指令编码转成右边的可读的汇编语言:
而要知道如何将二进制保存的指令编码转换成可读的汇编语言,就需要使用intel提供的白皮书。在白皮书的第35页给出了下图,该图给出了intel的指令编码的格式,可以看出一条指令编码是由6个部分构成的。
其中各个部分的含义如下:
部分 | 含义 |
Instruction Prefixes | 可选的前缀指令。有4种类型的前缀指令,在一条指令编码种每种类型最多只能有一个 |
Opcode | 操作码,可以是1,2或3字节 |
ModR/M | 1字节,由Opcode决定是否存在 |
SIB | 1字节,由ModR/M决定是否存在 |
DisplaceMent | 可选的偏移指令,可以是1,2或4字节。是否存在以及存在的形式由ModR/M决定 |
Immediate | 可选的立即数,可以是1,2或4字节。是否存在以及存在的形式由OpCode决定的 |
由此可知,每一条指令编码的长度最长为15个字节,最短为1个字节。而指令的长度,很大程度上又是由操作码,也就是Opcode来决定的。所以,对操作码的解析是最重要的,白皮书中对操作码的说明在1475页中的TableA-2。
在这两张表中,最左列一列数字代表的是操作码的高4位,第一行的数字代表的是操作码的低4位,这样就可以找到不同的操作码所对应的汇编指令从而解析出指令。
二.前缀指令
前缀指令一共有4个类别,分别有不同的作用。
1.操作数据宽度的指令
这一类的前缀指令只有一个,那就是0x66。该前缀指令的作用是修改操作数的数据宽度,如下图是不包含该前缀指令的指令编码,其功能就是将32位的ebx寄存器中的内容入栈。
而当在该指令前面加入前缀指令0x66的时候,此时这条指令就会变成将16位的bx入栈,也就是说该前缀指令的作用就是将32位操作数的数据宽度改变为16位的。
2.地址宽度前缀指令
这一类指令也只有一个,那就是0x67。该前缀指令的作用是可以改变地址计算时候的宽度。如下下面这条指令,在没有前缀指令的时候,计算地址时候是用32位寄存器ecx计算。
而当该条指令编码加上0x67前缀指令的时候,地址的计算就变成使用16位的bx和di
3.段前缀指令
该类指令,主要是用来改变指令操作时候的数据段。通过使用不同的前缀指令,可以指定指令运行时候使用不同的数据段,具体数值如下:
数值 | 数据段 |
0x2E | CS |
0x36 | SS |
0x3E | DS |
0x26 | ES |
0x64 | FS |
0x65 | GS |
当没有使用这些前缀指令的时候,指令的运行过程中所使用的段都是默认的段,比如下面这两条指令编码所使用的段就是ss段
而通过这一类的前缀指令就可以修改默认的段
4.LOCK和REPEAT前缀指令
这类指令一共有三种:
指令 | 数值 |
LOCK | 0xF0 |
REPNE/REPNZ | 0xF2 |
REP/REPZ | 0xF3 |
其中的LOCK指令的作用是在多核CPU情况下,保证只有一个CPU可以访问指定的指令。
三.定长指令
根据上面的TableA-2可以得知,高位为0x4,0x5,0x7,0x9,0xB,0xC,0xE的时候,代表的就是定长指令,因为这类指令里面所写的内容并没有像Eb,Gv这样的内容。对于表中操作码上的i64,o64,d64则在1473页的Table A-1中有说明
而对于需要使用寄存器作为操作数的操作码来说,寄存器的名字说明了其宽度(64位,32位,16位,8位。这部分的说明在1469页的A.2.3中
当一个操作码需要一个特定的寄存器作为操作数时,该寄存器将通过名称(例如,AX、CL或ESI)进行标识。该名称表示寄存器是64、32、16还是8位宽。
当寄存器宽度取决于操作数大小属性时,使用eXX或rXX的寄存器标识符。当可能使用16或32位大小时,则可使用EXX;当可能使用16、32或64位大小时,将使用RXX。例如:eAX表示当操作数大小属性为16时使用AX寄存器,当操作数大小属性为32时使用EAX寄存器。RAX可以表示AX、EAX或RAX。
而对于这些寄存器的操作,随着操作码低4位数值的增加,按照EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI(32位),AL,CL,DL,BL,AH,CH,DH,BH的顺序增加。所以对于0x50-0x57的操作码来说,对应的指令为push eax, push ecx, push edx, push ebx,push esp, push ebp, push esi, push edi。对于0x58-0x5F则是pop eax, pop ecx, pop edx, pop ebx, pop esp, pop ebp, pop esi, pop edi。同理,对于0x40-0x47就是inc eax到inc edi,对于0x48-0x4F就是dec eax到dec edi。而对于0x90-0x97,其中0x90比较特殊,为nop指令,其他的依旧按照顺序为xchg eax, ecx到xchg eax, edi。
对于非寄存器的操作数来说,操作数是由形式为Zz的两个字符代码标识。第一个字符大写字母指定寻址方法;第二个字符小写字母指定操作数的类型。
其中寻址方式在1467页的A.2.1中给出了说明
而第二部分的操作数类型则在随后的A.2.2中给出了说明
由上内容可以得知:
0xB0-0xB7对应的操作就是将1字节立即数赋值到寄存器中,比如0xB0 0x12对应的是mov ax, 0x12。而对于0xB8-0xBF则是按照寄存器的顺序将4字节的内容赋值到寄存器中,比如0xB878563412对应的是 mov eax, 0x12345678,而0xBF 0x12345678则是 mov edi, 0x12345678。
0x70-0x7F,该类指令为JCC跳转指令+一个字节的偏移地址,偏移地址的计算公式是(目标地址-指令地址-2)。偏移地址以补码的形式保存,所以当偏移地址的大小在0x00-0x7F的时候,指令会向高地址跳转,而如果大小在0x80到0xFF的时候,指令就会向低地址跳转,如下图所示
对于操作码高位为0xE的这一类指令中,只有0xEA比较特殊,它的指令格式是JMP Ap,将会读取6个字节长度的数据作为直接地址,代表的是JMP CS:Id,其中的高2位赋值给CS,低4位赋值给了EIP。
还有高位为0xC的4个定长指令分别是0xC3,0xC2,0xCB,0xCA,所代表函数可以按照上面的方法得到。
另外还有一类定长指令是当操作码等于0x0F的时候,由TableA-2中可以知道,此时的操作码至少为2位
而对下一个操作码的解析需要查看1478页的TableA-3才可以得知。
在这一类指令中,最常见的就是高位为0x8的这一组指令,它由JCC以及后面的4个字节作为偏移作为指令,偏移地址依然等于目的地址-指令地址-6,同样采用补码形式,也就是说0x00000000-0x7FFFFFFF会往高地址跳转,而0x80000000-0xFFFFFFFF则会向低地址跳转。
由此可以得出结论,在Intel指令集中,有一部分指令只需要通过操作码就可以得知完整的指令长度以及指令的作用,此类指令便是定长指令。
四.变长指令
这一类指令是当查表发现是Eb这样的指令的时候,就说明此时的指令是变长指令,指令需要通过ModR/M进一步解析。比如0x00,此时对应的指令是ADD Eb, Gb,那么接下去的一个字节就是ModR/M字段,用它的值才能决定指令的操作。
根据上面A.2.1可以知道E和G的含义是:
那也就是说E可以是寄存器也可以是内存,而G只能是通用寄存器。而E和G代表的内容是什么,就需要对ModR/M进行解析。一字节的ModR/M分成了三个部分,各部分的名称以及占用的比特位如下:
名称 | 占用的比特位 |
R/M | 0-2 |
Reg/Opcode | 3-5 |
Mod | 6-7 |
其中的R/M和Mod字段,决定了E的内容,而Reg/Opcode则决定了G的内容。在32位下,它们具体是如何决定的,就需要查看白皮书中第41页得Table2-2
这张表最开始得一行说明了Reg/Opcode的值所对应的G代表的寄存器。随着Reg/Opcode的增加,按照寄存器的顺序,不同的Reg/Opcode代表了不同的寄存器。
剩下的内容,则是由Mod和R/M字段来决定E的内容。
当Mod为0x11(3)的时候,随着R/M的增加Eb代表的是不同的通用寄存器,此时依然是按通用寄存器的顺序来增加的。
比如0x00C0这条指令,此时根据0x00得知,指令的格式为ADD Eb, Gb,那么就需要将0xC0作为ModR/M进行解析,0xC0的二进制为11 000 000,此时Reg/Opcode为0,所以Gb是al,而Mod是0x11(3),且R/M字段是000,那么Eb就是al,所以最终的指令是add al, al。
当Mod不为0x11(3)的时候,指令格式就是另外三种,其中的disp8和disp32代表的是8位和32位偏移,当出现它们的时候需要在ModR/M字段后在读取1字节或者4字节用来作为偏移。
比如 0x008012345678这条指令,此时通过TableA-2可以知道,此时的指令格式是ADD Eb, Gb。此时就需要将随后1作为ModR/M来解析,0x80的2进制位为10 000 000,那也就是说此时Reg/Opcode的值是0,根据Table 2-2知道,此时的Gb表示的是al,而Mod字段为10,R/M字段为0,这就说明Eb对应的是[eax + disp32],此时就会继续读取4字节作为偏移。因此最终得到的指令就是add byte ptr ds:[eax + 0x78563412], al。
Mod和R/M字段的组合还有两种情况:
一种是Mod=00, R/M=101的时候,此时表示的Eb按道理应该是[ebp],但是[ebp]中保存的是上一个栈的ebp地址,这样的画这个指令就没有意义,所以Intel设计的时候,就把它删掉,改成了立即数。
另外一种是[--][--],当出现这种格式的时候,意味着ModR/M后面还有一个字节的SIB,要对SIB进行解析才能得到Eb的内容。一个字节的SIB和ModR/M一样被分成了三个部分,各个部分的名称以及占用的比特位如下:
名称 | 占用的比特位 |
Base | 0-2 |
Index | 3-5 |
Scale | 6-7 |
而这三个部分是如何决定Eb的内容,就需要查看白皮书42页的Table 2-3
第一行代表的是Base的值所对应的寄存器,除了101以外的值都代表了一个寄存器,此时这个寄存器在与Scale和Index组成的寄存器一起构成了Eb。而在Scale与Index组成起来代表的寄存器中,在Index=100的时候,此时的none代表没有寄存器,那么Eb的值就是由Base决定的。
对于0x000400这个指令,首先0x00代表了add Eb,Gb。接着将0x04拆分为二进制则是00 000 100,此时Reg/Opcode为000,所以Eb就是al,Mod为00,R/M是100,此时就需要继续将后面的0x00作为SIB,将SIB拆分以后就是00 000 000,此时Base为000,Scale为00,Index为000,所以Eb的内容就是[eax + eax],所以对这条指令解析的最终结果就就会是add byte ptr ds:[eax + eax], al。
而对于0x000420指令,由于此时SIB为0x20,拆成二进制以后是00 100 000,所以此时的Eb寄存器就是eax,那么这条指令的解析结果就是add byte ptr ds:[eax], al。
对于Base为101的情况下,需要指令继续解析就需要看白皮书的Table 2-3下面的NOTES
当出现[*]情况的时候,Eb的具体形式是要根据Mod的值来确定的:
Mod为00,则Eb为Scale与Index所代表的寄存器+32位的偏移
Mod为01,则Eb为Scale与Index代表的寄存器加+8位偏移+[ebp]
Mod为10,则Eb为Scale与Index所代表的寄存器+32位偏移+[ebp]
比如0x00040578563412,此时的指令格式是add Eb, Gb,ModR/M的值为0x04,对应二进制是00 000 100,Reg/Opcode为000,则Gb为al,而Mod为00,R/M为100,就需要将后一字节,也就是0x05作为SIB解析,0x05的二进制为00 000 101,此时Scale与Index表示为eax,而Base为101,而此时的Mod为00,所以Eb的形式就是[eax+disp32],就需要继续读取4个字节作为偏移,那么最后的结果就是add byte ptr ds:[eax + 0x12345678], al。
而指令如果是0x00840578563412,此时的ModR/M就是0x84,对应二进制是10 000 100,Reg/Opcode为000,Mod为10,R/M为100,SIB为0x05,由上可以知道,此时Scale和Index表示为eax,而Base则是101,因为此时的Mod为10,所以Eb的形式应该是[eax + disp32 + ebp],所以最终指令的解析结果就将是add byte ptr ss:[ebp + eax + 0x12345678], al。
因此可以得出结论,当查Table A-2的时候,操作数带有Eb,Ev,Gb这类的指令时候,就需要对Mod/RM继续解析,而Mod/RM字段又决定了是否有SIB,此时指令的长度是不可以只通过操作码来确定的。
五.操作码扩展
此时Table A-2的指令已经基本解析完成,除了像0x80-0x84这种带有Grp的指令
这类指令的操作数的格式是确定的,比如0x80对应的操作数的为Eb,Ib。但是并不知道操作码是怎么样的,此时的ModR/M字段中的Reg/Opcode就被用来识别指令的操作码,而要通过Reg/Opcode正确识别操作码,需要查看Intel白皮书1486页的Table A-6。
这张表的作用就是根据Mod/RM字段中的Reg/Opcode字段来查询操作码,第一列,说明了Opcode的值为多少,而右边的八列则是不同的Reg/Opcode的值所对应的不同的操作码。
对于0x80301F这条指令,0x80说明了操作数为Eb, Ib,而操作码是未定的,继续解析值为0x30的Mod/RM,0x30对应的二进制为00 110 000,此时Mod为00,R/M为000,所以Eb的值就是[eax],Reg/Opcode为110,根据Table A-6说明此时的操作码是xor,而剩下的Ib说明是一字节的立即数,就会将1A继续读取进来作为Ib,所以指令最后的解析结果是xor byte ptr ds:[eax], 0x1F。
六.扫尾知识
常见的指令基本都解析完了,在这本表皮书中最主要的的表是Table A-2,这张表是用来查询Opcode的,不同的Opcode有着不同的指令形式。前面有说过当Opcode为0x0F的时候,操作码至少有两个,需要查Table A-3才能解析指令。
而在Table A-3中可以知道,当第二个操作码是0x38或者0x3A的时候,此时的操作码就是3个
而要知道第三个操作码的作用就需要1481页的Table A-4
以及1483页的Table A-5
由于这些指令并不常见,所以需要的时候查询就好了。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2022-1-8 12:38
被1900编辑
,原因: