linux gcc asm,GCC Inline ASM GCC内联汇编

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值