当前位置 博文首页 > 木多:【原创】X86_64/X86 GNU汇编、寄存器、内嵌汇编

    木多:【原创】X86_64/X86 GNU汇编、寄存器、内嵌汇编

    作者:木多 时间:2021-01-17 22:02

    整理的X86_64/X86汇编、寄存器、C内嵌汇编笔记,主要用于查阅使用。

    目录
    • 一、汇编语言
    • 二、指令
      • 数据传输指令
      • 栈操作指令
        • push
        • pop
      • 运算指令
      • 位操作
      • 比较操作指令
        • 标志寄存器
      • 流控制指令
    • 三、伪指令
      • .equ
      • .rept
      • .endr
      • .lcomm
      • .globl
      • .type
      • .ascii
      • .byte
      • .section
      • 变量
    • 四、X86_64寄存器
    • 五、常见汇编结构
      • 1. 函数调用传参
        • 使用寄存器传参
        • 使用栈传参
      • 2. 变量赋值
      • 3. 指针
      • 4. 结构
      • 5. 循环
      • 6. if语句
      • 7. 浮点数使用
    • 六、C嵌入汇编
      • 1.基本内嵌
      • 2.扩展内嵌汇编
        • 2.1汇编模板
        • 2.2操作数
        • 2.3 Clobber列表
        • 2.4 Volatile
        • 2.5 常用约束
        • 2.6约束修饰符
        • 2.7 一些有用的方法
    • 七、编译
      • 汇编与链接

    一、汇编语言

    计算机的处理器有很多不同的架构,比如 x86-64、ARM、Power 等,每种处理器的指令集都不相同,那也就意味着汇编语言不同。目前的电脑,CPU 一般是 x86-64 架构,是 64 位机。

    C语言代码:

    #include <stdio.h>
    int main(int argc, char* argv[])
    {
        printf("Hello %s!\n", "WSG");
        return 0;
    }
    

    编译为汇编:

    gcc -S -O2 hello.c -o hello.s
    或
    clang -S -O2 hello.c -o hello.s
    

    对应的汇编代码如下:

    .file   "hello.c"
            .section        .rodata.str1.1,"aMS",@progbits,1
    .LC0:
            .string "WSG"
    .LC1:
            .string "Hello %s!\n"
            .section        .text.unlikely,"ax",@progbits
    .LCOLDB2:
            .section        .text.startup,"ax",@progbits
    .LHOTB2:
            .p2align 4,,15
            .globl  main
            .type   main, @function
    main:
    .LFB23:
            .cfi_startproc
            subq    $8, %rsp
            .cfi_def_cfa_offset 16
            movl    $.LC0, %edx
            movl    $.LC1, %esi
            movl    $1, %edi
            xorl    %eax, %eax
            call    __printf_chk
            xorl    %eax, %eax
            addq    $8, %rsp
            .cfi_def_cfa_offset 8
            ret
            .cfi_endproc
    .LFE23:
            .size   main, .-main
            .section        .text.unlikely
    .LCOLDE2:
            .section        .text.startup
    .LHOTE2:
            .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
            .section        .note.GNU-stack,"",@progbits
    
    

    汇编语言的组成元素:指令、伪指令、标签和注释,每种元素独占一行

    指令:

    助记符 操作数(源,目的)
    

    伪指令以"."开头,末尾没有冒号":"。伪指令是是辅助性的,汇编器在生成目标文件时会用到这些信息,但伪指令不是真正的 CPU 指令,就是写给汇编器的。每种汇编器的伪指令也不同,要查阅相应的手册。常见的汇编器伪指令如下。

    .file   "hello.c"
    	.section        .rodata.str1.1,"aMS",@progbits,1
    

    标签以冒号“:”结尾,用于对伪指令生成的数据或指令做标记。标签很有用,它可以代表一段代码或者常量的地址(也就是在代码区或静态数据区中的位置)。可一开始,我们没法知道这个地址的具体值,必须生成目标文件后,才能算出来。所以,标签会简化汇编代码的编写。

    .LC1:
            .string "Hello %s!\n"
    

    注释以“#”号开头,与C语言中//表示注释是一样的。

    二、指令

    在代码中,助记符movq,xorl中的movxor是指令,而ql叫做后缀,表示操作数的位数。后缀一共有 b, w, l, q 四种,分别代表 8 位、16 位、32 位和 64 位。

    比如,movq 中的 q 代表操作数是 8 个字节,也就是 64 位的。movq 就是把 8 字节从一个地方拷贝到另一个地方,而 movl 则是拷贝 4 个字节。

    而在指令中使用操作数,可以使用四种格式,它们分别是:立即数、寄存器、直接内存访问和间接内存访问。

    操作数可以表示立即数(常数)值、寄存器值或是来自内存的值。比例因子\(s\)必须是1、2、4或者8.

    立即数以 $ 开头, 比如 $40。(下面这行代码是把 40 这个数字拷贝到 %eax 寄存器)。

    movl $40, %eax
    

    除此之外,在指令中最常见到的就是对寄存器的访问,GNU 的汇编器规定寄存器一定要以 % 开头

    直接内存访问:当我们在代码中看到操作数是一个数字时,它其实指的是内存地址。不要误以为它是一个数字,因为数字立即数必须以 $ 开头。另外,汇编代码里的标签,也会被翻译成直接内存访问的地址。比如callq _printf中的_printf是一个函数入口的地址。汇编器帮我们计算出程序装载在内存时,每个字面量和过程的地址。

    间接内存访问:带有括号,比如(%rbp),它是指 %rbp 寄存器的值所指向的地址。

    间接内存访问的完整形式是:

    偏移量(基址,索引值,字节数)这样的格式。

    其地址是:

    基址 + 索引值 * 字节数 + 偏移量

    举例来说:

    8(%rbp),是比 %rbp 寄存器的值加 8。

    -8(%rbp),是比 %rbp 寄存器的值减 8。

    (%rbp, %eax, 4)的值,等于 %rbp + %eax*4。这个地址格式相当于访问 C 语言中的数组中的元素,数组元素是 32 位的整数,其索引值是 %eax,而数组的起始位置是 %rbp。其中字节数只能取 1,2,4,8 四个值。

    几个常用的指令:

    数据传输指令

    mov

    mov 寄存器|内存|立即数, 寄存器|内存
    

    这个指令最常用到,用于在寄存器或内存之间传递数据,或者把立即数加载到内存或寄存器。mov 指令的第一个参数是源,可以是寄存器、内存或立即数。第二个参数是目的地,可以是寄存器或内存。

    lealea 是“load effective address”的意思,装载有效地址,实际是mov指令的变形。其操作不影响任何条件码

    lea 源,目的
    

    参数为标准格式中给定的内存位置,但并不加载内存位置的内容,而是加载计算得出的地址。例如:如果寄存器%rdx的值为x,那么指令leaq 7(%rdx,%rdx,4),%eax将设置寄存器%rax的值为5x+7

    cld

    该指令清除了标志寄存器中的DF位。 清除方向标志后,所有字符串操作(如stos,scas和其他操作)都会使索引寄存器esi或edi递增。

    std

    与cld相反,该指令置位了标志寄存器中的DF位。 置位方向标志后,所有字符串操作(如stos,scas和其他操作)都会使索引寄存器esi或edi递减。

    stosl

    stosl指令将eax复制到es:di中,若设置了EFLAGS中的方向位置位(即在STOSL指令前使用STD指令)则EDI自减4,否则(使用CLD指令)EDI自增4;

    rep

    重复执行%ecx次,如rep; stosl表示重复执行stosl,直到cx为0,例:

    cld;rep;stosl
    

    cld设置edi或同esi为递增方向,rep做(%ecx)次重复操作,stosl表示edi每次增加4。

    栈操作指令

    指令 描述
    push 源 把源压入栈
    pop 目的 把栈顶的元素放入目的

    push

    pushl %eax
    

    相当于:

    subl $4, %esp
    mvol %eax,(%esp)
    
    pushfl #表示将%eflage寄存器当前的数据入栈
    

    pop

    popl %eax
    

    相当于:

    movl (%esp), %eax
    addl $4, %esp
    

    运算指令

    指令 描述
    sub 源, 目的 把目的中值减去源的值
    imul 源, 目的 把目的乘上源
    clto 转换为8字(%rax符号扩展 →%rdx:%rax)
    xor 源, 目的 做异或运算
    or 源, 目的 或运算
    and 源, 目的 与运算
    inc 目的 加一
    dec 目的 减一
    neg 目的 取负值
    not 目的 按位取反

    add 指令是做加法运算,它可以采取下面的格式:

    add 立即数, 寄存器 
    add 寄存器, 寄存器 
    add 内存, 寄存器 
    add 立即数, 内存 
    add 寄存器, 内存
    

    比如,典型的 c=a+b 这样一个算术运算可能是这样的:

    movl -4(%rbp), %eax    #把%rbp-4的值拷贝到%eax
    addl -8(%rbp), %eax   #把%rbp-8地址的值加到%eax上
    movl %eax, -12(%rbp)   #把%eax的值写到内存地址%rbp-12
    

    and 对两个操作数的内容进行逻辑与运算,并将结果存储到第二个操作数,将溢出标志位及进位标志设置为FALSE。

    not 对操作数的每一位逻辑取反,也称为一个数的补数

    or 对两个操作数进行逻辑或,并将结果存储到第二个操作数,将溢出标志位设置为FLASE

    adc 带进位加法。将进位位与第一个操作数与第二个操作数相加,如果存在溢出,就将溢出及进位标志设置为真。

    cdq将%eax中的字带符号扩展为%eax:%eax组成的双字。q表示这是一个双字(64字节).这条指令通常在发出idivl指令之前。

    cmp 比较两个整数,将第二个操作数减去第一个操作数,舍弃结果,设置标志位。

    dec将寄存器或内存位置的数据减一。

    div执行无符号除法。将%edx:%eax所含的双字除以指定寄存器或内存位置的值。运算后%eax包含商,%edx包含余数,如果商对于%eax来说过大,导致溢出,将触发中断0.

    idiv执行有符号除法。

    imul执行有符号乘法,将结果保存到第二个操作数。如果第二个操作数空缺,就默认为%eax,且完好的结果将存在%eax:%eax中

    inc递增给定寄存器或地址。

    mul执行无符号乘法,运算规则与imull相同

    neg将给定寄存器或内存位置的内容补齐(二进制求补)

    sbb错位减法,与adc用法相同。通常使用sub

    sub将两个操作数相减,用第二个操作数减去第一个操作数,将结果保存的到第二个操作数,本指令可用于有符号整数及无符号整数

    位操作

    rcl将第一个操作数,向左循环移位给定次数,第一个操作数可以是立即数或寄存器%cl。循环移位包含进位标志,因此此指令实际上对33位而非32位进行操作。本指令将设置溢出标志

    rcr向右循环移位,其他与上一条指令相同

    rol向左循环移位,本指令设置溢出标志和进位标志,但不会将进位位作为循环移位的一部分。向左循环移位的次数可以通过立即寻址方式或寄存器%cl的值指定

    ror向右循环移位,其他与上一条指令相同

    sal算术左移,符号位移出至进位标志,最低有效位填充0,其他位左移。与一般左移相同,移动位数通过立即寻址方式或是寄存器%cl指定。

    sar算术右移(填上符号位),最低有效位移出至进位标志,符号位被向右移入,并保留原符号位。其他位只是向右移。移动位数通过立即寻址方式或是寄存器%cl指定。

    shl逻辑左移,将所有位左移(对符号位不做特殊处理).将最左一位推入进位标志,移动位数通过立即寻址方式或是寄存器%cl指定。

    shr逻辑右移,将所有位右移(对符号位不做特殊处理).将最右一位推入进位标志,移动位数通过立即寻址方式或是寄存器%cl指定。

    比较操作指令

    指令 描述
    cmp 源1, 源2 根据源1-源2设置状态码
    test 源1, 源2 根据源1& 源2设置状态码

    标志寄存器

    OF: 溢出标志.最近的操作导致一个补码溢出---正溢出或负溢出。

    SF : 符号标志.最近的操作得到的结果为负数。

    ZF:零标志,最近的操作得出的结果为0。

    A 辅助进位标志。

    P 奇偶标志,如果最后一个结果的低字节由偶数个1,此标志为真。

    CF 进位标志,最近的操作使最高位产生了进位。可用来检查无符号操作的溢出。

    指令 描述
    cli、sti 清除IF标志位(CLear Interrupt flag)、置位IF标志(SeT Interrupt flag)
    pushfq、popfq 将RFLAGS的值压栈和出栈

    例如,用一条ADD指令完成等价于t=a+b的功能,这里a、b和t都是整型。然后根据结果来设置条件码:

    CF (unsigned) t < (unsigned) a 无符号溢出

    ZF (t = 0)

    SF (t < 0) 负数

    OF (a < 0==b < 0) && (t < 0 !=a < 0) 有符号溢出

    流控制指令

    指令 描述
    jmp 标签或地址 跳转到某个位置的代码
    call 标签或地址 把返回地址压入栈,并跳转到指定位置的代码
    ret 从栈里弹出返回地址,并跳转过去

    call 将%eip所指的下一个值入栈,并跳转到目的地址。这用于函数调用。目的地址也可以是星号后跟寄存器的形式,这种方式为间接函数调用。例如 call *%eax将调用%eax中所含地址所指的函数

    int 引起给定数字的中断。

    jxx条件分支。xx为条件(由前一条指令设置)为TRUE,就跳转到给定的地址;否则,执行下一条指令。条件代码如下:

    指令 含义 状态码
    je或jz 跳转,如果相等(等于零) ZF
    jne或jnz 跳转,如果不相等(等于零) ~ZF
    js 跳转,如果为负值 SF
    jns 跳转,如果不为负值 ~SF
    jg或jnle 跳转,如果大于,有符号数 ~(SF^OF) & ~ZF
    jge或jnl 跳转,如果大于等于,有符号数 ~(SF^OF)
    jl或jnge 跳转,如果小于,有符号数 SF^OF
    jle或jne 跳转,如果小于等于,有符号数 SF^OF | ZF
    ……
    • [n]a[e]—大于(无符号大于)、不大于、大于等于
    • [n]b[e]—小于(无符号小于)
    • [n]e—等于
    • [n]z—0
    • [n]g[e]—大于(带符号比较)
    • [n]l[e]—小于(带符号比较)
    • [n]c——进位标志集
    • [n]o ——溢出标志集
    • [p]p ——相等标志集
    • [n]s ——符号标志集
    • ecxz——%ecx为0

    jmp无条件跳转。仅仅是将%eip设置为目的地址,目的地址也可以是星号后跟寄存器的形式,这种方式为间接函数调用。jmp *%eax用寄存器中的值作为跳转目标,jmp *(%rax)以内存地址%rax中的值作为跳转目标。

    ret从栈种弹出值,并将%eip设置为该值,用于从函数调用返回。

    三、伪指令

    .equ

    .equ允许你为数字分配名称。例如

    .equ LINUX_SYSCALL,0x80
    

    此时LINUX_SYSCALL就是一个常量,使用如下

    int $LINUX_SYSCALL
    

    计算一段数据的长度

    .section .data
    helloworld:
    	.ascii "hello world\n"
    helloworld_end;
    .equ helloworld_len, helloworld_end - helloworld
    

    .rept

    .rept用于填充每一项,.rept告诉汇编程序将.rept.endr之间的断重复指定次数.

    .rept 30  #填充30字节0
    .byte 0
    .endr
    

    .endr

    结束以.rept定义的重复节(section)

    .lcomm

    .locmm指令将创建一个符号,代指一个存储位置。使用.lcomm创建一个符号my_buffer,代指.bss段中用作缓冲区的500字节存储位置。

    .section .bss
    .locmm my_buffer, 500
    
    movl $my_buffer, %ecx #将缓冲区地址加载到%ecx中
    

    .globl

    .globl声明一个全局符号。

    .globl _start
    _start:
    

    .type

    .type指定一个符号作为某种类型。例如告诉链接器 符号power作为函数处理:

    .type power, @function
    power:
    	……
    

    如果其他程序中没有使用该函数,则这条指令可以不需要。power:将下一条指令的存储位置赋给符号power,这就是为什么调用该函数时需要如下执行:

    call power
    

    .ascii

    将给定带引号字符串转换为字节数据

    .byte

    将逗号分隔符的值列表作为数据插入程序

    .section

    切换正在使用的节。通用节包括.text、.data、.bss

    变量

    定义一个long型变量begin如下:

    begin:
    	.long 0
    

    四、X86_64寄存器

    x86-64 架构的 CPU 里有很多寄存器,我们在代码里最常用的是 16 个 64 位的通用寄存器,分别是:

    %rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp, %r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15

    这些寄存器在历史上有各自的用途,比如,rax 中的“a”,是 Accumulator(累加器) 的意思,这个寄存器是累加寄存器。

    但随着技术的发展,这些寄存器基本上都成为了通用的寄存器,不限于某种特定的用途。但是,为了方便软件的编写,我们还是做了一些约定,给这些寄存器划分了用途。针对 x86-64 架构有多个调用约定(Calling Convention),包括微软的 x64 调用约定(Windows 系统)、System V AMD64 ABI(Unix 和 Linux 系统)等,下面的内容属于后者:

    • %rax 除了其他用途外,通常在函数返回的时候,把返回值放在这里。

    • %rsp 作为栈指针寄存器,指向栈顶。

    • %rdi,%rsi,%rdx,%rcx,%r8,%r9 给函数传整型参数,依次对应第 1 参数到第 6 参数。超过 6 个参数使用。

    如果程序要使用 %rbx,%rbp,%r12,%r13,%r14,%r15 这几个寄存器,是由被调用者(Callee)负责保护的,也就是写到栈里,在返回的时候要恢复这些寄存器中原来的内容。其他寄存器的内容,则是由调用者(Caller)负责保护,如果不想这些寄存器中的内容被破坏,那么要自己保护起来。

    上面这些寄存器的名字都是 64 位的名字,对于每个寄存器,我们还可以只使用它的一部分,并且另起一个名字。比如对于 %rax,如果使用它的前 32 位,就叫做 %eax,前 16 位叫 %ax,前 8 位(0 到 7 位)叫 %al,8 到 15 位叫 %ah。

    原本含义 64位 32位 16位 高8位 低8位
    Accumulator 累加器 rax eax ax ah al
    Base 基地址 rbx ebx bx bh bl
    Counter 计数器 rcx ecx cx ch cl
    Data 数据 rdx edx dx dh dl
    Source rsi esi si sil
    Destination 目的 rdi edi di dil
    Stack Base Pointer 栈基址 rbp ebp bp bpl
    Stack Pointer 栈指针 rsp esp sp spi
    后增加的8个通用寄存器 r8-r15 r8d-r15d r8w-r15w r8b-r15b

    除了通用寄存器以外,有可能的话,还要了解下面的寄存器和它们的用途,我们写汇编代码时经常跟它们发生关联:

    • 8 个 80 位的 x87 寄存器,用于做浮点计算;

    • 8 个 64 位的 MMX 寄存器,用于 MMX 指令(即多媒体指令),这 8 个跟 x87 寄存器在物理上是相同的寄存器。在传递浮点数参数的时候,要用 mmx 寄存器。

    • 16 个 128 位的 SSE 寄存器,用于 SSE 指令。 (SIMD )。

    • 指令寄存器,rip,保存指令地址。CPU 总是根据这个寄存器来读取指令。

    • flags(64 位:rflags, 32 位:eflags)寄存器:每个位用来标识一个状态。比如,它们会用于比较和跳转的指令,比如 if 语句翻译成的汇编代码,就会用它们来保存 if 条件的计算结果。

    五、常见汇编结构

    1. 函数调用传参

    使用寄存器传参

    在 X86-64 架构下,有很多的寄存器,所以程序调用约定中规定尽量通过寄存器来传递参数,而且,只要参数不超过 6 个,都可以通过寄存器来传参,使用的寄存器如下:

    32位名称 64位名称 所传参数
    %eax %rax 参数1
    %esi %esi 参数2
    %edx %rdx 参数3
    %ecx %rcx 参数4
    %r8d %r8 参数5
    %r9d %r9 参数6

    使用栈传参

    超过 6 个的参数的话,要再加上栈来传参:

    根据程序调用约定的规定,参数 1~6 是放在寄存器里的,参数 7 和 8 是放到栈里的,函数参数以逆序的方向入栈,先放参数 8,再放参数 7。

    int fun1(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8){
        int c = 10; 
        return x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + c;
    }
    println("fun1:" + fun1(1,2,3,4,5,6,7,8));
    
    # function-call2-craft.s 函数调用和参数传递
        # 文本段,纯代码
        .section    __TEXT,__text,regular,pure_instructions
    _fun1:
        # 函数调用的序曲,设置栈指针
        pushq   %rbp           # 把调用者的栈帧底部地址保存起来   
        movq    %rsp, %rbp     # 把调用者的栈帧顶部地址,设置为本栈帧的底部
        movl    $10, -4(%rbp)  # 变量c赋值为10,也可以写成 movl $10, (%rsp)
        # 做加法
        movl    %edi, %eax     # 第一个参数放进%eax
        addl    %esi, %eax     # 加参数2
        addl    %edx, %eax     # 加参数3
        addl    %ecx, %eax     # 加参数4
        addl    %r8d, %eax     # 加参数5
        addl    %r9d, %eax     # 加参数6
        addl    16(%rbp), %eax  # 加参数7
        addl    24(%rbp), %eax  # 加参数8
        
        addl    -4(%rbp), %eax # 加上c的值
        # 函数调用的尾声,恢复栈指针为原来的值
        popq    %rbp           # 恢复调用者栈帧的底部数值
        retq                   # 返回
        .globl  _main          # .global伪指令让_main函数外部可见
    _main:                                  ## @main
        
        # 函数调用的序曲,设置栈指针
        pushq   %rbp           # 把调用者的栈帧底部地址保存起来  
        movq    %rsp, %rbp     # 把调用者的栈帧顶部地址,设置为本栈帧的底部
        
        subq    $16, %rsp      # 这里是为了让栈帧16字节对齐,实际使用可以更少
        # 设置参数
        movl    $1, %edi     # 参数1
        movl    $2, %esi     # 参数2
        movl    $3, %edx     # 参数3
        movl    $4, %ecx     # 参数4
        movl    $5, %r8d     # 参数5
        movl    $6, %r9d     # 参数6
        movl    $7, (%rsp)   # 参数7
        movl    $8, 8(%rsp)  # 参数8
        callq   _fun1                # 调用函数
        # 为pritf设置参数
        leaq    L_.str(%rip), %rdi   # 第一个参数是字符串的地址
        movl    %eax, %esi           # 第二个参数是前一个参数的返回值
        callq   _printf              # 调用函数
        # 设置返回值。这句也常用 xorl %esi, %esi 这样的指令,都是置为零
        movl    $0, %eax
        addq    $16, %rsp    # 缩小栈
        
        # 函数调用的尾声,恢复栈指针为原来的值
        popq    %rbp         # 恢复调用者栈帧的底部数值
        retq                 # 返回
        # 文本段,保存字符串字面量                                  
        .section    __TEXT,__cstring,cstring_literals
    L_.str:                                 ## @.str
        .asciz  "fun1 :%d \n"
    

    其栈帧的变化过程,如下:

    使用栈来传递参数时,需要将函数参数以逆序的方向入栈,并发出call指令。调用后在将参数出栈:

    printf("The numer is %d",88);
    
    .section .data
    test_string:
    .ascii "The numer is %d\0"
    .section .text
    
    pushl $88
    pushl $test_string
    call printf
    popl %eax
    popl %eax
    

    2. 变量赋值

    汇编语言中全局变量访问方式与局部变量不同。全局变量通过直接寻址访问,而局部变量使用基址寻址方式,例如

    int my_global_var;
    int foo()
    {
        int my_local_var;
        
        my_local_var = 1;
        my_glocal_var = 2;
        
        return 0
    }
    

    用汇编表示以上为:

    .section .data
    .lcomm my_globl_var, 4
    
    .type foo, @function
    foo:
    	pushl %ebp #保存原栈基址
    	movl %esp, %ebp #令栈指针指向新基址指针
    	subl $4, %esp #为变量my_local_var保留空间
    	.equ my_local_var, -4 #用my_local_var寻找局部变量
    	
    	movl $1, my_local_var(%ebp)
    	movl $2, my_global_var
    	
    	movl %ebp, %esp #清除函数变量并返回
    	popl %ebp
    	ret
    

    3. 指针

    指针,它只是保存某个值的地址。全局变量:

    int global_data = 30;
    

    其对应的汇编为:

    .section .data
    global_data:
    	.long 30
    

    C语言中取地址如下:

    p = &global_data;
    

    对应的汇编为:

    movl $global_data, %eax
    

    可以看到汇编语言中总是通说指针访问内存,也就是直接寻址方式,为了取得指针本身,必须采用立即寻址方式。

    局部变量略为复杂,C语言代码如下:

    void foo()
    {
        int a;
        int  *b;
        
        a = 30;
        
        b = &a;
        *b = 44;
    }
    

    对应汇编如下:

    foo:
    #标准函数开头
    	pushl %ebp
    	movl %ebp,%esp
    	
    	#保留两个字的内存
    	subl -8, %ebp
    	.equ A_VAR, -4
    	.equ B_VAR, -8
    	
    	#a = 30
    	movl $30, A_VAL(%ebp)
    	
    	#b = &a
    	movl $A_VAR, B_VAR(%ebp)
    	addl %ebp, B_VAR(%ebp)
    	
    	#*b = 30
    	movl B_VAR(%ebp), %eax  #B
    	movl $30, (%eax)
    	
    #标准结束函数
    	movl %ebp ,%esp
    	popl %ebp
    	ret
    

    要获取局部变量的地址,必须按基址寻址方式计算该地址。还有更简单的方式就是lea指令,该指令加载有效地址,会让计算机计算地址,然后在需要的时候加上地址:

    #b = &a
    leal A_VAR(%ebp), %eax
    movl %eax, B_VAR(%ebp)
    

    4. 结构

    结构时对内存块的简单描述,例如,在C语言中可以使用如下代码:

    struct person{
        char pristname[40];
        char lastname[40];
        int age
    };
    

    汇编中只是给予你一种使用84字节数据的方式。

    .equ PERSON_SIZE, 84
    .equ PERSON_FIRSTNAME_OFFSET, 0
    .equ PERSON_LASTNAME_OFFSET, 40
    .equ PERSON_AGE_OFFSET, 80
    

    当声明此类型的一个变量时,保留84字节空间就行,C代码如下:

    void foo()
    {
        struct person p;
        /**/
        ……
    }
    

    对应的汇编代码如下:

    foo:
    	#标准开头
    	pushl %ebp
    	movl %esp, %ebp
    	
    	#为局部变量分配空间
    	subl $PERSON_SIZE, %esp 
    	#这是变量相对于%ebp的偏移量
    	.equ P_VAR, 0-PERSON_SIZE
    	
    	……
    	#标准结束
    	movl %ebp, %esp
    	pop %ebp
    	ret
    

    访问结构体成员,必须使用基址寻址方式,偏移量为上面定义的值。如C语言设置年龄如下:

    p.age = 30;
    

    对应的汇编入下:

    movl $30, P_VAR + PERSON_AGE_OFFSET(%ebp)
    

    5. 循环

    C语言语句如下:

    while (a < b){
        /*某些操作*/
    }
    /*结束循环*/
    

    这些对应的汇编如下所示:

    loop_begin:
    	movl a, %eax
    	movl b, %ebx
    	cmpl %eax, %ebx
    	jge loop_end
    	
    	loop_body:
    	#某些操作
    	jmp loop_begin
    	
    loop_end:
    #结束循环
    

    上面说到寄存器%ecx可用作计数器,终止条件为0,loop 指令会递减%ecx,并在%ecx不为0 的条件下跳转到指定地址,例如,需执行某个语句100次C 语言如下:

    for (i = 0; i < 100; i++){
       /*某些操作*/
    }
    

    汇编实现如下:

    loop_initalize:
    	movl 100,%ecx
    loop_begin:
    	#某些操作
    	#递减%ecx,若%ecx不为0则继续循环
    	loop loop_begin
    	
    rest_of_program:
    

    6. if语句

    if(a == b){
        /*真分支操作*/
    }else{
        /*假分支操作*/
    }
    /*真假汇合*/
    

    在汇编中表示如下:

    	#将a.b移入寄存器用于比较
        movl a, %eax
        movl b, %ebx
    
        #比较
        cmpl %eax, %ebx
        #跳转到真分支
        je true_branch
    fale_branch: #非必要标签,只是为了说明这是假分支
    #假分支代码
    
    #跳到真假汇合
    	jmp reconverge
    true_branch:
    	#真分支代码
    reconverge:
    

    7. 浮点数使用

    之前我们用的例子都是采用整数,现在使用浮点数来做运算。下面这段代码:

    float fun1(float a, float b){
        float c = 2.0;
        return a + b + c;
    }
    
    
    下一篇:没有了