X86汇编简明教程

概要

本文介绍的是目前最常见的 x86 汇编语言,即 Intel 公司的 CPU 使用的那一种。

CPU 只负责计算,本身不具备智能。你输入一条指令(instruction),它就运行一次,然后停下来,等待下一条指令。

这种指令是二进制的,叫做操作码(机器码)。编译器的作用,就是将高级语言写好的程序,翻译成一条条操作码。

最早的时候,编写程序就是手写二进制指令,然后通过各种开关输入计算机,比如要做加法了,就按一下加法开关。后来,发明了纸带打孔机,通过在纸带上打孔,将二进制指令自动输入计算机。

对于人来说,机器码难以记忆,所以用汇编指令来表示。将汇编指令转换为机器码的程序叫编译器

汇编代码分为三种元素:

  • 指示 以"."开头,指示对汇编器,链接器或调试器有用的结构信息,但是它们本身并不是汇编指令。例如,.file记录了源文件的名称,.data表示程序的数据部分的开始,.text表示实际程序代码的开始,.string表示数据部分内的字符串常量,.globl main表示标签main是一个可以被其他代码模块访问的全局符号。.cfi开头的一系列指示全称是Call Frame Infromation,就是他的名字那样,记录一些程序调用堆栈的信息,用于程序调试打印堆栈信息。详细说明见 CFI-directives
  • 标签(符号) 以":"结尾用来标记它后面的一段内容(就像变量名一样)。例如,.LC0表示紧接着的字符串被调用时应该使用LC0作为它名字。main:表示指令pushq%rbp是主函数的第一条指令。按照惯例,以”.”开头的标签是编译器生成的临时局部标签,而其他标签是用户可见函数和全局变量
  • 指令 就是实际的汇编代码啦。我们直接可以根据代码的缩进,从格式上将指令和标签区分开来。

示例1

hello world示例:

test.c

#include <stdio.h>

int main( int argc, char *argv[] )
{
    printf("hello %s\n","world");
    return 0;
}

运行gcc -S -O0 test.c产生test.s文件:

        .file   "test.c"  // 指示
        .text
        .section        .rodata
.LC0: // 局部(标签)
        .string "world"
.LC1:
        .string "hello %s\n"
        .text
        .globl  main
        .type   main, @function
main:  // 全局标签(符号)
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movl    %edi, -4(%rbp)
        movq    %rsi, -16(%rbp)
        leaq    .LC0(%rip), %rsi
        leaq    .LC1(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
        .section        .note.GNU-stack,"",@progbits

C的编译的过程一般分为:

  • 预处理(Pre-Processing)
  • 编译(compiling)
  • 汇编(Assembling)
  • 链接(Linking)

上面我们使用gcc -s 产生汇编代码的过程其实已经完成了预处理和编译的过程,下面我们使用 gcc -o完成汇编和链接的过程产生可执行程序:

$ gcc test.s -o hello
$ ./hello
hello world

将汇编代码汇编成目标代码(Assembling)也很有趣,然后可以使用nm来显示代码中的符号:

$ gcc test.s -c -o hello.o
$ nm hello.o
0000000000000000 T main
                 U printf

这里显示链接器可用的信息,main位于.text(T)的位置0处。并且printf未定义(U):在产生的hello.o 对象中并没有定义printf这个函数。链接的时候,连接器会帮我们找标准库中的printf。你可能也看到了,没有出现像LC0,LFB0等这类的标签,正如我前面说的那样:因为它们是编译器生成的局部标签,没有被声明为.globl。

NAME
nm – list symbols from object files

示例2

汇编分类

汇编语言本质上是一套语法规则和助记符的集合,它可以包容不同的指令集

不同架构的CPU指令并不相同,如x86,powerpc,arm各有各的指令系统;甚至同一种架构的CPU有几套指令集,典型的如arm除了有32位的指令集外,还有一套16位的thumb指令集。如果从CPU体系来划分,常见的汇编有两种:IBM PC汇编(Intel的汇编)和ARM汇编。

汇编语言的伪指令等和编译器息息相关。最有名的也是两家:MASM和GNU ASM。前者是微软的,只支持x86,用在DOS/Windows平台中;后者是开源产品,主要用在Linux中,基本上支持大部分的CPU架构。这两者的区别在于伪指令的不同,伪指令是用来告诉编译器如何工作的,和编译器相关,和CPU无关。其实汇编的编译相当简单,这两套伪指令只是符号不相同,含义是大同小异,明白了一种,看另一种就很容易了。

从汇编格式分,还有Intel格式和AT&T格式的区别,前者是Intel的,windows平台常见,后者最早由贝尔实验室推出,用于Unix中,GUN汇编器的缺省格式就是AT&T。不过GNU的汇编器和调试器gdb对这两种格式都支持,可以随便切换。MASM只支持Intel格式。Intel格式和AT&T格式的区别只是符号系统的区别,这与x86和arm的区别可不一样,后者是CPU体系的区别。

GUN GCC使用传统的AT&T语法,它在Unix-like操作系统上使用,而不是dos和windows系统上通常使用的Intel语法。下面我们看一个最常见的AT&T语法的指令:

movl %esp, %ebp

movl是一个最常见的汇编指令的名称,百分号表示esp和ebp是寄存器,在AT&T语法中,有两个参数的时候,始终先给出源(source),然后再给出目标(destination)。 在其他地方(例如英特尔手册),您将看到英特尔语法,区别之处是Intel语法省去了百分号并颠倒了参数的顺序。例如,这是intel语法中的相同指令:

MOVQ EBP, ESP

在阅读手册和网页时,通过看有没有"%"就知道是用的哪种汇编格式了。

寄存器

CPU访问数据的顺序是:

CPU<--->寄存器<---> 缓存<--->内存

CPU 的运算速度远高于内存的读写速度,为了避免被拖慢,CPU 都自带一级缓存和二级缓存。

除了缓存之外,CPU 还自带了寄存器(register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。

寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。

CPU访问各个存储器的速度分别为

  • 寄存器: 300ps
  • Cache1: 1ns
  • Cache2: 3-10ns
  • Cache3: 10-20ns
  • Memory: 50-100ns
  • 硬盘:5-10ms

X86-(32/64)架构的寄存器

x86体系结构有8个通用寄存器(General-Purpose Registers, GPR)、6个段寄存器、1个标志寄存器和一个指令指针。64位x86有额外的寄存器。

8个通用寄存器,GPR

  • 累加器寄存器(Accumulator register,AX)。用于算术运算。
  • 计数器寄存器(Counter register, CX)。用于移位/旋转指令和循环。
  • 数据寄存器(Data registe, DX)。用于算术运算和I/O运算。
  • 基址寄存器(Base register, BX)。用作指向数据的指针(当处于分段模式时,位于段寄存器ds中)。
  • 堆栈指针寄存器(Sack Pointer register , SP)。指向堆栈顶部的指针。
  • 堆栈基指针寄存器(Stack Base Pointer register, BP)。用于指向堆栈的基部。
  • 源索引寄存器(Source Index register, SI)。在流操作中用作指向源的指针。
  • 目标索引寄存器(Destination Index register, DI)。在流操作中用作指向目标的指针。

它们在这里列出的顺序是有原因的:它与push-to-stack操作中使用的顺序相同,稍后将介绍。
在16位模式下,寄存器由上面列表中的两个字母缩写来标识,如ax
在32位模式下,此两个字母缩写的前缀为“E”(扩展)。例如,“eax”是作为32位值的累加器寄存器。
在64位版本中,“e”替换为“r”,因此64位版本的“eax”称为“rax”。
可将前四个寄存器(ax、cx、dx和bx)的16位大小作为两个8位半字节寻址。最低有效字节(LSB)用“L”替换“X”来标识。
最高有效字节(msb)使用“H”替换为”X“。
访问累加器、计数器、数据和基址寄存器的方法有五种:64位、32位、16位、8位lsb和8位msb。
其他四种访问方式只有四种:64位、32位、16位和8位。

6个段寄存器

  • 堆栈段(Stack Segment , SS)。指向堆栈的指针。
  • 代码段(Code Segment, CS)。指向代码的指针。
  • 数据段(Data Segment, DS)。指向数据的指针。
  • 附加段(Extra Segment, ES)。指向额外数据的指针(“e”表示“extra”)。
  • F段(F Segment, FS)。指向更多额外数据的指针(“f”在“e”之后)。
  • G段(G Segment, GS)。指向更多额外数据的指针(“g”在“f”之后)。

内存分段是访问内存区域的旧方法。 所有主要的操作系统,包括OSX,Linux(从版本0.1)和Windows(从NT)现在都使用分页,这是访问内存的更好方式。英特尔一直在其处理器中引入向后兼容性(IA-64除外,我们看到它失败了……)因此,在初始状态(复位后)处理器以一种称为实模式的模式启动,在这种模式下, 默认情况下启用分段以支持旧版软件。 在操作系统的引导过程中,处理器变为保护模式,然后在启用的分页中。

EFLAGS寄存器

eflags是一个32位寄存器,用作表示布尔值的位集合,用于存储操作结果和处理器状态。

这些标志的不同用途是:

  1. CF:携带标志。设置是否最后一个算术运算(加法)或借用(减法)超出寄存器的大小。然后检查操作时是否使用add-with-carry或subtract-with-borrow来处理只有一个寄存器包含的值太大的值。
  2. PF:奇偶标志。设置最低有效字节中的设置位数是否为2的倍数。
  3. AF:调整标志。携带二进制码十进制(BCD)数运算。
  4. ZF:零标志。如果操作结果为零(0),则置位。
  5. SF:签署标志。如果操作结果为负,则设置。
  6. TF:陷阱标志。设置是否一步一步调试。
  7. IF:中断标志。设置是否启用中断。
  8. DF:方向标志。流方向。如果设置,字符串操作将递减其指针而不是递增它,向后读取内存。
  9. OF:溢出标志。设置是否有符号算术运算导致值太大而无法包含寄存器。
    12-13。 IOPL:I / O特权级别字段(2位)。 I / O权限当前进程的级别。
  10. NT:嵌套任务标志。控制中断的链接。设置当前进程是否链接到下一个进程。
  11. RF:恢复标志。对调试异常的响应。
  12. VM:Virtual-8086模式。如果在8086兼容模式下设置。
  13. AC:对齐检查。设置是否完成内存引用的对齐检查。
  14. VIF:虚拟中断标志。 IF的虚拟图像。
  15. VIP:虚拟中断挂起标志。设置中断是否挂起。
  16. ID:识别标志。如果可以设置,则支持CPUID指令。

指令指针寄存器

EIP寄存器包含未执行分支时要执行的下一条指令的地址。

EIP只能在调用(call)指令后通过堆栈读取。

x86-64架构寄存器

X86-64有16个通用(几乎都是通用的)64位整数寄存器: %rax %rbx %rcx %rdx %rsi %rdi %rbp %rsp %r8 %r9 %r10 %r11 %r12 %r13 %r14 %r15

多年来X86架构已经从8位扩展到了32位,因此每个寄存器都有一些内部结构如下图:

为了简单起见,下面的讲述会把注意力集中在64位寄存器上,大多数生产编译器使用混合模式:32位寄存器通常用于整数算术,因为大多数程序不需要整数值超过2 ^ 32(例如在我的ubuntu 64bit使用gcc 64位的编译器, sizeof(int)的值为4)。64位寄存器通常用于保存存储器地址(指针),从而可以寻址最多16EB(exa-bytes)的虚拟内存。

寻址模式

寻址模式是数据在内存和寄存器之间进行移动时,取得数据地址的不同表达方式。
最常用的寻址的汇编指令是mov。mov与大多数指令一样,具有单字母后缀,用于确定要移动的数据量。下表用于描述各种大小的数据值:

对于AT&T语法使用MOV寻址时需要两个参数,第一个参数是源地址,第二个参数是目标地址。
根据源地址的表达方式不同,寻址的方式也不一样,可分为六种:

  • 全局符号寻址(Global Symbol):访问全局变量使用一个简单变量的名称比如x。示例:MOVQ x, %rax
  • 直接寻址(Immediate):printf一个整数常量,由美元符号+数值表示(例如$ 56)。示例:MOVQ $56, %rax
  • 寄存器寻址(Register):访问寄存器的值直接使用寄存器的名字如%rbx。示例:MOVQ %rbx, %rax
  • 间接寻址(Indirect):如果寄存器中存放的是一个地址,访问这个地址中的数据时需要在寄存器外面加上括号如(%rbx)。示例:MOVQ (%rbx), %rax
  • 相对基址寻址(Base-Relative):如果寄存器中存放的是一个数组的地址,我们需要访问数组中的元素时可能需要操作这个地址进行偏移,如8(%rcx)是指%rcx中存放的的地址加8字节存储单元的值,我们称之为相对基址寻址(此模式对于操作堆栈,局部变量和函数参数非常重要)。示例:MOVQ -8(%rbx), %rax
  • 相对基址偏移缩放寻址(Offset-Scaled-Base-Relative):访问排列在数组中的特殊大小的元素非常有用。disp(base, index, scale) 对于于 [base + index*scale + disp]。示例:MOVQ -16(%rbx,%rcx,8), %rax

基本运算指令

编译器需要四个基本的算术指令: ADD, SUB, IMUL, 和IDIV,即加减乘除

加、减(ADD, SUB)

ADD/SUB   源,目的   // 将目的加或减源,并将结果存在目的中

源操作数可以是:立即值、内存位置或寄存器
目的操作数可以是:寄存器或内存位置中存储的值
注意:源与目的不能同时使用内存位置。

示例:

ADD %rbx, %rax

递增、递减

在创建汇编语言程序时,常常必须遍历数组。这种程序设计很常见,intel提供了专门的指令来自动地提供计数功能。

inc, dec指令用于无符号整型的递增、递减,格式如下:

dec  destination
inc  destination

destination可以是寄存器或内存中的值

乘 MUL/IMUL

1. 无符号乘法 MUL

mul source

其中,目的操作数是RAX寄存器

2. 有符号乘法 imul
2.1 imul 格式1

imul source

其中,目的操作数是RAX寄存器

2.2 imul 格式2

imul source, destination

2.2 imul 格式3

imul multiplier, source, destination  // destination = multiplier*source

除 DIV/IDIV

  1. 无符号除法
div  divisor

被除数存储在AX寄存器中。

  1. 有符号除法
idiv divisor

比较与跳转

比较:

cmp operand1, operand2

在幕后,两个操作数执行减法操作:operand2 – operand1。
大于(小于)是指 operands2大于(小于)operand1.

intel文档中指令与gnu汇编器中顺序相反。

跳转:
使用JMP指令,创建一个简单的无限循环,使%eax寄存器从零开始计数:

MOVQ $0, %rax
loop:
    INCQ %rax
    JMP loop

为了定义更有用的程序结构,如终止循环和if-then等语句,必须有一个可以改变程序流程的机制,。在大多数汇编语言中,这些处理由两种不同的指令处理:比较和跳转:

  • 比较:通过CMP指令完成的。CMP比较两个不同的寄存器,并在内部EFLAGS寄存器中设置几个位,记录这些值是相同,更大还是更小。
  • 跳转:不需要直接看EFLAGS寄存器的值。而是根据结果的不同来做适当的跳转:
指令 意义
JE 如果等于则跳转
JNE 如果不等于则跳转
JL 若果小于则跳转
JLE 如果小于等于则跳转
JG 如果大于则跳转
JGE 如果大于等于则跳转

示例1:
一个循环来使%rax从0到5:

    MOVQ $0, %rax
loop:
    INCQ %rax
    CMPQ $5, %rax
    JLE loop

示例2:
一个条件赋值:如果全局变量 x>=0, 则 y = 10, 否则 y = 20

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

跳转的参数是目标标签。这些标签在一个汇编文件中必须是唯一且私密的,除了包含在.globl内的标签 ,其他标签不能在文件外部看到,也就是不能在文件外调用。用c语言来说,一个普通的汇编标签是static的,而.globl标签是extern。

栈是一种辅助数据结构,主要用于记录程序的函数调用历史记录以及不适合寄存器的局部变量。

栈从高地址向低地址增长。%rsp寄存器被称为“栈指针”并跟踪堆栈中最底层(也就是最新的)的数据。

压栈(入栈)或出栈是经常使用到的操作:

pushq %rax
popq %rax

pushq 的等价操作:
要将%rax压入堆栈,我们必须从%rsp中减去8(%rax的大小,rsp以字节为单位),然后写入%rsp指向的位置:

subq $8, %rsp
movq %rax, (%rsp)  // 正常传送操作,地址向高位移动

popq 的等价操作:

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

函数调用

约定

Linux上x86-64使用的调用约定有所不同,称之为System V ABI。完整的约定相当复杂,下面是足够简单的解释:

  • 整数参数(包括指针)按顺序放在寄存器%rdi,%rsi,%rdx,%rcx,%r8和%r9中。
  • 浮点参数按顺序放置在寄存器%xmm0-%xmm7中。
  • 超过可用寄存器的参数被压入栈。
  • 如果函数使用可变数量的参数(如printf),那么必须将%eax寄存器设置为浮点参数的数量。
  • 被调用的函数可以使用任何寄存器,但如果它们发生了变化,则必须恢复寄存器%rbx,%rbp,%rsp和%r12-%r15的值。
  • 函数的返回值存储在%eax中。

下面的表格总结了你需要了解的内容:

如何保证调用者当前使用的任何寄存器不被被调用的函数破坏?
为了防止这种情况发生,每个函数必须保存并恢复它使用的所有寄存器,方法是先将它们入栈,然后在返回之前将它们从堆栈弹出。
在函数调用的过程中,栈基址指针%rbp始终指向当前函数调用开始时栈的位置;
栈指针%rsp始终指向栈中最新的元素对应的位置。
%rbp和%rsp之间的元素被我们成为"栈帧",也叫"活动记录"。函数的调用过程其实就是栈帧被创建,扩张然后被销毁的过程。
在说明函数调用流程前,我们不得不提到 %rip(instruction pointer) 指令指针寄存器。
%rip中存放的是CPU需要执行的下一条指令的地址。每当执行完一条指令之后,这个寄存器会自动增加(可以这样理解)以便指向新的指令的地址。

示例

有了这些基础,接下来我们以一段完整的程序代码来解释函数的调用流程,有下面一段c代码:

#include <stdio.h>

int sum(int a, int b)
{
    return (a+b);
}
int main()
{
    int x = sum(1, 2);
    printf("result is:%d\n", x);
    return 0;
}

编译为汇编代码之后,为了方便读代码,我们去除一些不需要的指示段之后得到如下代码:

.file   "main.c"
    .text
    .globl  sum
    .type   sum, @function
sum:
.LFB0:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    popq    %rbp
    ret
.LFE0:
    .size   sum, .-sum
    .section    .rodata
.LC0:
    .string "result is:%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB1:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movl    $2, %esi
    movl    $1, %edi
    call    sum
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    ret
.LFE1:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609"
    .section    .note.GNU-stack,"",@progbits

我们知道linux系统中main函数是由glibc中的 exec()簇 函数调用的,比如我们从shell环境中启动程序最终就是由 execvp()调用而来。我们这里不展开说明,你只需要知道main函数其实也是被调用的函数。我们从main函数的第一条指令开始:

main:
.LFB1:
    pushq   %rbp
    movq    %rsp, %rbp

首先,将当前的栈基址指针%rbp入栈,函数调用结束后我们就可以从栈中取得函数调用前%rbp指向的位置,进而恢复栈到之前的样子。然后使当前栈指针指向新的位置。然后

subq    $16, %rsp
movl    $2, %esi
movl    $1, %edi

在栈上申请16字节的空间以便存放后面的临时变量x,然后根据System V ABI的调用约定将传递给sum函数的参数放入%esi和%edi中(因为是int类型占用4个字节,所以只需要用寄存器的低4字节即可)。这里你可能会发现编译器没有将需要调用者保存的%r10和%r11入栈,因为编译器知道在main函数中不会使用到%r10和%r11寄存器所以无需保存。然后发出调用指令:

call    sum

需要注意以上的CALL指令等同于:

pushq %rip
jmp sum

我们把%rip当前的内容放入栈中,以便函数sum调用结束我们可以知道接下来该执行哪条指令,我们假设栈从0xC0000000处开始向低处延伸。到这个阶段栈的变化过程如下所示:

现在程序跳转到sum处执行计算:

pushq   %rbp
movq    %rsp, %rbp
movl    %edi, -4(%rbp)
movl    %esi, -8(%rbp)
movl    -4(%rbp), %edx
movl    -8(%rbp), %eax
addl    %edx, %eax

和main函数被调用一样,sum函数被调用时,首先也是保存%rbp,然后更新栈指针%rsp,将两个参数拷贝到栈中进行使用。在这里你可能看到了和main 函数不一样的地方,局部变量保存在栈中并没有像main函数中那样引起%rsp的移动(对比main函数中的SUBQ 16)。是因为编译器知道sum中不会再调用其它函数,也就不用保存数据到栈中了,直接使用栈空间即可。所以就无需位移%rsp。计算完成后结果保存在%eax中,现在我们更新一下栈的变化:

然后返回到main函数时执行了如下操作:

popq    %rbp
ret

先恢复调用前的栈基址指针%rbp,然后此时栈顶的元素就是函数调用之后需要执行的下一条指令的地址,RET指令等价于:

popq    %rip

这样就可以跳转到函数结束后的下一条指令 "movl %eax, -4(%rbp)"处继续执行,至此我们看一下完整调用过程中栈的变化:

内存

x86架构是little-endian,这意味着多字节值首先写入最低有效字节。 (这仅指字节的顺序,而不是位。)

因此x86上的32位值B3B2B1B016将在内存中表示为:

参考

附录

x86 寄存器

让寄存器为己所用,就得了解它们的用途,这些用途都涉及函数调用,X86-64有16个64位寄存器,分别是:

%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。

其中:

  • %rax 作为函数返回值使用。
  • %rsp 栈指针寄存器,指向栈顶
  • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
  • %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
  • %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值

x86 指令集

X86和X87汇编指令大全(有注释)  
---------- 一、数据传输指令 ----------------------------------------------------  
它们在存贮器和寄存器、寄存器和输入输出端口之间传送数据.  
1. 通用数据传送指令.  
    MOV     传送字或字节.  
    MOVSX   先符号扩展,再传送.  
    MOVZX   先零扩展,再传送.  
    PUSH    把字压入堆栈.  
    POP     把字弹出堆栈.  
    PUSHA   把AX,CX,DX,BX,SP,BP,SI,DI依次压入堆栈.  
    POPA    把DI,SI,BP,SP,BX,DX,CX,AX依次弹出堆栈.  
    PUSHAD  把EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI依次压入堆栈.  
    POPAD   把EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX依次弹出堆栈.  
    BSWAP   交换32位寄存器里字节的顺序  
    XCHG    交换字或字节.(至少有一个操作数为寄存器,段寄存器不可作为操作数)  
    CMPXCHG 比较并交换操作数.(第二个操作数必须为累加器AL/AX/EAX)  
    XADD    先交换再累加.(结果在第一个操作数里)  
    XLAT    字节查表转换.----BX指向一张256字节的表的起点,AL为表的索引值(0-255,即0-FFH);返回AL为查表结果.([BX+AL]->AL)  
2. 输入输出端口传送指令.  
    IN      I/O端口输入. ( 语法: IN   累加器,    {端口号│DX} )  
    OUT     I/O端口输出. ( 语法: OUT {端口号│DX},累加器 )输入输出端口由立即方式指定时,    其范围是 0-255; 由寄存器 DX 指定时,其范围是    0-65535.  
3. 目的地址传送指令.  
    LEA     装入有效地址.例: LEA DX,string ;把偏移地址存到DX.  
    LDS     传送目标指针,把指针内容装入DS.例: LDS SI,string   ;把段地址:偏移地址存到DS:SI.  
    LES     传送目标指针,把指针内容装入ES.例: LES DI,string   ;把段地址:偏移地址存到ES:DI.  
    LFS     传送目标指针,把指针内容装入FS.例: LFS DI,string   ;把段地址:偏移地址存到FS:DI.  
    LGS     传送目标指针,把指针内容装入GS.例: LGS DI,string   ;把段地址:偏移地址存到GS:DI.  
    LSS     传送目标指针,把指针内容装入SS.例: LSS DI,string   ;把段地址:偏移地址存到SS:DI.  
4. 标志传送指令.  
    LAHF    标志寄存器传送,把标志装入AH.  
    SAHF    标志寄存器传送,把AH内容装入标志寄存器.  
    PUSHF   标志入栈.  
    POPF    标志出栈.  
    PUSHD   32位标志入栈.  
    POPD    32位标志出栈.  
---------- 二、算术运算指令 ----------------------------------------------------  
    ADD     加法.  
    ADC     带进位加法.  
    INC     加 1.  
    AAA     加法的ASCII码调整.  
    DAA     加法的十进制调整.  
    SUB     减法.  
    SBB     带借位减法.  
    DEC     减 1.  
    NEG     求反(以    0 减之).  
    CMP     比较.(两操作数作减法,仅修改标志位,不回送结果).  
    AAS     减法的ASCII码调整.  
    DAS     减法的十进制调整.  
    MUL     无符号乘法.结果回送AH和AL(字节运算),或DX和AX(字运算),  
    IMUL    整数乘法.结果回送AH和AL(字节运算),或DX和AX(字运算),  
    AAM     乘法的ASCII码调整.  
    DIV     无符号除法.结果回送:商回送AL,余数回送AH, (字节运算);或 商回送AX,余数回送DX, (字运算).  
    IDIV    整数除法.结果回送:商回送AL,余数回送AH, (字节运算);或 商回送AX,余数回送DX, (字运算).  
    AAD     除法的ASCII码调整.  
    CBW     字节转换为字. (把AL中字节的符号扩展到AH中去)  
    CWD     字转换为双字. (把AX中的字的符号扩展到DX中去)  
    CWDE    字转换为双字. (把AX中的字符号扩展到EAX中去)  
    CDQ     双字扩展. (把EAX中的字的符号扩展到EDX中去)  
---------- 三、逻辑运算指令 ----------------------------------------------------  
    AND     与运算.  
    OR      或运算.  
    XOR     异或运算.  
    NOT     取反.  
    TEST    测试.(两操作数作与运算,仅修改标志位,不回送结果).  
    SHL     逻辑左移.  
    SAL     算术左移.(=SHL)  
    SHR     逻辑右移.  
    SAR     算术右移.(=SHR)  
    ROL     循环左移.  
    ROR     循环右移.  
    RCL     通过进位的循环左移.  
    RCR     通过进位的循环右移.  
              以上八种移位指令,其移位次数可达255次.  
              移位一次时, 可直接用操作码. 如 SHL AX,1.  
              移位>1次时, 则由寄存器CL给出移位次数.  
              如 MOV CL,04   SHL AX,CL  
---------- 四、串指令 ----------------------------------------------------------  
              DS:SI 源串段寄存器 :源串变址.  
              ES:DI 目标串段寄存器:目标串变址.  
              CX 重复次数计数器.  
              AL/AX 扫描值.  
              D标志   0表示重复操作中SI和DI应自动增量; 1表示应自动减量.  
              Z标志   用来控制扫描或比较操作的结束.  
    MOVS    串传送.( MOVSB 传送字符. MOVSW 传送字. MOVSD 传送双字. )  
    CMPS    串比较.( CMPSB 比较字符. CMPSW 比较字. )  
    SCAS    串扫描.把AL或AX的内容与目标串作比较,比较结果反映在标志位.  
    LODS    装入串.把源串中的元素(字或字节)逐一装入AL或AX中.( LODSB 传送字符. LODSW 传送字.    LODSD 传送双字. )  
    STOS    保存串.是LODS的逆过程.  
    REP         当CX/ECX<>0时重复.  
    REPE/REPZ   当ZF=1或比较结果相等,且CX/ECX<>0时重复.  
    REPNE/REPNZ 当ZF=0或比较结果不相等,且CX/ECX<>0时重复.  
    REPC        当CF=1且CX/ECX<>0时重复.  
    REPNC       当CF=0且CX/ECX<>0时重复.  
---------- 五、程序转移指令 ----------------------------------------------------  
1. 无条件转移指令 (长转移)  
    JMP         无条件转移指令  
    CALL        过程调用  
    RET/RETF    过程返回.  
2. 条件转移指令   (短转移,-128到+127的距离内)( 当且仅当(SF XOR OF)=1时,OP1<OP2 )  
    JA/JNBE     不小于或不等于时转移.  
    JAE/JNB     大于或等于转移.  
    JB/JNAE     小于转移.  
    JBE/JNA     小于或等于转移.  
        以上四条,测试无符号整数运算的结果(标志C和Z).  
    JG/JNLE     大于转移.  
    JGE/JNL     大于或等于转移.  
    JL/JNGE     小于转移.  
    JLE/JNG     小于或等于转移.  
        以上四条,测试带符号整数运算的结果(标志S,O和Z).  
    JE/JZ       等于转移.  
    JNE/JNZ     不等于时转移.  
    JC          有进位时转移.  
    JNC         无进位时转移.  
    JNO         不溢出时转移.  
    JNP/JPO     奇偶性为奇数时转移.  
    JNS         符号位为 "0" 时转移.  
    JO          溢出转移.  
    JP/JPE      奇偶性为偶数时转移.  
    JS          符号位为 "1" 时转移.  
3. 循环控制指令(短转移)  
    LOOP            CX不为零时循环.  
    LOOPE/LOOPZ     CX不为零且标志Z=1时循环.  
    LOOPNE/LOOPNZ   CX不为零且标志Z=0时循环.  
    JCXZ            CX为零时转移.  
    JECXZ           ECX为零时转移.  
4. 中断指令  
    INT         中断指令  
    INTO        溢出中断  
    IRET        中断返回  
5. 处理器控制指令  
    HLT         处理器暂停,  直到出现中断或复位信号才继续.  
    WAIT        当芯片引线TEST为高电平时使CPU进入等待状态.  
    ESC         转换到外处理器.  
    LOCK        封锁总线.  
    NOP         空操作.  
    STC         置进位标志位.  
    CLC         清进位标志位.  
    CMC         进位标志取反.  
    STD         置方向标志位.  
    CLD         清方向标志位.  
    STI         置中断允许位.  
    CLI         清中断允许位.  
---------- 六、伪指令 ----------------------------------------------------------  
    DW          定义字(2字节).  
    PROC        定义过程.  
    ENDP        过程结束.  
    SEGMENT     定义段.  
    ASSUME      建立段寄存器寻址.  
    ENDS        段结束.  
    END         程序结束.  
---------- 七、处理机控制指令:标志处理指令 ------------------------------------  
    CLC     进位位置0指令  
    CMC     进位位求反指令  
    STC     进位位置为1指令  
    CLD     方向标志置1指令  
    STD     方向标志位置1指令  
    CLI     中断标志置0指令  
    STI     中断标志置1指令  
    NOP     无操作  
    HLT     停机  
    WAIT    等待  
    ESC     换码  
    LOCK    封锁  
========== 浮点运算指令集 ======================================================  
---------- 一、控制指令(带9B的控制指令前缀F变为FN时浮点不检查,机器码去掉9B)----  
FINIT                 初始化浮点部件                  机器码  9B DB E3  
FCLEX                 清除异常                         机器码  9B DB E2  
FDISI                 浮点检查禁止中断                 机器码  9B DB E1  
FENI                  浮点检查禁止中断二            机器码  9B DB E0  
WAIT                  同步CPU和FPU                    机器码  9B  
FWAIT                 同步CPU和FPU                    机器码  D9 D0  
FNOP                  无操作                          机器码  DA E9  
FXCH                  交换ST(0)和ST(1)                机器码  D9 C9  
FXCH ST(i)            交换ST(0)和ST(i)                机器码  D9 C1iii  
FSTSW ax              状态字到ax                       机器码  9B DF E0  
FSTSW   word ptr mem  状态字到mem                      机器码  9B DD mm111mmm  
FLDCW   word ptr mem  mem到状态字                      机器码  D9 mm101mmm  
FSTCW   word ptr mem  控制字到mem                      机器码  9B D9 mm111mmm  

FLDENV  word ptr mem  mem到全环境                      机器码  D9 mm100mmm  
FSTENV  word ptr mem  全环境到mem                      机器码  9B D9 mm110mmm  
FRSTOR  word ptr mem  mem到FPU状态                    机器码  DD mm100mmm  
FSAVE   word ptr mem  FPU状态到mem                    机器码  9B DD mm110mmm  

FFREE ST(i)           标志ST(i)未使用                   机器码  DD C0iii  
FDECSTP               减少栈指针1->0 2->1             机器码  D9 F6  
FINCSTP               增加栈指针0->1 1->2             机器码  D9 F7  
FSETPM                浮点设置保护                       机器码  DB E4  
---------- 二、数据传送指令 ----------------------------------------------------  
FLDZ                  将0.0装入ST(0)                  机器码  D9 EE  
FLD1                  将1.0装入ST(0)                  机器码  D9 E8  
FLDPI                 将π装入ST(0)                    机器码  D9 EB  
FLDL2T                将ln10/ln2装入ST(0)             机器码  D9 E9  
FLDL2E                将1/ln2装入ST(0)                机器码  D9 EA  
FLDLG2                将ln2/ln10装入ST(0)             机器码  D9 EC  
FLDLN2                将ln2装入ST(0)                  机器码  D9 ED  

FLD    real4 ptr mem  装入mem的单精度浮点数             机器码  D9 mm000mmm  
FLD    real8 ptr mem  装入mem的双精度浮点数             机器码  DD mm000mmm  
FLD   real10 ptr mem  装入mem的十字节浮点数             机器码  DB mm101mmm  

FILD    word ptr mem  装入mem的二字节整数              机器码  DF mm000mmm  
FILD   dword ptr mem  装入mem的四字节整数              机器码  DB mm000mmm  
FILD   qword ptr mem  装入mem的八字节整数              机器码  DF mm101mmm  

FBLD   tbyte ptr mem  装入mem的十字节BCD数            机器码  DF mm100mmm  

FST    real4 ptr mem  保存单精度浮点数到mem             机器码  D9 mm010mmm  
FST    real8 ptr mem  保存双精度浮点数到mem             机器码  DD mm010mmm  

FIST    word ptr mem  保存二字节整数到mem              机器码  DF mm010mmm  
FIST   dword ptr mem  保存四字节整数到mem              机器码  DB mm010mmm  

FSTP   real4 ptr mem  保存单精度浮点数到mem并出栈      机器码  D9 mm011mmm  
FSTP   real8 ptr mem  保存双精度浮点数到mem并出栈      机器码  DD mm011mmm  
FSTP  real10 ptr mem  保存十字节浮点数到mem并出栈      机器码  DB mm111mmm  

FISTP   word ptr mem  保存二字节整数到mem并出栈           机器码  DF mm011mmm  
FISTP  dword ptr mem  保存四字节整数到mem并出栈           机器码  DB mm011mmm  
FISTP  qword ptr mem  保存八字节整数到mem并出栈           机器码  DF mm111mmm  

FBSTP  tbyte ptr mem  保存十字节BCD数到mem并出栈     机器码  DF mm110mmm  

FCMOVB                ST(0),ST(i) <时传送              机器码  DA C0iii  
FCMOVBE               ST(0),ST(i) <=时传送             机器码  DA D0iii  
FCMOVE                ST(0),ST(i) =时传送             机器码  DA C1iii  
FCMOVNB               ST(0),ST(i) >=时传送             机器码  DB C0iii  
FCMOVNBE              ST(0),ST(i) >时传送              机器码  DB D0iii  
FCMOVNE               ST(0),ST(i) !=时传送            机器码  DB C1iii  
FCMOVNU               ST(0),ST(i) 有序时传送        机器码  DB D1iii  
FCMOVU                ST(0),ST(i) 无序时传送        机器码  DA D1iii  
---------- 三、比较指令   --------------------------------------------------------  
FCOM                  ST(0)-ST(1)                      机器码  D8 D1  
FCOMI                 ST(0),ST(i)  ST(0)-ST(1)         机器码  DB F0iii  
FCOMIP                ST(0),ST(i)  ST(0)-ST(1)并出栈   机器码  DF F0iii  
FCOM   real4 ptr mem  ST(0)-实数mem                      机器码  D8 mm010mmm  
FCOM   real8 ptr mem  ST(0)-实数mem                      机器码  DC mm010mmm  

FICOM   word ptr mem  ST(0)-整数mem                      机器码  DE mm010mmm  
FICOM  dword ptr mem  ST(0)-整数mem                      机器码  DA mm010mmm  
FICOMP  word ptr mem  ST(0)-整数mem并出栈               机器码  DE mm011mmm  
FICOMP dword ptr mem  ST(0)-整数mem并出栈               机器码  DA mm011mmm  

FTST                  ST(0)-0                          机器码  D9 E4  
FUCOM  ST(i)          ST(0)-ST(i)                      机器码  DD E0iii  
FUCOMP ST(i)          ST(0)-ST(i)并出栈                   机器码  DD E1iii  
FUCOMPP               ST(0)-ST(1)并二次出栈             机器码  DA E9  
FXAM                  ST(0)规格类型                    机器码  D9 E5  
---------- 四、运算指令   --------------------------------------------------------  
FADD                  把目的操作数 (直接接在指令后的变量或堆栈缓存器) 与来源操作数 (接在目的操作数后的变量或堆栈缓存器)  相加,并将结果存入目的操作数  
FADDP  ST(i),ST       这个指令是使目的操作数加上 ST  缓存器,并弹出 ST 缓存器,而目的操作数必须是堆栈缓存器的其中之一,最后不管目的操作数为何,经弹出一次后,目的操作数会变成上一个堆栈缓存器了  
FIADD                 FIADD 是把 ST   加上来源操作数,然后再存入 ST 缓存器,来源操作数必须是字组整数或短整数形态的变数  

FSUB                  减  
FSUBP  
FSUBR                 减数与被减数互换  
FSUBRP  
FISUB  
FISUBR  

FMUL                  乘  
FMULP  
FIMUL  

FDIV                  除  
FDIVP  
FDIVR  
FDIVRP  
FIDIV  
FIDIVR  

FCHS                  改变 ST 的正负值  

FABS                  把 ST  之值取出,取其绝对值后再存回去。  

FSQRT                 将 ST  之值取出,开根号后再存回去。  

FSCALE                这个指令是计算 ST*2^ST(1)之值,再把结果存入 ST 里而 ST(1)   之值不变。ST(1)  必须是在 -32768 到 32768 (-215 到 215 )之间的整数,如果超过这个范围计算结果无法确定,如果不是整数 ST(1)    会先向零舍入成整数再计算。所以为安全起见,最好是由字组整数载入到 ST(1) 里。  

FRNDINT               这个指令是把 ST 的数值舍入成整数,FPU    提供四种舍入方式,由 FPU 的控制字组(control    word)中的 RC 两个位决定  
                          RC    舍入控制  
                          00    四舍五入  
                          01    向负无限大舍入  
                          10    向正无限大舍入  
                          11    向零舍去  
================================================================================ 

hlist.c c 代码

#include <stdio.h>

int main(){
    int a = 777;
    int* b = &a;
    int** c = &b;
    *b = 888;
    **c = 999;
}

运行如下命令gcc -O0 -S hlist.c之后的汇编代码:

        .file   "hlist.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $32, %rsp
        movq    %fs:40, %rax
        movq    %rax, -8(%rbp)
        xorl    %eax, %eax
        movl    $777, -28(%rbp)
        leaq    -28(%rbp), %rax
        movq    %rax, -24(%rbp)
        leaq    -24(%rbp), %rax
        movq    %rax, -16(%rbp)
        movq    -24(%rbp), %rax
        movl    $888, (%rax)
        movq    -16(%rbp), %rax
        movq    (%rax), %rax
        movl    $999, (%rax)
        movl    $0, %eax
        movq    -8(%rbp), %rdx
        xorq    %fs:40, %rdx
        je      .L3
        call    __stack_chk_fail@PLT
.L3:
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
        .section        .note.GNU-stack,"",@progbits

运行如下命令后的部分机器码:

00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0100 3e00 0100 0000 0000 0000 0000 0000  ..>.............
00000020: 0000 0000 0000 0000 d802 0000 0000 0000  ................
00000030: 0000 0000 4000 0000 0000 4000 0c00 0b00  ....@.....@.....
00000040: 5548 89e5 4883 ec20 6448 8b04 2528 0000  UH..H.. dH..%(..
00000050: 0048 8945 f831 c0c7 45e4 0903 0000 488d  .H.E.1..E.....H.
00000060: 45e4 4889 45e8 488d 45e8 4889 45f0 488b  E.H.E.H.E.H.E.H.
00000070: 45e8 c700 7803 0000 488b 45f0 488b 00c7  E...x...H.E.H...
00000080: 00e7 0300 00b8 0000 0000 488b 55f8 6448  ..........H.U.dH
00000090: 3314 2528 0000 0074 05e8 0000 0000 c9c3  3.%(...t........
000000a0: 0047 4343 3a20 2855 6275 6e74 7520 372e  .GCC: (Ubuntu 7.
000000b0: 342e 302d 3175 6275 6e74 7531 7e31 382e  4.0-1ubuntu1~18.
000000c0: 3034 2e31 2920 372e 342e 3000 0000 0000  04.1) 7.4.0.....
000000d0: 1400 0000 0000 0000 017a 5200 0178 1001  .........zR..x..
000000e0: 1b0c 0708 9001 0000 1c00 0000 1c00 0000  ................
000000f0: 0000 0000 6000 0000 0041 0e10 8602 430d  ....`....A....C.
00000100: 0602 5b0c 0708 0000 0000 0000 0000 0000  ..[.............
00000110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000120: 0100 0000 0400 f1ff 0000 0000 0000 0000  ................
00000130: 0000 0000 0000 0000 0000 0000 0300 0100  ................
00000140: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000150: 0000 0000 0300 0300 0000 0000 0000 0000  ................
00000160: 0000 0000 0000 0000 0000 0000 0300 0400  ................
00000170: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000180: 0000 0000 0300 0600 0000 0000 0000 0000  ................
00000190: 0000 0000 0000 0000 0000 0000 0300 0700  ................
000001a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000001b0: 0000 0000 0300 0500 0000 0000 0000 0000  ................
000001c0: 0000 0000 0000 0000 0900 0000 1200 0100  ................
000001d0: 0000 0000 0000 0000 6000 0000 0000 0000  ........`.......
000001e0: 0e00 0000 1000 0000 0000 0000 0000 0000  ................
000001f0: 0000 0000 0000 0000 2400 0000 1000 0000  ........$.......
...

Was this helpful?

0 / 0

发表回复 0