AT&T汇编
1.语法格式
1.寄存器
引用寄存器要在寄存器号前加百分号%,如“movl %eax, %ebx
8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp
8个16-bit寄存器 它们事实上是上面8个32-bit寄存器的低16位:
%ax,%bx,%cx,%dx,%di,%si,%bp,%sp
8个8-bit寄存器 %ah,%al,%bh,%bl,%ch,%cl,%dh,%dl
它们事实上是寄存器%ax,%bx,%cx,%dx的高8位和低8位
6个段寄存器 %cs(code),%ds(data),%ss(stack), %es,%fs,%gs
3个控制寄存器 %cr0,%cr2,%cr3;
6个debug寄存器 %db0,%db1,%db2,%db3,%db6,%db7;
2个测试寄存器 %tr6,%tr7;
8个浮点寄存器栈 %st(0),%st(1),%st(2),%st(3),%st(4),%st(5),%st(6),%st(7)
寄存器功能
以E开头的寄存器为32位
EAX(累加器,是算术运算的主要寄存器)
EBX(基址寄存器,在内存中寻址时存放基址)
ECX(计数器)
EDX(数据寄存器)
ESI(源变址)
EDI(目标变址)
ESP(堆栈指针)
EBP(基址指针)
EIP(程序计数器,存储的是将要执行的下一条指令放在内存中的地址)
EFLAGS(保存的是根据运算得到的结果设置的条件码ZF,CF,SF,OF)
段寄存器:
CS:代码段寄存器
SS:堆栈段寄存器
DS:数据段寄存器
ES、FS、GS:附加数据段寄存器
2.指令的长度后缀
在指令后面加上后缀表示长度,根据操作的是1字节、2字节、4字节、8字节,分别对应后缀b, w, l, q
3.操作数顺序
操作数排列是从源(左)到目的(右)
movl %eax(源), %ebx(目的) (ebx) = (eax)
4.立即数
使用立即数,要在数前面加符号$
1:
movl $0x04, %ebx
2:
para = 0x04
movl $para, %ebx
5.注释
在AT&T中,注释既可以使用" ;“,也可以使用” ! "
6.符号常数
符号常数直接引用
value: .long 0x12a3f2de
movl value , %ebx
注:指令执行的结果是将常数0x12a3f2de装入寄存器ebx,引用符号地址在符号前加符号$, 如“movl $value, % ebx”则是将符号value的地址装入寄存器ebx
7.调用和跳转指令
段内调用和跳转指令为"call",“ret"和"jmp”,段间调用和跳转指令为"lcall",“lret"和"ljmp”
段间调用和跳转指令的格式
lcall/ljmp $SECTION, $OFFSET
段间返回指令为
lret $STACK-ADJUST
jmp, jcc, call指令跳转:
type example syntax address used
register *%rax contents of %rax
immediate .l3 address of .l3
memory *0x20(%rip) value stored at address %rip + 0x20
寄存器:如果想要跳到寄存器%rax内容对应的地址,不是写成jmpq %rax,而是写成*jmpq %rax
立即数:不用加*号
内存:如果想要跳转到%rip + 0x10对应的地址,不是写成jmpq 0x10(%rip),而是写成jmpq *0x10(%rip)
8.数据声明
格式:命令 数据类型
.ascii 文本字符串
.asciz 以空字符串结尾的文本字符串
.byte 字节值
.double 双精度浮点数
.float 单精度浮点数
.int 32位整数
.long 32位整数(同32)
.octa 16字节整数
.quad 8字节整数
.short 16位整数
.single 单精度浮点数(和.float同)
两个命令声明缓冲:
命令 描述
.comm 声明未初始化的数据的通用内存区域
.lcomm 声明未初始化的数据的本地通用内存区域
9.寻址方式
1.直接寻址
把某个地址上的值放到寄存器中
movl 0x8000, %eax # 把地址0x8000上的值放到eax中
2. 间址寻址
把寄存器上的值所代表的地址所指向的值放到寄存器中
movl $0x8000, %ebx
movl (%ebx), %eax # 间址寻址, 把地址0x8000(在寄存器ebx中)上的值放到eax中
3. 基址寻址
以寄存器里的数值作为基址,加上一个常数得到最终地址,把地址上的值放到寄存器中
movl $0x8000, %eax
movl 4(%eax), %ebx #基址寻址, 把地址0x8004(0x8000+4)上的值放到eax中
4. 变址寻址
以两个寄存器里的数值之和加上一个常数得到最终地址,把地址上的值放到寄存器中
movl $0x8000, %eax
movl $0x4, %ebx
movl (%eax,%ebx), %ecx #变址寻址, 把地址0x8004(0x8000+4)上的值放到ecx中
movl 4(%eax,%ebx), %ecx #变址寻址, 把地址0x8008(0x8000+4+4)上的值放到ecx中
5. 比例变址寻址
以一个寄存器里的数值加上另一个寄存器里的数字 乘以一个比例因子(1,2,4,8)再加上一个常数得到最终地址,把地址上的值放到寄存器中
movl $0x2000, %eax
movl $0x2, %ebx
movl (,%eax,4), %ecx #比例变址寻址, 把地址0x8000(0x2000 *4)上的值放到ecx中
movl 6(,%eax,4), %ecx #比例变址寻址, 把地址0x8006(0x2000 *4+6)上的值放到ecx中
movl (%ebx,%eax,4), %ecx #变址寻址, 把地址0x8002(0x2000*4+2)上的值放到ecx中
movl 6(%ebx,%eax,4), %ecx #变址寻址, 把地址0x8008(0x2000*4+2+6)上的值放到ecx中
10.文件组成
.text:存放代码对应的指令 正文段
.bss:存放未初始化的全局和静态变量,在运行时该区域初始是全0
.rodata:存放只读数据和变量,例如字符串字面量
.data:存放余下的数据和变量,可读可写 数据段
.ascii:定义一个字符串并且用双引号包含
.byte: 定义一个字符用单引号
.org:定义当前的汇编的位置
伪操作符语句是汇编器使用的指示符,它通常并不会产生任何代码,它由伪操作码和0个或多个操作数组成。每个操作码都是由一个一个点字符’.'开始,表示编译过程中的位置计数器。其值是点符号出现机器指令第一个字节的地址。
11.编译
1)使用as命令对汇编文件进行汇编生成目标文件:as xxx.s -o xxx.o
2)使用ld命令对目标文件进行链接生成可执行文件:ld xxx.o -o xxx
注意,ld命令进行链接要求目标文件的.text段必须有一个入口点,ld默认认为_start标签对应的代码是入口点
12.操作码前缀
用于修饰随后的代码
操作码前缀 说明
cs, ds, ss, es, fs, gs 区覆盖操作码前缀,通过指定使用
区:内存操作数 内存引用形式会自动添加这种前缀
data16, addr16 操作数/地址宽度前缀。
会把 32 位操作数/地址改变为 16 位。(as不支持16位寻址方式)
lock 总线锁存前缀。
用于指令执行期间禁止中断(仅对部分指令有效)
wait 协处理器指令前缀。
等待协处理器完成当前指令执行
rep, repe, repne 串指令操作前缀,重复执行 %ecx 中指定次数
1.前缀
Intel汇编寄存器和立即数无需前缀。
(1)后者寄存器前缀为%,立即数前缀为$
(2)十六进制和二进制立即数后缀是 h、b, at&t 中是 0x
Intel AT&T
mov eax, 8 movl $8, %eax
mov ebx, 0ffh movl $0x0ff, %ebx
int 80h int $0x80
2.操作数方向不同
Intel 目的 <- 源
AT&T 源 -> 目的
Intel AT&T
mov eax, [ecx] movl (%ecx), %eax
3.内存变量
Intel语法使用中括号[],后者使用小括号()
Intel AT&T
mov eax, [ecx + 5] movl 5(%ecx), %eax
4.寻址方式不同:
Intel的指令格式是segreg:[base+index*scale+disp],而AT&T的格式是%segreg:disp(base,index,scale)。其中index/scale/disp/segreg全部是可选的,完全可以简化掉。如果没有指定scale而指定了index,则scale的缺省值为1。当立即数用在scale/disp中时,不应当在其前冠以“$”前缀。
Intel语法 AT&T语法
指令 foo,segreg:[base+indexscale+disp] 指令 %segreg:disp(base,index,scale),foo
mov eax,[ebx+20h] movl 0x20(%ebx),%eax
add eax,[ebx+ecx2h addl (%ebx,%ecx,0x2),%eax
lea eax,[ebx+ecx] leal (%ebx,%ecx),%eax
sub eax,[ebx+ecx*4h-20h] subl -0x20(%ebx,%ecx,0x4),%eax
5.后缀
AT&T汇编指令有后缀(b,w,l,q),以表明数据类型(8位、16位等);
Intel则根据寄存器自动识别,需要在内存单元操作数前加 byte ptr, word ptr, dword ptr
操作数是参加某种功能操作的数据,有三种方式提供:立即数;通过寄存器;通过内存。
立即数
立即数是一个常量,可以写成十进制(D),十六进制(H),八进制(O),二进制(B)
例如:addl 0xFFH,%eax
立即数只能作为源操作数,不能放在目的操作数位置。
寄存器操作数
指操作数存放在寄存器中,用来存放参加运算的数据或存放运算结果。寄存器包括段寄存器、控制寄存器和通用寄存器。
(1)段寄存器(DS、SS、ES)
是存放段地址的,要将段地址送入,必须以通用寄存器为中间桥梁,即通过通用寄存器将数据传送到DS、SS、ES段寄存器中。
例如:
将段地址2000H送数据段寄存器中。
movl 0x20000,%eax
movl %eax,DS
通过通用寄存器传送,不能直接将立即数传送到段寄存器中。而代码段CS不准用户干预,是由系统分配的。
(2)控制寄存器
控制寄存器包括:指令指针寄存器IP,EIP和标志寄存器FLAGS,控制寄存器也不能直接送操作数
(3)通用寄存器
通用寄存器可以装操作数。
内存操作数
内存操作数是指操作数存放在内存中,其操作是往内存中写数或从内存中取操作数,CPU与内存打交道是通过内存地址完成的。
例如:
movl 0x20000,%eax
movl %eax,(0x20000)
注意:
对于单操作数指令:操作数只能是寄存器操作数和内存操作数。
对于双操作数指令:源操作数可以是寄存器操作数、内存操作数和立即数,目的操作数可以是寄存器操作数和内存操作数。源操作数、目的操作数不能同时为内存操作数。
内联汇编通常是指在 C/C++ 代码中嵌入汇编代码,在操作系统开发中会经常用到,可以提高代码的执行效率并且实现高级语言无法实现的操作。
内联汇编的格式为:
带有C/C++表达式的内联汇编格式为:
asm volatile (
“Instruction List”
:Output operands(optional)
:Input operands(optional)
:list of clobbered registers(optional)
);
注:_asm_相当于asm,这样写是为了防止与C/C++表达式关键字冲突;volatile_或volatile是可选的,可以用它也可以不用它。如果用了它,则是向GCC声明“不要动我所写的Instruction List,我需要原封不动的保留每一条指令”,否则当使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。所以希望确保编译器不会在“asm"内部优化指令,可以在“asm”后使用关键字"volatile"。如果程序必须与ANSIC兼容,则应该使用_asm_和_volatile,而不是asm和volatile。
实际上gcc在处理汇编时,是要把asm(…)的内容"打印"到汇编文件中,所以格式控制字符是必要的。例如:
asm( “pushl %eax\n\t”
“movl $0,%eax\n\t”
“popl %eax”
);
#另一种类型
asm(“movl %eax, %ebx”);
asm(“xorl %ebx, %edx”);
asm("movl $0, _boo);
内联汇编的扩展:在上面的例子中,由于我们在内联汇编中改变了 edx 和 ebx 的值,但是由于 gcc 的特殊的处理方法,即先形成汇编文件,再交给 GAS 去汇编,所以 GAS 并不知道我们已经改变了 edx和 ebx 的值,如果程序的上下文需要 edx 或 ebx 作其他内存单元或变量的暂存,就会产生没有预料的多次赋值,引起严重的后果,所以要对改变值的寄存器进行限制,防止在编译的时候多次使用到他们,产生没有预料的多次赋值。对于变量 _boo也存在一样的问题。为了解决这个问题,就要用到扩展 GCC 内联汇编语法。
2.1 操作数引用
在汇编程序模板内部,操作数由数字引用。如果总共有n个操作数(包括输入和输出),那么第一个输出操作数的编号为0,逐项递增,最后一个输入操作数的编号为n-1,总操作数的数目限制在10。下面的a和b就相当于操作数。b是输出,所以b的编号是0,%0就代表b,%1就代表a。
2.2 操作数约束
操作数约束主要是确定指令中操作数的寻址方式。约束也可以指定:
是否允许操作数位于寄存器中,以及它可以包括在哪些种类的寄存器中;
操作数是否可以是内存引用,以及在这种情况下使用哪些种类的地址;
操作数是否可以是立即数;
约束还要求两个操作数匹配。
表示约束条件的字母很多,下表给出几个主要的约束字母及其含义:
字母 含义
m, v, o 内存单元
r 任何通用寄存器
Q 寄存器eax, ebx, ecx,edx之一
I, h 直接操作数
E, F 浮点数
G 任意
a, b, c, d 寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl
S, D 寄存器esi或edi
I 常数(0~31)
2.2.1 寄存器操作数约束
使用r约束指定操作数时,它们存储在通用寄存器中。一般格式为:“constraint”(Cexpression)。每个操作数都由操作数约束字符串指定,后面跟用括弧括起的C表达式。操作数约束的主要功能是确定操作数的寻址方式。请看下例:
int cr3val;
asm (
“movl %%cr3,%0\n”
:“=r”(cr3val)
);
这里,"=r"说明相应的输出变量(指令部分的%0)可以使用任何一个通用寄存器,并且变量cr3val保存在这个寄存器中;%cr3的值复制到寄存器上,cr3val的值从该寄存器更新到内存中。指定“r”约束时,GCC编译器可以将变量cr3val保存在任何可用的通用寄存器中。
要指定寄存器,必须通过使用特定的寄存器约束直接指定寄存器名。
a %eax
b %ebx
c %ecx
d %edx
S %esi
D %edi
2.2.2 内存操作数约束(m)
当操作数位于内存中时,任何对它们执行的操作都将在内存位置中直接发生,这与寄存器约束正好相反,后者先将值存储在要修改的寄存器中,然后将它写回内存位置中。但寄存器约束通常只在对于指令来说它们是绝对必需的,或者它们可以大大提高进程速度时使用。当需要在“asm”内部更新C变量,而又确实不希望使用寄存器来保存其值时,使用内存约束最为有效。
:“=m”(v->counter)
:“ir”(i),“m”(v->counter))
2.2.3 匹配(数字)约束
在某些情况下,一个变量既要充当输入操作数,也要充当输出操作数。可以通过使用匹配约束在“asm”中指定这种情况。
asm(
“incl %0”//INC指令只有1个操作数,它将指定的操作数的内容加1;对于存储单元,需要用BYTE PTR或者WORD PTR说明是字节还是字操作。
:“=a”(var)
:“0”(var)
);
在上述示例中,寄存器%eax既用作输入变量,也用作输出变量。将var输入读取到%eax,增加后将更新的%eax再次存储在var中。这里的“0"指定第0个输出变量相同的约束。即,它指定var的输出实例只应该存储在%eax中。该约束可以用于以下情况:
输入从变量中读取,或者变量被修改后,修改写回到同一变量中;
不需要将输入操作数和输出操作数的实例分开。
使用匹配约束最重要的意义在于它们可以有效地使用可用寄存器。
/将a的值赋值给b/
int main()
{
int a=10, b;
asm (
“movl %1, %%eax;”
“movl %%eax, %0;”
//“movl %1, %%eax\n\t”
//“movl %%eax, %0\n\t”
:“=r”(b) /* output /
:“r”(a) / input /
:“%eax” / clobbered register */
);
printf(“Result:%d,%d\n”,a,b);
return 0;
}
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $10, -8(%rbp)
movl -8(%rbp), %edx
6 “testasm.c” 内联汇编被替换的结果,没有换行
movl %edx, %eax;movl %eax, %edx;
movl %edx, -4(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
在上例中,我们使用汇编指令使 “b” 的值等于 “a”。请注意以下几点:
“b” 是输出操作数,由 %0 引用,“a” 是输入操作数,由 %1 引用。 “r” 是操作数的约束,它指定将变量 “a” 和 “b” 存储在寄存器中。
请注意,输出操作数约束应该带有一个约束修饰符 “=”,指定它是输出操作数。
要在 “asm” 内使用寄存器 %eax,%eax 的前面应该再加一个 %,换句话说是%%eax,因为 “asm” 使用 %0、%1 等来标识变量。任何带有一个 % 的数都看作是输入/输出操作数,而不认为是寄存器。
第三个冒号后的修饰寄存器 %eax 告诉将在 “asm” 中修改 GCC %eax 的值,(占用这个寄存器)这样 GCC 就不使用该寄存器存储任何其它的值。
movl %1, %%eax 将 “a” 的值移到 %eax 中, movl %%eax, %0 将 %eax 的内容移到 “b” 中。 因为 “b” 被指定成输出操作数,因此当 “asm” 的执行完成后,它将反映出更新的值。换句话说,对 “asm” 内 “b” 所做的更改将在 “asm” 外反映出来。