版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
.3GCCInlineASM
GCC支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作GCCInline
ASM一一GCC内联汇编。这是一个特别有用的功能,有利于我们将一些C/C++语法无法
表达的指令干脆潜入C/C++代码中,另外也允许我们干脆写C/C++代码中运用汇编编写
简洁高效的代码。
1.基本内联汇编
GCC中基本的内联汇编特别易懂,我们先来看两个简洁的例子:
_asm_("movl%espz%eax");//看起来很熟识吧!
或者是
一asm—("
movl$l,%eax//SYS_exit
xor%ebx,%ebx
int$0x80
");
或
_asm_(
n
"movl$lz%eax\r\t\
"xor%ebxz%ebx\r\t"\
"int$0x80"\
);
基本内联汇编的格式是
_asm____volatile—("InstructionList");
_asm—
asm—是GCC关键字asm的宏定义:
#defineasmasm
_asm_或asm用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它
开头的,是必不行少的。
2、InstructionList
InstructionList是汇编指令序列。它可以是空的,比如:_asm____volatile—(•'");或
_asm_都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。但并
非全部InstructionList为空的内联汇编表达式都是没有意义的,比如:—asm—
("":::"memory");就特别有意义,它向GCC声明:”我对内存作了改动〃,GCC在编译
的时候,会将此因素考虑进去。
我们看一看下面这个例子:
$catexamplel.c
intmain(int_argczchar*_argv[])
int*_p=(int*)—argc;
(*_p)=9999;
//_asm_::"memory");
if((*_p)==9999)
return5;
return(*—p);
)
在这段代码中,那条内联汇编是被注释掉的。在这条内联汇编之前,内存指针_p所指向
的内存被赋值为9999,随即在内联汇编之后,一条If语句推断_p所指向的内存与9999
是否相等。很明显,它们是相等的。GCC在优化编译的时候能够很聪慧的发觉这一点。我
们运用下面的吩咐行对其进行编译:
$gcc-0-Sexamplel.c
选项-0表示优化编译,我们还可以指定优化等级,比如-02表示优化等级为2;选项5表
示将C/C++源文件编译为汇编文件,文件名和C/C++文件一样,只不过扩展名由.c变
为
我们来查看一下被放在examplel.s中的编译结果,我们这里仅仅列出了运用gcc2.96
在redhat7.3上编译后的相关函数部分汇编代码。为了保持清晰性,无关的其它代码未被
列出。
$catexamplel.s
main:
pushl%ebp
movl%espz%ebp
movl8(%ebp)z%eax#int*—p=(int*)—argc
movl$9999,(%eax)#(*—p)=9999
movl$5,%eax#return5
popl%ebp
ret
参照一下C源码和编译出的汇编代码,我们会发觉汇编代码中,没有if语句相关的代码,
而是在赋值语句(*_p)=9999后干脆return5:这是因为GCC认为在(*_p)被赋值之
后,在if语句之前没有任何变更(*_p)内容的操作,所以那条if语句的推断条件(*_p)==
9999确定是为true的,所以GCC就不再生成相关代码,而是干脆依据为true的条件生
成return5的汇编代码(GCC运用eax作为保存返回值的寄存器)。
我们现在将examplel.c中内联汇编的注释去掉,重新编译,然后看一下相关的编译结果。
$gcc-O-Sexamplel.c
$catexamplel.s
main:
pushl%ebp
movl%esp,%ebp
movl8(%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,return5
jmp,L2
.p2align2
.L3:
movl(%eax)z%eax
.L2:
popl%ebp
ret
由于内联汇编语句_asm_("“:::"memory")向GCC声明,在此内联汇编语句出现的位
置内存内容可能了变更,所以GCC在编译时就不能像刚才那样处理。这次,GCC老醇厚
实的将if语句生成了汇编代码。
可能有人会质疑:为什么要运用_asm_(""::/memory")向GCC声明内存发生了变
更?明明''InstructionList〃是空的,没有任何对内存的操作,这样做只会增加GCC生成
汇编代码的数量。
确实,那条内联汇编语句没有对内存作任何操作,事实上它的确什么都没有做。但影响内
存内容的不仅仅是你当前正在运行的程序。比如,假如你现在正在操作的内存是一块内存
映射,映射的内容是外围I/O设备寄存器。那么操作这块内存的就不仅仅是当前的程序,
I/O设备也会去操作这块内存。既然两者都会去操作同一块内存,那么任何一方在任何时
候都不能对这块内存的内容想当然。所以当你运用高级语言C/C++写这类程序的时候,你
必需让编译器也能够明白这一点,终归高级语言最终要被编译为汇编代码。
你可能已经留意到了,这次输出的汇编结果中,有两个符号:#APPGCC
将内联汇编语句中“InstructionList”所列出的指令放在#APP和#1\10_庆「「之间,由于
―asm—("“::/memory")中"InstructionList〃为空,所以#APP和#NO_APP中间也
没有任何内容。但我们以后的例子会更加清晰的表现这一点。
关于为什么内联汇编_asm_("“:"“memory”)是一条声明内存变更的语句,我们后面会
具体探讨。
刚才我们花了大量的内容来探讨“InstructionList”为空是的状况,但在实际的编程中,
"InstructionList"绝大多数状况下都不是空的。它可以有1条或随意多条汇编指令.
当在“InstructionList”中有多条指令的时候,你可以在一对引号中列出全部指令,也可以
将一条或几条指令放在一对引号中,全部指令放在多对引号中。假如是前者,你可以将每
一条指令放在一行,假如要将多条指令放在一行,则必需用分号(;)或换行符(\n,大多
数状况下\n后还要跟一个\t,其中\n是为了换行,\t是为了空出一个tab宽度的空格)
将它们分开。比如:
—asm—("movl%eaxz%ebx
sti
popl%edi
subl%ecx,%ebx");
asm("movl%eaxz%ebx;sti
popl%edi;subl%ecx,%ebx");
_asm_("movl%eax,%ebx;sti\n\tpopl%edi
subl%ecxz%ebx");
都是合法的写法。假如你将指令放在多对引号中,则除了最终一对引号之外,前面的全部引
号里的最终一条指令之后都要有一个分号(;)或(\「)或(\ri\t)。比如:
—asm—("movl%eaxz%ebx
sti\n"
"popl%edi;"
"subl%ecx,%ebx");
_asm_("movl%eaxz%ebx;sti\n\t"
"popl%edi;subl%ecxz%ebx");
_asm_("movl%eax,%ebx;sti\n\tpopl%edi\n"
"subl%ecxz%ebx");
_asm_("movl%eaxz%ebx;sti\n\tpopl%edi;""subl%ecx,%ebx");
都是合法的。
上述原则可以归结为:
随意两个指令间要么被分号(;)分开,要么被放在两行;
放在两行的方法既可以从通过\n的方法来实现,也可以真正的放在两行;
可以运用1对或多对引号,每1对引号里可以放任一多条指令,全部的指令都要被放到引
号中。
在基本内联汇编中,''InstructionList”的书写的格式和你干脆在汇编文件中写非内联汇编
没有什么不同,你可以在其中定义Label,定义对齐(.alignn),定义段(.sectionname)。
例如:
―asm—(".align2\n\t"
"movl%eax,%ebx\n\t"
"test%ebxz%ecx\n\t"
"jneerror\n\t"
"sti\n\t"
"error:popl%edi\n\t"
"subl%ecxz%ebx");
上面例子的格式是Linux内联代码常用的格式,特别整齐。也建议大家都运用这种格式来
写内联汇编代码。
3、_volatile_
volatile—是GCC关键字volatile的宏定义:
#define_volatile_volatile
—volatile—或volatile是可选的,你可以用它也可以不用它。假如你用了它,则是向
GCC声明''不要动我所写的InstructionList,我须要原封不动的保留每•条指令〃,否则
当你运用了优化选项(-0)进行编译时,GCC将会依据自己的推断确定是否将这个内联汇编
表达式中的指令优化掉。
那么GCC推断的原则是什么?我不知道(假如有哪位挚友清晰的话,请告知我)。我试验
了一下,发觉一条内联汇编语句假如是基本内联汇编的话(即只有''InstructionList",没
有Input/Output/Clobber的内联汇编,我们后面将会探讨这一点),无论你是否运用
_volatile_来修饰,GCC2.96在优化编译时,都会原封不动的保留内联汇编中的
''InstructionList、但或许我的试验的例子并不充分,所以这一点并不能够得到保证。
为了保险起见,假如你不想让GCC的优化影响你的内联汇编代码,你最好在前面都加上
_volatile_,而不要依靠于编译器的原则,因为即使你特别了解当前编译器的优化原则,
你也无法保证这种原则将来不会发生变更。而_volatHe_的含义却是恒定的。
2、带有C/C++表达式的内联汇编
GCC允许你通过C/C++表达式指定内联汇编中“InstrcuctionList”中指令的输入和输出,
你甚至可以不关切究竟运用哪个寄存器被运用,完全靠GCC来支配和指定。这一点可以让
程序员避开去考虑有限的存存器的运用,也可以提高目标代码的效率。
我们先来看几个例子:
_asm_("":::"memory");//前面提到的
_asm_("mov%%eax,%%ebx":"=b"(rv):na"(foo):"eax","ebx");
_asm____volatile_("lidt%0":"=m"(idt_descr));
_asm_("subl%2,%0\n\t”
"sbbl
:"=a"(endlow),"=d"(endhigh)
:"g"(startlow)z"g"(starthigh),"0"(endlow),"1"(endhigh));
怎么样,有点印象了吧,是不是也有点晕?没关系,下面探讨完之后你就不会再晕了。(当
然,也有可能更晕人_入)。探讨起先一一
带有C/C++表达式的内联汇编格式为:
_asm__volatile_("InstructionList":Output:Input:Clobber/Modif/);
从中我们可以看出它和基本内联汇编的不同之处在F:它多了3个部分(Input,Output,
Clobber7Modify)。在括号中的4个部分通过冒号(:)分开。
这4个部分都不是必需的,任何一个部分都可以为空,其规则为:
如果Clobber/Modify为空,则其前面的冒号(:)必需省略。比如
_asm_("mov%%eaxz%%ebx":"=b"(foo):“a"(inp):)就是非法的写法:而
―asm—("mov%%eax,%%ebx":"=b"(foo):"a"(inp))则是正确的。
假如InstructionList为空,MInput,Output,Clobber7Modify可以不为空,也可以
为空。比如_asm_"memory");^_asm_(""::);都是合法的写法。
如果Output,Input,Clobber7Modify都为空,Output,Input之前的冒号(:)既可以
省略,也可以不省略。假如都省略,则此汇编退化为一个基本内联汇编,否则,仍旧是一
个带有C/C++表达式的内联汇编,此时叫nstructionList”中的寄存器写法要遵守相关规
定,比如寄存器前必需运用两个百分号(%%),而不是像基本汇编格式一样在寄存器前只
运用一个百分号(%)„比如_asm_("mov%%eax,%%ebx"::);_asm_("
mov%%eax,%%ebx":)和—asm—("mov%eax,%ebx")都是正确的写法,而
_asm_("mov%eax,%ebx"::):_asm_("mov%eaxz%ebx":)和
—asm—("mov%%eax,%%ebx")都是错误的写法。
假如Input,Clobber/Modify为空,但Output不为空,Input前的冒号⑴既可以省略,
也可以不省略。比如—asm—("mov%%eax,%%ebx":"=b"(foo):);
_asm_("mov%%eaxz%%ebx":"=b"(foo))都是正确的。
假如后面的部分不为空,而前面的部分为空,则前面的冒号C)都必需保留,否则无法说明
不为空的部分原委是第几部分。比如,Clobber/Modify,Output为空,而Input不为空,
则Clobber/Modify前的冒号必需省略(前面的规则),而Output前的冒号必需为俣留。
假如Clobber/Modify不为空,而Input和Output都为空,则Input和Output前的冒
号都必需保留。比如_asm_("mov%%eaxz%%ebx"::"a"(foo))和
―asm—("mov%%eaxz%%ebx":::"ebx"),
从上面的规则可以看到另外一个事实,区分一个内联汇编是基本格式的还是带有C/C++表
达式格式的,其规则在于在“InstructionList”后是否有冒号(:)的存在,假如没有则是基本
格式的,否则,则是带有C/C++表达式格式的。
两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能运用一个百分号(%),这一
点和非内联汇编相同;而带有C/C++表达式格式则要求寄存器前必需运用两个百分号
(%%)>其缘由我们会在后面探讨。
1.Output
Output用来指定当前内联汇编语句的输出。我们看一看这个例子:
_asm_("movl%%crOz%0":"=a"(crO));
这个内联汇编语句的输出部分为“二r"(crO),它是一个''操作表达式“,指定了一个输出操
作。我们可以很清晰得看到这个输出操作由两部分组成:括号括住的部分(crO)和引号引住
的部分”二a"。这两部分都是每一个输出操作必不行少的。括号括住的部分是一个C/C++
表达式,用来保存内联汇编的一个输出值,其操作就等于C/C++的相等赋值crO=
output_value,因此,括号中的输出表达式只能是C/C++的左值表达式,也就是说它只
能是一个可以合法的放在C/C++赋值操作中等号(=)左边的表达式。那么右值
output_value从何而来呢?
答案是引号中的内容,被称作''操作约束〃(OperationConstraint),在这个例子中操作约
束为“二a",它包含两个约束:等号(=)和字母a,其中等号(=)说明括号中左值表达式c「0
是一个Write-Only的,只能够被作为当前内联汇编的输入,而不能作为输入。而字母a
是寄存器EAX/AX/AL的简写,说明crO的值要从eax寄存器中获得,也就是说crO二
eax,最终这一点被转化成汇编指令就是movl%eaxzaddress_of_crOo现在你应当清
晰了吧,操作约束中会给出:究竟从哪个寄存器传递值给crO.
另外,须要特殊说明的是,许多文档都声明,全部输出操作的操作约束必需包含一个等号
(=),但GCC的文档中却很清晰的声明,并非如此。因为等号(二)约束说明当前的表达式
是一个Write-Only的,但另外还有一个符号一一加号(+)用来说明当前表达式是一个
Read-Write的,假如一个操作约束中没有给出这两个符号中的任何一个,则说明当前表
达式是Read-Only的。因为对于输出操作来说,确定是必需是可写的,而等号(二)和加号
(十)都表示可写,只不过加号(+)同时也表示是可读的。所以对于一个输出操作来说,其操
作约束只须要有等号(二)或加号(+)中的随意一个就可以了。
二者的区分是:等号(二)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则表
示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。但无论是等号(二)约束还是
加号(+)约束所约束的操作表达式都只能放在Output域中,而不能被用在Input域中。
另外,有些文档声明:尽管GCC文档中供应了加号(+)约束,但在实际的编译中通不过;
我不知道老版本会怎么样,我在GCC2.96中对加号(十)约束的运用特别正常。
我们通过一个例子看一下,在一个输出操作中运用等号(二)约束和加号(+)约束的不同。
$catexample2.c
intmain(int_argc,char*_argv[])
{
intcrO=5;
_asm____volatile_("movl%%crOz%0":"=a"(crO));
return0;
)
$gcc-Sexample2.c
$catexample2.s
main:
pushl%ebp
movl%espz%ebp
subl$4,%esp
movl$5,-4(%ebp)#crO=5
#APP
movl%crO,%eax
#NO_APP
movl%eax,%eax
movl%eaxz-4(%ebp;#crO=%eax
movl$0,%eax
leave
ret
这个例子是运用等号(=)约束的状况,变量crO被放在内存-4(%ebp)的位置,所以指令
mov%eax,-4(%ebp)却表示将%砥乂的内容输出到变量crO中。
下面是运用加号(+)约束的状况:
$catexamples.c
intmain(int_argc,char*_argv[])
{
intcrO=5;
_asm____volatile_("movl%%crOz%0":"+a"(crO));
return0;
)
$gcc-Sexamples.c
$catexample3.s
main:
pushl%ebp
movl%espz%ebp
subl$4,%esp
movl$5,-4(%ebp)#crO=5
movl-4(%ebp)z%eax#input(%eax=crO)
#APP
movl%crO,%eax
#NO_APP
movl%eaxz-4(%ebp;#output(crO=%eax)
movl$0,%eax
leave
ret
从编译的结果可以看出,当运川加号(+)约束的时候,c「0不仅作为输出,还作为输入,所
运用寄存器都是寄存器约束(字母a,表示运用eax寄存器)指定的。关于寄存器约束我们后
面探讨。
在Output域中可以有多个输出操作表达式,多个操作表达式中间必需用逗号(,)分开。例
如:
—asm—(
"movl%%eax,%0\n\t"
"pushl%%ebx\n\t"
"popl%1\n\t"
"movl%1,%2"
:"+a"(crO)z"=b"(crl)z"=c"(cr2));
2、Input
Input域的内容用来指定当前内联汇编语句的输入。我们看一看这个例子:
_asm_("movl%0z%%db7"::"a"(cpu->db7));
例中Input域的内容为一个表达式“a”[cpu->db7),被称作''输入表达式〃,用来表示一个
对当前内联汇编的输入。
像输出表达式一样,一个输入表达式也分为两部分:带括号的部分(cpu->db7)和带引号的
部分“a“。这两部分对于一个内联汇编输入表达式来说也是必不行少的。
括号中的表达式cpu->db7是一个C/C++语言的表达式,它不必是一个左值表达式,也
就是说它不仅可以是放在C/C++赋值操作左边的表达式,还可以是放在C/C++赋值操作
右边的表达式。所以它可以是一个变量,一个数字,还可以是一个困难的表达式(比如
a+b/c*d)o比如上例可以改为:_asm_("movl%0,%%db7"::"a"(foo)),
_asm_("movl%0,%%db7"::"a"(0x1000))或
_asm_("movl%0z%%db7"::"a"(va*vb/vc))o
引号号中的部分是约束部分,和输出表达式约束不同的是,它不允许指定加号(十)约束和
等号(=)约束,也就是说它只能是默认的Read-Only的.约束中必需指定一个寄存器约束,
例中的字母a表示当前输入变量cpu->db7要通过寄存器eax输入到当前内联汇编中。
我们看一个例子:
$catexample4.c
intmain(int_argczchar*_argv[])
{
intcrO=5;
n
_asm____volatile_(movl%OZ%%crO"::"a"(crO));
return0;
?
$gcc-Sexample4.c
$catexample4.s
main:
pushl%ebp
movl%espz%ebp
subl$4,%esp
movl$5,-4(%ebp)#crO=5
movl-4(%ebp)z%eax#%eax=crO
#APP
movl%eax,%crO
#NO_APP
movl$0,%eax
leave
ret
我们从编译出的汇编代码可以看到,在“InstructionList”之前,GCC依据我们的输入约束
"a",将变量crO的内容装入了eax寄存器。
3.OperationConstraint
每一个Input和Output表达式都必需指定自己的操作约束OperationConstraint,我们
这里来探讨在80386平台上所可能运用的操作约束。
1、寄存器约束
当你当前的输入或输入须要借助一个寄存器时,你须要为其指定一个寄存器约束。你可以干
脆指定一个寄存器的名字,比如:
_asm____volatile—("movl%0,%%crO"::"eax"(crO));
也可以指定一个缩写,比如:
asm____volatile_("movl%0,%%crO"::"a"(crO));
假如你指定一个缩写,比如字母a,则GCC将会依据当前操作表达式中C/C++表达式的
宽度确定运用%eax,还是%比如:
unsignedshort_shrt;
_asm_("mov%0,%%bx"::"a"(_shrt));
由「变量_shrt是16-bitshort类型,则编译出来的汇编代码中,则会让此变量运用%ex
寄存器。编译结果为:
movw-2(%ebp),%ax#%ax=_shrt
#APP
movl%axz%bx
#NO_APP
无论是Input,还是Output操作表达式约束,都可以运用寄存器约束。
下表中列出了常用的寄存器约束的缩写。
约束Input/Output意义
rIz0表示运用一个通用寄存器,由GCC
®%eax/%ax/%al,%ebx/%bx/%blz%ecx/%cx/%cl,%edx/%dx/%dl中选取一
个GCC认为合适的。
q1,0表示运用一个通用寄存器,和r的意义相同。
a1,0表示运用%eax/%ax/%al
b1,0表示运用%65乂/%bx/%bl
clz0表示运用%ecx/%cx/%cl
d1,0表示运用%6(^/%dx/%dl
D1,0表示运用%65/%di
S1,0表示运用%051/%si
fLO表示运用浮点寄存器
tIz0表示运用第一个浮点寄存器
u1,0表示运用其次个浮点寄存器
2、内存约束
假如一个Input/Output操作表达式的C/C++表达式表现为一个内存地址,不想借助于任
何寄存器,则可以运用内存约束。比如:
_asm_("lidt%0""=m"(_idt_addr));或_asm
("lidt%0"::"m"(_idt_addr));
我们看一下它们分别被放在一个C源文件中,然后被GCC编译后的结果:
$catexamples.c
//本例中,变量sh被作为一个内存输入
intmain(int_argczchar*_argv[])
{
char*sh=(char*)&—argc;
_asm____volatile—("lidt%0"::"m"(sh));
return0;
?
$gcc-Sexamples.c
$catexamples.s
main:
pushl%ebp
movl%espz%ebp
subl$4,%esp
leal8(%ebp),%eax
movl%eax,-4(%ebpJ#sh=(char*)&—argc
#APP
lidt-4(%ebp)
#NO_APP
movl$0,%eax
leave
ret
$catexamples.c
//本例中,变量sh被作为一个内存输出
intmain(int_argczchar*_argv[])
{
char*sh=(char*)&._argc;
_asm____volatile_("lidt%0":"=m"(sh));
return0;
?
$gcc-Sexamples.c
$catexample6.s
main:
pushl%ebp
movl%espz%ebp
subl$4Z%esp
leal8(%ebp)z%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++表达式而言是输入
还是输出,完全依靠与你写在“InstructionList”中的指令对其操作的指令。
由于上例中,对其操作的指令为lidt,lidt指令的操作数是一个输入型的操作数,所以事
实上对变量sh的操作是一个输入操作,即使你把它放在Output域也不会变更这一点。所
以,对此例而言,完全符合语意的写法应当是将sh放在Input域,尽管放在Output域也
会有正确的执行结果。
所以,对于内存约束类型的操作表达式而言,放在Input域还是放在Output域,对编译
结果是没有任何影响的,可为原来我们将一个操作表达式放在Input域或放在Output域
是希望GCC能为我们自动通过寄存器将表达式的值输入或输出。既然对于内存约束类型的
操作表达式来说,GCC不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序
员的角度而言,为了增加代码的可读性,最好能够把它放在符合实际状况的地方。
约束Input/Output意义
m1,0表示运用系统所支持的任何一种内存方式,不须要借助寄存器
3、马上数约束
假如•个Input/Output操作表达式的C/C++表达式是一个数字常数,不想借助于任何寄
存器,则可以运用马上数约束。
由于马上数在C/C++中只能作为右值,所以对于运用马上数约束的表达式而言,只能放在
Input域。
比如:_asm____volatile_("movl%Of%%eax"::"i"(100));
马上数约束很简洁,也很简洁理解,我们在这里就不再赘述。
约束Input/Output意义
iI表示输入表达式是一个马上数(整数),不须要借助任何寄存器
FI表示输入表达式是一个马上数(浮点数),不须要借助任何寄存器
4、通用约束
约束Input/Output意义
g1,0表示可以运用通用寄存器,内存,马上数等任何一种处理方式。
0,1,2,3,4,5,6,7,8,9I表示和第n个操作表达式运用相同的寄存器/内存。
通用约束g是一个特别敏捷的约束,当程序员认为一个C/C++表达式在实际的操作中,
原委运用寄存器方式,,还是运用内存方式或马上数方式并无所谓时,或者程序员想实现一
个敏捷的模板,让GCC可以依据不同的C/C++表达式生成不同的访问方式时,就可以运
用通用约束g。比如:
#defineJUST_MOV(foo)_asm_("movl%0,%%eax"::"g"(foo))
JUST_MOV(100)和JUST_MOV(var)则会让编译器产生不同的代码。
intmain(int_argczchar*_argv[])
{
JUST_MOV(100);
return0;
?
编译后生成的代码为:
main:
pushl%ebp
movl%espz%ebp
#APP
movl$100,%eax
#NO_APP
movl$0,%eax
popl%ebp
ret
很明显这是马上数方式。而下一个例子:
intmain(int_argc,char*_argv[])
{
JUST_MOV(_argc);
return0;
}
经编译后生成的代码为:
main:
pushl%ebp
movl%espz%ebp
#APP
movl8(%ebp)z%eax
#NO_APP
movl$0,%eax
popl%ebp
ret
这个例子是运用内存方式,
一个带有C/C十十表达式£勺内联汇编,其操作表达式被依据被列出的依次编号,第一个是0,
第2个是1,依次类推,GCC最多允许有10个操作表达式。比如:
—asm—("popl%0\n\t"
"movl%lz%%esi\n\t"
"movl%2,%%edi\n\t"
:"=a"(_out)
:"r"(_inl)z"r"(_in2));
此例中,_out所在的Output操作表达式被编号为0,T'(_inl)被编号为l,"r"(_in2)
被编号为2。
再如:
n
_asm_("movl%%eaxz%%ebx"::a"(_inl)z"b"(_in2));
此例中,七”(_inl)被编号为O”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、ModifierCharacters(修饰符)
等号(二)和加号(+)用于对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%lz%%eax;addl%%eaxz%0":"=b"(foo):"b"(foo))
但假如运用后一种写法,"InstructionList”中的别名也要相应的改动。关于别名,我们后
面会探讨。
像等号(二)和加号(+)修饰符一样,符号(&)也只能用于对Output操作表达式的修饰。当
运用它进行修饰时,等于向GCC声明:"GCC不得为任何Input操作表达式安排与此
Output操作表达式相同的寄存器”。其缘由是&修饰符意味着被其修饰的Output操作表达
式要在全部的Input操作表达式被输入前输出。我们看下面这个例子:
intmain(int_argczchar*_argv[])
{
intini=8,in2=4,out=3:
―asm—("popl%0\n\t"
"movl%lz%%esi\n\t"
"movl%2Z%%edi\n\t"
:"=a"(_out)
:"r"(_inl),"r"(_in2));
return0;
)
此例中,%0对应的就是Output操作表达式,它被指定的寄存器是%eax,整个
InstructionList的第一条指令popl%0,编译后就成为popl%eax,这时%eax的内容
已经被修改,随后在InstructionList后,GCC会通过movl%eax,address_of_out
这条指令将%eax的内容放置到Output变量—out中。对于本例中的两个Input操作表
达式而言,它们的寄存器约束为“r",即要求GCC为其指定合适的寄存器,然后在
InstructionList之前将—ini和—in2的内容放入被选出的寄存器中,假如它们中的一个
选择了已经被_out指定的寄存器%eax,假如是_inl,那么GCC在InstructionList
之前会插入指令movladdress_of_inlz%eax,那么随后popl%eax指令就修改
了%63乂的值,此时%eax中存放的已经不是Input变量_ini的值了,那么随后的
movl%1,%%esi指令,将不会依据我们的本意一一即将_inl的值放入%6S1中一一而
是将—OUt的值放入%651中了。
下面就是本例的编译结果,很明显,GCC为_Jn2选择了和_out相同的寄存器%eax,
这与我们的初衷不符。
main:
pushl%ebp
movl%espz%ebp
subl$12z%esp
movl$8,-4(%ebp)
movl$4,-8(%ebp)
movl$3,-12(%ebp)
movl-4(%ebp)z%edx#—ini运用寄存器%edx
movl-8(%ebp)z%eax#_in2运用寄存器%eax
#APP
popl%eax
movl%edxz%esi
movl%eax,%edi
#NO_APP
movl%eaxz%eax
movl%eax,-12(%ebp)#—out运用寄存器%eax
movl$0,%eax
leave
ret
为了避开这种状况,我们必需向GCC声明这一点,要求GCC为全部的Input操作表达式
指定别的寄存器,方法就是在Output操作表达式(_out)的操作约束中加入&约束,
由于GCC规定等号(=)约束必需放在第一个,所以我们写作”=&a”(__out)。
下面是我们将&约束加入之后编译的结果:
main:
pushl%ebp
movl%espz%ebp
subl$12,%esp
movl$8,-4(%ebp)
movl$4,-8(%ebp)
movl$3,-12(%ebp)
movl-4(%ebp)z%edx#_ini运用寄存器%edx
movl-8(%ebp)z%eax
movl%eax,%ecx#_in2运用寄存器%ecx
#APP
popl%eax
movl%edx,%esi
movl%ecxz%edi
#NO_APP
movl%eaxz%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%lz%%esi\n\t"
"movl°/o2,%%edi\n\t"
:"=&a"(_out)
:"m"(_inl)z"c"(_in2));
此例中的Output操作表达式完全没有必要运用&来修饰,因为_inl和_in2都被指定了
固定的寄存器,或运用了内存方式,GCC无从选择。
但假如你已经为某个Output操作表达式指定了&修饰,并指定了某个固定的寄存器,你就
不能再为任何Input操作表达式指定这个寄存器,否则会出现编译错误。比如:
―asm—("popl%0\n\t"
"movl%lz%%esi\n\t"
"movl%2Z%%edi\n\t"
:"=&a"(_out)
:"a"(_ini),"cn(_in2));
本例中,由于_out已经指定了寄存器%eax,同时运用了符号&修饰,则再为_inl指定
寄存器%eax就是非法的。
反过来,你也可以为Output指定可选约束,比如”等,让GCC为其选择究竟运用哪
个寄存器,还是运用内存方式,GCC在选择的时候,会首先解除掉已经被Input操作表达
式运用的全部寄存器,然后在剩下的寄存器中选择,或干脆运用内存方式。比如:
―asm—("popl%0\n\t"
"movl%1,%%esi\n\t"
"movl%2Z%%edi\n\t"
:"=&r"(_out)
:"a"(_ini),"c-(_in2));
本例中,由于_out指定了约束“r",即让GCC为其确定运用哪一格寄存器,而寄存器%eax
和%ecx已经被_inl和_in2运用,那么GCC在为_out选择的时候,只会在%ebx
和%edx中选择。
前3个修饰符只能用在Output操作表达式中,而百分号[%]修饰符恰恰相反,只能用在
Input操作表达式中,用于向GCC声明:''当前Input操作表达式中的C/C++表达式可
以和下一个Input操作表达式中的C/C++表达式互换“。这个修饰符号一般用于符合交换
律运算,比如加(+),乘(*),与(&),或(|)等等。我们看一个例子:
intmain(int_argc,char*_argv[])
{
int_ini=8,_in2=4,_out=3;
_asm_("addl%1,%O\n\t"
:"=r"(_out)
:"%r"(_inl)z"0"(_in2));
return0;
}
在此例中,由于指令是一个加法运算,相当于等式_out=_inl+_in2,而它与等
式_out=_in2+_inl没有什么不同。所以运用百分号修饰,让GCC知道_inl和
_in2可以互换,也就是说GCC可以自动将本例的内联汇编变更为:
―asm—("addl%1,%0\n\t"
:"=r"(_out)
:"%r"(_in2)z"0"(_inl));
修饰符Input/Output意义
=0表示此Output操作表达式是Write-Only的
+0表示此Output操作表达式是Read-Write的
&0表示此Output操作表达式独占为其指定的寄存器
%I表示此Input操作表达式中的C/C++表达式可以和下一个Input操作表达式中的
C/C++表达式互换
4.占位符
什么叫占位符?我们看一看下面这个例子:
_asm_("addl%1,%0\n\t"
:"=a"(_out)
:"m"(_inl)z"a"(_in2));
这个例子中的%)0和%1就是占位符。每一个占位符对应一个Input/Output操作表达式。
我们在之前已经提到,GCC规定一个内联汇编语句最多可以有10个Input/Output操作
表达式,然后依据它们被列出的依次依次给予编号。到9。对于占位符中的数字而言,和这
些编号是对应的。
由于占位符前面运用一个百分号(%),为了区分占位符和寄存器,GCC规定在带有C/C++
表达式的内联汇编中,"InstructionList”中干脆写相的寄存器前必需运用两个百分号
(%%)«
GCC对其进行编译的时候,会将每一个占位符替换为对应的Input/Output操作表达式所
指定的寄存器/内存地址/马上数。比如在上例中,占位符%0对应Output操作表达式
"=a"(_out),而”=a”(_oiJt)指定的寄存器的%eax,所以把占位符%0替换为%eax,
占位符%1对应Input操作表达式而”E”(_inl)被指定为内存操作,所以
把占位符%1替换为变量_inl的内存地址。
或许有人认为,在上面这个例子中,完全可以不运用%0,而是干脆写%%eax,就像这样:
―asm—("addl%1,%%eax\n\t"
:"=a"(_out)
:"m"(—ini),"a"(_in2));
和上面运用占位符%0没有什么不同,那么运用占位符%。就没有什么意义。的确,两者
生成的代码完全相同,但这并不意味着这种状况下占位符没有意义。因为假如不运用占位
符,那么当有一天你想把变量_out的寄存器约束由a改为b时,那么你也必需将addl
指令中的%%eax改为%%ebx,也就是说你须要同时修改两个地方,而假如你运用占位
符,你只须要修改一次就够了。另外,假如你不运用占位符,将不利于代码的清晰性。在上
例中,假如你运用占位符,那么你一眼就可以得知,addl指令的其次个操作数内容最终会
输出到变量_out中:否则,假如你不用占位符,而是二脆将addl指令的第2个操作数写
为%%eax,那么你须要考虑一下才知道它最终须要输出到变量_out中。这是占位符最
粗浅的意义。终归在这种状况下,你完全可以不用。
但对于这些状况来说,不用占位符就完全不行了:
首先,我们看一看上例中的第1个Input操作表达式“m”(_ini),它被GCC替换之后,
表现为addladdress_of_inlz%%eax,_ini的地址是什么?编译时才知道。所以我
们完全无法干脆在指令中去写出_inl的地址,这时运用占位符,交给GCC在编译时进行
替代,就可以解决这个问题。所以这种状况下,我们必需运用占位符。
其次,假如上例中的Output操作表达式a”(__out)改为“=r”(_out),那么_out在
原委运用那么寄存器只有到编译时才能通过GCC来确定,既然在我们写代码的时候,我们
不知道原委哪个寄存器被选择,我们也就不能干脆在指令中写出寄存器的名称,而只能通
过占位符替代来解决。
5.Clobber/Modify
有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望GCC
在编译时能够将这一点考虑进去。那么你就可以在Clobber/Modify域声明这些寄存器或
内存。
这种状况一般发生在一个寄存器出现在"InstructionList",但却不是由Input/Output
操作表达式所指定的,也不是在一些Input/Output操作表达式运用“约束时由GCC
为其选择的,同时此寄存器被“InstructionList”中的指令修改,而这个寄存器只是供当前
内联汇编临时运用的状况,比如:
_asm_("movl%0,%%ebx"::"a"(—foo):"bx");
寄存器%)ebx出现在"InstructionList中“,并且被movl指令修改,但却未被任何
Input/Output操作表达式指定,所以你须要在Clobber7Modify域指定“bx”,以让GCC
知道这一点。
因为你在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操作
表达式运用”约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是特别清晰
的一一它知道这些寄存器是被修改的,你根本不须要在Clobber/Modify域再声明它们。
但除此之外,GCC对剩下的寄存器中哪些会被当前的内联汇编修改一窍不通。所以假如你
真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify中声明它们,让
GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一样,从而造成程序执
行错误。
在Clobber/Modify域中指定这些寄存器的方法很简洁,你只须要将寄存器的名字运用双
引号("")引起来。假如有多个寄存器须要声明,你须要在随意两个声明之间用逗号隔开。
比如:
_asm_("movl%0z%%ebx;popl%%ecx"::"a"(_foo):"bx","ex");
这些串包括:
声明的串代表的寄存器
"al'Y'ax'Y'eax"%eax
"bl"z"bx"z"ebx"%ebx
"cr'/'cx'Y'ecx"%ecx
"dl"z"dx"z"edx"%edx
"si'7'esi"%esi
"di","edi"%edi
由上表可以看出,你只须要运用"》”「七X”,要乂”,”<^”,叼",“附'就可以了,因为其它的都和
它们中的一个是等价的。
如果你在一个内联汇编语句的Clobber/Modify域向GCC声明某个寄存器内容发生了变
更,GCC在编译时,假如发觉这个被声明的寄存器的内容在此内联汇编语句之后还要接着
运用,那么GCC会首先将此寄存器的内容保存起来,然后在此内联汇编语句的相关生成代
码之后,再将其内容复原,我们来看两个例子,然后对比一下它们之间的区分。
这个例子中声明白寄存器%ebx内容发生了变更:
$catexample7.c
intmain(int_argczchar*_argv[])
{
intin=
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 生物标志物AI辅助发现的监管框架
- 生物打印技术在肝脏移植中的替代方案探索
- 银行金融行业岗位技能测评题库与答案解析
- 生存质量评估工具
- 生物制药研发员面试专业知识测试
- 证券从业资格考试科目重点突破与模拟测试含答案
- 建筑预算员工作手册及考核题目
- 年产xxx塑料水表项目可行性分析报告
- 预约员岗位面试题库含答案
- 程序员求职宝典常见面试题库与答题策略
- 【MOOC】电子线路设计、测试与实验(二)-华中科技大学 中国大学慕课MOOC答案
- DB3301∕T 65.1-2024 反恐怖防范系统管理规范 第1部分:通则
- 外贸企业国际市场开拓方案
- DL∕T 5210.6-2019 电力建设施工质量验收规程 第6部分:调整试验
- 高中物理学业水平测试常用公式及知识点
- 肝胆科学科发展规划
- 2024年保安员资格考试初级理论知识试题库及答案(共290题)
- 心脑血管疾病的健康管理
- 2024年浙江省大学生物理竞赛
- 普通诊所污水、污物、粪便处理方案 及周边环境情况说明
- 国开02150-计算机网络(本)机考复习资料
评论
0/150
提交评论