定义数据元素
Data段
程序的数据段是最常见的定义数据元素的位置。数据段定义用来存储数据项的特定内存位置。这些数据项可以被程序中的指令码引用,并且可以被随意读取和修改。
使用data命令声明数据段。在这个段中声明的任何数据元素都保留在内存中并且可以被汇编语言程序中的指令读取和写入。
还有另外一种类型的数据段,称为.rodata。在这种数据段中定义的任何数据元素只能按照只 读(read-only)模式访问(因此使用ro前缀)。
在数据段中定义数据元素需要用到两个语句: 一个标签和一个命令。
标签用作引用数据元素所使用的标记,它很像C程序中的变量名称。标签对处理器是没有意义的,它只是汇编器试图访问内存位置时用作引用指针的一个位置。
除了标签之外,还必须定义为数据元素保留多少字节。这是使用一个汇编器命令完成的。 这个命令指示汇编器为通过标签引用的数据元素保留特定数量的内存。
保留的内存数量取决于定义的数据的类型,以及要声明的这个类型的项目的数扯。下表介绍IA-32平台可以用于为特定数据元素类型保留内存的不同命令。
声明命令之后,必须定义一个(或者多个)默认值。这样把保留的内存位置中的数据设置为特定值。
命令 | 数据类型 |
---|---|
.ascii | 文本字符串 |
.asciz | 以空字符结尾的文本字符串 |
.byte | 字节值 |
.double | 双精度浮点数 |
.float | 单精度浮点数 |
.int | 32位整数 |
.long | 32位整数(和.int相同) |
.octa | 16字节整数 |
.quad | 8字节整数 |
.short | 16位整数 |
.single | 单精度浮点数(等同于.float) |
例:
.section data
output:
.ascii "this is a string"
pi:
.float 3.1415926
size:
.long 100,200,300,400,500
按照数据段中定义数据元素的顺序, 每个数据元素被存放到内存中。 带有多个值的元素按照命令中列出的顺序存放。
最低的内存位置包含第一个数据元素。字节被顺序放在内存中。下一个数据元素紧跟在前一个元素后面。
定义静态符号
虽然数据段主要用于定义变量数据, 但是也可以在这里声明静态数据符号。.equ命令用于把常量值设置 为可以在文本段中使用的符号, 设置方法如下:
.equ factor, 3
.equ LINUX_SYS_CALL, Ox80
经过设置之后, 数据符号值是不能在程序中改动的。.equ命令可以出现在数据段中的任何位置,但是为了使需要阅读你的程序的其他人更加方便,最好在定义其他数据之前或者之后集中定义所有数据符号。
为了引用静态数据元素, 必须在标签名称前面使用$符号。
例如:
movl $LINUX_SYS_CALL, %eax
把赋值给LINUX_SYS_CALL符号的值传送给EAX寄存器。
BSS段
在bss段中定义数据元素和在数据段中定义有些不同。无须声明特定的数据类型,只要声明为所需目的保留的原始内存部分即可。
GNU汇编器使用两个命令声明缓冲区,如下表所示。
命令 | 描述 |
---|---|
.comm | 声明未初始化的数据的通用内存区域 |
.lcomm | 声明未初始化的数据的本地通用内存区域 |
虽然这两种区域的工作情况类似,但是本地通用内存区域是为不会从本地汇编代码之外进行访问的数据保留的。 这两个命令的格式是:
.comm symbol, length
其中symbol是赋给内存区域的标签,length是内存区域中包含的字节数损,就像下面的例子所示:
.section.bss
.lcomm buffer, 10000
这些语句把10000字节的内存区域赋值给buffer标签。在声明本地通用内存区域的程序之外的函数是不能访问它们的(不能在globl命令中使用它们)。
在bss段中声明数据的一个好处是数据不包含在可执行程序中。在数据段中定义数据时,它必须被包含在可执行程序中,因为必须使用特定值初始化它。因为不使用程序数据初始化bss段中声明的数据区域,所以内存区域被保留在运行时使用,并且不必包含在最终的程序中。
操作系统会在运行程序的时候将.bss段给清零,但是裸机是不会的,需要手动进行清零。
.fill命令使汇编器自动创建指定字节数的数据元素,并使用0填充。
例:
.section .data
buffer:
.fill 10000
上述代码声明一万字节的buffer,并使用0填充,但是这会造成程序二进制膨胀。
传送数据元素
MOV指令格式
MOV指令的基本格式如下:
movx source, destination
source和destination的值可以是内存地址、存储在内存中的数据值、指令语句中定义的数据值,或者是寄存器。
GNU 汇编器使用AT&T样式的语法,所以其中的源和目标操作数的顺序和Intel 文档中给出的顺序是相反的。
GNU汇编器为MOV指令添加了另一维度,在其中必须声明要传送的数据元素的长度。通过把一个附加字符添加到MOV助记符来声明这个长度。因此, 指令就变成了 movx
。
其中x
可以是下面的字符:
- l用于32位的长字值
- w用于16位的字值
- b用于8位的字节值
使用MOV指令有非常特殊的规则。 只有某些位置可以传送给其他位置,MOV指令的源和目标操作数组合如下所示:
- 把立即数据元素传送给通用寄存器
- 把立即数据元素传送给内存位置
- 把通用寄存器传送给另一个通用寄存器
- 把通用寄存器传送给段寄存器
- 把段寄存器传送给通用寄存器
- 把通用寄存器传送给控制寄存器
- 把控制寄存器传送给通用寄存器
- 把通用寄存器传送给调试寄存器
- 把调试寄存器传送给通用寄存器
- 把内存位置传送给通用寄存器
- 把内存位置传送给段寄存器
- 把通用寄存器传送给内存位置
- 把段寄存器传送给内存位置
MOVS指令是一个作用特殊的指令,用于把字符串值从一个内存位置传送给另一个内存位置。
从寄存器到寄存器是最快的数据传递方式。8个通用寄存器(EAX、EBX、ECX、EDX、EDI、ESI、EBP和ESP)是用于保存数据的最常用的寄存器。这些寄存器的内容可以传送给可用的任何其他类型的寄存器。和通用寄存器不同,专用寄存器(控制、调试和段寄存器)的内容只能传送给通用寄存器,或者接收从通用寄 存器传送来的内容。
在长度相同的寄存器之间传送数据是很容易的。在长度不同的寄存器之间传送数据要困难一些。当指定长度大一些的寄存器接收长度小些的数据时要小心。
例:
movl $123 %eax
movl $123 value
movl %eax %ebx
movl %eax %cs
movl %cs %eax
使用变址的内存位置,对于存放在内存中的一系列数据值(通常是数组),每个数据值都占用内存的一个单元。引用数组中的数据元素时,必须使用变址来访问指定的内存位置。
变址内存模式访问由以下因素确定:
- 基址
- 添加到基址上的偏移地址
- 数据元素的长度
- 确定选择哪个数据元素的变址
表达式格式:
base_address(offset_address,index,size)
获取的内存位置是:
base_address + offset_address + index * size
如果其中任何值为0,可以不写,但是必须保留逗号作为占位符。
offset_address
和index
的值必须是寄存器,size
的值可以是数字值。
例:
movl value(,%edi,4),%eax
寄存器间接寻址,除了保存数据外,寄存器也可以用于保存内存地址。当寄存器保存内存地址时,它被称为指针。使用指针访问存储在内存位置中的数据称为间接寻址。
当使用标签引用内存位置中包含的数据值时,可以通过在指令中的标签前面加上$
符号获得数据值的内存位置的地址。
例:
movl $values %edi
间接寻址的另一半是通过寄存器中的地址访问内存中的数据。在存放地址的寄存器外加上()
,用以指向寄存器中地址所指向的内存。
例:
movl %ebx (%edi)
GNU汇编器不允许把数值与寄存器相加,所以我们想要访问寄存器所指定地址的前后位置时,需要把数值放在括号前。
例:
movl %edx 4(%edi) //后4字节
movl %edx -4(%edi) //前4字节
CMOV指令
CMOV条件传送指令集包含许多指令,格式如下:
cmovx source destination
其中x是一个或两个字母,表示触发传送操作的条件。条件取决于EFLAGS寄存器的当前值。
EFLAGS位 | 名称 | 描述 |
---|---|---|
CF | 进位(Carry) 标志 | 数学表达式产生了进位或者借位 |
OF | 溢出(Overflow) 标志 | 整数值过大或者过小 |
PF | 奇偶校验(Parity) 标志 | 寄存器包含数学操作造成的错误数据 |
SF | 符号(Sign) 标志 | 指出结果为正还是负 |
ZF | 零(Zero) 标志 | 数学操作的结果为零 |
条件传送指令成对地分组在一起, 两个指令具有相同的含义。 例如, 个值可以大于另一个值, 但是也可以说它是不小于或者等于另一个值。 这两个条件是等同的, 但是二者具有各自的条件传送指令。
条件传送指令分为用于带符号操作的指令和用于无符号操作的指令。 带符号操作涉及使用符号标志的比较,而无符号操作涉及忽略符号标志的比较。
无符号条件传送指令依靠进位、零和奇偶校验标志来确定两个操作数之
间的区别。
指令对 | 描述 | EFLAGS状态 |
---|---|---|
CMOVA/CMOVNBE | 大于/不小于或者等于 | (CF或ZF) =0 |
CMOVAE/CMOVNB | 大于或者等于/不小于 | CF = 0 |
CMOVNC | 无进位 | CF = 0 |
CMOVB/CMOVNAE | 小于1不大于或者等于 | CF = 1 |
CMOVC | 进位 | CF = 1 |
CMOVBE/CMOVNA | 小于或者等于/不大于 | (CF或ZF) = 1 |
CMOVE/CMOVZ | 等于/零 | ZF= 1 |
CMOVNE/CMOVNZ | 不等于/不为零 | ZF = 0 |
CMOVP/CMOVPE | 奇偶校验/偶校验 | PF = 1 |
CMOVNP/CMOVPO | 非奇偶校验/奇校验 | PF = 0 |
如果操作数是带符号值,就必须使用不同的条件传送指令集。
指令对 | 描述 | EFLAGS状态 |
---|---|---|
CMOVGE/CMOVNL | 大于或者等于/不小于 | (SF异或OF) = 0 |
CMOVL/CMOVNGE | 小于/不大于或者等于 | (SF异或OF) = l |
CMOVLE/CMOVNG | 小于或者等于/不大于 | ((SF异或OF) 或ZF ) = 1 |
CMOVO | 溢出 | OF = 1 |
CMOVNO | 未溢出 | OF = 0 |
CMOVS | 带符号(负) | SF = 1 |
CMOVNS | 无符号(非负) | SF = 0 |
带符号条件传送指令使用符号和溢出标志表示操作数之间比较的状态。
条件传送指令需要某种类型的数学指令来设置EFLAGS寄存器以便进行操作。
交换数据
数据交换指令
数据交换指令集中包含几个指令。每个指令都有特定的用途,在程序中处理数据时可以很方便地使用它们。
指令 | 描述 |
---|---|
XCHG | 在两个寄存器之间或者寄存器和内存位置之间交换值 |
BSWAP | 反转一个32位寄存器中的字节顺序 |
XADD | 交换两个值并且把总和存储在目标操作数中 |
CMPXCHG | 把一个值和一个外部值进行比较,并且交换它和另一个值 |
CMPXCHG8B | 比较两个64位值井且交换它们 |
XCHG指令
是这组指令中最简单的。它在两个通用寄存器之间或者寄存器和内存位置之间交换数据值。这条指令的格式如下:
xchg operand1, operand2
operand1 或者 operand2可以是通用寄存器,也可以是内存位置(但是二者不能都是内存位置)。
可以对任何通用的8位、16位和32位寄存器使用这个命令,但是两个操作数的长度必须相同。
当一个操作数是内存位置时,处理器的LOCK信号被自动标明,防止在交换过程中任何其他处理器访问这个内存位置。
使用XCHG对内存位置进行操作时要小心。LOCK处理是非常耗费时间的,并且可能对程序性能有不良影响。
BSWAP指令
是一种功能强大的工具,当所使用的系统具有不同的字节排列方式时,它很有用。BSWAP指令反转寄存器中字节的顺序。第0-7
位和第24-31
位进行交换,第8-15
位和第16-23
位交换。
位的顺序没有被反转;被反转的是寄存器中包含的各个字节。这样就
从小尾数(little-endian) 的值生成了大尾数(big-endian) 的值,反之亦然。
XADD指令
用于交换两个寄存器或者内存位置和寄存器的值,把两个值相加,然后把结果存储在目标位(寄存器或者内存位置)。XAD D指令的格式是:
xadd source, destination
其中source必须是寄存器,destination可以是寄存器,也可以是内存位置,并且destination包含相加的结果。寄存器可以是8位、16位或者32位寄存器,XADD指令从80486处理器开始可用。
CMPXCHG指令
比较目标操作数和EAX、AX或者AL寄存器中的值。如果两个值相等,就把源操作数的值加载到目标操作数中。如果两个值不等,就把目标操作数加载到EAX、AX或者AL寄存器中。CMPXCHG指令在早于80486之前的处理器上是不可用的。
在GNU汇编器中, CMPXCHG指令的格式是
cmpxchg source, destination
其中的源和目标操作数的顺序和Intel文档中的顺序是相反的。目标操作数可以是8位、16位或者32位寄存器,或者是内存位置。源操作数必须是长度和目标操作数匹配的寄存器。
CMPXCHG8B和CMPXCHG指令类似,但是有些区别一它处理8字节值(因此末尾是8B) 。早于奔腾处理器的IA-32处理器不支持这条指令。CMPXCHG8B 指令只有单一操作数:
cmpxchg8b destination
destination操作数引用一个内存位置,其中的8字节值会与EDX和EAX 寄存器中包含的值进行比较(EDX是高位寄存器,EAX是低位寄存器) 。如果目标值和EDX:EAX 寄存器对中包含的值匹配,就把位于ECX:EBX 寄存器对中的64位值传送给目标内存位置。如果不匹配,就把目标内存位置地址中的值加载到EDX:EAX寄存器对中。
栈
栈是内存中专门保留以用于存放数据的区域,被保留在内存区域的末尾位置(顶部),放入数据时它向下增长(低地址)。
栈底部(内存顶部)包含程序运行时由操作系统存放到这里的数据元素。运行程序时使用的任何命令行参数都被送入堆栈中,并且堆栈指针被设置为指向数据元素的底部,接下来是可以用来存放程序数据的区域。
压入和弹出数据
把新的数据项目存到堆栈中称为压入(pushing)。用于执行这个任务的指令是PUSH指令。PUSH 指令的简单格式是:
pushx source
其中x
是一个字符的代码,表示数据的长度,source
是要放入堆栈的数据元素。可以对其进行PUSH操作的数据元素如下:
- 16位寄存器值
- 32位寄存器值
- 16位内存值
- 32位内存值
- 16位段寄存器
- 8位立即数值
- 16位立即数值
- 32位立即数值
用于表示数据长度的字符和MOV指令中是一样的格式,但是只能对16位和32位数据值进行PUSH 操作:
- l用于长字(32 位)
- w用于字(16 位)
长度代码必须和指令中声明的数据元素匹配,否则会发生错误。
注意使用标签data和内存位置$data之间的区别。笫一种格式(不带美元符号)把内存位置中包含的数据值存放到堆栈中,而笫二种格式把标签引用的内存地址存放到堆栈中。
既然已经把所有数据放入了堆栈,就可以从堆栈获取数据了。POP指令用于完成这一部分工作。
和PUSH 指令类似, POP指令使用下面的格式:
popx destination
其中x
是一个字符的代码,表示数据元素的长度,destination
是接收数据的位置。通过POP指令可以使用下面的数据元素接收数据:
- 16位寄存器
- 16位段寄存器
- 32位寄存器
- 16位内存位置
- 32位内存位置
显然,不能把堆栈中的数据存放到立即数值中。
压入和弹出所有寄存器
对于同时快速地设置和获得所有通用寄存器的当前状态,PUSHA和POPA指令非常有用。PUSHA指令压入16位寄存器,使它们按照DI、SI、BP、BX、DX、CX,最后是AX的顺序出现在堆栈中。PUSHAD指令按照相同的顺序,把这些寄存器对应的32位寄存器压入堆栈。POPA和POPAD指令按照压入寄存器的相反顺序获得寄存器状态。
指令 | 描述 |
---|---|
PUSHA/POPA | 压入或者弹出所有16位通用寄存器 |
PUSHAD/POPAD | 压人或者弹出所有32位通用寄存器 |
PUSHF/POPF | 压入或者弹出EFLAGS寄存器的低16位 |
PUSHFD/POPFD | 压入或者弹出EFLAGS寄存器的全部32位 |
POPF和POPFD指令的行为因处理器的操作模式而不同。当处理器运行在保护模式下的ring 0(特权模式)下时,EFLAGS寄存器中的所有非保留标志都可以被修改,除VIP、VIF和VM标志之外。VIP和VIF标志被清零,VM标志不会被修改。
当处理器运行在保护模式的更高级别的ring(非特权模式)下时,会得到和ring 0模式下的相同结果,并且不允许修改IOFL字段。
内存访问优化
内存访问是处理器执行的最慢的功能之一。编写需要高性能的汇编语言程序时,最好尽可能地避免内存访问。只要可能,最好把变量保存在处理器的寄存器中。处理器的寄存器访问是经过高度优化的,并且是处理数据的最快方式。
当不可能把所有应用程序数据都保存在寄存器中时,应该试图优化应用程序的内存访问。
对于使用数据缓存的处理器来说,在内存中按照连续的顺序访问内存能够帮助提高缓存命中率,因为内存块会一次被读取到缓存中。
当使用内存时,另一个要考虑的问题是处理器如何处理内存的读取和写入。大多数处理器(包括IA-32系列)都被优化为从数据段的开始位置,在特定的缓存块中读取和写入内存位置。
在奔腾4处理器中,缓存块的长度是64位,如果定义的数据元素超过64位块的边界,就必须用两次缓存操作才能获取或者存储内存中的数据元素。
为了解决这个问题, Intel建议在定义数据时遵循下面这些原则:
- 按照16字节边界对准16位数据。
- 对准32位数据使它的基址是4的倍数。
- 对准64位数据使它的基址是8的倍数。
- 避免很多小的数据传输。而是使用单一的大型数据传输。
- 避免在堆栈中使用大的数据长度(比如80位和128位浮点值)。
在数据段中对准数据可能是困难的。数据元素被定义的顺序对于应用程序的性能可能是至关重要的。如果有很多长度类似的数据元素,比如整数和浮点值,可以把它们一起安排在数据段的开始。这确保它们保持适当的对准方式。如果有很多长度不一的数据元素,比如字符串和缓冲区,可以把它们安排在数据段的结尾,以便它们不会破坏其他数据元素的对准方式。
gas 汇编器支持.align命令,它用于在特定的内存边界对准定义的数据元素。在数据段中,.align命令紧贴在数据定义的前面,它指示汇编器按照内存边界安置数据元素。
中介绍)。
上述笔记内容学习自 AT&T Professional Assembly Language — Chapter V