AT&T汇编总结
一. 传送数据
1. 定义数据元素:GNU汇编器提供了在汇编语言程序中定义和处理数据元素的很多方式,选择程序需要的处理数据的最佳方式,在数据段和bss段都提供定义数据元素的方式。
a.数据段:是最常见的定义数据元素的位置。
b.使用.data命令声明数据段在这个段声明的任何数据元素都保留在内存中并且可以被汇编语言程序中的指令读取和写入。
c.另一种类型的数据段,称为.rodata。在这种数据段中定义的任何数据元素只能按照只读(read-only)模式访问(因此使用ro前缀)。
d.数据段定义元素需要用到两个语句:一个标签和一个命令。标签用作引用数据元素所使用的标记,很像C程序中的变量名称。标签对处理器是没有意义,它只是汇编器试图访问内存位置时用作引用指针的一个位置;除了标签之外,还必须定义为数据元素保留多少字节,是一个汇编命令完成,这个命令指示汇编器为通过标签引用的数据元素保留特定数量的内存,保留的内存数量取决于定义的数据的类型,以及要声明的这个类型的项目的数量。声明命令之后,定义一个(或者多个)默认值,这样把保留的内存位置中的数据设置为特定值。
命令 | 数据类型 |
---|---|
.ascii | 文本字符串 |
.asciz | 以空字符串结尾的文本字符串 |
.byte | 字节值 |
.double | 双精度浮点数 |
.float | 单精度浮点数 |
.int | 32位整数 |
.long | 32位整数(和.int同) |
.octa | 16字节整数 |
.quad | 8字节整数 |
.short | 16位整数 |
.short | 单精度浮点数(和.float同) |
f.数据段声明数据元素的一个例子:
output:
.ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n"
- 这个代码片段留出42字节的内存,把定义的字符串顺序地存放到内存字节中,并且把标签output赋值为第一个字节。以后当程序中引用内存位置output时,汇编器知道要转到文本字符串开头的内存位置。
g.例如此方法可以用与数字例子:
pi:
.float 3.14159
并不限制在一个命令语句行中只能定义一个值。可以在一行里定义多个值,按照每个值出现在命令中的顺序在内存中存放他们,例如:
标签必须在定义数据的命令的前面
sizes:
.long 100,150,200,250,300
定义数据元素,然后在程序中使用他们时需要小心。因为程序不会知道是否正确地处理了数据,如果你定义了两个16位整数数据值,但是把其中一个作为32位整数引用,汇编器仍然会读取必须的4字节内存,尽管这个值是错误的。
- 定义静态符号:.equ命令用于把常量值设置为可以在文本段中使用的符号。
.equ factor, 3
.equ LINUX_SYS_CALL, 0x80
经过设置后,数据符号值是不能在程序中改动的。.equ命令可以出现在数据段中的任何位置,但是为了使需要阅读你的程序的其他人更加方便,最好在定义其他数据之前或者之后集中定义所有数据符号
a.引用静态数据元素,必须在标签名称之前使用$符号,例如:把赋值给LINUX_SYS_CALL符号的值传送给EAX寄存器
movel $LINUX_SYS_CALL, %eax
- bss段:bss段中定义数据元素和在数据段中定义有些不同。无需声明特定的数据类型,只需声明为所需目的保留的原始内存部分即可。
a.GNU汇编器使用两个命令声明缓冲:
命令 | 描述 |
---|---|
.comm | 声明未初始化的数据的通用内存区域 |
.lcomm | 声明未初始化的数据的本地通用内存区域 |
本地通用内存区域是为不会从本地汇编代码之外进行访问的数据保留的:
.comm synmbol, length
- 其中symbol是赋给内存区域的标签,length是内存区域中包含的字节数量
.section .bss
.lcomm buffer. 10000
- 这些语句把10000字节的内存区域赋值给buffer标签。在声明本地通用内存区域的程序之外的函数是不能访问他们的(不能在.globl命令中使用他们)。
- 在bss段中声明数据的一个好处是数据不包含在可执行程序中。在数据段中定义数据时,它必须被包含在可执行在程序中,因为必须使用特定值初始化它。因为不使用程序数据初始化bss段中声明的数据区域,所以内存区域被保留在运行时使用,并且不必包含在最终的程序中。
二. 传送数据元素:定义数据后,需要处理它们。因数据元素位于内存中,并且处理器的很多指令要利用寄存器,所以处理器元素的第一个步骤是在内存和寄存器之间传送他们。
1. MOV指令格式:MOV指令用作通用的数据传送指令。
a.MOV指令的基本格式:
movx source, destination
- source和destination的值可以是内存地址、存储在内存中的数据值、指令语句中定义的数据值、或者是寄存器。
- GNU汇编器为MOV指令添加了另一个唯独,在其中必须声明要传送的数据元素的长度。通过把一个附加字符添加到MOV助记符来声明这个长度,则指令变为:movx,其中x可以是下面字符
x | 描述 |
---|---|
l | 用于32位的长字值 |
w | 用于16位的字值 |
b | 用于8位的字节值 |
b.把32、16、8位的EAX寄存器值传送给32位的EBX寄存器值:
#32位
movl %eax, %ebx
#16位
movw %ax, %bx
#8位
movb %al, %bl
-
把立即数传送到寄存器和内存
- 立即数是在指令码语句中直接指定的,并且在运行时不能改动。
movl $0, %eax
movl $0x80, %ebx
movl $100, height
每个值前面都必须加上$符号,表明它是立即数。程序被汇编和连接为可执行文件之后,这些值是不能被改变的。
- 立即数是在指令码语句中直接指定的,并且在运行时不能改动。
-
在寄存器之间传送数据
- 把数据从一个处理器寄存器传送到另一个。这是使用处理器传送数据的最快的方式。
尽可能把数据把数据保存在处理器寄存器中,这样可以减少试图访问内存位置所花费的时间
“`
EAX:用于操作数和结果数据的累加器
- 把数据从一个处理器寄存器传送到另一个。这是使用处理器传送数据的最快的方式。
EBX:指向数据内存段中的数据的指针
ECX:字符串和循环操作的计数器
EDX:I/O指针
EDI:用于字符串操作的目标的数据指针
ESI:用于字符串操作的源的数据指针
ESP:堆栈指针
EBP:堆栈数据指针
“`
* 8个通用寄存器(EAX、EBX、ECX、EDX、EDI、ESI、ESP、EBP)是用于保存数据的最常用的寄存器。
#寄存器之间传送数据的例子:
#32位
movl %eax, %ecx
#16位
movw %ax, %cx
当指定长度大一些的寄存器接受长度小一些的数据要小心,以下指令,汇编器会报错。
movb %al, %bx
这条指令试图把AL寄存器中的8位传送给BX寄存器中的低8位。替换的做法是,应该使用MOVM指令把整个%AX寄存器的内存传送给%bx寄存器
- 在内存和寄存器之间传送数据:把数据传送给内存位置,以及把内存位置中的数据传送出。
a.把数据值从内存传送到寄存器:
#必须决定的第一件事是如何在指令码中表示内存地址
#最简单的是:使用用于定义内存位置的标签。
#指令把位于value标签指定的内存位置的数据传送给EAX寄存器。
# MOVL指令传送32位的信息,则它传送从value标签引用的内存位置开始的4字节数据。
# 若数据长度小于4字节,则必须使用其他MOV指令之一
movl value, %eax
# 例子:
.section .data
value:
.int 1
.section .txt
.globl _start
_start
nop
movl value, %ecx
movl $1, %eax
movl $0, %ebx
int $0x80
b.把数据值从寄存器传送给内存
# 把数据放回内存位置:
# 指令是把ECX寄存器中存储的4字节数据传送给value标签指定的内存位置
movl %eax, value
# movtest2.s - An example of moving register data to momory
.section .data
value:
.int 1
.section .text
.globl _start
_start:
nop
movl $100, %eax
movl %eax, value
movl $1, %eax
movl $0, %ebx
int $0x80
- 这个程序使用一个Linux系统调用(inti $0x80)从Linux内核访问控制台显示:
c.使用变址的内存位置
# 在一个命令中指定把多个值存放到内存中:
value:
.int 10. 15, 20, 32, 45, 23, 54, 12, 34, 60
# 创建存放到内存中的连续的一系列数据值。
# 每个数据值都占用内存的一个单元(这个例子是长整型,即4个字节)。
# 引用数组中的数据时,必须使用变址系统确定你要访问的哪个值。
# 称:变址内存模式
- 内存位置有下列因素确定:
a.基址
b.添加到基址上的编译地址
c.数据元素的长度
d.确定选择哪个数据元素的变址
* 表达式格式:
base_address (offset_address, index, size)
# 获取的数据值位于:
base_address + offset_address + index * size
# 若其中任何值为零,则可以忽略其(但是仍然需要用作逗号作为占位符)
# offset_address和index的值必须是寄存器、但size的值可以是数字值
- 引用之前value数组中的第三个值20
movl $2, %edi
movl value(, %edi, 4). %eax
# 这条指令把从value标签开始的第3个4字节的变址值加载到EAX寄存器
数组从变址0开始
# movtest3.s - Auther example of using indexed memory locations
.section .data
output:
.asciz "The value is %d\n"
.section .text
.globl _start
_start:
nop
movl $0, %edi
loop:
movl value(, %edi, 4), %eax
pushl %eax
pushl $output
call printf
addl $8, %esp
inc %edi
cmpl $11, %edi
jne loop
movl $0, %ebx
movl $1, %eax
int $0x80
# 程序遍历values标签指定的数据组,在控制太=台显示其中的每个值。
# 它使用EDI寄存器作为遍历数据组的变址
# 因这个例子使用C函数printf,所以需要使用所在系统上的C库动态连接器连接它:
as -o movtest3.o movtest3.s
ld -dynamic-linker /lib/ld-linux.so.*(系统不同,版本不同) -lc -o movtest3 movtest3.o
# 程序遍历values标签指定的数据组,在控制太=台显示其中的每个值。
# 它使用EDI寄存器作为遍历数据组的变址
movl values (, %edi, 4), %eax
# 每个值显示后,EDI寄存器的值被递增(使用INC指令,它把寄存器值加1)。
# 程序检查EDI寄存器的值,如果还没有达到最大值,就循环回去获取数组的下一个值。
# 这里检查EDI寄存器是否到达数组尾时使用了硬编码的值。
-
使用寄存器间接寻址
- 当使用标签引用内存位置中包含的数据值时,可以通过在指令中的标签前面加上美元符号$,获得数据值的内存位置的地址:
# 用于把values标签引用的内存地址传送给EDI寄存器
movl $values, %edi
在平坦内存模型中,所有内存地址都是使用32位数字表示的
# 是间接寻址的另一半。
# 若EDI寄存器外面没有括号,那么指令只是把EBX寄存器中的值加载到EDI寄存器中。
# 若EDI寄存器外面加上括号,那么指令就把EBX寄存器中的值传送给EDI寄存器中包含的内存位置。
movl %edx, (%edi)- GNU汇编器不允许把值与寄存器相加,必须把值放在括号之外
# 指令把EDX寄存器中的值放在EDI寄存器指向的位置之后4个字节的内存位置
movl %edx, 4(%edi)
# 同上,之前4个字节的内存位置
movl %edx, 4(%edi)
- 当使用标签引用内存位置中包含的数据值时,可以通过在指令中的标签前面加上美元符号$,获得数据值的内存位置的地址:
# movtest4.s - An example of indirect addressing
.section .data
value:
.int 10, 15, 23, 34, 15, 52, 13
.section .text
.globl _start
_start:
nop
movl values, %eax
movl $values, %edi
movl $100, 4(%edi)
movl $1, %edi
movl values(, %edi, 4), %ebx
movl $1, %eax
int $0x80
-
条件传送指令
- 旧式汇编语言代码:
# 代码首先使ECX寄存器中的值递增1.
# 若ECX寄存器没有溢出(进位标志没有被设置为1),JNC指令就跳转到continue标签。
# 若寄存器溢出,JNC指令就会捕获这个溢出,并且ECX寄存器被设置回0。
# ECX寄存器的值取决于它的状态,必须使用跳转指令检查它的状态。
# 不一定非要使用跳转指令检查进位标志,可以使用条件传送指令来完成。
# 在更复杂的产品型应用程序中,条件传送指令可以避免处理器执行JMP指令,
# 这有助于处理器的预取缓存状态,通常能够提高应用程序的速度。
dec %ecx
jz continue
movl $0, %ecx
continue:
- 旧式汇编语言代码:
a.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 | 小于/不大于或者等于 | CF=1 |
CMOVC | 进位 | CF=1 |
CMOVBE/CMOVNA | 小于或者等于/不大于 | CF=1 |
CMOVE/CMOVZ | 不等于/不为零 | ZF=0 |
CMOVP/CMOVPE | 奇偶校验/偶校验 | PF=1 |
CMOVNP/CMOVPO | 非奇偶校验/奇校验 | PF=0 |
从上表看出,无符号条件传送指令依靠进位、零和奇偶校验标志来确定两个操作数之间的区别
- 操作数是带符号:
指令对 | 描述 | EFLAGS状态 |
---|---|---|
CMOVGE/CMOVNL | 大于或者等于/不小于 | (SF异或OF)=0 |
CMOVL/CMOVNGE | 小于/不大于或者等于 | (SF异或OF)=1 |
CMOVLE/CMOVNG | 小于或者等于/不大于 | ((SF异或OF)或ZF)=1 |
CMOVO | 溢出 | OF=1 |
CMOVNO | 未溢出 | OF=0 |
CMOVS | 带负号(负) | SF=1 |
CMOVNS | 无符号(非负) | SF=0 |
带符号条件传送指令使用符号和溢出标志表示操作数之间比较的状态
条件传送指令需要某种类型的数字指令来设置EFLAGS寄存器以便进行操作
# 把value标签的引用的数据值加载到ECX寄存器中。
# 指令把位于value标签指定的内存位置的数据传送给EAX寄存器。
# 使用CMP指令把这个值和EBX寄存器中的值进行比较
# 如果ECX寄存器中的值大于EBX寄存器中的原始值,就使用CMOVA指令把EBX的值替换为ECX中值
movl value, %ecx
cmp %ebx, %ecx
cmova %ecx, %ebx
b.使用CMOV指令
# cmovtes.s - An example of the CMOV instructions
# 输出最大的值
.section .data
output:
.asciz "The larggest value is %d\n"
values:
.int 105, 123, 45, 546, 66, 67, 117, 5
.section .text
.globl _start
_start:
nop
movl values, %ebx
movl $1, %edi
loop:
movl values(, %edi, 4), %eax
cmp %ebx, %eax
cmova %eax, %ebx
inc %edi
cmp $10, %edi
jne loop
pushl %ebx
pushl $output
call printf
addl $8, %esp
pushl $0
call exit
-
交换数据
- MOV指令的一个缺陷就是难以在不使用临时中间寄存器的情况下交换两个寄存器的值。
# 临时中间寄存器
movl %eax, %ecx
movl %ebx, %eax
movl %ecx, %ebx
- MOV指令的一个缺陷就是难以在不使用临时中间寄存器的情况下交换两个寄存器的值。
a.数据交换指令
指令 | 描述 |
---|---|
XCHG | 在两个寄存器之间或者寄存器和内存器之间交换值 |
BSWAP | 反转一个32位寄存器中的字节顺序 |
XADD | 交换来嗯改制并且把总和存储在目标操作数中 |
CMPXCHG | 把一个值和一个外部值进行比较,并且交换它和另一个值 |
CMPXCHG8B | 比较两个64位值并且交换他们 |
1.XCHG:
xchg operand1, operand2
# operand1和2可以是寄存器也可以是内存位置(但二者都不能都是内存位置、)
# 可以对任何通用的8位、16位和32位寄存器使用这个命令,但操作数长度必须相同
# 当一个操作数是内存位置时,处理器的LOCK信号被自动表明,防止交换过程中任何其他处理器访问此内存位置
# 使用XCHG对内存位置进行操作时要小心。LOCK处理是非常耗费时间的,并且可能对程序性能有不良影响。
b.BSWP:
* BSWAP指令是一种功能强大的工具,等所使用的系统具有不同的字节排列方式。BSWAP指令反转寄存器中字节的顺序。位的顺序没有被反转; 被反转的是寄存器中包含的各个字节。:
# swaptest.s -An exasmple of using the BSWAP instruction
.section .text
.globl _start
_start:
nop
movl $0x12345678, %ebx
bswap %ebx
movl $1, %eax
int $0x80
c.XADD:
* XADD指令用于交换两个寄存器或者内存位置和寄存器的值,把两个值相加,然后把结果存储在目标位置(寄存器或者内存位置)。
# XADD指令的格式:
# 其中source必须是寄存器,destination可以是寄存器或内存位置,并且destination包含相加结果
# XADD指令从80486处理器开始使用
xadd source, destination
d.CMPXCHG:
* CMPXCHG指令比较目标操作数和EAX、AX或者AL寄存器的值。若两值等,就把源操作数值加载到目标操作数中。若两个值不等,就把目标操作数加载到EAX、AX或者AL寄存器中。在80486前不可用
# 目标操作数可以是8、16、32位寄存器或者内存位置。
# 但源操作数必须是长度和目标操作数匹配的寄存器。
cmpxchg source, destination
e.CMPXCHG8B:
# CMPXCHG8B指令只有单一操作数
cmpxchg8b destintiona
# destination操作数引用一个内存位置。
# 其中的8字节值会与EDX和EAX寄存器中包含的值进行比较(EDX是高位寄存器、EAX是低位寄存器)
# 若目标值和EDX、EAX寄存器对中包含的值匹配、就把位于ECX、EBX寄存器对中的64位值传送给目标内存位置
-
使用数据交换指令:
-
堆栈
- 堆栈是内存中用于存放数据的专门保留的区域。
-
堆栈的行为正好相反。堆栈被保留在内存区域的末尾位置,并且当数据存放在堆栈中时,它向下下增长。
# PUSH指令的简单格式:
# l用长字节(32位)
# w用于字(16位)
pushx $source
pushl data
pushl $data
# 使用data和内存位置$data之间的区别。第一种格式(不带美元符号)把内存位置中包含的数据值存放到堆栈中,
# 而第二种格式把标签引用的内存地址存放到堆栈中。 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位 |
-
手动使用ESP和EBP寄存器
# PUSH和POP不是把数据压入和弹出堆栈的唯一途径。
# 还可以通过使用ESP寄存器作为内存指针,手工的把数据存放到堆栈中。
# 通常,会看到很多的程序把ESP寄存器的值复制到EBP寄存器,而不是使用ESP寄存器本身。- 优化内存访问