GNU x86-64汇编简单介绍

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博客

推荐阅读更多精彩内容

  • 这是程序栈话题的最后一篇,可能有人会问,你前面5篇写那么多x86程序栈的文章干什么?请耐心看下去,即便现在x64硬...
    铁甲万能狗阅读 210评论 0 0
  • 本文首发于我的博客 Bomb Lab 实验代码见GitHub 简介 BombLab是CS:APP中对应第三章内容:...
    viseator阅读 7,116评论 0 11
  • x86汇编指令集包括x86-64(intel-64,amd64, emt64), x86-32, x86-16 内...
    bitzoo阅读 1,359评论 0 0
  • 0. 引言 如果你学的第一门程序语言是C语言,那么下面这段程序很可能是你写出来的第一个有完整的 “输入---处理-...
    pandolia阅读 9,829评论 13 25
  • 由于不是科班出生,又是自学开发,对很多方面的知识都是只知其然而不知其所以然。加上最近公司事情不多,刚好乘此机会把长...
    寒咯阅读 4,574评论 1 5