朝花夕拾—GNU 汇编笔记二(数据段&MOV指令)


数据段

使用.data命令声明数据段,这个段中声明的任何数据元素都保留在内存中并且可以被 汇编语言中的指令读取或写入。 另外,还有另一种类型的数据段,成为.rodata。在这种数据段中定义的任何数据元素 都只能按照只读模式访问。 保留的内存数量取决于定义的数据类型和声明的这种类型的数量。

在GAS中,常用一下伪指令来定义数据类型:

命令 数据类型
.ascii 文本字符串
.asciz 以’\0’结束的字符串
.byte 字节
.double 双精度浮点数
.float 单精度浮点数
.int 32位整数
.long 32位整数(和.int相同)
.octa 16字节整数
.quad 8字节整数
.short 16位整数
.single 单精度浮点数(和.float)相同
定义静态符号

.equ伪指令类似与C的#define。它用于把常量值设置为可以在文本段中使用的符号。 它并不会占用实际的空间。

1
2
.equ factor, 3
.equ LINUX_SYS_CALL, 0x80

为了引用静态数据元素,必须在标签名称前使用美元符号($).

1
movl $LINUX_SYS_CALL, %eax
bss段

bss段(Block Started by Symbol)是一个比较特殊的数据段,当代码编译成二进制可执行文件或库时, 其中并不会按照实际大小来存储bss段,只有在加载到内存中时,再按照实际bss段大小分配内存空间。 操作系统一般会将此段初始化成0. 它通常用来存放程序中未初始化的全局变量。

在bss段中定义数据元素时,无须声明特定的数据类型,只要声明为所需目的保留原始内存部分即可。 GAS使用两个命令声明缓冲区:

  • .common: 声明未初始化的数据的通用内存区域
  • .lcommon: 声明未初始化的数据的本地通用内存区域

本地通用内存区域是为不会从本地汇编代码之外进行访问的数据保留的。

1
命令格式: .common symbol, length

symbol是赋给内存区域的标签,length是内存区域中包含的字节数量。就像下面例子:

.section .bss .lcommon buffer, 10000


MOV指令

mov指令对CPU来说是非常重要并且非常基础的指令。C/C++的指针实现基石就是mov指令。从CPU角度看,它就相当于内存的IO。 mov指令的基本格式

1
movx source, destination

source, destination的值可以是内存地址, 存储在内存中的数据值, 指令语句中定义的数据值, 寄存器

GAS为MOV指令添加了另一个维度,在其中必须声明要传送数据元素的长度。通过一个附加字符添加到MOV助记符 来声明这个长度。因此,指令就便成

1
movX

其中,X可以是下面字符:

  • l : 32位
  • w : 16位
  • b : 8位
  • q : 64位(x64指令集)
把立即数传送到寄存器和内存
1
2
3
movl $0, %eax       # 传送立即数0到EAX寄存器中
movl $0x80, %ebx    # 传送0x80到EBX寄存器中
movl $100, height   # 传送100到height内存位置
寄存器间传送数据
1
IA32的8个通用寄存器: EAX, EBX, ECX, EDX, EDI, ESI, EBP, ESP。

专利寄存器(控制,调试和段寄存器)的内容只能传送给通用寄存器或接收从通用寄存器传送来的内容。

在长度相同的寄存器间传送数据很容易,当长度不一致时,需要小心对待。例如, movb %al, %bx 编译器 会报告错误。这条指令试图把AL寄存器中的8位传送给BX寄存器中的低8位。替换的做法是使用MOVW

内存和寄存器之间传送数据

1 把数据从内存传送到寄存器

1
movl value, %eax

上面指令将标签value指定的内存位置数据传送到EAX寄存器。它指定从value标签引用的内存位置开始的 4字节传送到EAX寄存器中。

2 把数值从寄存器传送到内存

1
movl %ecx, value

上面指令将ECX寄存器中存储的4字节传送到value标签引用的内存中。

3 使用变址的内存位置

1
2
3
变址表达格式: base_address (offset_address, index, size)

获取的数据值位于: base_address + offset_address + index * size

如果其中的任何值为0,就可以忽略它们,但是任然要保留逗号作为占位符。 offset_address和index的值必须为寄存器, size的值可以为立即数

    .section .data
    output:
        .asciz "The value is %d\n"

    values:
        .int 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60

    .section .text
    .globl main # 我们直接使用gcc来编译,所以这里指定为`main`
    main:
        movl $0, %edi # 可以是其他通用寄存器

    loop:
        movl values(, %edi, 4), %eax
        push %eax
        push $output
        call printf
        addl $8, %esp
        
        inc %edi
        cmpl $11, %edi
        jne loop

        movl $0, %ebx
        movl $1, %eax
        int $0x80

编译说明: 我们上面的代码段声明了main标签,这是gcc连接需要使用的默认用户程序入口点。 如果在64位系统下,需要使用gcc -m32 demo.s -o demo来生成32位汇编代码。gcc 默认帮我们连接了libc (-lc)。如果你的系统下没有32位C库,可执行apt-get install gcc-multilib

4 使用寄存器间接寻址

当寄存器保存内存地址时,他被称为指针。 当使用标签引用内存位置中包含的数据值时,可以通过在指令中的标签前面加上$获取数据值的内存位置地址。

1
movl $values, %edi

以上指令用于把values标签引用的内存地址传送到EDI寄存器。标签名称前的$指示汇编器使用内存地址, 而不是位于这个地址的数据值。

1
movl %ebx, (%edi)

这条指令中的()指示将EBX寄存器中的值传送给EDI寄存器中包含的内存位置。如果没有括号,则指示汇编器 把EBX寄存器中的值加载到EDI寄存器中。

间接寻址是一种非常强大的功能。与C/C++中的指针类似,它可以自由的灵活的访问操作内存。

GAS不允许把值与寄存器相加,必须把值放在括号之外。必须像采取这种方式:

1
movl %edx, 4(%edi)

它指示把EDX寄存器中的值存放在EDI寄存器指向的位置之后4字节的内存位置中。 也可以把它放在相反的方向:

1
movl %edx, -4(%edi)