三、操作约束:Operation Constraint
操作约束只会出现在带有C/C++表达式的内联汇编语句中;
每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint;约束的类型有:寄存器约束、内存约束、立即数约束、通用约束;
操作表达式的格式:
"约束"(C/C++表达式)
即:"Constraint"(C/C++ expression)
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__ __volatile__("movl %0,%%bx"::"a"(__shrt));
由于变量__shrt是16位无符号类型m大小是两个字节,所以,编译器编译出来的汇编代码中,则会让此变量使用寄存器%ax;
无论是Input还是Output操作约束,都可以使用寄存器约束;
常用的寄存器约束的缩写:
r:I/O,表示使用一个通用寄存器,由GCC在%eax/%ax/%al、%ebx/%bx/%bl、%ecx/%cx/%cl、%edx/%dx/%dl中选取一个GCC认为是合适的;
q:I/O,表示使用一个通用寄存器,与r的意义相同;
g:I/O,表示使用寄存器或内存地址;
m:I/O,表示使用内存地址;
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,表示使用第二个浮点寄存器;
A:I/O,表示把%eax与%edx组合成一个64位的整数值;
o:I/O,表示使用一个内存位置的偏移量;
V:I/O,表示仅仅使用一个直接内存位置;
i:I/O,表示使用一个整数类型的立即数;
n:I/O,表示使用一个带有已知整数值的立即数;
F:I/O,表示使用一个浮点类型的立即数;
2.内存约束:
如果一个Input/Output操作表达式的C/C++表达式表现为一个内存地址(指针变量),不想借助于任何寄存器,则可以使用内存约束;比如:
__asm__("lidt %0":"=m"(__idt_addr));或__asm__("lidt %0"::"m"(__idt_addr));
内存约束使用约束名"m",表示的是使用系统支持的任何一种内存方式,不需要借助于寄存器;
使用内存约束方式进行输入输出时,由于不借助于寄存器,所以,GCC不会按照你的声明对其做任何的输入输出处理;GCC只会直接拿来使用,对这个C/C++表达式而言,究竟是输入还是输出,完全依赖于你写在"instruction list"中的指令对其操作的方式;所以,不管你把操作约束和操作表达式放在Input部分还是放在Output部分,GCC编译生成的汇编代码都是一样的,程序的执行结果也都是正确的;本来我们将一个操作表达式放在Input或Output部分是希望GCC能为我们自动通过寄存器将表达式的值输入或输出;既然对于内存约束类型的操作表达式来说,GCC不会为它做任何事情,那么放在哪里就无所谓了;但是从程序员的角度来看,为了增强代码的可读性,最好能够把它放在符合实际情况的地方;
3.立即数约束:
如果一个Input/Output操作表达式的C/C++表达式是一个数字常数,不想借助于任何寄存器或内存,则可以使用立即数约束;
由于立即数在C/C++表达式中只能作为右值使用,所以,对于使用立即数约束的表达式而言,只能放在Input部分;比如:
__asm__ __volatile__("movl %0,%%eax"::"i"(100));
立即数约束使用约束名"i"表示输入表达式是一个整数类型的立即数,不需要借助于任何寄存器,只能用于Input部分;使用约束名"F"表示输入表达式是一个浮点数类型的立即数,不需要借助于任何寄存器,只能用于Input部分;
4.通用约束:
约束名"g"可以用于输入和输出,表示可以使用通用寄存器、内存、立即数等任何一种处理方式;
约束名"0,1,2,3,4,5,6,7,8,9"只能用于输入,表示与第n个操作表达式使用相同的寄存器/内存;
通用约束"g"是一个非常灵活的约束,当程序员认为一个C/C++表达式在实际操作中,无论使用寄存器方式、内存方式还是立即数方式都无所谓时,或者程序员想实现一个灵活的模板,以让GCC可以根据不同的C/C++表达式生成不同的访问方式时,就可以使用通用约束g;
例如:
#define JUST_MOV(foo) __asm__("movl %0,%%eax"::"g"(foo))
则,JUST_MOV(100)和JUST_MOV(var)就会让编译器产生不同的汇编代码;
对于JUST_MOV(100)的汇编代码为:
#APP
movl $100,%eax #立即数方式;
#NO_APP
对于JUST_MOV(var)的汇编代码为:
#APP
movl 8(%ebp),%eax #内存方式;
#NO_APP
像这样的效果,就是通用约束g的作用;
5.修饰符:
等号(=)和加号(+)作为修饰符,只能用于Output部分;等号(=)表示当前输出表达式的属性为只写,加号(+)表示当前输出表达式的属性为可读可写;这两个修饰符用于约束对输出表达式的操作,它们俩被写在输出表达式的约束部分中,并且只能写在第一个字符的位置;
符号&也写在输出表达式的约束部分,用于约束寄存器的分配,但是只能写在约束部分的第二个字符的位置上;
用符号&进行修饰时,等于向GCC声明:"GCC不得为任何Input操作表达式分配与此Output操作表达式相同的寄存器"; 其原因是修饰符&意味着被其修饰的Output操作表达式要在所有的Input操作表达式被输入之前输出; 即:GCC会先使用输出值对被修饰符&修饰的Output操作表达式进行填充,然后,才对Input操作表达式进行输入; 这样的话,如果不使用修饰符&对Output操作表达式进行修饰,一旦后面的Input操作表达式使用了与Output操作表达式相同的寄存器,就会产生输入输出数据混乱的情况;相反,如果没有用修饰符&修饰输出操作表达式,那么,就意味着GCC会先把Input操作表达式的值输入到选定的寄存器中,然后经过处理,最后才用输出值填充对应的Output操作表达式;
所以,修饰符&的作用就是要求GCC编译器为所有的Input操作表达式分配别的寄存器,而不会分配与被修饰符&修饰的Output操作表达式相同的寄存器;修饰符&也写在操作约束中,即:&约束;由于GCC已经规定加号(+)或等号(=)占据约束的第一个字符,那么&约束只能占用第二个字符;
例如:
int __out, __in1, __in2;
__asm__("popl %0\n\t"
"movl %1,%%esi\n\t"
"movl %2,%%edi\n\t"
:"=&a"(__out)
:"r"(__in1),"r"(__in2));
注意:如果一个Output操作表达式的寄存器约束被指定为某个寄存器,只有当至少存在一个Input操作表达式的寄存器约束为可选约束(意思是GCC可以从多个寄存器中选取一个,或使用非寄存器方式)时,比如"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; movl %1,%%esi; movl %2,%%edi;":"=&a"(__out):"a"(__in1),"c"(__in2));
对这条语句的编译就会报错;
相反,你也可以为Output指定可选约束,比如"r"或"g"等,让GCC为此Output操作表达式选择合适的寄存器,或使用内存方式,GCC在选择的时候,会排除掉已经被Input操作表达式所使用过的所有寄存器,然后在剩下的寄存器中选择,或者干脆使用内存方式;
比如:
__asm__("popl %0; movl %1,%%esi; movl %2,%%edi;":"=&r"(__out):"a"(__in1),"c"(__in2));
这三个修饰符只能用在Output操作表达式中,而修饰符%则恰恰相反,它只能用在Input操作表达式中;
修饰符%用于向GCC声明:"当前Input操作表达式中的C/C++表达式可以与下一个Input操作表达式中的C/C++表达式互换";这个修饰符一般用于符合交换律运算的地方;比如:加、乘、按位与&、按位或|等等;
例如:
__asm__("addl %1,%0\n\t":"=r"(__out):"%r"(__in1),"0"(__in2));
其中,"0"(__in2)表示使用与第一个Input操作表达式("r"(__in1))相同的寄存器或内存;
由于使用符号%修饰__in1的寄存器方式r,那么就表示,__in1与__in2可以互换位置;加法的两个操作数交换位置之后,和不变;
修饰符 I/O 意义
= O 表示此Output操作表达式是只写的
+ O 表示此Output操作表达式是可读可写的
& O 表示此Output操作表达式独占为其指定的寄存器
% I 表示此Input操作表达式中的C/C++表达式可以与下一个Input操作表达式中的C/C++表达式互换
|