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");
一、__asm__
__asm__是GCC关键字asm的宏定义:
#define __asm__ asm
__asm__或asm用来声明一个内联汇编表达式,因此任何一个内联汇编表达式都是以它开头的,是必不可少的。
二、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.96在redhat 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_APP,GCC将内联汇编语 句中"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内联代码经常使用的格式,很是整齐。也建议你们都使用这种格式来写内联汇编代码。
三、__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__的含义倒是恒定的。
二、带有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个部分(Input,Output,Clobber/Modify)。在括号中的4个部分经过冒号(:)分开。
这4个部分都不是必须的,任何一个部分均可觉得空,其规则为:
如 果Clobber/Modify为空,则其前面的冒号(:)必须省略。好比__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) : )就是非法的写法;而__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) )则是正确的。
若是Instruction List为空,则Input,Output,Clobber/Modify能够不为空,也能够为空。好比__asm__ ( " " : : : "memory" );和__asm__(" " : : );都是合法的写法。
如 果Output,Input,Clobber/Modify都为空,Output,Input以前的冒号(:)既能够省略,也能够不省略。若是都省略,则 此汇编退化为一个基本内联汇编,不然,仍然是一个带有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" )都是错误的写法。
若是Input,Clobber/Modify为空,但Output不为空,Input前的冒号(:)既能够省略,也能够不省略。好比 __asm__( " mov %%eax, %%ebx" : "=b"(foo) : );__asm__( " mov %%eax, %%ebx" : "=b"(foo) )都是正确的。
若是后面的部分不为空,而前面的部分为空,则前面的冒号(:)都必须保留,不然没法说 明不为空的部分到底是第几部分。好比, Clobber/Modify,Output为空,而Input不为空,则Clobber/Modify前的冒号必须省略(前面的规则),而Output 前的冒号必须为保留。若是Clobber/Modify不为空,而Input和Output都为空,则Input和Output前的冒号都必须保留。好比 __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,其中等号(=)说明括号中左值表达式cr0是一个 Write-Only的,只可以被做为当前内联汇编的输入,而不能做为输入。而字母a是寄存器EAX / AX / AL的简写,说明cr0的值要从eax寄存器中获取,也就是说cr0 = eax,最终这一点被转化成汇编指令就是movl %eax, address_of_cr0。如今你应该清楚了吧,操做约束中会给出:到底从哪一个寄存器传递值给cr0。
另外,须要特别说明的是,不少 文档都声明,全部输出操做的操做约束必须包含一个等号(=),但GCC的文档中却很清楚的声明,并不是如此。由于等号(=)约束说明当前的表达式是一个 Write-Only的,但另外还有一个符号——加号(+)用来讲明当前表达式是一个Read-Write的,若是一个操做约束中没有给出这两个符号中的 任何一个,则说明当前表达式是Read-Only的。由于对于输出操做来讲,确定是必须是可写的,而等号(=)和加号(+)都表示可写,只不过加号(+) 同时也表示是可读的。因此对于一个输出操做来讲,其操做约束只须要有等号(=)或加号(+)中的任意一个就能够了。
两者的区别是:等号(=)表示当前操做表达式指定了一个纯粹的输出操做,而加号(+)则表示当前操做表达式不只仅只是一个输出操做仍是一个输入操做。但不管是等号(=)约束仍是加号(+)约束所约束的操做表达式都只能放在Output域中,而不能被用在Input域中。
另外,有些文档声明:尽管GCC文档中提供了加号(+)约束,但在实际的编译中通不过;我不知道老版本会怎么样,我在GCC 2.96中对加号(+)约束的使用很是正常。
咱们经过一个例子看一下,在一个输出操做中使用等号(=)约束和加号(+)约束的不一样。
$ cat example2.c
int main(int __argc, char* __argv[])
{
int cr0 = 5;
__asm__ __volatile__("movl %%cr0, %0":"=a" (cr0));
return 0;
}
$ gcc -S example2.c
$ cat example2.s
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $5, -4(%ebp) # cr0 = 5
#APP
movl %cr0, %eax
#NO_APP
movl %eax, %eax
movl %eax, -4(%ebp) # cr0 = %eax
movl $0, %eax
leave
ret
这个例子是使用等号(=)约束的状况,变量cr0被放在内存-4(%ebp)的位置,因此指令mov %eax, -4(%ebp)即表示将%eax的内容输出到变量cr0中。
下面是使用加号(+)约束的状况:
$ cat example3.c
int main(int __argc, char* __argv[])
{
int cr0 = 5;
__asm__ __volatile__("movl %%cr0, %0" : "+a" (cr0));
return 0;
}
$ gcc -S example3.c
$ cat example3.s
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $5, -4(%ebp) # cr0 = 5
movl -4(%ebp), %eax # input ( %eax = cr0 )
#APP
movl %cr0, %eax
#NO_APP
movl %eax, -4(%ebp) # output (cr0 = %eax )
movl $0, %eax
leave
ret
从编译的结果能够看出,当使用加号(+)约束的时候,cr0不只做为输出,还做为输入,所使用寄存器都是寄存器约束(字母a,表示使用eax寄存器)指定的。关于寄存器约束咱们后面讨论。
在Output域中能够有多个输出操做表达式,多个操做表达式中间必须用逗号(,)分开。例如:
__asm__(
"movl %%eax, %0 \n\t"
"pushl %%ebx \n\t"
"popl %1 \n\t"
"movl %1, %2"
: "+a"(cr0), "=b"(cr1), "=c"(cr2));
二、Input
Input域的内容用来指定当前内联汇编语句的输入。咱们看一看这个例子:
__asm__("movl %0, %%db7" : : "a" (cpu->db7));
例中Input域的内容为一个表达式"a"[cpu->db7),被称做“输入表达式”,用来表示一个对当前内联汇编的输入。
像输出表达式同样,一个输入表达式也分为两部分:带括号的部分(cpu->db7)和带引号的部分"a"。这两部分对于一个内联汇编输入表达式来讲也是必不可少的。
括 号中的表达式cpu->db7是一个C/C++语言的表达式,它没必要是一个左值表达式,也就是说它不只能够是放在C/C++赋值操做左边的表达式, 还能够是放在C/C++赋值操做右边的表达式。因此它能够是一个变量,一个数字,还能够是一个复杂的表达式(好比a+b/c*d)。好比上例能够改成: __asm__("movl %0, %%db7" : : "a" (foo)),__asm__("movl %0, %%db7" : : "a" (0x1000))或__asm__("movl %0, %%db7" : : "a" (va*vb/vc))。
引号号中的 部分是约束部分,和输出表达式约束不一样的是,它不容许指定加号(+)约束和等号(=)约束,也就是说它只能是默认的Read-Only的。约束中必须指定 一个寄存器约束,例中的字母a表示当前输入变量cpu->db7要经过寄存器eax输入到当前内联汇编中。
咱们看一个例子:
$ cat example4.c
int main(int __argc, char* __argv[])
{
int cr0 = 5;
__asm__ __volatile__("movl %0, %%cr0"::"a" (cr0));
return 0;
}
$ gcc -S example4.c
$ cat example4.s
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $5, -4(%ebp) # cr0 = 5
movl -4(%ebp), %eax # %eax = cr0
#APP
movl %eax, %cr0
#NO_APP
movl $0, %eax
leave
ret
咱们从编译出的汇编代码能够看到,在"Instruction List"以前,GCC按照咱们的输入约束"a",将变量cr0的内容装入了eax寄存器。
3. Operation Constraint
每个Input和Output表达式都必须指定本身的操做约束Operation Constraint,咱们这里来讨论在80386平台上所可能使用的操做约束。
一、寄存器约束
当你当前的输入或输入须要借助一个寄存器时,你须要为其指定一个寄存器约束。你能够直接指定一个寄存器的名字,好比:
__asm__ __volatile__("movl %0, %%cr0"::"eax" (cr0));
也能够指定一个缩写,好比:
__asm__ __volatile__("movl %0, %%cr0"::"a" (cr0));
若是你指定一个缩写,好比字母a,则GCC将会根据当前操做表达式中C/C++表达式的宽度决定使用%eax,仍是%ax或%al。好比:
unsigned short __shrt;
__asm__ ("mov %0,%%bx" : : "a"(__shrt));
因为变量__shrt是16-bit short类型,则编译出来的汇编代码中,则会让此变量使用%ex寄存器。编译结果为:
movw -2(%ebp), %ax # %ax = __shrt
#APP
movl %ax, %bx
#NO_APP
不管是Input,仍是Output操做表达式约束,均可以使用寄存器约束。
下表中列出了经常使用的寄存器约束的缩写。
约束 Input/Output 意义
r I,O 表示使用一个通用寄存器,由GCC在%eax/%ax/%al, %ebx/%bx/%bl, %ecx/%cx/%cl, %edx/%dx/%dl中选取一个GCC认为合适的。
q I,O 表示使用一个通用寄存器,和r的意义相同。
a I,O 表示使用%eax / %ax / %al
b I,O 表示使用%ebx / %bx / %bl
c I,O 表示使用%ecx / %cx / %cl
d I,O 表示使用%edx / %dx / %dl
D I,O 表示使用%edi / %di
S I,O 表示使用%esi / %si
f I,O 表示使用浮点寄存器
t I,O 表示使用第一个浮点寄存器
u I,O 表示使用第二个浮点寄存器
二、内存约束
若是一个Input/Output操做表达式的C/C++表达式表现为一个内存地址,不想借助于任何寄存器,则可使用内存约束。好比:
__asm__ ("lidt %0" : "=m"(__idt_addr)); 或 __asm__ ("lidt %0" : :"m"(__idt_addr));
咱们看一下它们分别被放在一个C源文件中,而后被GCC编译后的结果:
$ cat example5.c
// 本例中,变量sh被做为一个内存输入
int main(int __argc, char* __argv[])
{
char* sh = (char*)&__argc;
__asm__ __volatile__("lidt %0" : : "m" (sh));
return 0;
}
$ gcc -S example5.c
$ cat example5.s
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
leal 8(%ebp), %eax
movl %eax, -4(%ebp) # sh = (char*) &__argc
#APP
lidt -4(%ebp)
#NO_APP
movl $0, %eax
leave
ret
$ cat example6.c
// 本例中,变量sh被做为一个内存输出
int main(int __argc, char* __argv[])
{
char* sh = (char*)&__argc;
__asm__ __volatile__("lidt %0" : "=m" (sh));
return 0;
}
$ gcc -S example6.c
$ cat example6.s
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
leal 8(%ebp), %eax
movl %eax, -4(%ebp) # sh = (char*) &__argc
#APP
lidt -4(%ebp)
#NO_APP
movl $0, %eax
leave
ret
首先,你会注意到,在这两个例子中,变量sh没有借助任何寄存器,而是直接参与了指令lidt的操做。
其次,经过仔细观察,你会发现一个惊人的事实,两个例子编译出来的汇编代码是同样的!虽然,一个例子中变量sh做为输入,而另外一个例子中变量sh做为输出。这是怎么回事?
原来,使用内存方式进行输入输出时,因为不借助寄存器,因此GCC不会按照你的声明对其做任何的输入输出处理。GCC只会直接拿来用,究竟对这个C/C++表达式而言是输入仍是输出,彻底依赖与你写在"Instruction List"中的指令对其操做的指令。
由 于上例中,对其操做的指令为lidt,lidt指令的操做数是一个输入型的操做数,因此事实上对变量sh的操做是一个输入操做,即便你把它放在 Output域也不会改变这一点。因此,对此例而言,彻底符合语意的写法应该是将sh放在Input域,尽管放在Output域也会有正确的执行结果。
所 以,对于内存约束类型的操做表达式而言,放在Input域仍是放在Output域,对编译结果是没有任何影响的,由于原本咱们将一个操做表达式放在 Input域或放在Output域是但愿GCC能为咱们自动经过寄存器将表达式的值输入或输出。既然对于内存约束类型的操做表达式来讲,GCC不会自动为 它作任何事情,那么放在哪儿也就无所谓了。但从程序员的角度而言,为了加强代码的可读性,最好可以把它放在符合实际状况的地方。
约束 Input/Output 意义
m I,O 表示使用系统所支持的任何一种内存方式,不须要借助寄存器
三、当即数约束
若是一个Input/Output操做表达式的C/C++表达式是一个数字常数,不想借助于任何寄存器,则可使用当即数约束。
因为当即数在C/C++中只能做为右值,因此对于使用当即数约束的表达式而言,只能放在Input域。
好比:__asm__ __volatile__("movl %0, %%eax" : : "i" (100) );
当即数约束很简单,也很容易理解,咱们在这里就再也不赘述。
约束 Input/Output 意义
i I 表示输入表达式是一个当即数(整数),不须要借助任何寄存器
F I 表示输入表达式是一个当即数(浮点数),不须要借助任何寄存器
四、通用约束
约束 Input/Output 意义
g I,O 表示可使用通用寄存器,内存,当即数等任何一种处理方式。
0,1,2,3,4,5,6,7,8,9 I 表示和第n个操做表达式使用相同的寄存器/内存。
通 用约束g是一个很是灵活的约束,当程序员认为一个C/C++表达式在实际的操做中,究竟使用寄存器方式,仍是使用内存方式或当即数方式并没有所谓时,或者程 序员想实现一个灵活的模板,让GCC能够根据不一样的C/C++表达式生成不一样的访问方式时,就可使用通用约束g。好比:
#define JUST_MOV(foo) __asm__ ("movl %0, %%eax" : : "g"(foo))
JUST_MOV(100)和JUST_MOV(var)则会让编译器产生不一样的代码。
int main(int __argc, char* __argv[])
{
JUST_MOV(100);
return 0;
}
编译后生成的代码为:
main:
pushl %ebp
movl %esp, %ebp
#APP
movl $100, %eax
#NO_APP
movl $0, %eax
popl %ebp
ret
很明显这是当即数方式。而下一个例子:
int main(int __argc, char* __argv[])
{
JUST_MOV(__argc);
return 0;
}
经编译后生成的代码为:
main:
pushl %ebp
movl %esp, %ebp
#APP
movl 8(%ebp), %eax
#NO_APP
movl $0, %eax
popl %ebp
ret
这个例子是使用内存方式。
一个带有C/C++表达式的内联汇编,其操做表达式被按照被列出的顺序编号,第一个是0,第2个是1,依次类推,GCC最多容许有10个操做表达式。好比:
__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=a"(__out)
: "r" (__in1), "r" (__in2));
此例中,__out所在的Output操做表达式被编号为0,"r"(__in1)被编号为1,"r"(__in2)被编号为2。
再如:
__asm__ ("movl %%eax, %%ebx" : : "a"(__in1), "b"(__in2));
此例中,"a"(__in1)被编号为0,"b"(__in2)被编号为1。
如 果某个Input操做表达式使用数字0到9中的一个数字(假设为1)做为它的操做约束,则等于向GCC声明:“我要使用和编号为1的Output操做表达 式相同的寄存器(若是Output操做表达式1使用的是寄存器),或相同的内存地址(若是Output操做表达式1使用的是内存)”。上面的描述包含两个 限定:数字0到数字9做为操做约束只能用在Input操做表达式中,被指定的操做表达式(好比某个Input操做表达式使用数字1做为约束,那么被指定的 就是编号为1的操做表达式)只能是Output操做表达式。
因为GCC规定最多只能有10个Input/Output操做表达式,因此事 实上数字9做为操做约束永远也用不到,由于Output操做表达式排在Input操做表达式的前面,那么若是有一个Input操做表达式指定了数字9做为 操做约束的话,那么说明Output操做表达式的数量已经至少为10个了,那么再加上这个Input操做表达式,则至少为11个了,以及超出GCC的限 制。
五、Modifier Characters(修饰符)
等号(=)和加号(+)用于对Output操做表达式的修 饰,一个Output操做表达式要么被等号(=)修饰,要么被加号(+)修饰,两者必居其一。使用等号(=)说明此Output操做表达式是Write- Only的,使用加号(+)说明此Output操做表达式是Read-Write的。它们必须被放在约束字符串的第一个字母。好比"a="(foo)是非 法的,而"+g"(foo)则是合法的。
当使用加号(+)的时候,此Output表达式等价于使用等号(=)约束加上一个Input表达式。好比
__asm__ ("movl %0, %%eax; addl %%eax, %0" : "+b"(foo)) 等价于
__asm__ ("movl %1, %%eax; addl %%eax, %0" : "=b"(foo) : "b"(foo))
但若是使用后一种写法,"Instruction List"中的别名也要相应的改动。关于别名,咱们后面会讨论。
像 等号(=)和加号(+)修饰符同样,符号(&)也只能用于对Output操做表达式的修饰。当使用它进行修饰时,等于向GCC声明:"GCC不得 为任何Input操做表达式分配与此Output操做表达式相同的寄存器"。其缘由是&修饰符意味着被其修饰的Output操做表达式要在全部的 Input操做表达式被输入前输出。咱们看下面这个例子:
int main(int __argc, char* __argv[])
{
int __in1 = 8, __in2 = 4, __out = 3;
__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=a"(__out)
: "r" (__in1), "r" (__in2));
return 0;
}
此 例中,%0对应的就是Output操做表达式,它被指定的寄存器是%eax,整个Instruction List的第一条指令popl %0,编译后就成为popl %eax,这时%eax的内容已经被修改,随后在Instruction List后,GCC会经过movl %eax, address_of_out这条指令将%eax的内容放置到Output变量__out中。对于本例中的两个Input操做表达式而言,它们的寄存器约 束为"r",即要求GCC为其指定合适的寄存器,而后在Instruction List以前将__in1和__in2的内容放入被选出的寄存器中,若是它们中的一个选择了已经被__out指定的寄存器%eax,假如是__in1,那 么GCC在Instruction List以前会插入指令movl address_of_in1, %eax,那么随后popl %eax指令就修改了%eax的值,此时%eax中存放的已经不是Input变量__in1的值了,那么随后的movl %1, %%esi指令,将不会按照咱们的本意——即将__in1的值放入%esi中——而是将__out的值放入%esi中了。
下面就是本例的编译结果,很明显,GCC为__in2选择了和__out相同的寄存器%eax,这与咱们的初衷不符。
main:
pushl %ebp
movl %esp, %ebp
subl $12, %esp
movl $8, -4(%ebp)
movl $4, -8(%ebp)
movl $3, -12(%ebp)
movl -4(%ebp), %edx # __in1使用寄存器%edx
movl -8(%ebp), %eax # __in2使用寄存器%eax
#APP
popl %eax
movl %edx, %esi
movl %eax, %edi
#NO_APP
movl %eax, %eax
movl %eax, -12(%ebp) # __out使用寄存器%eax
movl $0, %eax
leave
ret
为 了避免这种状况,咱们必须向GCC声明这一点,要求GCC为全部的Input操做表达式指定别的寄存器,方法就是在Output操做表达式"=a" (__out)的操做约束中加入&约束,因为GCC规定等号(=)约束必须放在第一个,因此咱们写做"=&a"(__out)。
下面是咱们将&约束加入以后编译的结果:
main:
pushl %ebp
movl %esp, %ebp
subl $12, %esp
movl $8, -4(%ebp)
movl $4, -8(%ebp)
movl $3, -12(%ebp)
movl -4(%ebp), %edx #__in1使用寄存器%edx
movl -8(%ebp), %eax
movl %eax, %ecx # __in2使用寄存器%ecx
#APP
popl %eax
movl %edx, %esi
movl %ecx, %edi
#NO_APP
movl %eax, %eax
movl %eax, -12(%ebp) #__out使用寄存器%eax
movl $0, %eax
leave
ret
OK!这下好了,彻底与咱们的意图吻合。
如 果一个Output操做表达式的寄存器约束被指定为某个寄存器,只有当至少存在一个Input操做表达式的寄存器约束为可选约束时,(可选约束的意思是可 以从多个寄存器中选取一个,或使用非寄存器方式),好比"r"或"g"时,此Output操做表达式使用&修饰才有意义。若是你为全部的 Input操做表达式指定了固定的寄存器,或使用内存/当即数约束,则此Output操做表达式使用&修饰没有任何意义。好比:
__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=&a"(__out)
: "m" (__in1), "c" (__in2));
此例中的Output操做表达式彻底没有必要使用&来修饰,由于__in1和__in2都被指定了固定的寄存器,或使用了内存方式,GCC无从选择。
但若是你已经为某个Output操做表达式指定了&修饰,并指定了某个固定的寄存器,你就不能再为任何Input操做表达式指定这个寄存器,不然会出现编译错误。好比:
__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=&a"(__out)
: "a" (__in1), "c" (__in2));
本例中,因为__out已经指定了寄存器%eax,同时使用了符号&修饰,则再为__in1指定寄存器%eax就是非法的。
反过来,你也能够为Output指定可选约束,好比"r","g"等,让GCC为其选择到底使用哪一个寄存器,仍是使用内存方式,GCC在选择的时候,会首先排除掉已经被Input操做表达式使用的全部寄存器,而后在剩下的寄存器中选择,或干脆使用内存方式。好比:
__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=&r"(__out)
: "a" (__in1), "c" (__in2));
本例中,因为__out指定了约束"r",即让GCC为其决定使用哪一格寄存器,而寄存器%eax和%ecx已经被__in1和__in2使用,那么GCC在为__out选择的时候,只会在%ebx和%edx中选择。
前3 个修饰符只能用在Output操做表达式中,而百分号[%]修饰符偏偏相反,只能用在Input操做表达式中,用于向GCC声明:“当前Input操做表 达式中的C/C++表达式能够和下一个Input操做表达式中的C/C++表达式互换”。这个修饰符号通常用于符合交换律运算,好比加(+),乘(*), 与(&),或(|)等等。咱们看一个例子:
int main(int __argc, char* __argv[])
{
int __in1 = 8, __in2 = 4, __out = 3;
__asm__ ("addl %1, %0\n\t"
: "=r"(__out)
: "%r" (__in1), "0" (__in2));
return 0;
}
在 此例中,因为指令是一个加法运算,至关于等式__out = __in1 + __in2,而它与等式__out = __in2 + __in1没有什么不一样。因此使用百分号修饰,让GCC知道__in1和__in2能够互换,也就是说GCC能够自动将本例的内联汇编改变为:
__asm__ ("addl %1, %0\n\t"
: "=r"(__out)
: "%r" (__in2), "0" (__in1));
修饰符 Input/Output 意义
= O 表示此Output操做表达式是Write-Only的
+ O 表示此Output操做表达式是Read-Write的
& O 表示此Output操做表达式独占为其指定的寄存器
% I 表示此Input操做表达式中的C/C++表达式能够和下一个Input操做表达式中的C/C++表达式互换
4. 占位符
什么叫占位符?咱们看一看下面这个例子:
__asm__ ("addl %1, %0\n\t"
: "=a"(__out)
: "m" (__in1), "a" (__in2));
这 个例子中的%0和%1就是占位符。每个占位符对应一个Input/Output操做表达式。咱们在以前已经提到,GCC规定一个内联汇编语句最多能够有 10个Input/Output操做表达式,而后按照它们被列出的顺序依次赋予编号0到9。对于占位符中的数字而言,和这些编号是对应的。
因为占位符前面使用一个百分号(%),为了区别占位符和寄存器,GCC规定在带有C/C++表达式的内联汇编中,"Instruction List"中直接写出的寄存器前必须使用两个百分号(%%)。
GCC 对其进行编译的时候,会将每个占位符替换为对应的Input/Output操做表达式所指定的寄存器/内存地址/当即数。好比在上例中,占位符%0对应 Output操做表达式"=a"(__out),而"=a"(__out)指定的寄存器为%eax,因此把占位符%0替换为%eax,占位符%1对应 Input操做表达式"m"(__in1),而"m"(__in1)被指定为内存操做,因此把占位符%1替换为变量__in1的内存地址。
也许有人认为,在上面这个例子中,彻底能够不使用%0,而是直接写%%eax,就像这样:
__asm__ ("addl %1, %%eax\n\t"
: "=a"(__out)
: "m" (__in1), "a" (__in2));
和 上面使用占位符%0没有什么不一样,那么使用占位符%0就没有什么意义。确实,二者生成的代码彻底相同,但这并不意味着这种状况下占位符没有意义。由于若是 不使用占位符,那么当有一天你想把变量__out的寄存器约束由a改成b时,那么你也必须将addl指令中的%%eax改成%%ebx,也就是说你须要同 时修改两个地方,而若是你使用占位符,你只须要修改一次就够了。另外,若是你不使用占位符,将不利于代码的清晰性。在上例中,若是你使用占位符,那么你一 眼就能够得知,addl指令的第二个操做数内容最终会输出到变量__out中;不然,若是你不用占位符,而是直接将addl指令的第2个操做数写为%% eax,那么你须要考虑一下才知道它最终须要输出到变量__out中。这是占位符最粗浅的意义。毕竟在这种状况下,你彻底能够不用。
但对于这些状况来讲,不用占位符就彻底不行了:
首 先,咱们看一看上例中的第1个Input操做表达式"m"(__in1),它被GCC替换以后,表现为addl address_of_in1, %%eax,__in1的地址是什么?编译时才知道。因此咱们彻底没法直接在指令中去写出__in1的地址,这时使用占位符,交给GCC在编译时进行替 代,就能够解决这个问题。因此这种状况下,咱们必须使用占位符。
其次,若是上例中的Output操做表达式"=a"(__out)改成" =r"(__out),那么__out在究竟使用那么寄存器只有到编译时才能经过GCC来决定,既然在咱们写代码的时候,咱们不知道究竟哪一个寄存器被选 择,咱们也就不能直接在指令中写出寄存器的名称,而只能经过占位符替代来解决。
5. Clobber/Modify
有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,但愿GCC在编译时可以将这一点考虑进去。那么你就能够在Clobber/Modify域声明这些寄存器或内存。
这 种状况通常发生在一个寄存器出如今"Instruction List",但却不是由Input/Output操做表达式所指定的,也不是在一些Input/Output操做表达式使用"r","g"约束时由GCC 为其选择的,同时此寄存器被"Instruction List"中的指令修改,而这个寄存器只是供当前内联汇编临时使用的状况。好比:
__asm__ ("movl %0, %%ebx" : : "a"(__foo) : "bx");
寄存器%ebx出如今"Instruction List中",而且被movl指令修改,但却未被任何Input/Output操做表达式指定,因此你须要在Clobber/Modify域指定"bx",以让GCC知道这一点。
因 为你在Input/Output操做表达式所指定的寄存器,或当你为一些Input/Output操做表达式使用"r","g"约束,让GCC为你选择一 个寄存器时,GCC对这些寄存器是很是清楚的——它知道这些寄存器是被修改的,你根本不须要在Clobber/Modify域再声明它们。但除此以外, GCC对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。因此若是你真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify 中声明它们,让GCC针对这些寄存器作相应的处理。不然有可能会形成寄存器的不一致,从而形成程序执行错误。
在Clobber/Modify域中指定这些寄存器的方法很简单,你只须要将寄存器的名字使用双引号(" ")引发来。若是有多个寄存器须要声明,你须要在任意两个声明之间用逗号隔开。好比:
__asm__ ("movl %0, %%ebx; popl %%ecx" : : "a"(__foo) : "bx", "cx" );
这些串包括:
声明的串 表明的寄存器
"al","ax","eax" %eax
"bl","bx","ebx" %ebx
"cl","cx","ecx" %ecx
"dl","dx","edx" %edx
"si","esi" %esi
"di", "edi" %edi
由上表能够看出,你只须要使用"ax","bx","cx","dx","si","di"就能够了,由于其它的都和它们中的一个是等价的。
如 果你在一个内联汇编语句的Clobber/Modify域向GCC声明某个寄存器内容发生了改变,GCC在编译时,若是发现这个被声明的寄存器的内容在此 内联汇编语句以后还要继续使用,那么GCC会首先将此寄存器的内容保存起来,而后在此内联汇编语句的相关生成代码以后,再将其内容恢复。咱们来看两个例 子,而后对比一下它们之间的区别。
这个例子中声明了寄存器%ebx内容发生了改变:
$ cat example7.c
int main(int __argc, char* __argv[])
{
int in = 8;
__asm__ ("addl %0, %%ebx"
: /* no output */
: "a" (in) : "bx");
return 0;
}
$ gcc -O -S example7.c
$ cat example7.s
main:
pushl %ebp
movl %esp, %ebp
pushl %ebx # %ebx内容被保存
movl $8, %eax
#APP
addl %eax, %ebx
#NO_APP
movl $0, %eax
movl (%esp), %ebx # %ebx内容被恢复
leave
ret
下面这个例子的C源码与上一个例子除了没有声明%ebx寄存器发生了改变以外,其它都相同。
$ cat example8.c
int main(int __argc, char* __argv[])
{
int in = 8;
__asm__ ("addl %0, %%ebx"
: /* no output */
: "a" (in) );
return 0;
}
$ gcc -O -S example8.c
$ cat example8.s
main:
pushl %ebp
movl %esp, %ebp
movl $8, %eax
#APP
addl %eax, %ebx
#NO_APP
movl $0, %eax
popl %ebp
ret
仔细对比一下example7.s和example8.s,你就会明白在Clobber/Modify域声明一个寄存器的意义。
另 外须要注意的是,若是你在Clobber/Modify域声明了一个寄存器,那么这个寄存器将不能再被用作当前内联汇编语句的Input/Output操 做表达式的寄存器约束,若是Input/Output操做表达式的寄存器约束被指定为"r"或"g",GCC也不会选择已经被声明在 Clobber/Modify中的寄存器。好比:
__asm__ ("movl %0, %%ebx" : : "a"(__foo) : "ax", "bx");
此例中,因为Output操做表达式"a"(__foo)的寄存器约束已经指定了%eax寄存器,那么再在Clobber/Modify域中指定"ax"就是非法的。编译时,GCC会给出编译错误。
除 了寄存器的内容会被改变,内存的内容也能够被修改。若是一个内联汇编语句"Instruction List"中的指令对内存进行了修改,或者在此内联汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其Output操做表达式使用"m" 约束,这种状况下你须要使用在Clobber/Modify域使用字符串"memory"向GCC声明:“在这里,内存发生了,或可能发生了改变”。例 如:
void * memset(void * s, char c, size_t count)
{
__asm__("cld\n\t"
"rep\n\t"
"stosb"
: /* no output */
: "a" (c),"D" (s),"c" (count)
: "cx","di","memory");
return s;
}
此 例实现了标准函数库memset,其内联汇编中的stosb对内存进行了改动,而其被修改的内存地址s被指定装入%edi,没有任何Output操做表达 式使用了"m"约束,以指定内存地址s处的内容发生了改变。因此在其Clobber/Modify域使用"memory"向GCC声明:内存内容发生了变 动。
若是一个内联汇编语句的Clobber/Modify域存在"memory",那么GCC会保证在此内联汇编以前,若是某个内存的内 容被装入了寄存器,那么在这个内联汇编以后,若是须要使用这个内存处的内容,就会直接到这个内存处从新读取,而不是使用被存放在寄存器中的拷贝。由于这个 时候寄存器中的拷贝已经极可能和内存处的内容不一致了。
这只是使用"memory"时,GCC会保证作到的一点,但这并非所有。由于使用"memory"是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点。好比咱们在前面讲到的例子:
int main(int __argc, char* __argv[])
{
int* __p = (int*)__argc;
(*__p) = 9999;
__asm__("":::"memory");
if((*__p) == 9999)
return 5;
return (*__p);
}
本 例中,若是没有那条内联汇编语句,那个if语句的判断条件就彻底是一句废话。GCC在优化时会意识到这一点,而直接只生成return 5的汇编代码,而不会再生成if语句的相关代码,而不会生成return (*__p)的相关代码。但你加上了这条内联汇编语句,它除了声明内存变化以外,什么都没有作。但GCC此时就不能简单的认为它不须要判断都知道 (*__p)必定与9999相等,它只有老老实实生成这条if语句的汇编代码,一块儿相关的两个return语句相关代码。
当一个内联汇编 指令中包含影响eflags寄存器中的条件标志(也就是那些Jxx等跳转指令要参考的标志位,好比,进位标志,0标志等),那么须要在 Clobber/Modify域中使用"cc"来声明这一点。这些指令包括adc, div,popfl,btr,bts等等,另外,当包含call指令时,因为你不知道你所call的函数是否会修改条件标志,为了稳妥起见,最好也使用 "cc"。
我不多在相关资料中看到有关"cc"的确切用法,只有一份文档提到了它,但还不是i386平台的,只是说"cc"是处理器平台 相关的,并不是全部的平台都支持它,但即便在不支持它的平台上,使用它也不会形成编译错误。我作了一些实验,但发现使用"cc"和不使用"cc"所生成的代 码没有任何不一样。但Linux 2.4的相关代码中用到了它。若是谁知道在i386平台上"cc"的细节,请和我联系。
另外,还能够在 Clobber/Modify域指定数字0到9,以声明第n个Input/Output操做表达式所使用的寄存器发生了变化,但正如咱们在前面所提到的, 若是你为某个Input/Output操做表达式指定了寄存器,或使用"g","r"等约束让GCC为其选择寄存器,GCC已经知道哪一个寄存器内容发生了 变化,因此这么作没有什么意义;我也做了相关的试验,没有发现使用它会对GCC生成的汇编代码有任何影响,至少在i386平台上是这样。Linux 2.4的全部i386平台相关内联汇编代码中都没有使用这一点,但S390平台相关代码中有用到,但因为我对S390汇编没有任何概念,因此,也不知道这 么作的意义何在。