汇编学习01

汇编学习01(X86)

0x01 寄存器(Registers)

  1. 现代的x86处理器有8个32位通用寄存器:

huibian1

​ 同时,在里面EAX也被称为累加器ECX计数器,其被用为保存循环的索引(次数)。

  1. 对于EAX、EBX、ECX以及EDX,它们可被分段开来使用。例如,可以把EAX的最低2位字节视为16位寄存器(AX),也可将AX的最低位1位字节视为8位寄存器(AL),同时AX的高位1个字节也可看成8位寄存器(AH)。当2字节大小的数据放入DX中,原本DH、DL、EDX的数据会受到相应的影响。

0x02 内存&寻址模式

  1. 声明静态数据区域:

    a.可在内存中声明静态数据区域(类似全局变量)。.data指令用来声明数据,使得**.byte、.short、.long**可分别声明 1 、2和 4 个字节的数据。

    b.同时我们可以打上标签,来引用所创建的数据地址。其给内存地址命名,而编译器&链接器 将其翻译成机器代码。

    c.例子如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .data
    var:
    .byte 64 ;声明字节型变量var,所对应数据为64
    .byte 10 ;声明数据10,无标签,内存地址为 var+1

    x:
    .short 42 ;声明大小为2字节的数据,有标签"x"

    y:
    .long 3000 ;声明大小为4字节的数据,有标签"y",初始 化值为3000
  2. 内存寻址:

    a.现代的x86处理器可寻址高达2^32位字节的内存(内存地址为32位宽)。

    b.除了支持标签引用存储区域外,x86还提供了另一种计算&引用内存地址的方案:最多可将两个32位寄存器与一个32位有符号常量相加以计算存储器地址(其中一个可选择先*2、4或8)。

    c.用mov做例子:

    1
    2
    3
    4
    5
    mov (%ebx), %eax ;从EBX中的内存地址加载4字节的数据到EAX,(%ebx)表示寄存器ebx中所存储的内容。

    mov %ebx, var(,1) ;将EBX中4字节大小的数据-->内存中标签为var的地方去。(var为32位常数)

    mov (%esi, %ebx, 4), %edx ;将内存中标签为ESI+4*EBX所对应的4字节大小的数据-->EDX中。
  3. 操作后缀

    a.当我们加载一个32位寄存器时,编译器可推断出所用内存为4个字节宽,但有时候大小并不明确。

    b.这时得用到前缀 b、w和 l 来分别表示1、2和4个字节的大小

    1
    2
    3
    1.movb $2, (%ebx) ;将2-->ebx所代表的地址单元中
    2.movw $2, (%ebx) ;将 16 位整数2-->从ebx地址单元开始的2个字节中
    3.movl $2, (%ebx) ;将 32 位整数 2-->从ebx中的值表示的地址单元 开始的4个字节中
  4. 指令:分为 数据移动指令逻辑运算指令流程控制指令

    使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    1.<reg32 ;任意32位寄存器(%eax, %ebx, %ecx, %edx, %esi, %edi, %esp或者%eb)

    2.<reg16 ;任意16位寄存器(%ax, %bx, %cx 或者%dx)

    3.<reg8 ;任意8位寄存器(%ah, %al, %bh, %bl, %ch, %cl, %dh, %dl)

    4.<reg ;任意寄存器
    5.<mem ;一个内存地址,如(%eax), 4+var, (%eax, %ebx, 1)
    6.<con32 ;32位常数
    7.<con16 ;16位常数
    8.<con8 ;8位常数
    9.<con ;任意32位, 16位或者8位常数

    (同时所有标签&数字常量以**$**为前缀,需要时前缀 0x 表示十六进制数)

    a.数据移动–mov(移动):当寄存器到寄存器之间的数据移动可行时, 直接从内存单元中将数据移动到另一内存单元中是不行的. 在这种需要在内存单元中传递数据的情况下, 它数据来源的那个内存单元必须首先把那个内存单元中的数据加载到一个寄存器中, 之后才可通过这个寄存器来把数据移动到目标内存单元中。

    1
    2
    3
    4
    5
    6
    7
    8
    mov <reg, <reg		;语法
    mov <reg, <mem
    mov <mem, <reg
    mov <con, <reg
    mov <con, <mem

    mov %ebx, %eax ;将EBX中的值复制到EAX中 ;例子
    mov $5, var(,1) ;将5存到字节型内存单元"var"

    b.数据移动–push(入栈):将其参数移动到硬件支持的内存顶端. 特别地, 其先将 ESP 中的值减少 4, 然后移动到一个 32 位地址单元 ( %esp ). ESP ( 栈指针 ) 会随着不断入栈持续递减, 即栈内存是从高地址单元到低地址单元增长

    1
    2
    3
    4
    5
    6
    push <reg32			;语法
    push <mem
    push <con32

    push %eax ;将EAX送入栈 ;例子
    push var(,1) ;将var对应4字节大小数据送入栈中

    c.数据移动–pop(出栈):从硬件支持的栈内存顶端移除4字节数据, 并把其放到该指令指定的参数中 ( 寄存器/内存单元 ). 其首先将内存中 ( %esp ) 的 4 字节数据放到指定寄存器或者内存单元中, 然后让 ESP + 4。

    1
    2
    3
    4
    5
    6
    7
    pop <reg32			;语法
    pop <mem

    pop %edi ;将栈顶的元素移除, 并放入到寄存器EDI中
    pop (%ebx) ;将栈顶的元素移除, 并放入从EBX开始的4字节大小内存单元中

    注意:栈的访问形式为“先进后出,后进先出”

    d.数据移动–lea(加载有效地址):将其第一个参数指定的内存单元放入到 第二个参数指定的寄存器中。注意, 该指令不加载内存单元中的内容, 只是计算有效地址并将其放入寄存器。

    与 mov 的区别? mov是传送数据(如MOV AX,[1000H]是将1000H作为偏移地址寻址到内存单元,将数据–>AX ) ;而lea是取偏移地址(如LEA AX,[1000H]是将[1000H]的偏移地址–>AX,等同于MOV AX,1000H)。

    1
    2
    3
    4
    lea <mem, <reg32

    lea (%ebx,%esi,8), %edi ;EBX+8*ESI的值被移入EDI
    lea val(,1), %eax ;val的值被移入EAX

    e.逻辑运算–add(整数相加):将两参数相加, 然后将结果存放到第二个参数中. 注意, 参数可以是寄存器,但参数中最多只有一个内存单元。

    逻辑运算–sub(整数相减):将第二个参数的值与第一个相减, 就是后面那个减去前面那个, 然后把结果存储到第二个参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    add <reg, <reg
    add <mem, <reg
    add <reg, <mem
    add <con, <reg
    add <con, <mem

    add $10, %eax ;EAX中的值被设置为EAX+10
    addb $10, (%eax) ;往EAX中内存单元地址加1字节数字10

    sub <reg, <reg
    sub <mem, <reg
    sub <con, <reg
    sub <con, <mem

    sub %ah, %al ;AL被设置成AL-AH
    sub $216, %eax ;将EAX值减216

    f.逻辑运算–inc、dec(自增,自减):分别让参数+1、-1。

    1
    2
    3
    4
    5
    6
    7
    inc <reg
    inc <mem
    dec <reg
    dec <mem

    dec %eax ;EAX中的值-1
    incl var(,1) ;将var所代表的32位整数+1.

    g.逻辑运算–imul(整数相乘):有两种基本格式 : 第一种是2 个参数的 ( 先将两参数相乘, 然后把结果存到第二个参数中. 运算结果必须是一个寄存器 ); 第二种格式是3 个参数的 ( 先将其第 1 个参数和第 2 个参数相乘, 然后把结果存到第 3 个参数中,其必须是一个寄存器。此外, 第 1 个参数必须是一个常数 ).

    ​ 逻辑运算–idiv(整数相除):只有一个操作数,此操作数为除数,而被除数则为EDX: EAX 中的内容(一个64位整数), 除法结果 ( 商 ) 存在于EAX 中, 而所得的余数存在 EDX 中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    imul <reg32, <reg32
    imul <mem, <reg32
    imul <con, <reg32, <reg32
    imul <con, <mem, <reg32

    imul (%ebx), %eax ;将EAX中的32位整数,与EBX中内存单元相乘, 然后把结果存到EAX中
    imul $25, %edi, %esi ;ESI被设置为EDI*25

    idiv <reg32
    idiv <mem

    idiv %ebx ;用EDX:EAX的值除以EBX的值.商存放在EAX中,余数存放在EDX中.
    idivw (%ebx) ;将EDX:EAX的值除以存储在EBX所对应内存单元的32位值. 商存放在EAX中, 余数存放在EDX中

    h.逻辑运算–and, or, xor(按位逻辑 与,或,非):分别对它们的参数进行相应的逻辑运算, 运算结果存到第一个参数中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    and <reg, <reg
    and <mem, <reg
    and <reg, <mem
    and <con, <reg
    and <con, <mem
    or <reg, <reg
    or <mem, <reg
    or <reg, <mem
    or <con, <reg
    or <con, <mem
    xor <reg, <reg
    xor <mem, <reg
    xor <reg, <mem
    xor <con, <reg
    xor <con, <mem

    and $0x0F, %eax ;只留下EAX中最后4位数字(二进制位)
    xor %edx, %edx ;将EDX的值全设置成0

    i.逻辑运算–not(逻辑位运算 非):对参数进行逻辑非运算, 即翻转参数中所有位的值。

    1
    2
    3
    4
    not <reg
    not <mem

    not %eax ;将EAX所有值翻转

    j.逻辑运算–shl, shr(按位左移/右移):对第一个参数进行位运算, 移动的位数由第二个参数决定, 移动过后的空位拿 0 补上.被移的参数最多可以被移 31 位. 第二个参数可以是 8 位常数或者寄存器 CL. 在任意情况下, 大于 31 的移位都默认是与 32 取模。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    shl <con8, <reg
    shl <con8, <mem
    shl %cl, <reg
    shl %cl, <mem
    shr <con8, <reg
    shr <con8, <mem
    shr %cl, <reg
    shr %cl, <mem

    shl $1, %eax ;将EAX的值*2 (如果最高有效位是0)
    shr %cl, %ebx ;将EBX的值/2n, 其n为CL中的值, 运算最终结果存到EBX中.

    [!IMPORTANT]

    x86处理器有**指令指针寄存器(EIP),为32位寄存器,用来在内存中指示输入汇编指令的位置,指向哪个内存单元。

    我们用<label来当作标签,输入标签+冒号,可将其插入x86汇编代码任意位置。

    k.流程控制–jmp(跳转指令):将程序跳转到参数指定的内存地址, 然后执行该内存地址的指令。

    1
    2
    3
    jmp <label

    jmp begin ;跳转到打了"begin"标签的位置

    l.流程控制–jcondition(有条件跳转):是条件跳转指令,基于一组条件代码的状态,这些状态存放在叫机器状态字的寄存器中,其内容包括关于最后执行的算术运算的信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    je <label ;当相等的时候跳转
    jne <label ;当不相等的时候跳转
    jz <label ;当最后结果为 0 的时候跳转
    jg <label ;当大于的时候跳转
    jge <label ;当大于等于的时候跳转
    jl <label ;当小于的时候跳转
    jle <label ;当小于等于的时候跳转

    cmp %ebx, %eax
    jle done
    ;若EAX的值 <= EBX的值, 就跳转到"done"标签,否则继续执行下一条指令

    m.流程控制–cmp(比较指令):比较两个参数的值, 适当地设置机器状态字中的条件代码. 此指令与sub指令类似,但是cmp不用将计算结果保存在操作数中。

    1
    2
    3
    4
    5
    6
    7
    8
    cmp <reg, <reg
    cmp <mem, <reg
    cmp <reg, <mem
    cmp <con, <reg

    cmpb $10, (%ebx)
    jeq loop
    ;若EBX的值等于整数常量10,则跳转到标签"loop"的位置

    n.流程控制–call、ret(子程序调用&返回):实现子程序的调用和返回。call指令首先把当前代码位置推到内存中硬件支持的栈内存上,然后无条件跳转到标签参数指定的代码位置,在其结束后,返回调用之前的位置;ret指令则实现子程序的返回,其首先从栈中取出代码(类似于pop),然后无条件跳转到检索到的代码位置。

    1
    2
    call &lt;label
    ret5
  5. 调用约定(关于如何从例程调用&返回的协议):分为两组:第一组是面向子例程的调用者的;第二组则是面向子例程的编写者,即被调用者

    huibian2

    a.调用者约定:要调用子例程,请使用call指令(将返回地址存到栈上,并跳转到子程序的代码,其中子程序应遵循被调用者约定)

    1
    2
    3
    4
    5
    6
    例子:
    push (%ebx) ;最后一个参数最先入栈
    push $216 ;把第二个参数入栈
    push %eax ;第一个参数最后入栈
    call myFunc ;调用这个函数(假设以C语言模式命名)
    add $12, %esp ;清理栈内存

    b.被调用者约定:应先将EBP的值入栈,再将ESP的值复制到EBP中(保留基指针EBP以作为栈上找到参数&变量的参考点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    例子:
    ;启动代码部分
    .text
    ;将myFunc定义为全局(导出)函数
    .globl myFunc
    .type myFunc, @function
    myFunc :
    ;子程序序言
    push %ebp ;保存基指针旧值
    mov %esp, %ebp ;设置基指针新值
    sub $4, %esp ;为一个 4 字节的变量腾出位置
    push %edi
    push %esi ;这个函数会修改 EDI 和 ESI, 所以先给它们入栈
    ;不需要保存 EBX, EBP 和 ESP
    ;子程序主体
    mov 8(%ebp), %eax ;把参数 1 的值移到 EAX 中
    mov 12(%ebp), %esi ;把参数 2 的值移到 ESI 中
    mov 16(%ebp), %edi ;把参数 3 的值移到 EDI 中
    mov %edi, -4(%ebp) ;把 EDI 移给局部变量
    add %esi, -4(%ebp) ;把 ESI 添加给局部变量
    add -4(%ebp), %eax ;将局部变量的内容添加到EAX(最终结果)中
    ;子程序结尾
    pop %esi ;恢复寄存器的值
    pop %edi
    mov %ebp, %esp ;释放局部变量
    pop %ebp ;恢复调用者的基指针值
    ret

    子程序序言执行标准操作,即在EBP中保存栈指针的副本,通过递减栈指针来分配局部变量,并在栈上保存寄存器的值。

    函数的结尾则基本上是函数序言的镜像,从栈上恢复调用者的寄存器值,通过重置栈指针来释放局部变量,恢复调用者的EBP值,并使用ret指令返回调用者中相应代码位置。