美文网首页
GNU x86-64汇编简单介绍

GNU x86-64汇编简单介绍

作者: Tomtoms | 来源:发表于2019-11-07 23:08 被阅读0次

    GNUx86-64汇编

    寄存器

    X86-64大约有16个64位整数寄存器,其中栈指针rsp和基址指针rbp较为特殊,rsirdi跟处理字符串相关。后面的八个寄存器是编号的,使用起来没有特殊限制。

    • rax rbx rcx rdx
    • rsi rdi rbp rsp
    • r8 - r15

    其中rax的结构如下
    [image:ECA803C6-AECB-4593-8CAA-34CF915FBC41-86030-0001242002E5C3CA/20171008192750274.png]
    rax的低八位为al,接着八位是ah,合并为ax,低32位为eax,整个64位是rax

    R8的结构如下
    [image:8D73982F-FE1D-4750-ADD6-9CDBEC733828-86030-0001243A3CCBAFA8/20171008193142962.png]
    大多数编译器产品会混合使用32位和64位模式。32位用来做整数计算,64位一般用来保存内存地址(指针)。

    寻址模式

    mov指令有一个决定移动多大数据的单字母前缀

    • movb Byte 8bits
    • movw Word 16bits
    • movl Long 32bits
    • movq Quadword 64bits

    直接寻址

    不同的数据有不同的寻址模式
    全局值和函数:直接使用名字,如printf
    常数:带有美元符号的立即数,如$56
    寄存器:使用寄存器名称,如%rbx

    间接寻址

    简介寻址是使用与寄存器保存的地址对应的内存中的值,如(%rsp)表示rsp寄存器指向的内存中的值。

    相对基址寻址

    表示把一个常数加到寄存器值上,例如-16(%rcx)表示把rcx指向的地址前移16个字节后对应的内存值。

    寻址模式相对于管理栈空间、局部变量、函数参数很重要,相对基址寻址也有很多变种,例如-16(%rbx, %rcx, 8)表示-16+%rbx+%rcx*8对应的地址的内存值,这种寻址模式在访问元素大小特殊的数组时很有用。
    下面都表示将一个值加载到rax寄存器上
    [image:676C703E-273B-45BF-8548-7A17272C9F8E-86030-000124D32C54E673/20171008200403991.png]

    计算

    编译器会用到四个基本算数计算指令

    • ADD
    • SUB
    • IDIV
    • IMUL
      上面的三个操作都有两个操作数,目的操作数在操作以后会被改写。

    ADDQ %rbx, %rax
    表示rbx的值加上rax的值,写到rax内。在例如写b=b*(b+a)的时候需要注意不要把b的值覆盖了,如下

    movq a, %rax
    movq b, %rbx
    addq %rbx, %rax
    imulq %rbx
    movq %rax, c
    

    IMUL操作只有一个操作数,表示把%rax的值乘以操作数,把低64位放在%rax,高64位放在%rdx。IDIV相反,把低64位的%rax,高64位的%rdx表示的数除以操作数,商放在%rax,余数在%rdx。cdqo指令会把%rax符号扩展到%rdx

    movq a, %rax
    cdqo
    idivq $5    # divide %rdx:%rax by 5, leaving result in %eax
    

    INC和DEC会把寄存器的值破坏掉。例如,语句a=++b可以这样翻译:

    movq b, %rax
    incq %rax
    movq %rax, a
    

    布尔操作的工作方式类似,AND,OR,XOR,NOT也会破坏寄存器的值。

    小贴士: 浮点数 
    我们不讨论浮点数操作细节,只需要知道它们使用一套不同的指令和寄存器。在老式机器上,浮点指令是使用可选的外部8087 FPU处理的,所以被称作X87操作,虽然现在已经集成到了CPU里面。X87 FPU包含 
    8个排列在栈中的80位寄存器(R0-R7)。做浮点算术前,代码必须先把数据push到FPU栈,然后操作栈顶的数据,并回写到内存。内存中双精度浮点数是以64位的长度存储的。这种架构的一个奇怪的地方是,FPU的精度是80位,比内存中的存储方式精度高。结果,浮点计算的值会改变,取决于数据在内存和寄存器之间移动的具体顺序。 
    浮点数数学计算比它看上去要难懂,推荐阅读: 
    1. Intel 手册8-1章节。 
    2. 计算机科学家必知之浮点数 
    3. 程序员必知之浮点数
    --------------------- 
    作者:阿威_t 
    来源:CSDN 
    原文:https://blog.csdn.net/pro_technician/article/details/78173777 
    

    比较和跳转

    JMP指令可以构造一个无限循环,%eax开始计数

        movq $0, %rax
    loop:
        incq %rax
        jump loop
    

    所有的比较都用CMP指令,指令比较两个不同的寄存器中的值,设置eflag寄存器的比特位,记录下结果,jump指令集会利用eflag寄存器中的结果进行跳转
    [image:A28DA285-C819-4485-BBCF-63999A382B76-86030-00012629E8112A97/20171013211645843.png]
    下面是一个从0累加到5的循环

        movq $0, %rax
    loop:
        incq %rax
        cmp $5, %rax
        jle loop
    

    设置y的值,如果x大于0,y=10,否则为20

        movq x, %rax
        cmpq $0, %rax
        jle twenty
    ten:
        movq $10, %rbx
        jmp done
    twenty:
        movq $20, %rbx
        jmp done
    done:
        movq %rbx, y
    

    注意:上面的ten/twenty/done都是标签,标签在一个汇编文件中私有,对外部不可见,除非有.globl标志。c语言的说法,汇编中没有修饰的标签是static的,.globl修饰的标签是extern的。

    堆栈

    一般内存有如下结构

    |----内存高位----|
    |--------------|
    |--------------|<-------栈底
    |--------------|
    |--------------|(栈空间向下增长)
    |--------------|
    |--------------|<-------栈顶
    |--------------|
    |--------------|
    |--------------|<-------堆顶
    |--------------|
    |--------------|(堆空间向上增长)
    |--------------|
    |--------------|<-------堆底
    |--------------|
    |----内存低位----|
    

    函数调用会将参数压入栈中,等调用完后再恢复栈结构,完成一次调用。
    %rsp栈指针,指向栈顶,压栈的操作是将%rsp减去8字节,预留出64位,并把%rax写到%rsp指向的内存空间。

    subq $8, %rsp
    movq %rax, (%rsp)
    

    等价于

    pushq %rax
    

    Pop刚好相反

    movq (%rsp), %rax
    addq $8, %rsp
    

    等价于

    popq %rax
    

    如果想丢弃栈中的值,只需要增加%rsp的值

    addq $8, %rsp
    

    函数调用

    X86-64的函数堆栈System V ABI较为复杂,这里只做简单的介绍

    • 整形参数(和指针)以此放在%rdi, %rsi, %rdx, %rcx, %8, %9寄存器中
    • 浮点参数依次放在%xmm0-%xmm7中
    • 寄存器不够用时,参数放在栈中
    • 可变参数(printf),寄存器%eax需要记录下有多少个浮点参数的个数
    • 被调用的函数可以使用任何寄存器,但必须保证%rbx, %rbp, %rsp和%r12-%15恢复到原来的值
    • 返回值放在%eax中
      [image:E96C7FBD-E6FF-405C-8372-3C74672A64E3-86030-00012E397428F584/20171015115531621.png]
      函数调用前,需要先把参数放到寄存器中,将%r10和%r11的值保存到栈中,之后执行call指令,把IP指针的值保存到栈中,然后跳转执行,从函数恢复后,恢复%r10和%r11的值,并从%eax中获取返回值。
    long x=0;
    long y=10;
    int main()
    {
        x = printf("value: %d", y);
    }
    

    对应的汇编

    .data
    x:
        .quad 0
    y:
        .quad 10
    str:
        .string "value: %d"
    
    .text
    .globl main
    main:
        movq $str, %rdi
        movq y, %rsi
        movq $0, %eax #没有浮点数
        pushq %r10
        pushq %r11
        
        call printf
        
        popq %r11
        popq %r10
        
        movq %rax, x
        ret
    
    long square(long x)
    {
        return x*x;
    }
    
    .globl square
    square:
        movq %rdi, %rax
        imulq %rdi, %rax
        ret
    

    一个复杂函数的调用都有如下步骤

    1. 改变栈底值
    2. 将参数依次压入栈中
    3. 预留函数调用的local variables的空间
    4. 保护好原有的寄存器rbx, r12-r15
    5. 函数调用
    6. 恢复原有的寄存器
    7. 恢复栈底
    .globl func
    func:
        pushq %rbp          # save the base pointer
        movq  %rsp, %rbp    # set new base pointer
    
        pushq %rdi          # save first argument on the stack
        pushq %rsi          # save second argument on the stack
        pushq %rdx          # save third argument on the stack
    
        subq  $16, %rsp     # allocate two more local variables
    
        pushq %rbx          # save callee-saved registers
        pushq %r12
        pushq %r13
        pushq %r14
        pushq %r15
    
        ### body of function goes here ###
    
        popq %r15            # restore callee-saved registers
        popq %r14
        popq %r13
        popq %r12
        popq %rbx
    
        movq   %rbp, %rsp    # reset stack to previous base pointer
        popq   %rbp          # recover previous base pointer
        ret                  # return to the caller
    

    %rbp和%rsp之间的内存缴存stack frame也叫做活动记录。
    下面是func内部的栈内存布局。
    [image:D59227A1-B882-4E26-B2E1-1926878C9833-86030-00012F66B9789F54/20171015133500035.png]
    %rbp指明了栈帧的开始。在函数体内,我们可以用%rbp基址相对寻址方式来引用参数和局部变量。参数0在 -8(%rbp)位置,参数1在 -16(%rbp),以此类推。 -32(%rbp) 对应局部变量,-48(%rbp)对应保存的寄存器。%rsp指向栈中最后一个元素。如果栈还要另作他用,则需要向更低地址的区域压栈。(注意:我们假设所有参数和变量都是8字节长度, 实际上不同的类型的长度不一样,对应的偏移也不一样)。

    下面是一个真实的汇编

    #include <stdio.h>
    int sum(int a, int b)
    {
        return a+b;
    }
    int main()
    {
        int x=10;
        int y=20;
        printf("sum is:%d\n", sum(x,y));
        return 0;
    }
    
            .globl  __Z3sumii               ## -- Begin function _Z3sumii
    __Z3sumii:                              ## @_Z3sumii
            .cfi_startproc
            pushq   %rbp
            movq    %rsp, %rbp
            movl    %edi, -4(%rbp)
            movl    %esi, -8(%rbp)
            movl    -4(%rbp), %esi
            addl    -8(%rbp), %esi
            movl    %esi, %eax
            popq    %rbp
            retq
            .cfi_endproc
                                            ## -- End function
            .globl  _main                   ## -- Begin function main
    _main:                                  ## @main
            .cfi_startproc
            pushq   %rbp #保存栈底
            movq    %rsp, %rbp #将栈顶用作新的栈底,保存旧栈帧
            subq    $16, %rsp  #预留4个字节作为栈大小
            movl    $0, -4(%rbp) #0压栈,我也不知道为什么
            movl    $10, -8(%rbp)#两个变量压栈
            movl    $20, -12(%rbp)
            movl    -8(%rbp), %edi#将值写入edi/esi寄存器,准备调用
            movl    -12(%rbp), %esi
            callq   __Z3sumii
            leaq    L_.str(%rip), %rdi
            movl    %eax, %esi#记录返回值到esi
            movb    $0, %al
            callq   _printf
            xorl    %esi, %esi
            movl    %eax, -16(%rbp)#保存结果         ## 4-byte Spill
            movl    %esi, %eax
            addq    $16, %rsp #恢复栈顶
            popq    %rbp #恢复栈底
            retq
            .cfi_endproc
                                            ## -- End function
    L_.str:                                 ## @.str
            .asciz  "sum is:%d\n"
    

    深入浅出GNU X86-64 汇编 - pro_technician的专栏 - CSDN博客

    相关文章

      网友评论

          本文标题:GNU x86-64汇编简单介绍

          本文链接:https://www.haomeiwen.com/subject/znmbbctx.html