汇编指令初步
机器指令是CPU能直接识别并执行的指令,它的表现形式是二进制编码,如00001001010101100110010000101011。用机器指令编写出来的程序执行效率高,没有多余的额外操作。但由于机器指令十分复杂,难以记忆,且不方便阅读和修改,容易产生错误,所以设计了汇编指令,通过一些能反映机器指令功能的单词或词组来代表该机器指令,而不再关心机器指令的具体二进制编码。与此同时,汇编指令也把CPU内部的各种资源符号化,使用该符号名也等于引用了该具体的物理资源。
寄存器
80386提供以下其中不同类型的32位寄存器
通用寄存器
- EAX:累加器(Accumulate),是算术运算的主要寄存器
- EBX:基址寄存器(Base),在内存中寻址时存放基址
- ECX:计数器(Counter)
- EDX:数据寄存器(Data)
- ESI:源变址
- EDI:目标变址
- ESP:堆栈指针
- EBP:基址指针
以上通用寄存器的低16位又构成一个16位寄存器,以兼容更短的指令,分别为:AX、BX、CX、DX、SI、DI、SP、BP。而AX、BX、CX、DX又可划分为两个8位的寄存器,分别为AH和AL,BH和BL,CH和CL,DH和DL,它们实际上是相应的16位寄存器的高8位和低8位。
段寄存器
- CS:代码段寄存器
- SS:堆栈段寄存器
- DS:数据段寄存器
- ES、FS、GS:附加数据段寄存器
指令指针寄存器和标志寄存器: EIP、EFLAGS
系统表寄存器: GDTR、IDTR、LDTR、TR
控制寄存器: CR0、CR1、CR2、CR3、CR4
调试寄存器: DR0、DR1、DR2、DR3、DR4、DR5、DR6、DR7
测试寄存器: TR6、TR7
AT&T汇编代码概览
格式:指令 源操作数, 目的操作数
示例:movl $8, %eax
操作数可分为立即数(immediate)、寄存器(register)操作数和存储器(memory)操作数。立即数不必存储于寄存器或存储器中,可以直接出现在指令中,不可作为目的操作数,相当于高级语言中的常量,使用立即数时,需要在立即数前加上$;CPU计算总是仅从寄存器直接存/取数据,寄存器操作数可作为源操作数或目的操作数,引用寄存器操作数时需要在寄存器名称前加上%。
还有一种符号常量,可以直接引用,相当于高级语言中的变量。引用符号常量的地址时需要在符号常量前加上$。
操作数的长度通过为指令添加后缀来指定:
- b:byte,长度为8位
- w:word,长度为16位
- l:long-word,长度为32位
- q:quad-word,长度为64位
如mov指令,添加后缀l后的movl表示传送32位的长字值。如果指令没有指定操作数长度,那么编译器将自动设置为目标操作数的长度;如果指令没有指定操作数长度,而编译器又无法猜测操作数长度,编译器则会报错,如指令"push $1"。
Linux下一个简单的AT&T汇编代码示例:
.section .data #.section .data段用来申明变量
value1:
.int 1
value2:
.short 2
value3:
.byte 3
.section .text
#.ascii 文本字符串
#.asciz 以空字符结尾的文本字符串
#.byte 字节
#.double双精度浮点数
#.float 单精度浮点数
#.int
#.long 32位整数
#.octa 16字节整数
#.quad 8字节整数
#.short 16位整数
#.single 同float
.global _start
_start:
nop
movl $100, %ecx
movl %ecx, value2
movw $20, value2
movl $1, %eax
movl $0, %ebx
int $0x80
附上一些Linux下gdb调试的指令:
- print &value,查看value变量在内存中的地址
- x /4bt + 内存地址,查看内存内容(b表示单字节,h表示双字节, w表示四字节,g表示八字节)( x 按十六进制格式显示变量。d 按十进制格式显示变量。 u 按十六进制格式显示无符号整型。o 按八进制格式显示变量。 t 按二进制格式显示变量。a 按十六进制格式显示变量。 c 按字符格式显示变量。f 按浮点数格式显示变量。)
间接寻址
当寄存器中保存的是内存地址时,它就被称为指针。通过指针可进行间接寻址,称为寄存器间接寻址,格式为size(%register)。括号内的是寄存器,括号外size是偏移值,可为负数。最终表示的地址为寄存器%register中的值 + size。
变址寻址
变址寻址适合访问内存中的连续数据,比如数组。变址寻址的格式为offset_address(base_address, index, size),最终表示的地址为base_address + offset_address + index*size。其中,offset_address为偏移量,base_address为基址(比如数组首地址),index为变址。base_address和index必须为寄存器,size的值必须为2的次幂(1, 2, 4, 8...)。如果其中的任何值为0,可以忽略,但必须有逗号作为占位符。
.section .data
value1:
.int 400
values:
.int 400,15,20,25,30,35,40,45,50,55,60
.section .text
.global _start
_start:
nop
movl $1, %edi
movl $4, %ebx
movl $2, %edi
movl value1, %ecx
movl values, %eax
movw values, %cx
movb values, %ch
movl values(, %edi, 4), %eax
movl values(%edi, %ebx, 1), %eax
#movl values(, %edi, 3), %eax
# offset_address(base_address, index, size)
# base_address + offset_address + index*size
# 其中base_address和index的值必须为寄存器
# 如果其中的任何值为0,可以忽略,但必须有逗号作为占位符
movl $1, %eax
movl $0, %ebx
int $0x80
一些指令
- mov S, D:数据传送指令,表示D <-- S。
- lea S, D:mov指令的变形。 lea 指令形式是从存储器读数据到寄存器,但实际并没有引用存储器,而是将有效地址写入目的操作数(D必须是寄存器) ,表示D <-- &S。举个例子:若寄存器%edx的值为x,那么 leal 7(%edx,%edx,4), %eax 表示寄存器%eax的值为5x+7。lea执行效率很高,且常用来执行简单的算术操作,比如模拟一些三元运算。举个例子,若要实现%ebx = %eax + %edx,不使用lea需要多条指令,而使用lea只需lea(%eax, %edx, 1), %ebx一条指令。
lea与mov的区别
leaq 8(%edi), %eax ==> R[%eax] = 8 + R[%edi]
movq 8(%edi), %eax ==> R[%eax] = M[8 + R[%edi]]
- jmp label:无条件跳转指令,跳转到label。
.section .text
.global _start
_start:
nop
movl $0xffffffff, %eax
jmp exit
movl $5, %eax
exit:
movl $1, %eax
int $0x80
- push S:压栈。对于pushl S,表示R[%esp] <-- R[%esp] – 4;M[R[%esp]] <-- S(4为l的字节长,假设栈往低地址扩展)。
- pop D:出栈。对于pop D,表示D <-- M[R[%esp]];R[%esp] <-- R[%esp] + 4。
.section .data
value:
.int 0x87654321
.section .text
.global _start
_start:
nop
movl $0x12345678, %ebx
push %ebx
#pushl %ebx #%ebx的长度可判断, 故l也可省略
pushw %bx
pushw value
push $value
#pushb %ah
popl %ebx
popl %eax
popw %cx
movl $1, %eax
int $0x80