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

__asm__ __volatile__ GCC的内嵌汇编语法 AT&T汇编语言语法(二)

2018年04月27日 ⁄ 综合 ⁄ 共 10580字 ⁄ 字号 评论关闭

__asm__ __volatile__ GCC的内嵌汇编语法 AT&T汇编语言语法(二)

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));

此例中,__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的限 制。

5、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汇编没有任何概念,所以,也不知道这
么做的意义何在。

抱歉!评论已关闭.