"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));
2、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平台上所可能使用的操作约束。
1、寄存器约束
当你当前的输入或输入需要借助一个寄存器时,你需要为其指定一个寄存器约束。你可以直接指定一个寄存器的名字,比如:
__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 表示使用第二个浮点寄存器
2、内存约束
如果一个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 表示使用系统所支持的任何一种内存方式,不需要借助寄存器
3、立即数约束
如果一个Input/Output操作表达式的C/C++表达式是一个数字常数,不想借助于任何寄存器,则可以使用立即数约束。
由于立即数在C/C++中只能作为右值,所以对于使用立即数约束的表达式而言,只能放在Input域。
比如:__asm__ __volatile__("movl %0, %%eax" : : "i" (100) );
立即数约束很简单,也很容易理解,我们在这里就不再赘述。
约束 Input/Output 意义
i I 表示输入表达式是一个立即数(整数),不需要借助任何寄存器
F I 表示输入表达式是一个立即数(浮点数),不需要借助任何寄存器
4、通用约束
约束 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));
文章出处:http://www.diybl.com/course/6_system/linux/Linuxjs/2008108/149153_2.html