首页
社区
课程
招聘
[翻译]The Svin 的OpCode教程(22楼提供doc和pdf版本下载)
发表于: 2008-6-13 18:37 72020

[翻译]The Svin 的OpCode教程(22楼提供doc和pdf版本下载)

2008-6-13 18:37
72020

说明:
    1. 这篇教程原文分为8帖,后继的会在不久的将来面世.
    2. 原帖并没有标题,标题是翻译的时候加上去的.
    3. 由于原作者是俄罗斯人,英语不是太好(有语法错误),有些语句我是根据自己的理解翻译过来的.
    4. 不知道有前辈翻译过这个没有?至少我在google上没有找到.
    5. 希望这个对你有用.

                            一 概览
    这本书为那些像本人一样渴求知识而不得的人而写.
    这套教程的风格是通过练习得出结论,而不是依据结论来做练习 .换句话说,这篇教程包含很多的例子和练习.深入学习本教程理论最好的方法是做所有的练习,以及试验所有的例子.
    这本教程并不讲汇编语言编程,我们讲的是"OpCode".

What "OpCode" is?
    这是课程要解决的主要问题,现在我给一个简短的回答.
    当我们在源代码中写入"lod**",在编译阶段汇编器(如 ml.exe)遇到"lod**",它会在可执行文件(或.obj文件)中用一个字节ACh来替代它.
    ACh就是所谓的OpCode,"lod**"则是助记符(mnemonic).
    助记符"lod**"对汇编器ml.exe说,"给我用字节ACh替换lod**".处理器并不知道lod**是什么.当寄存器EIP指向ACh字节时,处理器解码器对字节ACh解码,通过这个字节处理器就知道程序要将寄存器ESI指向的一个字节的内容送入AL寄存器.
    OpCode就是如此简单.

    你可能会说:一个助记符是某个特定的操作码的别称.
    但是,事实并没有如此简单.对于"lod**"这个特定的助记符来说,它确实对应ACh,并且操作码ACh也唯一确定助记符"lod**".然而并非所有的情形都是这样.我们很快会见到"为什么不"的例子.
    就像现实生活中一个名称往往并不唯一对应一个事物一样,OpCode和助记符的关系如下:不同的OpCode可能有同样的助记符;一个OpCode可能有几个助记符对应.有些事情可能很简单但却会吓倒初学者,在不久的将来我将会做详细的解释.

    查看OpCode或mnemonics的方法有很多种,我认为最快且最好的方法是使用调试器.我将在OllyDbg中试验所有的例子.

    Exersize1. 在调试器中输入助记符和OpCode
    1. 用OD打开任意一个Win32可执行文件,加载完成后在代码窗口中双击某一行,在弹出的指令输入对话框中输入lod**回车,你将看到(类似下面的内容):
       0040108C>  AC    LODS  [BYTE  DS:ESI]
       第一列(0040108c)是指令在内存中的地址,第二列(AC)是OpCode,第三列(LODS [BYTE DS:ESI])是OpCode AC的助记符.
    2. 你可以试着输入一些你其他的助记符,并观察他们的OpCode.
    3. 按CTRL + e,你将看到一个对话框,在HEX + 00 对应的文本框中输入 AC回车,可以看到第二列和第三列如下:
       AC    LODS  [BYTE  DS:ESI]
    4. 试着输入一些其他的OpCode,并观察对应的助记符.

    Exersize2. 一个助记符对应一个OpCode吗?
    1. 按CTRL+ e,输入OpCode 90,回车,可以看到OD将90认为是 NOP:
       0040108E  90    NOP
    2. 输入助记符 "NOP",同样看到:
       0040108E  90    NOP
    3. 输入助记符      XCHG  EAX, EAX
    糟糕! OllyDbg并没有插入我们的指令,而是用NOP代替!!!
    不用着急,这是OllyDbg的问题,当看到OpCode 90的时候它总是显示NOP,而不是 xchg eax,eax.对我们来说这两个助记符"xchg eax,eax"和"nop"都显示90h.
    由此可见,一个事物(OpCode)不只对应一个名字(助记符).

    我再重复一次:在计算机世界里有的只是OpCode,助记符只是这些OpCode的名字,这些助记符组成的系统就是汇编语言,助记符并不完美,因为没有完美的语言--任何语言的描述和现实总是有或多或少的区别的.

    在我们得出某些结论之前,我们先来做第二部分的练习.
    Exersize3.
    1. 输入助记符    add eax,1  ,你将看到
       0040108E  83C0 01      add  eax,1
    2. 输入OpCode   05 01000000  ,我们将惊奇的发现
       0040108E  05 01000000  add  eax,1
    同样的助记符但是不一样的OpCode!
    事实上它们不仅大小不同,结构也不一样.第一个(83C0 01)3字节长,包含3个域;第二个(05 01000000)5字节长,但只有两个域.

    在我们学习这些域之前,我们先来想一想:当你看着这两个OpCOde:
    83C0 01
    05 01000000
    你是否想到:
    第一个OpCode中 01和第二个OpCode中 01000000 是加到 EAX 的立即数?
    既然助记符向OpCode可以有多种转换的方式,那么到底什么OpCode将会被插入exe文件中?这是由什么决定的呢?
    答案是:这是由汇编器决定的.

    当我们在数据段中写入一些值时,我们往往会使用:somevar  db  0AH,0DH;而在代码段中我们不需要输入名字而只需定义值:db  ACH.所以你可以用16进制写代码段,比如用
    Mov    esi,offset  somedata          Mov    esi,offset comedata
    Lod**                         代替   db    ACH

    好了,又到了我断开网络的时候了,我必须把我所写发送出去,否则我什么也不能提交.下次我们将从OpCode的结构开始,只是一个介绍而已,还涉及OpCOde的一些重要而一般的特性.你将发现这是多么的易学和实用.

原文链接


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 8
支持
分享
最新回复 (55)
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
2
二 OpCode结构
    机器指令回答处理器3个问题:
    1. What to do ?
    2. With what to do ?
    3. How to do ?
    其中第三个问题是可选的.
    要回答这些问题,机器指令可能有多个逻辑块.在列举这些逻辑块之前我们先来做一些练习.

    为了更好的试验,在开始这些练习之前我们先来定制一个特别的程序:
    让程序变得小一点,这样OllyDbg可以更快的加载我们的程序;
    用一个字节的助记符填充它,这样就可以把代码段当作白纸一样使用;
    我们可以利用批处理文件或者设定快捷键等方式,更方便地加载我们的程序.

    下面是这个程序的源代码:
        .code
    start:
        rept 256
        nop
        endm
        call ExitProcess
    end start
    它将在程序中插入256 个 nop 指令.

    既然现在你已经知道在call  ExitProcess 之前有256个 90h ,我们是否可以用16进制来编码?
        .code
    start:
        rept 256
        db   90h
        endm
        call ExitProcess
    end start

    或者我们检验一下看看
    db   90h
    nop
    xchg eax,eax
    这三个是不是一样的.我们可以这样改写程序:
        .code
    start:
        rept 20
        nop
        endm
        rept 118
        xchg eax,eax
        endm
        rept 118
        db   90h
        endm
        call ExitProcess
    end  start
    当我们用OllyDbg加载编译好的可执行文件后,就可以看到在call  ExitProcess之前全部是90  nop.

    OpCode域简介
    有6个域是OpCode可能会用到的,它们的名字是什么这并不重要,重要的是它们的排列顺序.
    它们是:
    1. Prefixes
    2. Code
    3. byte Mod r/m
    4. Byte SIB
    5. Offset in command
    6. imm. Operand
    并不是这所有的6个域都会被用到,但是有一项却是一定会有的,那就是第2项 Code,有些指令甚至只会用到这一项.

    在试验程序中输入
    C3
    2F
    90
    AD
    所有输入的这些OpCode都只有Code域.
    (提示:它们对应的助记符依次为:retn, das,nop,lod**.)

    现在我们知道lod**的OpCode是ACh.我们来看看是不是可以增加额外的域来扩展它的功能呢.我们输入
    F3  AC
    可以看到
    rep  lod**.
    我们可以确定AC 是Code域,F3 AC 则是[Prefixes] [Code],所以F3是Prifixes域.F3表示的是Rep Prefixes,它也能与mov**, sto**等指令连用.
    现在请试着输入一些可以使用rep prefixes的助记符,并观察对应的OpCode;然后再在Ctrl+e窗口中输入这些以F3开头的OpCode,观察对应的助记符。

    我们首先应该记住:OpCode由6个域组成,它们不必都用上,但是Code是一定会有的.

    我们再来看六个BCD码运算指令,输入
    DAA
    DAS
    AAS
    AAA
    AAM
    AAD
    在Intel的文档中,这些都是只包含1个域的指令,这意味着它们都只有Code域.然而最后两条指令却是2字节的.我们可以通过观察这些指令,然后断定这些指令的格式和附加的域.
    先不要看下面的答案,试着自己想一想AAM和AAD和别的指令有什么不同……

    27              DAA
    2F              DAS
    3F              AAS
    37              AAA
    D4 0A           AAM
    D5 0A           AAD
    很明显我们可以看到:
    1. AAM和AAD都是2字节的,然而其余的4条指令都是1字节的.
    2. AAM和AAD的OpCode的第2 个字节都是0Ah
    不知你是否想起这两条指令的描述:
    AAM: divide  AL  by  10
    商   放在AH里
    余数 放在AL里
    AAD: AL  =  AL * 10 + AL
    两者的操作都与10 有关,而且它们的第二个字节都是 10(0Ah).那么0Ah会不会是偶然的呢?会不会是操作数的一部分呢?那AAM和AAD的指令格式会不会不是
    AAM:  D4  0A
    AAD:  D5  0A
    而是:
    AAM:  D4  imm8
    AAD:  D5  imm8
    以及 imm8 可以是任何别的值呢?

    我们可以验证一下,输入:
    Mov  al,8
    D4   07  ;作用和D40A相同,只是除以7而不是10
    如果我们是正确的,那么按F8单步执行刚输入的指令后 AH(商) = 1, AL(余数) = 1.

    现在,我们又知道了一种新的指令格式:
    [ CODE ] [ imm ] (域2 和 域6)
    同时我们也发现某些 OpCode 没有对应的助记符.在这里我得说一下,Oleh (OllyDbg的作者)允许使用立即数作为AAM和AAD的操作数.

    理解了OpCode 的规则,将有助于底层程序员明白一些鲜为人知的事情--一些未在文档上列出的OpCode,这也是掌握OpCode的另一个好处.

    之后我们将详细学习OpCode的每一个域.
    现在我们来看一些基本的规则:虽然并不是6个域都是必要的,但是,它们的排列顺序绝对不能乱,必须严格按照上面的顺序进行.有些域或许不会出现,但只要出现了,编号小的域就绝对不允许出现在编号大的域的后面,反之亦然.
    例如,[Prefixes] [code] 的顺序绝不允许变为[code] [Prefixes].

    输入
    0110
    和   
    1001
    你将看到它们的不同.
    (提示:0110 --add  dword ptr [eax], edx         1001--adc  byte ptr [ecx], al)

    下次我们将讨论处理器是如何确认OpCode的开始和结束的,我们也会看到 2 + 2是怎样等于3的.

原文第二帖
2008-6-13 18:45
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
3
三 开始和结束
    EB:imm8 --- near jmp
    Where commands start and end .why 2 + 2 = 3
    虽然这部分基于一些非常简单的事实,但并不是每一个程序员都能看到这些事实背后的本质.所以我们最好不要太过于轻视它.

    用OllyDbg 加载上次编写好的“Nop”程序.看着左列的指令地址,起始的指令地址是多少,在我的程序里是:
        00401000>        90        nop
    再看看右边的寄存器窗口,EIP的值又是什么,在我的程序里是
        EIP        00401000

    EIP寄存器指向内存中某个字节的地址,这个地址将被认为是某条指令的开始地址.当内存中的字节未被处理器加载并解码,还不知道这条指令到何处结束.
    现在我们按F8执行,可以看到
        00401001        90        nop
        EIP:                00401001
    EIP指向了内存中下一个字节并把它当作是指令.这里的下一个字节是指紧接上一条指令的结束地址的那个字节.如果你认为我没有写“EIP指向下一条指令”是因为我糟糕的英语,那你就错了.(尽管你并不是错在我糟糕的英语^_^(原作者是俄罗斯人)).
    如果一切正常的话,这些字节将被解码然后被当作下一条指令执行.事实上这些字节可能仅仅是一些垃圾,而处理器则不能正确的对其解码而产生异常.

    在程序的当前代码行(高亮显示的那一行)输入OpCode
         FFFF
    OllyDbg不会把它当作是一条指令,你将在助记符那一栏看到 “???” .同时EIP指向我们刚刚输入的FFFFh.所以如果不管这里的垃圾(对于处理器来说是“非法指令”)的话,处理器将解码并执行它.我们按下F8键,可以看到提示的错误信息(在OllyDbg下方的状态栏),处理器产生了异常.相应的,OllyDbg中断并询问如何继续.
    我们将FFFFh 替换为 9090h,然后输入不同的助记符,比如长指令或者使用lea指令,单步执行并仔细观察EIP 和指令长度的变化.你将看到处理器识别出指令的尺寸,且EIP现在指向紧接着刚输入的指令即nop.
    处理器是如何做到这个的呢?

    事实上处理器是通过解码来正确识别指令的.通过分析指令格式可以知道组成指令的域以及域的大小,不同的位指示了某个大小的域被使用.
    例如:短跳转指令EB: imm8 为两个字节,EB 是CODE域,当EIP指向的内容是EB 09,处理器通过解码第一个字节将得知EB 的指令长度是2个字节.

    所以问题的一个初步回答是:
        1. 开始:处理器认为当前EIP指向的内存单元中的第一个字节就是指令的开始.
        2. 结束:处理器通过对OpCode进行解码(大多数情况是根据[ code ]域),从而得知哪里是结束
        3. 如果指令不是“控制类型“的,那么下一条指令的开始将紧接当前指令的末尾.
    注意:除非被处理器加载,处理器是没有办法确定这些内容是否是真正的合法指令.

    现在回顾一下第三点,什么是“控制类型“指令?一般来说它们是ret,call,jmp,jcc,int等.它们之间的共同点是什么呢?不要回答我:”它们都跳转到某个地址“.或者更加糟糕的回答:”它们跳转到另外某条指令开始的地方“.
    我们在程序的当前行输入EB 00,并按F8执行.它将跳到哪里?我们看到处理器停在下一条指令开始的地方.我们再输入EB FE,运行,但是并没有跳转到任何地方,就像所有的事情都停止响应一样.就算是普通的指令执行后,也会步向下一条指令,而现在却既不跳转也不步进到下一条指令.

    真正的底层程序员应该理解指令的本质,而不单单从指令的字面意义上去理解它.真正的底层程序员不会说cmp指令比较操作数;他会说cmd指令是用第一个操作数减去第二个操作数,由此来设置相应的标志位.同时,我们关心的只是标志位,并不关心键操作后的结果,所以不需要把减操作的结果储存到第一个操作数中.这才是cmp指令的本质.
    如果知道了它是如何工作的,你就可以用cmp指令来做任何它擅长的事情,而不仅仅是比较.其实并没有什么cmp指令,有的只是简单的逻辑的,数学和数据转换\写入\读取的OpCode.

    让我们回到“控制指令”.它们真正做了什么样的操作呢?答案是:它们改变EIP寄存器.
    我们知道EIP指向的内存数据被处理器当作是指令的开始.
    通过这些控制指令我们又可以将什么值赋给EIP呢?
    答案是:如果你非常了解OpCode和这些指令的算法,那么可以是任何32位的值.所以我们可以强迫处理器把内存中任意某个字节当作指令的开始.

    输入OpCode       
        04 AC
    你将看到
        00401008        04 AC        ADD AL,0AC
    我们也知道ACh是lodsb的OpCode,地址00401008是OpCode 04 AC的起始地址,但是被地址00401009指向的指令是什么呢?如果我们使EIP指向00401009,那么处理器将不会把ACh当作是04:imm8的操作数,而是把它看做一个OpCode  ACh->lodsb?

    现在如果我们需要实现一个算法
        IF        ZF = 0
                Add        al,ACh
        ELSE
                Lodsb
    请试着用助记符写下这个算法.
    请看下面的汇编代码:
        Jne        $+1
        Add        al,ACh
    这就是我们所需要的.请不要试着运行它,除非你清楚的知道[esi]指向的地方是什么内容.
        如果ZF = 1,EIP将指向指令add        al,ACh的第二个字节,因为我们知道第二个字节的值是ACh,而操作码是ACh ->lodsb,我们实现这个算法仅用了4个字节!!!
        00401005        75 01            JNZ            SHORT
        00401007        04 AC            ADD            AL,0AC

    我们一起来看看各个字节都表示什么意思.
    75:imm 是75 01 的域格式,75是JNZ的OpCode,imm在这里是01,会被加到EIP里面去,整个7501表示:如果这条指令被执行了,则EIP会指向下一条指令的第二个字节.04 AC的域格式:04:imm,04->OpCode,AC->imm.两条指令都有两个域,格式为:,[ code ] [ imm ].
    如果ZF = 0,7501这条指令就会把EIP中下一条指令的起始地址 + 1,使得EIP指向下一条指令的第二个字节,处理器将认为AC所在的地址才是下一条指令的开始,这时AC将被认为是一个新的OpCode.
    否则EIP将指向04 AC所在的地址,开始的字节是04h,处理器将认出格式域:04:imm8(add  al,imm8),这时AC将被当成操作数,而不是操作码.

    现在我们看看谁是最快最聪明的程序员.
    1. 实现一个4字节的算法:
        IF                ZF = 1
                Inc        eax
        ELSE
                Mov        al,40h

    2. 另一个测试(5字节):
        IF                PF = 1
                Set all bits in        ax to 0
        ELSE
                Set all        bits in        eax to 0

    3. 再一个测试(5字节):
        IF                PF = 1
                Set all bits in        eax to 1
        ELSE
                Set all        bits in        ax to 1
    (Try it yourself)

    (1 提示:        je        $ + 1
                Mov        al,40h
                74h,01h,B0h,40h  )

    (2 提示:        jpe        $ + 3
                Xor        eax,eax
                74h,01h,66h,31h,C0h)

原文第三帖
2008-6-13 18:51
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
4
四 Prefixes 66h

    Prefixes
    用OllyDbg加载试验用程序,输入
     or          eax,-1
        Mov      ecx,edx
        And      ebx,ebp
    接着对上面每一个OpCode再按这种方式输入一次,如第一个OpCode
    第一个字节:      66
    然后是OpCode:    83F8FF  (Or    eax,-1)
    对剩余的两条指令用同样的方式输入.可以看到我们用这种方式产生的指令对3条指令都适用,除了16位寄存器指令,如
     Or          ax,-1
        Mov       cx,dx
        And       bx,bp

    66h就是所谓的Prefix.
    Prefixes是产生OpCode的第一个域.

    回忆一下OpCode的6个域:
   1. prefixes
    2. Code
    3. byte mod r/m
    4. byte sib
    5. offset in command
    6. imm. Operand
    记住:
        在实际的应用中,并不是所有的这6个域都会被用到,但是有一项是一定会有的,即第二项Code,这6个域的顺序绝对不能乱,必须严格按照上面的顺序进行.

    Prefixes是所有域中最容易理解的一个,请先明了它的一些特性:
    1. 它是唯一的一个可能出现在code之前的域
    2. 所有的Prefixes都只有一个字节
    3. 在一个OpCode中可能会有多个Prefixes

    Prefix  66 的意思是“切换默认的操作数的大小”.例如在有的系统中有两种默认的操作数大小:16位和32位.在Win32编程环境中默认的操作数的大小是32位,但操作数有可能会被写成16位或者32位,唯一的区分方法是看它有没有Prefix  66.

    我们来看一些例子,输入下面这些单字节指令:
        LODSB
        LODSW
        LODSD
    啊!LODSW和LODSD使用同样的code域AD!其实,LODSW和LODSD这两条指令是同一个指令,只不过它们的操作数的大小不一样而已—LODSW使用WORD(不是Win32默认操作数大小)作为操作数,而LODSD则使用DWORD(是Win32默认操作数大小)作为操作数.

    所以对于LODSW指令需要用prefix 66切换操作数大小.请注意我们并没有说“指定”而是说“切换”,反映到这个例子中,就是“切换默认的32位操作数到16位”,而不是“指定操作数的大小为16位“.如果默认的操作数大小是WORD,那么切换后就是DWORD;反之,如果默认的操作数大小是DWORD,那么切换后就是WORD.那些没有使用WORD或DWORD的指令(BYTES,QWORDS等),不会对操作数的大小做任何改变.

    输入:
      Mov        al,0ff
        Mov        al,cl
    接着在这两条指令之前加上66h,可以看到
     66:B0 FF           MOV        AL,0FF
        66:8A C1          MOV        AL,CL
    什么都没有被改变.

    记住:Prefix 66 仅对操作数为WORD和DWORD的指令起作用.

    我们在使用BYTE操作数的指令前加入prefix 66 ,是不是产生了一条非法指令呢?嗯,事实并不是这样子的.
    运行指令
     66:B0 FF            MOV       AL,0FF
        66:8A C1           MOV       AL,CL
    没有任何问题,它们和
     MOV        AL,0FF
        MOV        AL,CL
    的功能是一样的.

    如果Prefixes不能对随它之后的OpCode起作用,那么处理器将忽略它.

    我们来看看另一个有趣的例子.
    另外的一个Prefix  rep的作用是让处理器对随后的指令循环执行ecx(cx)次,指令
        Inc       eax的OpCode是40,我们来看看如何使用prefix  F3(rep)使inc      eax连续执行3次.
    在我们的试验程序里输入
      Xor       eax,eax
        Mov       ecx,3
        Rep       inc        eax
    然后运行.你将看到两点:
    1. 最后eax = 1,这意味着 prefix F3并没有起作用—它被忽略了;
    2. 没有任何异常(execption)产生.

    在OllyDbg里面我们可能会遇到以下两个问题:
    1. 如果你输入rep    inc    eax它将提示“无法识别的指令”,我们试下看,输入F3 40行不行.奥!OllyDbg还是没有正确的识别!~~如果按F8单步执行我们刚刚输入的指令,这样运行的结果是:
     1) Eax = 1
        2) 没有任何异常发生,处理器忽略了prefix F3.
    如果Prefixes不能对随它之后的OpCode起作用,那么处理器将忽略它.

    输入包含两个prefix的指令
    rep    lodsw   
      66: F3: AD      (REP    LODS  [WORD DS:ESI])
    在这条指令中我们看到两个prefix,66和F3.
    所以Prefixes域的另一个重要的特性是:一条指令可能只有一个CODE域,一个mod  r/m域,或者一个offset域等,但是可以有多个Prefixes.

    有关prefix 66h的最后的“新闻”是:
    你可能错误的认为在实模式下默认的操作数大小是WORD,而在保护模式下是DWORD,事实并不完全这样.我们唯一可以确定的是—当在Win32环境下默认操作数DWORD.因此,你在Win32下使用的任何操作字(WORD)的指令都会变长一个字节(prefix 66),并且需要多花一个时钟周期去执行.这个并不是一点好处也没有,当你习惯于计算数学问题,你可以通过取舍指令大小和运行速度来确定是不是用WORD更好一些.任何使用字的指令将多花你一个字节和一个用来解码的时钟周期.

    那么默认操作数大小是如何确定的呢—这是被段描述符的第0Dh位确定的.在实模式下它被赋值位0,所以在实模式下默认的操作数往往是WORD.在保护模式下可能是0或者1(在Win32应用程序中是1).
    所以:
        If (PROTECTED MODE  &&  BIT D = = 1)
                AD = LODSD
                66 AD = LODSW
        Else
                AD = LODSW
                66 AD = LODSD
    这就是我们之前所说的:不同的OpCode可以有同样的助记符,一个OpCode可能有多个不同的助记符.
    我们也看到某些OpCode可能有两种不同的功能,而这依赖一些条件.如果你认为指令可以和prefix 66配合使用,你将明白这里的条件和功能分别指的什么.

    下次我们继续学习OpCode的第一个域Prefixes.同时我推荐读者查找一下x86指令集查询手册,上面有所有的OpCode对应的助记符和指令构成.

原文第四帖
2008-6-13 18:54
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
5
五 Prefixes 67h,F2h,F3h

    在上一篇教程里我们学习了有关Prefixes的一般特性:
    1.所有的Prefixes都只有1个字节.
    2.在一个OpCode中可能会有多个Prefixes.
    3.如果Prefixes不能对随他之后的OpCode起作用,那么它会被处理器忽略.
    我们举例学习了Prefixes 66H的作用和用法—切换默认操作数大小.

    现在我们学习另外的Prefixes,看看它们是否有什么好玩的地方可以影响我们的应用程序的执行.
    Prefixes可以被划分为5个集合:
    1. Change default operand size (66h)
    2. Change default address size (67h)
    3. Rep. prefixes (F3,F2)
    4. Segnment specifying (change DEFAULT segment)(2e,36,3e,26,64,65)
    5. Bus lock (F0)

    Prefixes 66H
    我们已经在之前的教程里面学习过.
    一个测试题:
    下面哪一个指令可以在32位应用程序中拥有66H Prefix:
        scasb
        scasw
        scasd
    你可以在调试器里输入这些操作码来检验你的答案.
    (提示:答案是scasw)

    Prefixes 67H
    改变默认地址大小.
    输入助记符
        mov al,[eax]
    可以看到
        8A00           MOV AL,[BYTE DS:EAX]
    现在再输入以67H开头的OpCode.
        67:8A00        MOV AL,[BYTE DS:BX+SI]
    1.可以看到相应的字节已经被16位的寄存器bx, si寻址.在32位寻址模式中所有的数据都被32位的地址确定,包括32位基址,32位索引,32位偏移.在16位模式中这些都是16位的.
    2.可能你还注意到地址部分并不是变为mov al,[ax],而是变成了[bx][si].为什么呢?我们将在学习[mod r/m]和[sib]的时候给出答案.
    现在我们可以暂时认为,在16位地址模式中无法像32位地址模式一样使用所有的基址寄存器和索引寄存器,OpCode用来指定寻址的寄存器的位域也是不同的.
    不知道你是否需要常在32位环境下使用16位的寻址模式,不用担心,我们会深入的学习这个。在32位环境下使用16位的寻址模式可能会非常高效,不过这个需要你去控制总的地址范围不能超过0FFFF(一个内存页面的大小4kb)。

    Rep. Prefixes F2, F3
    如果你了解串操作指令(如movs, scas, lods 等),你一定知道什么时候使用rep\repe\repne前缀.
    一些串操作指令只能使用前缀rep(如movsb, lodsb----在计数器e(cx)=0的时候终止;另一些则可以使用repe或repne----在计数器变为0并且ZF标志位不满足前缀指定的条件的时候结束.换句话说repne在ZF=1而repe在ZF=0的时候终止.当然它们都会在满足ecx=0而当ZF=0或1的时候各有不同.

    有两点规则:
    1. 你可以看到有3种Rep prefixes助记符:rep,repe,repne,但是只有2个OpCode:F2,F3
    2. 如果某些指令只使用前缀rep,那么这里的rep可以用repe或者repne来代替.这种情况下
        Rep       Lodsb
        Repe      Lodsb
        Repne     Lodsb
    按照同样的方式运行:它们会重复运行指令LODSB共ecx(cx)次,而不管prefix是F2还是F3.

    我们可以验证一下:
        xor       eax,eax     ;使ZF = 0
        mov       esi,esp     ;esi指向栈顶
        mov       ecx,10
        repe      lodsb       ;opcode with repe and rep indentical - F3
        mov       esi,esp
        mov       ecx,10
        repne     lodsb
    按F7或F8运行后可以看到它们的作用是一样的.

    只有在可能会改变某些标志位的重复串指令时,F2和F3才会表现出不同,如scasb.在这种情况下,有些指令与重复前缀操作搭配使用,F2和F3会把最后一位与标志位ZF进行比较,如果它们不相同,则重复串指令的操作将会结束.而有些指令不用进行这个比较的操作,因此标志位ZF对这些指令的运行结果无影响.只需将F2(1111 0010)和F3(1111 0011)用二进制表示你就会明白我的意思.
    (提示:F2(1111 0010)的最后一位是0,指令执行的时候CPU会将F2的最后一位0与ZF标志位比较,如果此时e(cx)为0,并且ZF为1,与F2最后一位不同,则指令不在重复;否则重复。F3同理)

    下次我们将完成对prefixes的讨论并且我们将进一步学习改变EIP的OpCode.

原文第五帖
2008-6-14 10:28
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
6
六 Prefixes Segment override and LOCK

    这次我们将结束对prefixes的学习.
    剩下的两个Prefixes是"Segment override"和 "LOCK" prefixes.术语"Segment override"可能会让某些初学者感到困惑,但它却像是为Intel 指令系统定制的.

    打开我们之前的试验用程序.输入:
    Mov      eax,[ebx]
    可以看到
    8B03     MOV     EAX,[DWORD DS:EBX]
    输入
    Mov      eax,GS:[ebx]
    可以看到
    65:8B03  MOV     EAX,[DWORD GS:EBX]
    65就是一个"segment override prefix",用来改变默认的段为GS.事实上,在使用内存中的数据时,处理器必须首先知道它的段地址和偏移量,但是如果在每个地方都要显式的直接指出段地址,那么在OpCode格式中就必须额外增加一个域,这将会比现有的OpCode体系多占用大量的字节,而且要处理器必须多花费额外的时钟周期来进行解码—无论在空间上还是时间上,都不值得!

    因此,为了解决这个问题,一个方案诞生了.指令按不同的定义被划分为不同的组,每个组各自有一个默认的段:
    CS: for EIP pointer
    ES: 目的操作数是内存单元的串指令(movs, cmps等),在这里源操作数是储存在段DS里面.
    SS: 堆栈操作(push, pop等)
    DS: 剩下的数据操作指令.
    有了这个规则,处理器识别当前应该用哪个段将会变得非常简单而直接:如果有“Segment override prefix”,那么就使用这个prefix所指定的段;否则就使用默认的段.

    输入
        AC
        3E AC
    可以看到
        AC          LODS [BYTE DS:ESI]
        3E:AC       LODS [BYTE DS:ESI]
    3E是表示段DS,但是实际上在这里即使不直接指明3E,处理器也是会使用DS的,因为DS是指令LODS的默认段.

    对于程序员来说,使用非默认的段将多占用1个字节,并且多花费1个时钟周期去解码.对于其他的那些改变默认内容的指令也如此(如66,67).
    编写Win32用户态汇编程序往往不需要去改变段寄存器,但底层程序员可能会用到.现在我们总结一下Win32用户态模式段寄存器的内容:
    CS:对于所有的用户态程序这个是一样的,在NT系列操作系统中是1Bh,而在9x中则为227h.如果你记得关于绝对地址跳转的宏,我可以给出一个简单的绝对地址跳转指令格式,但这个在9x中不同.
    我们一起看看远跳转的OpCode.EA –byte "code",这个字节告诉处理器这是一个直接远跳转的OpCode,当处理器遇到EA,他将得知后面会跟着一个48位地址,低32位是偏移量而高16位确定段寄存器.

    既然现在我们知道对于NT代码段寄存器的值是1Bh,所以为了跳转到地址12345678h,我们可以这样编写代码:
        db     EA              ;long jump
        dd     12345678h       ;offset where to jump
        dw     1Bh             ;segment selector for code in NT
    对于9x段来说只是代码段选择子的值不同而已
        db     EA              ;long jump
        dd     12345678h       ;offset where to jump
        dw     227h            ;segment selector for code in 9x
    如果限制应用程序运行在特定的操作系统(NT或者9x),这样做很有好处:
        ;NT=1
        absjmp macro addr
        ifdef     NT
            db     0EAh
            dd     addr
            dw    01Bh
        else
            db     0EAh
            dd     addr
            dw     227h
        endif
        endm
    用法:
        absjump     401000h
    注释行NT = 1 依赖与你想使用这个绝对地址跳转的系统.

    可以试着在OllyDbg中写入一些跳转指令,比如你想跳转到一行,比如
        00401013  |. 90         NOP
    你可以输入
        EA 13 10 40 00
    剩下的两个字节在NT中是1B 00 ,而在9x中是27 02.

原文第六帖
2008-6-14 19:57
0
雪    币: 1844
活跃值: (35)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
7
一直没打断你帖子的顺序,现在可以顶了
2008-6-25 01:05
0
雪    币: 321
活跃值: (271)
能力值: ( LV13,RANK:1050 )
在线值:
发帖
回帖
粉丝
8
好文,出来顶,期待第七帖
2008-6-25 09:26
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
9
有少数语句不是很好翻译,后面有相应的原文以供对照.

    重要的一点是DS,SS,CS段都只是用户态段的别名.(Important thing to remember is that DS SS CS segment are alias segments in Win32 user mode app. )这意味着我们可以通过任一段寄存器寻址数据.
    如果你正试着做"String到Dwords的转换",你可以使用栈操作,当栈顶指针直接指向.data节,使用push可以直接将常量字符串放置在数据节.
    同样,使用.code节的数据可以这样子:
        .code
    msg db 'Some text',0
    start:
    invoke MessageBox,0,offset sometext,......
    另外,下面的代码完成的功能是一致的:
        .data
    somedw dd 12343
    ......
        .code
    ......
    mov eax,somedw
    mov eax,dword ptr offset somedw
    mov eax,dword ptr DS:offset somedw
    mov eax,dword ptr CS:offset somedw
    mov eax,dword ptr SS:offset somedw
    ......
    我们还须记住的是某些节上的数据可能受到保护,(可以在链接的时候使用/section:[sectionname] option选项改变).然而如果你可以用"ptr somedata"的方式寻址数据,那么你也一定可以任意使用DS,CS,SS作为段选择子.我们将在系统编程方面详细讨论这个.
   
    那么剩下的寄存器呢?
    我们先看ES,我们在串操作指令经常要关心它.
    在DOS时代我们经常要比较ES是否等于DS,来确认我们的源和目的是不是在同一个段.
    在Win32用户态编程中我们不需要这样,但这并不是说ES不再被串操作指令使用.

    我们开始试验之前先做点笔记:ES在包含源和目的两个内存操作数的串操作指令中仍然被使用.
    例如:movsb = move dword ptr DS:ESI to ES:EDI
那么如果对于上例包含两个内存操作数的指令运用Segment override,那么是哪个段选择子被改变呢?
    输入:
    A5 65 A5
    A5        movs  [dword es:edi], [dword ds:esi]
    65:A5     movs  [dword es:edi], [dword gs:esi]
我们看到Segment override改变了源.改变目的选择子是不被允许的!如果你想改变目的,你可以改变ES的值,而不是改变作为选择子的寄存器.所有的包含两个内存操作数的串操作指令都是如此.
    顺便说一下,如果你开始试着去查看OpCode,那么你肯定会看到所有的串操作指令都是单字节的,假设你不使用66 prefix去改变操作数大小,那么在速度允许的情况下,可以写出非常简洁的代码.

    我们先前说到,ES仍然在被使用,但是我们不需要特意的用DS去初始化它,当系统加载程序的时候就已经为我们做好这个了.然而你可能会轻易的破坏它的值然后糟糕的后果立马而至.
    我们看看选择子是如何影响串操作指令的.栈从高址往低址生长,这意味这我们可以使用比当前栈顶指针小的地址,而不会破坏栈内的数据(返回地址,参数等).例如,在OllyDbg里当前栈顶的内容为:
    0012FFC4   77F1B9EA  RETURN to KERNEL32.77F1B9EA
    0012FFC8   0012E2D4
    0012FFCC   77F92CD4  ntdll.77F92CD4
    0012FFD0   7FFDF000
所以我们可以使用0012FFC0以下的地址空间,需要注意的一点是默认的栈大小是1MB,当然可以在链接的时候使用相应的选项去改变这个值;并且不要使用开头的1kb,那是用来捕捉错误的.
    你可以使用一些push和pop指令观察栈的变化情况.

    现在我们给局部变量分配一些空间来检测一下movs对于不同的选择子是怎么样工作的.我们要做的是:
1.用短的字符串填充栈空间
2.以字节为单位将字符串拷贝到接近栈的区域,并且每一步我们指定不同的段寄存器
- to get impression that selectors in DS,SS,CS pointing to alias segment
- to check if ES is still matter by spoiling it in last step

    输入:
    lea  edi,[esp-4]
现在edi指向局部变量所在的空间,所以我们不用担心破坏栈中的返回值和参数等等.我们将使用小于等于esp-4的地址空间.
    输入助记符
    std
我们设置方向位为1,串操作指令将使用递减的地址.顺便说一下,当你在窗口回调函数中置方向位,请务必记得在返回前清方向位,因为回调函数是通过置DF=0为系统返回值的.(When you change DF to 1 in your window callback procedure - remember to set it to 0 before your proc returns - your proc returns to system code and the code assumes that DF=0)

    输入另一个助记符
    mov  al,5
    stosb
    dec  al
    输入OpCode
    75 FB
这些指令类似于下面的汇编源码:
    lea edi,[esp-4]
    std
    mov al,5
@@:   
    stosb
    dec al
    jne @B

   
    这里面有一个问题,短转指令的二进制格式是
        0111tttn:imm8
前四位0111确定了只是一个段跳转指令,在调试器里它表现为第一个十六进制位是7,接着的四位是用来确定条件的”tttn”位域,这和许多指令中用来测试标志位的格式是一样的.第二个字节是在跳转指令被解码后将被加到EIP的有符号数.
我们试着解码OpCode 75 FB:
    7 – 短条件跳转的标志
    5 – tttn 0101,0100 代表‘e’,zf = 0;0101 代表‘ne’,zf = 1;可以看到改变最后一位就相当于把条件改变位‘非’,这就是为什么我们叫它“tttn“.
    FB – 即-5,我们需要EIP会跳5个字节
1字节  AA             STOS [BYTE ES:EDI]
2字节 FEC8           DEC AL
2字节 75 FB          JNZ SHORT
------
5字节

    好,我们回到"register override".
    切换到数据窗口,在OllyDbg的Command文本框里输入命令 d  esp-4 ,并往上面滚动一行,这样好看到当前栈顶的内容,因为我们将用字符串来填充esp-4往下(地址变小)的空间.我们按F8单步执行,可以清楚的看到字节01 02 03 04 05 是如何存放的.
   
    我们首先来验证一下DS,CS,SS是否是段选择子的别名.(First we check if DS CS SS are alias semgent selectors)
    输入
    lea  esi,[edi+5]
这条指令执行后esi将指向之前我们构造的字符串(01 02 03 04 05).并且edi已经指向该字符串的末尾,这样我们就可以开始拷贝了.
    movsb的OpCode是 A4
    A4 = COPY FROM [BYTE DS:ESI] TO [BYTE ES:EDI]
    在A4前使用segment override prefix我们就能改变源选择子,这样的prefix有CS-2E,SS-36,ES-26.
   
    我们先看看默认的情况:
    输入
    A4
然后使用F8单步执行,并观察数据窗口内容的变化.

    现在我们看看CS的情形:
    输入
    2E A4
然后使用F8单步执行,并观察数据窗口内容的变化.
同样测试一下36和26.

    如果操作正确,你将可以看到字符串的头四个字节被成功拷贝,这表明(That shows that all SS DS CS ES
pointed to the same alias segment through different selectors.).
    你可以试一下别的segment override prefix,你得到的肯定是一个错误提示.
    我们用刚才字符串剩下的一个字节来测试一下ES.
    输入
    66 6A 00
即push word 0
    输入
    pop es
    A4
同样的,按F8键,当到达lodsb指令行时,OD的状态栏会提示异常.
    这表明ES仍然在双内存操作数串操作指令中被使用.我们可以输入
    push ds
    pop  es
来解决刚才的问题.
    执行这两条指令,然后输入A4,然后一切正常.

    值得提到的另一个段寄存器是FS,FS被用作异常处理.然而现在你记住这句话就好了,每当你使用FS操作数据的时候,将多消耗你一个字节以及一个时钟.
    当然使用这个寄存器要看你的意图是什么.

    下一个Prefix是LOCK.
    如果在Pentium 和 Pentium MMX下使用如F00FC7C8这样的指令,可以冻结处理器,在这里我仅仅引用其中的一些描述.如果你想了解这个可以去看Dr.Dobbs的文章.
    对于LOCK prefix我没有什么要说的,在Intel的手册里有很好的解释.在结束这部分之前我想提一些关于prefixes的特殊的用途,但是在P III下处理器会忽略掉这些,Intel提出在新的模型中那些特殊的用法可能有新的特殊意义,不正当的用法可能导致一些不可预料的结果.关于新一代Prefixes应该提到3E “hint” prefix.

    只有在多处理器系统才需要LOCK.当对内存操作数执行如下操作过程:
    1.读取内存
    2.在算术逻辑单元操作读取到的内存变量
    3.写回内存
这时才需要.
    可以看到在第1和第3阶段有一段间隔,当目前的处理器正在处理读取到的内存数据的时候,其他处理器也可以读取同一个内存数据,最后可能导致其中的一个被覆盖.而LOCK信号可以阻止任何其他处理器访问共享内存,直到指令将数据写回内存.大多在同步系统中被使用.
2008-7-9 13:00
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
10
哈哈,难得combojiang大侠喜欢~~
2008-7-9 13:01
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
11
在我们继续学习OpCode块中的域之前,记住一个非常重要且常被使用的域--"reg field"--是非常必要的.

    reg field在域mod r/m,sib,以及某些单字节OpCode内部被使用.Reg bit field有3位,所以可以包含2^3 = 8个值,对应8个通用寄存器.
    000 = EAX
    001 = ECX
    010 = EDX
    011 = EBX
    100 = ESP
    101 = EBP
    110 = ESI
    111 = EDI
    Reg field也可以对应一种不同集合的寄存器--"局部"寄存器(partial registers),我们将在后面碰到的时候再详细讨论这个。现在我们只需要记住:它的前四个值表示全寄存器(full registers)的低局部寄存器,
    000 = AL
    001 = CL
    010 = DL
    011 = BL
    后四个值则全寄存器的高局部寄存器,
    100 = AH
    101 = CH
    110 = DH
    111 = BH

    我们先看看只有全寄存器被使用的情况(在带reg field的单字节OpCode中).在这种指令中,高5位是CODE bitfield,低3位是REG field.
    我写了一个简单的用来解码带reg field单字节指令的程序,稍后我将给出有关它的详细文档.希望你们可以充分使用它,并能通过简单的浏览这些文档记住这所有的bit fields.

    在之前的教程里,我们用OllyDbg 就足以实践和讨论OpCode.
    现在我们来看关于指令的至关重要的一部分—地址.但这个问题关系到bit field.
    两个最重要的域mod r/m,sib都有bit field,你可以用大脑联想一下如何用一个或多个十六进制数字表示bit field.
    bit field的格式是: 2 : 3 : 3.这意味着高2位代表一个东西,低3位代表一个东西,中间的3位代表另一个东西.但我们将它用16进制表示,它又是2个十六进制数.因为每个十六进制位由四位组成,所以对于位格式2 : 3 : 3,高十六进制位由第一个域的两位和第二个域的前两位组成,剩余的位则组成低十六进制位.
    例如,在mod r/m中,如果要表示两个操作数都是寄存器,且寄存器分别是edx和edi,那么mod r/m的值是F9.如
    mov    edi,ecx的OpCode是8B F9,
    sub    edi,ecx是2B F9,
可以看到两个OpCode的modr/m相同都是F9.F9的二进制位格式是11 111 001,所以
    11  -- mod(11表示操作数都是寄存器)
    111 -- reg(111代表edi)
    001 -- 寄存器或者内存数(001代表ecx)
    关于mod r/m的另外一点是(两个操作数都是寄存器,且这寄存器是edx或者edi):也可以这样编码11 001 111,也就是 1100 1111即CF(可以看到只是111 和001的位置互换),希望这没有使你感到困惑.

    如果想通过mod r/m和sib创建任意地址,我们只要知道
    1.reg field 8个可能的值
    2.modr/m    4个可能的值
    3.sib       4个可能的值

    我们还需要一些简单通用的规律,和少数的几个Exeption.
    当我们学完这个,在输入指令的时候你就能够很容易的确定任意OpCode的大小(尺寸).OpCode中的地址部分是非常耗空间的.我们经常使用操作数,这意味着我们总是指定操作数或者说是它们的地址(包括寄存器).而Exeption是操作数预先定义的助记符,例如串操作指令的操作码都非常简短,这是因为它们没有地址部分.
    假如我们不了解OpCode的地址部分的话,我们无法断定指令长短或者尺寸.
    举个例子,下面两条指令实现同样的功能:
    1.  mov        edx,[ecx*4]
    2.  xor        eax,eax
        Mov        edx,[ecx*4][eax]
从表面看似乎第二个比第一个多一行,它的第二条指令甚至要多一个域,还多一些字符等等.好像似乎一定是第二种情况要长一些^_^.
    然而,这只是我们的主观臆断而已!
    第二个版本比第一个要少两个字节:
    8B 14 8D 00 00 00 00    MOV    EDX,[DWORD DS:ECX*4]

    33 C0                   XOR    EAX,EAX
    8B 14 88                MOV    EDX,[DWORD DS:EAX+ECX*4]
通过一些不是很艰苦的工作,我们也可以在很短的时间内学会这个.

    关于bit field的编码:虽然使用十六进制对位格式5 : 3或者2 : 3 : 3进行编码不是那么简单,但是在汇编源码中使用二进制是非常容易的.例如指令"inc reg"的CODE域是01000,剩下的3位是寄存器.因为
    eax = 000,所以
    inc   eax就是
    01000 000即40h.
    这里唯一的问题是,在位之间加入空格是不会被汇编器接受的,所以我们应该写作01000000b.但这种方式导致我们不易区分不同的bit fields.我们可以编写一个简单的宏(macro),用它来帮助我们轻易的分开这些bit fields.我们给这个宏取名bcr(a Byte OpCode with Code and Reg fields)
    Bcr     macro    _code,_reg
        db  _code & _reg & b
    endm
现在我们可以用下面的写法来代替db 01000000b
    Bcr  01000,000    ;inc    eax
    Bcr  01000,001    ;inc    ecx等等

    Bcr  01010,000    ;xchg    eax,eax 或者 nop
    Bcr  01010,001    ;xchg    eax,ecx
    ......
    Bcr  01010,111    ;xchg    eax,edi等等.

    同样的,我们可以编写一个宏帮助我们编码mod r/m以及sib,它们的格式是2 : 3 : 3.
    Modrm    macro    _mod, _regcode, _rm
        Db   _mod & _regcode & _rm & b
    Endm
由于sib有同样的格式所以我们可以定义
    bsib     EQU    modrm.
现在我们可以很容易的看出各个field:
    Modrm    11,000,101
    比如
    mov      reg1,reg2 的CODE是8Bh,跟在它后面的是mod r/m字节,其中mod field是11.
例如:
    Db       8bh
    Modrm    11,000,001        ;mov    eax,ecx(eax=000, ecx=001)
    Db       8bh
    Modrm    11,111,001        ;mov    edi, ecx(edi=111)
    Db       8bh
    Modrm    11,001,111        ;mov    ecx,edi
这里我举的例子只是说明使用二进制编码bit fields是没有问题的.
2008-7-10 23:30
0
雪    币: 321
活跃值: (271)
能力值: ( LV13,RANK:1050 )
在线值:
发帖
回帖
粉丝
12
多谢,已经拜读。
2008-7-17 13:49
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
13
哈哈,既然送了个精华,我又有继续翻译的动力啦
其实后面还有好大一篇,估计将来做成PDF或DOC得百来页也
而我只好慢慢翻译啦(要毕业了,琐事缠身)
2008-7-18 09:50
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
14
现在我们切实的学习这些fields的作用.
    我们从使用REG field的单字节指令开始,格式是5:3,高5位是CODE field,低3位是register field.
    这里有几个问题:
    1.为什么我们从REG field开始呢?
REG field在寻址中使用最多,学习它我们可以更深入的学习指令的地址部分.Reg field在mod r/m和sib中被用到.Mod r/m和sib在绝大多数OpCode中是用来确定指令的地址部分的,它们总共有6个域,其中的4个用来存放寄存器的值.
    2.为什么从CODE:REG 5 : 3格式的单字节OpCode开始?
首先这是使用了bit field的最简单的格式,如果习惯了使用二进制和十六进制,我们可以更容易处理2 : 3 : 3的格式.另外,在5 : 3格式中,只有一个操作数,这使得编码和解码更加容易.

    现在我们运行regfield.exe(在附件中).它一共有三个Tab页.
    第一个“Reference”.你可以通过它生成单字节OpCode,通过二进制和十六进制显示.还有一些用来选择指令和操作数的按钮.
    这里有一些小技巧:依次单击第一列按钮,你可以看到他们产生的CODE域:
    INC  - 01000
    DEC  - 01001
    PUSH - 01010
    POP  - 01011
    -----------
    XCHG - 10010
除了XCHG - 10010,其他的是从01000至01011按1递增的.
    而寄存器操作数也是同样的.
    EAX  - 000
    ECX  - 001
    EDX  - 010
    EBX  - 011
    ESP  - 100
    EBP  - 101
    ESI  - 110
    EDI  - 111

    第一个Tab页实际上是一个最简易的汇编器,虽然它只能翻译5种类型的指令,且只能使用全寄存器.而剩下的两个Tab也是类似的,"tell mnemonic"页是一个饭汇编器,而"tell opcode"则是汇编器.

    在tell mnemonic页中可以看到一些类似“Reference”页中的按钮.你可以按动这些按钮,如果你所按按钮代表的OpCode和页面上以二进制或十六进制显示的是一致的,那么你可以继续下一个,否则重试.
    一些小提示:如果第一个十六进制数是
    4   - inc/dec , reg
    5   - push/pop, reg
    9   - xchg eax, reg
    如果同一个数字对应两个OpCode,那么就要看第二位十六进制数大于等于8.

    最后一个Tab页则是要我们解码或者说是汇编助记符(mnemonics).依照界面的提示:
    第二行就是需要被汇编的汇编指令,如XCHG EAX,EDX;
    你可以按下第三行的相应按钮使其正好是XCHG EAX,EDX的OpCode 10010 010;
    或者是在第四行的黑色文本框中输入十六进制数92;
    然后单击“TEST”按钮,如果你的输入是正确的就可以继续下一个,否则重试.(但是第三行的按钮似乎不怎么管用)
    多多联系直到你可以在5分钟之类得到100个正确的回答并且没有任何错误提示^_^.

    inc dec push pop xchg eax,reg这些都是单字节指令.但并不是只有这些指令才是单字节指令.例如
    00001111:11001 reg(bswap reg)
    第一个字节只是告诉处理器接下来的OpCode属于“新”的指令集.在这条指令中有用的部分也只用一个字节,并且属于5 : 3的格式,那么最后的三位就是Reg field了.如
    bswap eax = 0FC8  (11001 000)
    bswap ecx = 0FC9  (11001 001)
    ....
    bswap edi = 0FCF  (11001 111)
    你可以试着汇编/反汇编一些包含只使用一个寄存器操作数的modr/m的指令.如mul reg,iul reg等.
    在你开始之前我们先说说16位的寄存器.你一定还记得prefix 66h吧.在32位模式中,我们只需在OpCode前加上66就可以了.如:
    INC EAX   = 40h
    INC AX    = 66h 40h
    DEC ECX   = 49h
    DEC CX    = 66h 49h
    ...
    BSWAP EDI = 0F CF
    BSWAP DI  = 66 0F CF

    我们一起看看局部寄存器.
    以MUL和IMUL指令为例,他们是:
    MUL    1111011w:11 100 reg
    IMUL   1111011w:11 101 reg
    我们首先看到的是,两条指令首字节相同,并且最低位都是同样的标记"w"--F7(w=1),F6(w=0).
    讨论局部寄存器,这是不的不讲的一位(bit).
    若 w = 1,reg field为全寄存器,否则reg field为局部寄存器.
    所以:
    w = 1               w = 0
    reg      value      reg
    EAX      000        AL
    ECX      001        CL
    EDX      010        DL
    EBX      011        BL

    ESP      100        AH
    EBP      101        CH
    ESI      110        DH
    EDI      111        BH
    一共有8个通用寄存器,其中的4个可以作为两个局部寄存器使用.
    你可能会问,reg field有3bit,加上w bit一共4位,2^4=16,刚好足够表示这16个寄存器,既然这样,那么可以给每一个寄存器(包括全寄存器和局部寄存器)分配一个数字代号,为什么不采用这种方式呢?
    这个问题问得非好!(但是我的午饭时间到了,留个悬念也好,哈哈,下次继续啊~~)
    需要用到的小程序: regfield.rar
上传的附件:
2008-7-18 12:01
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
不是我们不顶,是怕把贴的循序打乱了
2008-7-19 00:33
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
16
谢谢支持,由于最近回家休养,没有电脑,可能要到8月才会把文章全部翻译,到时会提供Doc和pdf版本的。慢的像蜗牛大家不要见怪
2008-7-20 20:07
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
17
首先解答上次的悬念.
    "w"位只影响寄存器而不影响指针(pointer,即地址),而指针只能是全寄存器.
    像reg,[reg][reg*4][displacement]这样的地址,所有寄存器可以使用的域都被使用了,但是这里只有一个操作数(即reg)可以是局部寄存器.
    code域指示了寄存器是指针与否.
    在现在的编码系统中,这4个域一共是4*3=12位,被两个字节的[modrm][sib]包含,也包括其中的scale和mod.如果给每一个reg域4 bit,那么仅仅reg域就占用了16bit,在加上scale和mod,那么每条指令都会相应的增大.

    现在你可能明白了汇编器是如何编码/汇编的了.
    看看下面的语句:
    byte  ptr ...
    word  ptr ...
    dword ptr ...
    大多数情况下(操作数默认为32位):
    byte  ptr - 汇编器置w位为 0
    word  ptr - 置w位为1且增加prefix 66h
    dword ptr - 置w位为1
    对于16位的操作数,一般都需要加prefix 66h

    我们回到MUL/IMUL reg opcode:
    MUL   1111011w:11 100 reg
    IMUL  1111011w:11 101 reg
    你一定发现第二个字节仅仅是中间的三位不同(以列分隔).第二个字节就是所谓的ModR/M,它的格式是2 : 3 : 3.
    bits  7,6   :mod
    bits  5,4,3 :code or reg
    bits  2,1,0 :mem or reg

    在OllyDbg中输入以下几条指令:   
    mov   reg,reg
    sub   reg,reg
    add   reg,reg
    and   reg,reg
    它们都是双字节PoCode,且第二个字节都是ModR/M.
    在输入一些使用同样寄存器操作数的指令如:
    mov   ecx,edi
    sub   ecx,edi
    add   ecx,edi
    and   ecx,edi
    你可能会看到他们的第二个字节是一致的.之所以说是可能,是因为这种指令可以有两种编码的方式,呆会你就会明白.
    (在OD中结果如下:
    8BCF  mov  ecx, edi
    2BCF  sub  ecx, edi
    03CF  add  ecx, edi
    21F9  and  ecx, edi

    8BF9  mov  edi, ecx
    2BF9  sub  edi, ecx
    03F9  add  edi, ecx
    21CF  and  edi, ecx ).

    将其中的第二个字节以二进制表示,并且以2 : 3 : 3的格式分隔.通过最后的两个3 bit我们可以看出使用的寄存器.以mov ecx,edi为例,
    8BCF  mov     ecx, edi
    第二个字节即11 001(ecx) 111(edi).
    在这个例子中,第二个字节用于指定寄存器,但并不总是这样,对于某些OpCode,它被用于附加的code位.MUL/iMUL reg就是如此:
    MUL   1111011w:11 [100] reg
    IMUL  1111011w:11 [101] reg
    对于1111011w,100意味着MUL,而101则为IMUL.

    你应该记住的是--如果你不练习,那我这些话对于你没有任何鸟用:
    在OD中输入一些MUL/IMUL指令的OpCode:
    使第一个字节是F7(bit w = 1),使用全寄存器;F6,局部寄存器.
    使第二字节的第一个十六进制数为E,第二个<8,code/reg的最低位为0,即code/reg为100,即MUl;>=8,code/reg的最低位为1,即code/reg为101,即IMUl.
    试着练习下.

    现在你肯定对MUL/IMUL指令比较顺手了,那么你可以试试DIV/IDIV指令:
    DIV   1111011w:11 110 reg
    IDIV  1111011w:11 111 reg
    可以看到只有code/reg域和MUL/IMUL的有些不一样,实际上对于这4条指令:
    code/reg  instruction
    100       MUL
    101       IMUL
    110       DIV
    111       IDIV
    code/reg域决定了这是什么指令(MUL,iMUL,DIV,iDIV).

    不仅仅是MUL,IMUL,DIV,IDIV reg,
    MUL,IMUL,DIV,IDIV [mem]也是如此:
    在OD中输入如下指令:
    MUL   EBX
    IMUL  EBX
    DIV   EBX
    IDIV  EBX
    MUL   [EBX]
    IMUL  [EBX]
    DIV   [EBX]
    IDIV  [EBX]
    将每条指令的第二个字节转化为2 : 3 : 3的格式,然后比较它们的异同.如:
    Second byte     instruction
    11 100 011      MUL EBX
    11 101 011      IMUL EBX
    ......
    (OD的结果如下:
    F7E3  mul     ebx
    F7EB  imul    ebx
    F7F3  div     ebx
    F7FB  idiv    ebx
    F723  mul     dword ptr [ebx]
    F72B  imul    dword ptr [ebx]
    F733  div     dword ptr [ebx]
    F73B  idiv    dword ptr [ebx] ).

    下一次我们将详细讨论modr/m和sib域.
2008-7-20 22:13
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
18
modr/m和sib用于指定操作数.

    我们应当记住的第一点是:
    可能只有modr/m,也可能既有modr/m又有sib;
    不可能只有sib而没有modr/m.
    或者你可以这样的认为sib是modr/m的扩展.
   
    我们应当记住的第二件事是:
    modr/m和sib都拥有同样的格式2 : 3 : 3.
    modr/m的高两位被称为mod,可以有2^2=4种值:
    11       - 没有内存操作数,即所有的操作数都是寄存器
    10,01,00 - 其中一个操作数是内存操作数,我们等下再讨论详细的情形.
    sib的高两位可以有2^2=4种值,对应着被索引寄存器(index register)相应的倍数1,2,4,8.
    modr/m和sib剩下的两个3 bit通常被寄存器用于寻址操作数,但我们稍后也会看到一些别的用途.

    第三就是:
    modr/m和sib能指定什么样的操作数,以及不能指定什么样的操作数.
    它们不能指定预定义的操作数.例如在串操作指令中是没有modr/m和sib字节的,以及MUL/DIV等的结果是预先定义的.
    它们不能指定立即数,因为在处理器看来并不是立即数而是OpCode的一部分.
    它们只能自定非预定义的寄存器操作数,以及带displacement的内存操作数.displacement紧随ModR/M和sib,它的大小是由ModR/M域指定的:
    00 - 无 displacement
    01 - 8  位的 displacement
    10 - 32 位的 displacement

    我们先来解答三个简单的问题:
    处理器是如何知道:
    1. OpCode有1个还是2个操作数?
    2. 什么样的寄存器集被ModR/M和sib使用,全寄存器还是局部寄存器?
    3. 若有两个操作数,那么哪一个是源,哪一个是目的?
    答案是:
    处理器是根据code域而不是ModR/M和sib域得知这些信息的.

    若指令只使用了寄存器操作数,即不使用内存操作数,modr/m的高两位是11,也就是这样:
    [8位code域]:11 *** ***
    例如 mov reg,reg:
    100010dw:11 reg reg
    所以第一个问题的答案是:
    处理器通过modr/m的高两位得知是1个还是2个操作数,若只有一个操作数,那么code/reg的低三位即寄存器操作数,中间的三位则是code域的扩展. 例:
    MUL EBX
    1111 011 1: 11 100 011
    看着第二个字节
    11 100 011
    11  - mod 域,只包含寄存器操作数
    100 - code/reg field,在这种情况下意味着只有一个操作数,即该域是code的扩展
    011 - EBX寄存器.在只有一个操作数的情况下,操作数一般放在modr/m的低三位.

    练习:
    在OD中输入一些OpCode,仅改变:
    1. code/reg
    2. mem/reg

    一般的,如果用到两个操作数,那么code/reg和mem/reg都被用来放置操作数.如:
    MOV  EAX,EBX
    1000 1011: 11 000 011
    第二个字节(modr/m)
    11   - 仅使用寄存器
    000  - EAX
    011  - EBX

    第二个问题:这是由code域的w位决定的.而且我们应该记住的是局部寄存器不能作为指针使用而只能作为寄存器.
    if w = 1
        全寄存器
    else if w = 0
        局部寄存器
    例:
    Mul reg
    1111 011w:11 100 reg
    w = 1
    1111 0111:11 100 001 = MUL ECX
    w = 0
    1111 0110:11 100 001 = MUL CL
    你也可以试试div,idiv,mul,imul,改变它们code域的最低位,看看使用的是什么寄存器集.
    又如:
    MOV reg,reg
    1000 101w: 11 reg reg
    w=1
    1000 1011: 11 000 001    MOV EAX,ECX
    w=0
    1000 1010: 11 000 001    MOV AL,CL
    它们的modr/m字节也是一样的,不同的仅仅是code域的w位.
    试试别的使用寄存器操作数的指令,如
    sub reg,reg
    add reg,reg
    改变它们的w位,并观察寄存器集合的变化.

    第三个问题:这是由code域的d 位(code字节的第1位,设从第0位开始计数)决定的.如:
    mov reg,reg
    1000 10dw:11 reg reg
    d = 1
    1000 1011:11 000 001 = MOV EAX,ECX
    d = 0
    1000 1001:11 000 001 = MOV ECX,EAX
    可以看到它们的modr/m字节也是一致的.正是由于d 位,导致MOV EAX,ECX有两种编码方式:
    1000 1011:11 000 001
    1000 1001:11 001 000
    这两个OpCode的功能是一致的.
    现在试着在OD中输入
    instr reg,reg
    然后更换编码的方式,例如
    add eax,ecx
    OD生成的OpCode是  03 C1,即
    0000 0011:11 000 001
    可以改成如下的编码方式:
    1. 将第一个字节的第2位(code域的d 位)置0
    0000 0001
    2. 然后交换第二个字节的bit 0-2和bit 3-5(modr/m的code/reg和mem/reg)
    11 001 000
    完整的OpCode即
    0000 0001:11 001 000
    十六进制为 01C8
    现在按Ctrl+e,然后在对话框中输入03 C1
    OD显示的助记符也是add eax,ecx,即03C1和01C8对应同样的指令.
    再看看他们的二进制格式:
    ADD EAX,ECX
    0000 0011:11 000 001 ;EAX=000 ECX=001
    0000 0001:11 001 000
2008-7-21 10:05
0
雪    币: 321
活跃值: (271)
能力值: ( LV13,RANK:1050 )
在线值:
发帖
回帖
粉丝
19
非常期待下文。
2008-7-21 13:06
0
雪    币: 203
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
不顶对不起,顶了打乱了顺序啊 ,顶了
2008-7-21 15:32
0
雪    币: 193
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
期待,期待啊..
2008-7-31 08:07
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
22
......
上传的附件:
2008-8-19 21:58
0
雪    币: 558
活跃值: (43)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
23
......
上传的附件:
2008-8-20 09:54
0
雪    币: 1844
活跃值: (35)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
24
收。。。。。。。。。。。。。。。。。。。。。。。。
2008-8-20 12:39
0
雪    币: 163
活跃值: (41)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
25
不顶不行。。。
2008-9-23 16:22
0
游客
登录 | 注册 方可回帖
返回
//