• 第18章-x86指令集之常用指令


    x86的指令集可分为以下4种:

    1. 通用指令
    2. x87 FPU指令,浮点数运算的指令
    3. SIMD指令,就是SSE指令
    4. 系统指令,写OS内核时使用的特殊指令

    下面介绍一些通用的指令。指令由标识命令种类的助记符(mnemonic)和作为参数的操作数(operand)组成。例如move指令:

    指令 操作数 描述
    movq I/R/M,R/M 从一个内存位置复制1个双字(64位,8字节)大小的数据到另外一个内存位置
    movl I/R/M,R/M 从一个内存位置复制1个字(32位,4字节)大小的数据到另外一个内存位置
    movw I/R/M, R/M 从一个内存位置复制2个字节(16位)大小的数据到另外一个内存位置
    movb I/R/M, R/M 从一个内存位置复制1个字节(8位)大小的数据到另外一个内存位置

    movl为助记符。助记符有后缀,如movl中的后缀l表示作为操作数的对象的数据大小。l为long的缩写,表示32位的大小,除此之外,还有b、w,q分别表示8位、16位和64位的大小。

    指令的操作数如果不止1个,就将每个操作数以逗号分隔。每个操作数都会指明是否可以是立即模式值(I)、寄存器(R)或内存地址(M)。

    另外还要提示一下,在x86的汇编语言中,采用内存位置的操作数最多只能出现一个,例如不可能出现mov M,M指令。

    通用寄存器中每个操作都可以有一个字符的后缀,表明操作数的大小,如下表所示。

    C声明 通用寄存器后缀 大小(字节)
    char b 1
    short w 2
    (unsigned) int / long / char* l 4
    float s 4
    double l 5
    long double t 10/12

    注意:通用寄存器使用后缀“l”同时表示4字节整数和8字节双精度浮点数,这不会产生歧义,因为浮点数使用的是完全不同的指令和寄存器。

    我们后面只介绍call、push等指令时,如果在研究HotSpot VM虚拟机的汇编遇到了callq,pushq等指令时,千万别不认识,后缀就是表示了操作数的大小。

    下表为操作数的格式和寻址模式。

    格式

    操作数值

    名称

    样例(通用寄存器 = C语言)

    $Imm

    Imm

    立即数寻址

    $1 = 1

    Ea

    R[Ea]

    寄存器寻址

    %eax = eax

    Imm

    M[Imm]

    绝对寻址

    0x104 = *0x104

    (Ea)

    M[R[Ea]]

    间接寻址

    (%eax)= *eax

    Imm(Ea)

    M[Imm+R[Ea]]

    (基址+偏移量)寻址

    4(%eax) = *(4+eax)

    (Ea,Eb)

    M[R[Ea]+R[Eb]]

    变址

    (%eax,%ebx) = *(eax+ebx)

    Imm(Ea,Eb)

    M[Imm+R[Ea]+R[Eb]]

    寻址

    9(%eax,%ebx)= *(9+eax+ebx)

    (,Ea,s)

    M[R[Ea]*s]

    伸缩化变址寻址

    (,%eax,4)= *(eax*4)

    Imm(,Ea,s)

    M[Imm+R[Ea]*s]

    伸缩化变址寻址

    0xfc(,%eax,4)= *(0xfc+eax*4)

    (Ea,Eb,s)

    M(R[Ea]+R[Eb]*s)

    伸缩化变址寻址

    (%eax,%ebx,4) = *(eax+ebx*4)

    Imm(Ea,Eb,s)

    M(Imm+R[Ea]+R[Eb]*s)

    伸缩化变址寻址

    8(%eax,%ebx,4) = *(8+eax+ebx*4)

    注:M[xx]表示在存储器中xx地址的值,R[xx]表示寄存器xx的值,这种表示方法将寄存器、内存都看出一个大数组的形式。

    汇编根据编译器的不同,有2种书写格式:

    (1)Intel : Windows派系
    (2)AT&T: Unix派系

    下面简单介绍一下两者的不同。

    下面就来认识一下常用的指令。

    下面我们以给出的是AT&T汇编的写法,这两种写法有如下不同。 

    1、数据传送指令

    将数据从一个地方传送到另外一个地方。

    1.1 mov指令

    我们在介绍mov指令时介绍的全一些,因为mov指令是出现频率最高的指令,助记符中的后缀也比较多。

    mov指令的形式有3种,如下:

    mov   #普通的move指令
    movs  #符号扩展的move指令,将源操作数进行符号扩展并传送到一个64位寄存器或存储单元中。movs就表示符号扩展 
    movz  #零扩展的move指令,将源操作数进行零扩展后传送到一个64位寄存器或存储单元中。movz就表示零扩展

    mov指令后有一个字母可表示操作数大小,形式如下:

    movb #完成1个字节的复制
    movw #完成2个字节的复制
    movl #完成4个字节的复制
    movq #完成8个字节的复制
    

    还有一个指令,如下:

    movabsq  I,R
    

    与movq有所不同,它是将一个64位的值直接存到一个64位寄存器中。  

    movs指令的形式如下:

    movsbw #作符号扩展的1字节复制到2字节
    movsbl #作符号扩展的1字节复制到4字节
    movsbq #作符号扩展的1字节复制到8字节
    movswl #作符号扩展的2字节复制到4字节
    movswq #作符号扩展的2字节复制到8字节
    movslq #作符号扩展的4字节复制到8字节
    

    movz指令的形式如下:  

    movzbw #作0扩展的1字节复制到2字节
    movzbl #作0扩展的1字节复制到4字节
    movzbq #作0扩展的1字节复制到8字节
    movzwl #作0扩展的2字节复制到4字节
    movzwq #作0扩展的2字节复制到8字节
    movzlq #作0扩展的4字节复制到8字节
    

    举个例子如下:

    movl   %ecx,%eax
    movl   (%ecx),%eax
    

    第一条指令将寄存器ecx中的值复制到eax寄存器;第二条指令将ecx寄存器中的数据作为地址访问内存,并将内存上的数据加载到eax寄存器中。 

    1.2 cmov指令

    cmov指令的格式如下:

    cmovxx
    

    其中xx代表一个或者多个字母,这些字母表示将触发传送操作的条件。条件取决于 EFLAGS 寄存器的当前值。

    eflags寄存器中各个们如下图所示。

    其中与cmove指令相关的eflags寄存器中的位有CF(数学表达式产生了进位或者借位) 、OF(整数值无穷大或者过小)、PF(寄存器包含数学操作造成的错误数据)、SF(结果为正不是负)和ZF(结果为零)。

    下表为无符号条件传送指令。

     指令对 描述  eflags状态 
    cmova/cmovnbe 大于/不小于或等于  (CF或ZF)=0 
    cmovae/cmovnb  大于或者等于/不小于 CF=0 
    cmovnc  无进位  CF=0 
    cmovb/cmovnae  大于/不小于或等于  CF=1
    cmovc  进位 CF=1
    cmovbe/cmovna  小于或者等于/不大于 (CF或ZF)=1
    cmove/cmovz  等于/零 ZF=1
    cmovne/cmovnz  不等于/不为零 ZF=0 
    cmovp/cmovpe 奇偶校验/偶校验 PF=1 
    cmovnp/cmovpo 非奇偶校验/奇校验  PF=0 

     无符号条件传送指令依靠进位、零和奇偶校验标志来确定两个操作数之间的区别。

    下表为有符号条件传送指令。

    指令对

    描述

    eflags状态

    cmovge/cmovnl

    大于或者等于/不小于

    (SF异或OF)=0

    cmovl/cmovnge

    大于/不大于或者等于

    (SF异或OF)=1

    cmovle/cmovng

    小于或者等于/不大于

    ((SF异或OF)或ZF)=1

    cmovo

    溢出

    OF=1

    cmovno

    未溢出

    OF=0

    cmovs

    带符号(负)

    SF=1

    cmovns

    无符号(非负)

    SF=0

    举个例子如下:

    // 将vlaue数值加载到ecx寄存器中
    movl value,%ecx 
    // 使用cmp指令比较ecx和ebx这两个寄存器中的值,具体就是用ecx减去ebx然后设置eflags
    cmp %ebx,%ecx
    // 如果ecx的值大于ebx,使用cmova指令设置ebx的值为ecx中的值
    cmova %ecx,%ebx 
    

    注意AT&T汇编的第1个操作数在前,第2个操作数在后。    

    1.3 push和pop指令 

    push指令的形式如下表所示。 

    指令

    操作数

    描述

    push

    I/R/M

    PUSH 指令首先减少 ESP 的值,再将源操作数复制到堆栈。操作数是 16 位的,

    则 ESP 减 2,操作数是 32 位的,则 ESP 减 4

    pusha

    指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI)将 16 位通用寄存器压入堆栈。

    pushad

    指令按照 EAX、ECX、EDX、EBX、ESP(执行 PUSHAD 之前的值)、

    EBP、ESI 和 EDI 的顺序,将所有 32 位通用寄存器压入堆栈。

    pop指令的形式如下表所示。 

    指令

    操作数

    描述

    pop

    R/M

    指令首先把 ESP 指向的堆栈元素内容复制到一个 16 位或 32 位目的操作数中,再增加 ESP 的值。

    如果操作数是 16 位的,ESP 加 2,如果操作数是 32 位的,ESP 加 4

    popa

    指令按照相反顺序将同样的寄存器弹出堆栈

    popad

    指令按照相反顺序将同样的寄存器弹出堆栈

     

     1.4 xchg与xchgl

    这个指令用于交换操作数的值,交换指令XCHG是两个寄存器,寄存器和内存变量之间内容的交换指令,两个操作数的数据类型要相同,可以是一个字节,也可以是一个字,也可以是双字。格式如下:

    xchg    R/M,R/M
    xchgl   I/R,I/R、  
    

    两个操作数不能同时为内存变量。xchgl指令是一条古老的x86指令,作用是交换两个寄存器或者内存地址里的4字节值,两个值不能都是内存地址,他不会设置条件码。

    1.5 lea

    lea计算源操作数的实际地址,并把结果保存到目标操作数,而目标操作数必须为通用寄存器。格式如下:

    lea M,R

    lea(Load Effective Address)指令将地址加载到寄存器。

    举例如下:

    movl  4(%ebx),%eax
    leal  4(%ebx),%eax  
    

    第一条指令表示将ebx寄存器中存储的值加4后得到的结果作为内存地址进行访问,并将内存地址中存储的数据加载到eax寄存器中。

    第二条指令表示将ebx寄存器中存储的值加4后得到的结果作为内存地址存放到eax寄存器中。

    再举个例子,如下:

    leaq a(b, c, d), %rax 
    

    计算地址a + b + c * d,然后把最终地址载到寄存器rax中。可以看到只是简单的计算,不引用源操作数里的寄存器。这样的完全可以把它当作乘法指令使用。  

    2、算术运算指令

    下面介绍对有符号整数和无符号整数进行操作的基本运算指令。

    2.1 add与adc指令

    指令的格式如下:

    add  I/R/M,R/M
    adc  I/R/M,R/M
    

    指令将两个操作数相加,结果保存在第2个操作数中。

    对于第1条指令来说,由于寄存器和存储器都有位宽限制,因此在进行加法运算时就有可能发生溢出。运算如果溢出的话,标志寄存器eflags中的进位标志(Carry Flag,CF)就会被置为1。

    对于第2条指令来说,利用adc指令再加上进位标志eflags.CF,就能在32位的机器上进行64位数据的加法运算。

    常规的算术逻辑运算指令只要将原来IA-32中的指令扩展到64位即可。如addq就是四字相加。  

    2.2 sub与sbb指令

    指令的格式如下:

    sub I/R/M,R/M
    sbb I/R/M,R/M
    

    指令将用第2个操作数减去第1个操作数,结果保存在第2个操作数中。

    2.3 imul与mul指令

    指令的格式如下:

    imul I/R/M,R
    mul  I/R/M,R
    

    将第1个操作数和第2个操作数相乘,并将结果写入第2个操作数中,如果第2个操作数空缺,默认为eax寄存器,最终完整的结果将存储到edx:eax中。

    第1条指令执行有符号乘法,第2条指令执行无符号乘法。

    2.4 idiv与div指令

    指令的格式如下:

    div   R/M
    idiv  R/M
    

    第1条指令执行无符号除法,第2条指令执行有符号除法。被除数由edx寄存器和eax寄存器拼接而成,除数由指令的第1个操作数指定,计算得到的商存入eax寄存器,余数存入edx寄存器。如下图所示。

        edx:eax
    ------------ = eax(商)... edx(余数)
        寄存器
    

    运算时被除数、商和除数的数据的位宽是不一样的,如下表表示了idiv指令和div指令使用的寄存器的情况。

    数据的位宽 被除数 除数

    余数

    8位 ax 指令第1个操作数 al ah
    16位 dx:ax 指令第1个操作数 ax dx
    32位 edx:eax 指令第1个操作数 eax edx

    idiv指令和div指令通常是对位宽2倍于除数的被除数进行除法运算的。例如对于x86-32机器来说,通用寄存器的倍数为32位,1个寄存器无法容纳64位的数据,所以 edx存放被除数的高32位,而eax寄存器存放被除数的低32位。

    所以在进行除法运算时,必须将设置在eax寄存器中的32位数据扩展到包含edx寄存器在内的64位,即有符号进行符号扩展,无符号数进行零扩展。

    对edx进行符号扩展时可以使用cltd(AT&T风格写法)或cdq(Intel风格写法)。指令的格式如下:

    cltd  // 将eax寄存器中的数据符号扩展到edx:eax
    

    cltd将eax寄存器中的数据符号扩展到edx:eax。 

    2.5 incl与decl指令

    指令的格式如下:

    inc  R/M
    dec  R/M 
    

    将指令第1个操作数指定的寄存器或内存位置存储的数据加1或减1。

    2.6 negl指令

    指令的格式如下:

    neg R/M
    

    neg指令将第1个操作数的符号进行反转。  

    3、位运算指令

    3.1 andl、orl与xorl指令 

    指令的格式如下:

    and  I/R/M,R/M
    or   I/R/M,R/M
    xor  I/R/M,R/M
    

    and指令将第2个操作数与第1个操作数进行按位与运算,并将结果写入第2个操作数;

    or指令将第2个操作数与第1个操作数进行按位或运算,并将结果写入第2个操作数; 

    xor指令将第2个操作数与第1个操作数进行按位异或运算,并将结果写入第2个操作数; 

    3.2 not指令 

    指令的格式如下:

    not R/M
    

    将操作数按位取反,并将结果写入操作数中。

    3.3 sal、sar、shr指令

    指令的格式如下:

    sal  I/%cl,R/M  #算术左移
    sar  I/%cl,R/M  #算术右移
    shl  I/%cl,R/M  #逻辑左移
    shr  I/%cl,R/M  #逻辑右移
    

    sal指令将第2个操作数按照第1个操作数指定的位数进行左移操作,并将结果写入第2个操作数中。移位之后空出的低位补0。指令的第1个操作数只能是8位的立即数或cl寄存器,并且都是只有低5位的数据才有意义,高于或等于6位数将导致寄存器中的所有数据被移走而变得没有意义。

    sar指令将第2个操作数按照第1个操作数指定的位数进行右移操作,并将结果写入第2个操作数中。移位之后的空出进行符号扩展。和sal指令一样,sar指令的第1个操作数也必须为8位的立即数或cl寄存器,并且都是只有低5位的数据才有意义。

    shl指令和sall指令的动作完全相同,没有必要区分。

    shr令将第2个操作数按照第1个操作数指定的位数进行右移操作,并将结果写入第2个操作数中。移位之后的空出进行零扩展。和sal指令一样,shr指令的第1个操作数也必须为8位的立即数或cl寄存器,并且都是只有低5位的数据才有意义。

    4、流程控制指令

    4.1 jmp指令

    指令的格式如下:

    jmp I/R
    

    jmp指令将程序无条件跳转到操作数指定的目的地址。jmp指令可以视作设置指令指针(eip寄存器)的指令。目的地址也可以是星号后跟寄存器的栈,这种方式为间接函数调用。例如:

    jmp *%eax
    

    将程序跳转至eax所含地址。

    4.2 条件跳转指令

    条件跳转指令的格式如下:

    Jcc  目的地址
    

    其中cc指跳转条件,如果为真,则程序跳转到目的地址;否则执行下一条指令。相关的条件跳转指令如下表所示。

    指令

    跳转条件

    描述

    指令

    跳转条件

    描述

    jz

    ZF=1

    为0时跳转

    jbe

    CF=1或ZF=1

    大于或等于时跳转

    jnz

    ZF=0

    不为0时跳转

    jnbe

    CF=0且ZF=0

    小于或等于时跳转

    je

    ZF=1

    相等时跳转

    jg

    ZF=0且SF=OF

    大于时跳转

    jne

    ZF=0

    不相等时跳转

    jng

    ZF=1或SF!=OF

    不大于时跳转

    ja

    CF=0且ZF=0

    大于时跳转

    jge

    SF=OF

    大于或等于时跳转

    jna

    CF=1或ZF=1

    不大于时跳转

    jnge

    SF!=OF

    小于或等于时跳转

    jae

    CF=0

    大于或等于时跳转

    jl

    SF!=OF

    小于时跳转

    jnae

    CF=1

    小于或等于时跳转

    jnl

    SF=OF

    不小于时跳转

    jb

    CF=1

    大于时跳转

    jle

    ZF=1或SF!=OF

    小于或等于时跳转

    jnb

    CF=0

    不大于时跳转

    jnle

    ZF=0且SF=OF

    大于或等于时跳转

    4.3 cmp指令

    cmp指令的格式如下:

    cmp I/R/M,R/M
    

    cmp指令通过比较第2个操作数减去第1个操作数的差,根据结果设置标志寄存器eflags中的标志位。cmp指令和sub指令类似,不过cmp指令不会改变操作数的值。

    操作数和所设置的标志位之间的关系如表所示。

    操作数的关系 CF ZF OF
    第1个操作数小于第2个操作数 0 0 SF
    第1个操作数等于第2个操作数 0 1 0
    第1个操作数大于第2个操作数 1 0 not SF


    4.4 test指令

    指令的格式如下:

    test I/R/M,R/M
    

    指令通过比较第1个操作数与第2个操作数的逻辑与,根据结果设置标志寄存器eflags中的标志位。test指令本质上和and指令相同,只是test指令不会改变操作数的值。

    test指令执行后CF与OF通常会被清零,并根据运算结果设置ZF和SF。运算结果为零时ZF被置为1,SF和最高位的值相同。

    举个例子如下:

    test指令同时能够检查几个位。假设想要知道 AL 寄存器的位 0 和位 3 是否置 1,可以使用如下指令:

    test al,00001001b    #掩码为0000 1001,测试第0和位3位是否为1
    

    从下面的数据集例子中,可以推断只有当所有测试位都清 0 时,零标志位才置 1:

    0  0  1  0  0  1  0  1    <- 输入值
    0  0  0  0  1  0  0  1    <- 测试值
    0  0  0  0  0  0  0  1    <- 结果:ZF=0
    
    0  0  1  0  0  1  0  0    <- 输入值
    0  0  0  0  1  0  0  1    <- 测试值
    0  0  0  0  0  0  0  0    <- 结果:ZF=1
    

    test指令总是清除溢出和进位标志位,其修改符号标志位、零标志位和奇偶标志位的方法与 AND 指令相同。

    4.5 sete指令

    根据eflags中的状态标志(CF,SF,OF,ZF和PF)将目标操作数设置为0或1。这里的目标操作数指向一个字节寄存器(也就是8位寄存器,如AL,BL,CL)或内存中的一个字节。状态码后缀(cc)指明了将要测试的条件。

    获取标志位的指令的格式如下:

    setcc R/M
    

    指令根据标志寄存器eflags的值,将操作数设置为0或1。

    setcc中的cc和Jcc中的cc类似,可参考表。

    4.6 call指令

    指令的格式如下:

    call I/R/M
    

    call指令会调用由操作数指定的函数。call指令会将指令的下一条指令的地址压栈,再跳转到操作数指定的地址,这样函数就能通过跳转到栈上的地址从子函数返回了。相当于

    push %eip
    jmp addr
    

    先压入指令的下一个地址,然后跳转到目标地址addr。    

    4.7 ret指令

    指令的格式如下:

    ret
    

    ret指令用于从子函数中返回。X86架构的Linux中是将函数的返回值设置到eax寄存器并返回的。相当于如下指令:

    popl %eip
    

    将call指令压栈的“call指令下一条指令的地址”弹出栈,并设置到指令指针中。这样程序就能正确地返回子函数的地方。

    从物理上来说,CALL 指令将其返回地址压入堆栈,再把被调用过程的地址复制到指令指针寄存器。当过程准备返回时,它的 RET 指令从堆栈把返回地址弹回到指令指针寄存器。

    4.8 enter指令

    enter指令通过初始化ebp和esp寄存器来为函数建立函数参数和局部变量所需要的栈帧。相当于

    push   %rbp
    mov    %rsp,%rbp

    4.9 leave指令

    leave通过恢复ebp与esp寄存器来移除使用enter指令建立的栈帧。相当于

    mov %rbp, %rsp
    pop %rbp
    

    将栈指针指向帧指针,然后pop备份的原帧指针到%ebp  

    5.0 int指令

    指令的格式如下:

    int I
    

    引起给定数字的中断。这通常用于系统调用以及其他内核界面。 

    5、标志操作 

    eflags寄存器的各个标志位如下图所示。

    操作eflags寄存器标志的一些指令如下表所示。 

    指令 操作数 描述
    pushfd R PUSHFD 指令把 32 位 EFLAGS 寄存器内容压入堆栈
    popfd R  POPFD 指令则把栈顶单元内容弹出到 EFLAGS 寄存器
     cld   将eflags.df设置为0 

    推荐阅读:

    第1篇-关于JVM运行时,开篇说的简单些

    第2篇-JVM虚拟机这样来调用Java主类的main()方法

    第3篇-CallStub新栈帧的创建

    第4篇-JVM终于开始调用Java主类的main()方法啦

    第5篇-调用Java方法后弹出栈帧及处理返回结果

    第6篇-Java方法新栈帧的创建

    第7篇-为Java方法创建栈帧

    第8篇-dispatch_next()函数分派字节码

    第9篇-字节码指令的定义

    第10篇-初始化模板表

    第11篇-认识Stub与StubQueue

    第12篇-认识CodeletMark

    第13篇-通过InterpreterCodelet存储机器指令片段

    第14篇-生成重要的例程

    第15章-解释器及解释器生成器

    第16章-虚拟机中的汇编器

    第17章-x86-64寄存器

    如果有问题可直接评论留言或加作者微信mazhimazh

    关注公众号,有HotSpot VM源码剖析系列文章!

     

  • 相关阅读:
    遗传算法-目标函数与适应度函数变换
    遗传算法-编码
    Python 绘制甘特图
    fiddler抓包
    使用msf查询补丁和可利用提权漏洞
    Shodan入坑指南
    python 项目自动生成requirements.txt文件
    Tomcat 基于端口的虚拟主机配置
    python简单搭建http server
    metasploit后渗透 之 portfwd端口重定向
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15241450.html
Copyright © 2020-2023  润新知