X86 汇编

58 阅读6分钟

X86汇编

常见寄存器(64位)

Ref: zhuanlan.zhihu.com/p/502718676

通用寄存器(16个)

rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8~r15

  • rsi,rdi, rdx, rcx //用来参数传递(linux)
  • rsp, rbp //指向栈顶与栈底

标志寄存器

rflags 标识指令执行的状态

指令寄存器

rip 存放下一条指令的内存地址

控制寄存器

控制CPU的工作状态,例如保护模式,分页等

模型特定寄存器

这是一组寄存器,常常与一些新功能有关,例如VMX

指令

  • push : esp-- + mov data *%esp 以下为对称设计
  • call :push ip + jump
  • enter : push ebp + mov %esp $ebp
  • leave :mov %ebp %esp + pop %ebp //与enter 搭配使用
  • ret :pop ip + jump //与call搭配使用

lea 与 mov 区别

mov %eax, %ebx      # 把eax拷贝到ebx
mov (%eax), %ebx    # 把地址为eax的数据拷贝到ebx寄存器
mov 0x1(%eax), %ebx # 把地址为eax+1的数据拷贝到ebx寄存器
lea (%eax), %ebx    # 把eax拷贝到ebx 与 mov %eax, %ebx 相同
lea 0x1(%eax), %ebx # 把eax+1拷贝到ebx(只是地址,不是数据)
lea %eax, %ebx      # 错误写法。

函数调用栈

C代码

int fun(int a, int b)
{
  int c=0;
  c = a + b;
  return c;
}
int main()
{
   int a =1;
   int result = fun(a,2);
   return 0;
}

反汇编代码(64位)

00000000000005fa <_Z3funii>:
 5fa:   55                      push   %rbp                     //压栈,用于后期还原,会修改esp
 5fb:   48 89 e5                mov    %rsp,%rbp                //修改rbp, 两者指向同一个位置。
 5fe:   89 7d ec                mov    %edi,-0x14(%rbp)         //传入参数1,写到内存
 601:   89 75 e8                mov    %esi,-0x18(%rbp)         //传入参数2,写到内存
 604:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)          //内存上创建临时变量c
 60b:   8b 55 ec                mov    -0x14(%rbp),%edx         //从内存中读取参数1到寄存器
 60e:   8b 45 e8                mov    -0x18(%rbp),%eax         //从内存中读取参数2到寄存器
 611:   01 d0                   add    %edx,%eax                //执行加法计算
 613:   89 45 fc                mov    %eax,-0x4(%rbp)          //结果从寄存器写入内存
 616:   8b 45 fc                mov    -0x4(%rbp),%eax
 619:   5d                      pop    %rbp                     //将5fa中压入栈的地址还原
 61a:   c3                      retq                            //pop ip,进而修改函数的执行顺序。

000000000000061b <main>:
 61b:   55                      push   %rbp
 61c:   48 89 e5                mov    %rsp,%rbp
 61f:   48 83 ec 10             sub    $0x10,%rsp
 623:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
 62a:   8b 45 f8                mov    -0x8(%rbp),%eax
 62d:   be 02 00 00 00          mov    $0x2,%esi               //待传参数放到寄存器1
 632:   89 c7                   mov    %eax,%edi               //待传参数放到寄存器2
 634:   e8 c1 ff ff ff          callq  5fa <_Z3funii>          //jump + push ip
 639:   89 45 fc                mov    %eax,-0x4(%rbp)         //将返回值从寄存器拷贝到内存
 63c:   b8 00 00 00 00          mov    $0x0,%eax               
 641:   c9                      leaveq
 642:   c3                      retq
 643:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 64a:   00 00 00
 64d:   0f 1f 00                nopl   (%rax)

栈变化图解(右边表示指向此地址的寄存器)

#原始状态
data1 | esp
data2 | ebp
#after call
old ip  | esp
data1   |
data2   | ebp
#at the begining of function, after push ebp, move esp ebp
old ebp   | esp&ebp
old ip    |
data1     |
data2     |
#at the end of function, after mov rbp rsp , pop rbp
old ip     | esp
data1      |
data2      | ebp
#after ret
data1 | esp
data2 | ebp

栈缩略图

返回地址(old IP)
------前后栈分割线----------
old EBP(当前EBP寄存器指向此位置)
函数参数
局部变量
返回地址(old IP)
------前后栈分割线----------
old EBP(当前EBP寄存器指向此位置)
......

函数的前后缀

push %rbp  # 把rbp指针压栈
mov %rsp,%rbp # 此时rbp,与rsp相同,都指向了一个

mov %rbp, %rsp
pop %rbp
ret # 就是pop %rip,还原到原来的位置。

Tips:

  1. 有两套存储系统,分别是寄存器以及内存,其中cpu只能在寄存器上完成计算,内存的话分为代码区与数据区。ip寄存器与代码区打交道,ebp以及esp等与数据区打交道。
  2. esp指向frame的最后一个数据,而非空数据。此值会随着push,pop修改,也可以手动改变。
  3. mov 与movl,在这里一样,mov会被自动翻译成(movl)
  4. 汇编不能原操作地址与目的地址都是内存。
  5. EBP就是暂存ESP的内容,EBP寄存器所指向地址内容为上一个EBP。可以通过加编译优化选项,将ebp优化掉。
  6. 32位参数传递与64位不同,32通过push到内存栈的方式,而64位通过寄存器的方式。
  7. 32位程序中__x86.get_pc_thunk.ax,是因为无法直接读取eip寄存器,会通过借助call,将eip寄存器内容push到内存中,然后mov 到ax,进而实现全局变量的访问。
  8. 程序在运行的时候,不断根据ip寄存器,取址执行,但是jmp,call,ret之类的指令会修改ip寄存器,改变程序执行的方向。

Ref: juejin.im/post/5d1d46…

Inline assemble in C language

在C里面嵌入asm代码。

#include <stdio.h>
int main()
{
int src = 1;
int dst;

asm ("mov %1, %0\n\t"
    "add $1, %0"
    : "=r" (dst)
    : "r" (src));

printf("%d\n", dst);

        return 0;
}

格式一:

asm asm-qualifiers ( AssemblerTemplate  // %0,%1,%2分别代表了后面的第一个,第二个,第三个;AT&T格式默认从输入输出顺序为从左到右;常量需要加$符号
                      : OutputOperands // 输出必须要加+,r/m分别代表寄存器,也可同时指定,让编译器去决定。
                      : InputOperands  // 输入不需要加=
                      : Clobbers
                      : GotoLabels)

格式二:就是不使用序号,使用符号,这里的d_c,e_c指定是c代码里的变量。

asm("mov %[e], %[d]"
: [d] "=rm" (d_c)
: [e] "rm" (*e_c))

如果要对寄存器进行操作,需要加两个百分号, 例如:%%EAX

clobber的作用如下:
内联代码并非存粹的,不需要任何修改的汇编代码,gcc asm会进行相关的编译(载入c局部变量等),这时某些指令会修改额外的寄存器,或者汇编代码里面直接使用了某些寄存器,gcc并不知道,这里必须告诉gcc,我的asm代码中这个寄存器会被cloberring(修改),你在转汇编代码的时候,不能使用这些寄存器。这里以clobber是否添加rax为例,对比了两个汇编的区别。

int fun2()
{
        int input =1;
        int output;
        asm("mov $2, %[dst]\n\t"
            "add %[src], %[dst]"
            :[dst] "=rm" (output)
            :[src] "rm" (input)
           );
        return 0;
}
int fun3()
{
        int input =1;
        int output;
        asm("mov $2, %[dst]\n\t"
            "add %[src], %[dst]"
            :[dst] "=rm" (output)
            :[src] "rm" (input)
            : "rax");
        return 0;
}

对应的汇编代码如下。

0000000000001193 <fun2>:
    1193:       f3 0f 1e fa             endbr64
    1197:       55                      push   %rbp
    1198:       48 89 e5                mov    %rsp,%rbp
    119b:       c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
    11a2:       b8 02 00 00 00          mov    $0x2,%eax
    11a7:       03 45 f8                add    -0x8(%rbp),%eax
    11aa:       89 45 fc                mov    %eax,-0x4(%rbp)
    11ad:       b8 00 00 00 00          mov    $0x0,%eax
    11b2:       5d                      pop    %rbp
    11b3:       c3                      retq

00000000000011b4 <fun3>:
    11b4:       f3 0f 1e fa             endbr64
    11b8:       55                      push   %rbp
    11b9:       48 89 e5                mov    %rsp,%rbp
    11bc:       c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
    11c3:       ba 02 00 00 00          mov    $0x2,%edx
    11c8:       03 55 f8                add    -0x8(%rbp),%edx
    11cb:       89 55 fc                mov    %edx,-0x4(%rbp)
    11ce:       b8 00 00 00 00          mov    $0x0,%eax
    11d3:       5d                      pop    %rbp
    11d4:       c3                      retq

两个特别的参数:

  • cc:修改了flag寄存器。
  • memory:告诉编译器,我的代码会对其他内存进行读写,这里加入一个barrier,让其他值flush到相关的寄存器。

PAUSE指令

spin-lock,如果一直在循环里,会让branch predictor 以为一直会执行循环的指令,但是如果某个时刻开始满足条件,就要flush整个pipeline,这会让锁的时间超过期望时间,加入PAUSE之后,会延缓内存的读取,这样整个pipeline不会填充满推测的指令了。

  • tpause(timed pause)指令是对pause的优化,pause会消耗140个时钟周期,tpause会程序进入C0.1(弱睡眠)的状态,进而节省电量。
  • Ref: stackoverflow.com/questions/1…