现在的位置: 首页 > 综合 > 正文

AT&T汇编与GCC内嵌汇编语法

2013年10月18日 ⁄ 综合 ⁄ 共 9766字 ⁄ 字号 评论关闭

讨论AT&T的汇编语法,以及GCC的内嵌汇编语法。

转自:http://blog.chinaunix.net/space.php?uid=7396950&do=blog&id=2056358

0.3.2 Syntax 

1.寄存器引用

引用寄存器要在寄存器号前加百分号%,如“movl %eax, %ebx”

80386有如下寄存器:

8个32-bit寄存器 %eax%ebx%ecx%edx%edi%esi%ebp%esp; 
8
16-bit寄存器,它们事实上是上面832-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)
2. 
操作数顺序

操作数排列是从源(左)到目的(右),如“movl %eax(源), %ebx(目的)”

3. 立即数

使用立即数,要在数前面加符号$如“movl $0x04, %ebx”

或者:

para = 0x04

movl $para, %ebx

指令执行的结果是将立即数04h装入寄存器ebx

4. 符号常数

符号常数直接引用 如

value: .long 0x12a3f2de

movl value , %ebx

指令执行的结果是将常数0x12a3f2de装入寄存器ebx

引用符号地址在符号前加符号$, 如“movl $value, % ebx”则是将符号value的地址装入寄存器ebx

5. 操作数的长度

操作数的长度用加在指令后的符号表示b(byte, 8-bit), w(word, 16-bits), l(long, 32-bits),如“movb %al, %bl”,“movw
%ax, %bx”
,“movl %eax, %ebx ”

如果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。比如指令“mov %ax, %bx”,由于目标操作数bx的长度为word,那么编译器将把此指令等同于“movw
%ax, %bx”
。同样道理,指令“mov $4, %ebx”等同于指令“movl $4, %ebx”,“push
%al”
等同于“pushb %al”。对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push $4”

6. 符号扩展和零扩展指令

绝大多数面向80386AT&T汇编指令与Intel格式的汇编指令都是相同的,符号扩展指令和零扩展指令则是仅有的不同格式指令。

符号扩展指令和零扩展指令需要指定源操作数长度和目的操作数长度,即使在某些指令中这些操作数是隐含的。

AT& T语法中,符号扩展和零扩展指令的格式为,基本部分"movs""movz"(对应Intel语法的movsxmovzx),后面跟上源操作数长度和
目的操作数长度。movsbl意味着movs frombytetolongmovbw意味着movs frombyte towordmovswl意味着movs fromword tolong。对于movz指令也一样。比如指令“movsbl
%al, %edx”
意味着将al寄存器的内容进行符号扩展后放置到edx寄存器中。

其它的Intel格式的符号扩展指令还有:

cbw -- sign-extend byte in %al to word in %ax; 
cwde -- sign-extend word in %ax to long in %eax
; 
cwd -- sign-extend word in %ax to long in %dx:%ax
; 
cdq -- sign-extend dword in %eax to quad in %edx:%eax

对应的AT&T语法的指令为cbtwcwtlcwtdcltd

7. 调用和跳转指令

段内调用和跳转指令为"call""ret""jmp",段间调用和跳转指令为"lcall""lret""ljmp"

段间调用和跳转指令的格式为“lcall/ljmp $SECTION, $OFFSET”,而段间返回指令则为“lret $STACK-ADJUST”

8. 前缀

操作码前缀被用在下列的情况:

字符串重复操作指令(rep,repne); 
指定被操作的段(cs,ds,ss,es,fs,gs); 
进行总线加锁(lock); 
指定地址和操作的大小(data16,addr16)
AT&T汇编语法中,操作码前缀通常被单独放在一行,后面不跟任何操作数。例如,对于重复scas指令,其写法为:

repne
scas

上述操作码前缀的意义和用法如下:

指定被操作的段前缀为cs,ds,ss,es,fs,gs。在AT&T语法中,只需要按照section:memory-operand的格式就指定了相应的段前缀。比如:lcall
%cs:realmode_swtch 
操作数/地址大小前缀是“data16”"addr16",它们被用来在32-bit操作数/地址代码中指定16-bit的操作数/地址。 
总 线加锁前缀“lock”,它是为了在多处理器环境中,保证在当前指令执行期间禁止一切中断。这个前缀仅仅对ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG,
NOT, OR, SBB, SUB, XOR, XADD,XCHG
指令有效,如果将Lock前缀用在其它指令之前,将会引起异常。 
字符串重复操作前缀"rep","repe","repne"用来让字符串操作重复“%ecx”次。

9. 内存引用

Intel语法的间接内存引用的格式为:

section:[base+index*scale+displacement]

而在AT&T语法中对应的形式为:

section:displacement(base,index,scale)

其 中,baseindex是任意的32-bit baseindex寄存器。scale可以取值1248。如果不指定scale值,则默认值为1section可以指定任意的段寄存器作为段前
缀,默认的段寄存器在不同的情况下不一样。如果你在指令中指定了默认的段前缀,则编译器在目标代码中不会产生此段前缀代码。

下面是一些例子:

-4(%ebp):base=%ebpdisplacement=-4section没有指定,由于base%ebp,所以默认的section=%ssindex,scale没有指定,则index0

foo(,%eax,4):index=%eaxscale=4displacement=foo。其它域没有指定。这里默认的section=%ds

foo(,1):这个表达式引用的是指针foo指向的地址所存放的值。注意这个表达式中没有baseindex,并且只有一个逗号,这是一种异常语法,但却合法。

%gs:foo:这个表达式引用的是放置于%gs段里变量foo的值。

如果calljump操作在操作数前指定前缀“*”,则表示是一个绝对地址调用/跳转,也就是说jmp/call指令指定的是一个绝对地址。如果没有指定"*",则操作数是一个相对地址。

任何指令如果其操作数是一个内存操作,则指令必须指定它的操作尺寸(byte,word,long),也就是说必须带有指令后缀(b,w,l)

.3 GCC Inline ASM

GCC 支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作GCC Inline ASM——GCC内联汇编。这是一个非常有用的功能,有利于我们将一些C/C++语法无法表达的指令直接潜入C/C++代码中,另外也允许我们直接写C/C++代码中使用汇编编写简洁高效的代码。

1.基本内联汇编

GCC中基本的内联汇编非常易懂,我们先来看两个简单的例子:

__asm__("movl %esp,%eax"); // 看起来很熟悉吧!

或者是

__asm__("
movl $1,%eax // SYS_exit
xor %ebx,%ebx
int $0x80
");

__asm__(
"movl $1,%eax\r\t" \
"xor %ebx,%ebx\r\t" \
"int $0x80" \
);

基本内联汇编的格式是

__asm__ __volatile__("Instruction List");

1、__asm__

__asm__是GCC关键字asm的宏定义:

#define __asm__ asm

__asm__或asm用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它开头的,是必不可少的。

2、Instruction List

Instruction List是汇编指令序列。它可以是空的,比如:__asm__ __volatile__(""); __asm__ ("");都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。但并非所有Instruction
List
为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory"); 就非常有意义,它向GCC声明:“我对内存作了改动”,GCC在编译的时候,会将此因素考虑进去。

我们看一看下面这个例子:

$ cat example1.c

int main(int __argc, char* __argv[]) 

int* __p = (int*)__argc; 

(*__p) = 9999; 

//__asm__("":::"memory"); 

if((*__p) == 9999) 
return 5; 

return (*__p); 
}

在 这段代码中,那条内联汇编是被注释掉的。在这条内联汇编之前,内存指针__p所指向的内存被赋值为9999,随即在内联汇编之后,一条if语句判断__p 所指向的内存与9999是否相等。很明显,它们是相等的。GCC在优化编译的时候能够很聪明的发现这一点。我们使用下面的命令行对其进行编译:

$ gcc -O -S example1.c

选项-O表示优化编译,我们还可以指定优化等级,比如-O2表示优化等级为2;选项-S表示将C/C++源文件编译为汇编文件,文件名和C/C++文件一样,只不过扩展名由.c变为.s

我们来查看一下被放在example1.s中的编译结果,我们这里仅仅列出了使用gcc 2.96redhat
7.3
上编译后的相关函数部分汇编代码。为了保持清晰性,无关的其它代码未被列出。

$ cat example1.s

main: 
pushl %ebp 
movl %esp, %ebp 
movl 8(%ebp), %eax # int* __p = (int*)__argc
movl $9999, (%eax) # (*__p) = 9999 
movl $5, %eax # return 5
popl %ebp 
ret

参 照一下C源码和编译出的汇编代码,我们会发现汇编代码中,没有if语句相关的代码,而是在赋值语句(*__p)=9999后直接return
5
;这是因为GCC认为在(*__p)被赋值之后,在if语句之前没有任何改变(*__p)内容的操作,所以那条if语句的判断条件(*__p)
== 9999
肯定是为true的,所以GCC就不再生成相关代码,而是直接根据为true的条件生成return
5
的汇编代码(GCC使用eax作为保存返回值的寄存器)。

我们现在将example1.c中内联汇编的注释去掉,重新编译,然后看一下相关的编译结果。

$ gcc -O -S example1.c

$ cat example1.s

main: 
pushl %ebp 
movl %esp, %ebp 
movl 8(%ebp), %eax # int* __p = (int*)__argc
movl $9999, (%eax) # (*__p) = 9999
#APP 

# __asm__("":::"memory")
#NO_APP
cmpl $9999, (%eax) # (*__p) == 9999 ?
jne .L3 # false 
movl $5, %eax # true, return 5 
jmp .L2 
.p2align 2 
.L3: 
movl (%eax), %eax 
.L2: 
popl %ebp 
ret

由于内联汇编语句__asm__("":::"memory")GCC声明,在此内联汇编语句出现的位置内存内容可能了改变,所以GCC在编译时就不能像刚才那样处理。这次,GCC老老实实的将if语句生成了汇编代码。

可能有人会质疑:为什么要使用__asm__("":::"memory")GCC声明内存发生了变化?明明“Instruction
List”
是空的,没有任何对内存的操作,这样做只会增加GCC生成汇编代码的数量。

确实,那条内联汇编语句没有对内存作任何操作,事实上它确实什么都没有做。但影响内存内容的不仅仅是你当前正在运行的程序。比如,如果你现在正在操作的内存是一块内存映射,映射的内容是外围I/O设备寄存器。那么操作这块内存的就不仅仅是当前的程序,I/O设备也会去操作这块内存。既然两者都会去操作同一块内存,那么任何一方在任何时候都不能对这块内存的内容想当然。所以当你使用高级语言C/C++写这类程序的时候,你必须让编译器也能够明白这一点,毕竟高级语言最终要被编译为汇编代码。

你可能已经注意到了,这次输出的汇编结果中,有两个符号:#APP#NO_APPGCC将内联汇编语句中"Instruction
List"
所列出的指令放在#APP#NO_APP之间,由于__asm__("":::"memory")中“Instruction
List”
为空,所以#APP#NO_APP中间也没有任何内容。但我们以后的例子会更加清楚的表现这一点。

关于为什么内联汇编__asm__("":::"memory")是一条声明内存改变的语句,我们后面会详细讨论。

刚才我们花了大量的内容来讨论"Instruction List"为空是的情况,但在实际的编程中,"Instruction List"绝大多数情况下都不是空的。它可以有1条或任意多条汇编指令。

当 在"Instruction List"中有多条指令的时候,你可以在一对引号中列出全部指令,也可以将一条或几条指令放在一对引号中,所有指令放在多对引号中。如果是前者,你可以将每一条指令放在一行,如果要将多条指令放在一行,则必须用分号(;)或换行符(\n,大多数情况下\n后还要跟一个\t,其中\n是为了换行,\t是为了
空出一个tab宽度的空格)将它们分开。比如:

__asm__("movl %eax, %ebx 
sti 
popl %edi 
subl %ecx, %ebx"); 

__asm__("movl %eax, %ebx; sti 
popl %edi; subl %ecx, %ebx");

__asm__("movl %eax, %ebx; sti\n\t popl %edi
subl %ecx, %ebx");

都是合法的写法。如果你将指令放在多对引号中,则除了最后一对引号之外,前面的所有引号里的最后一条指令之后都要有一个分号()(\n)(\n\t)。比如:

__asm__("movl %eax, %ebx 
sti\n" 
"popl %edi;" 
"subl %ecx, %ebx"); 

__asm__("movl %eax, %ebx; sti\n\t" 
"popl %edi; subl %ecx, %ebx");

__asm__("movl %eax, %ebx; sti\n\t popl %edi\n"
"subl %ecx, %ebx");

__asm__("movl %eax, %ebx; sti\n\t popl %edi;" "subl %ecx, %ebx");

都是合法的。

上述原则可以归结为:

任意两个指令间要么被分号()分开,要么被放在两行; 
放在两行的方法既可以从通过\n的方法来实现,也可以真正的放在两行; 
可以使用1对或多对引号,每1对引号里可以放任一多条指令,所有的指令都要被放到引号中。
在基本内联汇编中,“Instruction List”的书写的格式和你直接在汇编文件中写非内联汇编没有什么不同,你可以在其中定义Label,定义对齐(.align
n )
,定义段(.section name )。例如:

__asm__(".align 2\n\t" 
"movl %eax, %ebx\n\t" 
"test %ebx, %ecx\n\t" 
"jne error\n\t" 
"sti\n\t" 
"error: popl %edi\n\t" 
"subl %ecx, %ebx");

上面例子的格式是Linux内联代码常用的格式,非常整齐。也建议大家都使用这种格式来写内联汇编代码。

3、__volatile__

__volatile__是GCC关键字volatile的宏定义:

#define __volatile__ volatile

__volatile__ volatile是可选的,你可以用它也可以不用它。如果你用了它,则是GCC声明“不要动我所写的Instruction
List
,我需要原封不动的保留每一条指令”,否则当你使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉

那么GCC判断的原则是什么?我试验了一下,发现一条内联汇编语句如果是基本内联汇编的话(即只有“Instruction List”,没有Input/Output/Clobber的内联汇编,我们后面将会讨论这一点),无论你是否使用__volatile__来修饰, GCC
2.96
在优化编译时,都会原封不动的保留内联汇编中的“Instruction List”。但或许我的试验的例子并不充分,所以这一点并不能够得到保证。

为了保险起见,如果你不想让GCC的优化影响你的内联汇编代码,你最好在前面都加上__volatile__,而不要依赖于编译器的原则,因为即使你非常了解当前编译器的优化原则,你也无法保证这种原则将来不会发生变化。而__volatile__的含义却是恒定的。

2、带有C/C++表达式的内联汇编

GCC允许你通过C/C++表达式指定内联汇编中"Instrcuction List"中指令的输入和输出,你甚至可以不关心到底使用哪个寄存器被使用,完全靠GCC来安排和指定。这一点可以让程序员避免去考虑有限的寄存器的使用,也可以提高目标代码的效率。

我们先来看几个例子:

__asm__ (" " : : : "memory" ); // 前面提到的

__asm__ ("mov %%eax, %%ebx" : "=b"(rv) : "a"(foo) : "eax", "ebx");

__asm__ __volatile__("lidt %0": "=m" (idt_descr));

__asm__("subl %2,%0\n\t"
"sbbl %3,%1"
: "=a" (endlow), "=d" (endhigh)
: "g" (startlow), "g" (starthigh), "0" (endlow), "1" (endhigh));

怎么样,有点印象了吧,是不是也有点晕?没关系,下面讨论完之后你就不会再晕了。(当然,也有可能更晕^_^)。讨论开始——

带有C/C++表达式的内联汇编格为:

__asm__ __volatile__("Instruction
List" : Output : Input : Clobber/Modify);

从中我们可以看出它和基本内联汇编的不同之处在于:它多了3个部分(InputOutputClobber/Modify)。在括号中的4个部分通过冒号(:)分开。

4个部分都不是必须的,任何一个部分都可以为空,其规则为:

如果Clobber/Modify为空,则其前面的冒号(:)必须省略。比如__asm__("mov
%%eax, %%ebx" : "=b"(foo) : "a"(inp)
 : )
就是非法的写法;而__asm__("mov
%%eax, %%ebx" : "=b"(foo) : "a"(inp) )
则是正确的。 
如果Instruction List为空,则InputOutputClobber/Modify可以不为空,也可以为空。比如__asm__
( " " : : : "memory" );
__asm__(" " : : );都是合法的写法。 
如果OutputInputClobber/Modify都为空,OutputInput之前的冒号(:)既可以省略,也可以不省略。如果都省略,则此汇编退化为一个基本内联汇编,否则,仍然是一个带有C/C++表达式的内联汇编,此时"Instruction
List"
中的寄存器写法要遵守相关规定,比如寄存器前必须使用两个百分号(%%),而不是像基本汇编格式一样在寄存器前只使用一个百分号(%)。比如 __asm__(
" mov %%eax, %%ebx" : : )
__asm__( " mov %%eax, %%ebx" : )__asm__(
" mov %eax, %ebx" )
都是正确的写法,而__asm__( " mov %eax, %ebx" : : )__asm__(
" mov %eax, %ebx" : )
__asm__( " mov %%eax, %%ebx" )都是错误的写法。 
如果InputClobber/Modify为空,但Output不为空,Input前的冒号(:)既可以省略,也可以不省略。比如__asm__(
" mov %%eax, %%ebx" : "=b"(foo) : )
__asm__( " mov %%eax, %%ebx" : "=b"(foo) )都是正确的。 
如果后面的部分不为空,而前面的部分为空,则前面的冒号(:)都必须保留,否则无法说
明不为空的部分究竟是第几部分。比如, Clobber/Modify
Output为空,而Input不为空,则Clobber/Modify前的冒号必须省略(前面的规则),而Output 前的冒号必须为保留。

⑥如果Clobber/Modify不为空,而InputOutput都为空,则InputOutput前的冒号都必须保留。比如__asm__(
" mov %%eax, %%ebx" : : "a"(foo) )
__asm__( " mov %%eax, %%ebx" : : : "ebx" )
从上面的规则可以看到另外一个事实,区分一个内联汇编是基本格式的还是带有C/C++表达式格式的,其规则在于在"Instruction List"后是否有冒号(:)的存在,如果没有则是基本格式的,否则,则是带有C/C++表达式格式的。

两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个百分号(%),这一点和非内联汇编相同;而带有C/C++表达式格式则要求寄存器前必须使用两个百分号(%%),其原因我们会在后面讨论。

1. Output

Output用来指定当前内联汇编语句的输出。我们看一看这个例子:

__asm__("movl %%cr0, %0": "=a" (cr0));
这个内联汇编语句的输出部分为"=r"(cr0),它是一个“操作表达式”,指定了一个输出操作。我们可以很清楚得看到这个输出操作由两部分组成:括号括住的部分(cr0)和引号引住的部分"=a"。这两部分都是每一个输出操作必不可少的。括号括住的部分是一个C/C++表达式,用来保存内联汇编的一个输出值,其操作就等于C/C++的相等赋值cr0
= output_value
,因此,括号中的输出表达式只能是C/C++的左值表达式,也就是说它只能是一个可以合法的放在C/C++赋值操作中等号(=) 左边的表达式。那么右值output_value从何而来呢?

答案是引号中的内容,被称作“操作约束”(Operation Constraint),在这个例子中操作约束为"=a",它包含两个约束:等号(=)和字母a

抱歉!评论已关闭.