AT&T 汇编 (AT&T ASM) 参考

 

AT&TASM

 

 发一个OS尽管绝大部分代码只需要用C/C++等高级语言就可以了但至少和硬件相关部分的代码需要使用汇编语言另外由于启动部分的代码有大小限制使用精练的汇编可以缩小目标代码的尺寸另外对于某些需要被经常调用的代码使用汇编可以提高性

 

能。所以我们必须了解汇编语言,即使你有可能并不喜欢它。

 

如果你是计算机专业的话,在大学里你应该学习过Intel格式的8086/80386汇编,这里就不再讨论。如果我们选择的OS发工具GCC以及GAS话,就必须了解AT&T 汇编语言语法,因为GCC/GAS只支持这种汇编语法。

 

书不会去讨论8086/80386汇编编程,这类的书籍很多,你可以参考它们。这里只讨论AT&T汇编语法,以及GCC的内嵌汇编语法。

 

 

1. Syntax

 

 

 

Register Reference

 

 

 

     引用寄存器要在寄存器号前加百分号%,“movl %eax, %ebx

 

     80386有如下寄存器:

 

     832-bit寄存 %eax%ebx%ecx%edx%edi%esi%ebp%esp

 

     816-bit寄存器,们事实上是上832-bit寄存器的16位:%ax%bx

 

%cx%dx%di%si%bp%sp

 

     88-bit寄存%ah%al%bh%bl%ch%cl%dh%dl。它们事实上是寄存器%ax%bx%cx%dx 的高8位和低8位;

 

     6个段寄存器:%cs(code)%ds(data)%ss(stack),%es%fs%gs

 

     3个控制寄存器:%cr0%cr2%cr3

 

     6debug寄存器:%db0%db1%db2%db3%db6%db7

 

     2测试寄存器:%tr6%tr7

 

     个浮点寄存器%st(0)%st(1)%st(2)%st(3)%st(4)%st(5)%st(6)

 

%st(7)

 

 

 

Operator Sequence

 

 

 

操作数排列是从源(左)到目的(右,如“movl%eax(源),%ebx(目的)

 

 

ImmediatelyOperator

 

 

 

使用立即数,要在数前面加符号$,  “movl$0x04,%ebx”

 

或者:

 

 

para=0x04

 

movl$para,%ebx

 

指令执行的结果是将立即数04h装入寄存器ebx

 

 

SymbolConstant

 

符号常数直接引用如value:.long0x12a3f2demovlvalue,%ebx

 

指令执行的结果是将常数0x12a3f2de装入寄存器ebx

 

引用符号地址在符号前加符号$,  “movl$value,%ebx则是将符号value的地址装入寄存器ebx

 

 

Length of Operator

 

 

 

操作数的长度用加在指令后的符号表b(byte 8-bit), w(word, 16-bits), l(long,

 

32-bits),如“movb %al, %bl“movw%ax,%bx“movl%eax,%ebx

 

如果没有指定操作数长度的话编译器将按照目标操作数的长度来设置比如指令“mov

 

%ax,%bx由于目标操作数bx长度为word那么编译器将把此指令等同于“movw %ax,

 

%bx。同样道理,指令“mov$4,%ebx等同于指令“movl$4,%ebx“push%al等同于

“pushb%al对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push$4

 

 

 

SignandZeroExtension

 

 

绝大多数面向80386AT&T汇编指令与Intel格式的汇编指令都是相同的符号扩展指令和零扩展指令则是仅有的不同格式指令。

 

符号扩展指令和零扩展指令需要指定源操作数长度和目的操作数长度即使在某些指令中这些操作数是隐含的。

 

AT&T语法中,符号扩展和零扩展指令的格式,基本部分"movs""movz"对应Intel语法movsxmovzx后面跟上源操作数长度和目的操作数长度movsbl意味着movsfrombytetolongmovbw意味着movsfrombytetowordmovswl 意味着movs fromword  tolongmovz指令也一比如指令“movsbl %al,

 

%edx意味着将al寄存器的内容进行符号扩展后放置到edx寄存器中。

 

其它的Intel格式的符号扩展指令还有:

 

     cbw--sign-extendbytein%altowordin%ax

 

     cwde--sign-extendwordin%axtolongin%eax

 

     cwd--sign-extendwordin%axtolongin%dx:%ax

 

     cdq--sign-extenddwordin%eaxtoquadin%edx:%eax

 

对应的AT&T语法的指令为cbtwcwtlcwtdcltd

 

 

CallandJump

 

 

段内调用和跳转指令"call""ret""jmp"间调用和跳转指令"lcall""lret"

 

"ljmp"

 

间调用和跳转指令的格式为“lcall/ljmp$SECTION,$OFFSET,而段间返回指令则“lret$STACK-ADJUST

 

 

Prefix

 

 

 

操作码前缀被用在下列的情况:

     字符串重复操作指令(rep,repne)     指定被操作的段(cs,ds,ss,es,fs,gs)      进行总线加锁(lock)

 

     指定地址和操作的大小(data16,addr16)

 

AT&T汇编语法中操作码前缀通常被单独放在一行后面不跟任何操作数例如,对于重复scas指令,其写法为:

 

repne

 

  scas

 

上述操作码前缀的意义和用法如下:

 

     指定被操作的段前cs,ds,ss,es,fs,gs。在AT&T  语法中,只需要按section:memory-operan的格式就指定了相应的段前缀。如:lcall%cs:realmode_swtch

 

     操作数/地址大小前缀是“data16"addr16"们被用来在32-bit操作数/地址码中指定16-bit的操作数/地址。

 

     总线加锁前缀“lock它是为了在多处理器环境中保证在当前指令执行期间禁止一切中断。这个前缀仅仅对ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG指令有效,如果将Lock缀用在其它指令之前,将会引起异常。

 

     字符串重复操作前"rep","repe","repne"用来让字符串操作重复“%ecx次。

 

 

Memory Reference

 

 

 

Intel语法的间接内存引用的格式为:

 

section:[base+index*scale+displacement]

 

而在AT&T语法中对应的形式为:

 

 

section:displacement(base,index,scale)

 

其中baseindex是任意的32-bitbaseindex寄存器scale可以取1248如果不指定scale则默认值为1section可以指定任意的段寄存器作为段前缀默认的段寄存器在不同的情况下不一样如果你在指令中指定了默认的段前缀则编译器在目标代码中不会产生此段前缀代码。

 

 

下面是一些例子:

 

-4(%ebp)base=%ebpdisplacement=-4section没有指定,由于base%ebp,所以默认的section=%ssindex,scale没有指定index0

 

foo(,%eax,4)index=%eaxscale=4displacement=foo其它域没有指定。这里默认section=%ds

 

foo(,1)这个表达式引用的是指foo指向的地址所存放的值。注意这个表达式中没有

 

 

baseindex,并且只有一个逗号,这是一种异常语法,但却合法。

 

%gs:foo这个表达式引用的是放置于%gs段里变量foo值。

 

如果calljump操作在操作数前指定前“*则表示是一个绝对地址调用/就是jmp/call指令指定的是一个绝对地址如果没有指定"*"则操作数是一个相对地址。

 

任何指令如果其操作数是一个内存操作,则指令必须指定它的操作尺

 

(byte,word,long,也就是说必须带有指令后(b,w,l)

 

 

2. GCC Inline ASM

 

 

GC 支持C/C++码中嵌入汇编代码,这些汇编代码被称GCC Inline ASM— — GCC  联汇编。这是一个非常有用的功能,有利于我们将一些C/C++语法无法达的指令直接潜入C/C++码中另外也允许我们直接写C/C++码中使用汇编编写简洁高效的代码。

 

 

2.1 EssentialInlineASM

 

 

 

GCC中基本的内联汇编非常易懂,我们先来看两个简单的例子:

 

__asm__("movl%esp,%eax"); //看起来很熟悉吧!

 

或者是

 

 

__asm__("

movl$1,%eax   //SYS_exit xor%ebx,%ebx

 

int $0x80

");

 

 

 

 

 

__asm__(

 

"movl$1,%eax/r/t"

"xor%ebx,%ebx/r/t"

 

"int$0x80"

 

);

 

 

 

基本内联汇编的格式是

 

 

__asm____volatile__("InstructionList");

 

 

 

1.__asm__

 

 

 

__asm__GCCasm的宏定义:

 

#define__asm__asm

 

__asm__asm  用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它开头的,是必不可少的。

 

 

 

2.InstructionList

 

 

 

InstructionList汇编指令序列。它可以是空的,比如:__asm__ __volatile__("");  

__asm__("");都是完全合法的内联汇编表达式只不过这两条语句没有什么意义但并非所InstructionList为空的内联汇编表达式都是没有意义比如__asm__("":::"memory"); 就非常有意义,它向GCC  声明:对内存作了改动GCC  编译的时候,会将此因素虑进去。

 

们看一看下面这个例子:

 

 

$catexample1.c

 

int main(int__argc,char*__argv[])

 

{

 

int*__p=(int*)__argc;

(*__p)=9999;

 

 

//__asm__("":::"memory");

 

 

if((*__p)==9999)

return5;

 

 

return(*__p);

 

}

 

 

这段代码中那条内联汇编是被注释掉的在这条内联汇编之前内存指针__p所指向的内存被赋值为9999,随即在内联汇编之后,一条if语句判断__p所指向的内存与9999 是否相等。很明显,它们是相等的。GCC  优化编译的时候能够很聪明的发现这一点。我们使用下面的命令行对其进行编译:

 

$gcc-O-Sexample1.c

 

 

选项-O表示优化编译我们还可以指定优化等级比如-O2表示优化等级为2选项-S

 

表示将C/C++源文件编译为汇编文件,文件名和C/C++文件一样,只不过扩展名由.c  

 

 

.s

 

们来查看一下被放在example1.s中的编译结果,我们这里仅仅列出了使用gcc 2.96 redhat7.3编译后的相关函数部分汇编代为了保持清晰性无关的其它代码未被列出。

$catexample1.s main:

push%ebp

 

movl   %esp,%ebp

movl   8(%ebp),%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的条件生return

 

5汇编代码(GCC使eax为保存返回值的寄存器)。

 

们现在将example1.c中内联汇编的注释去掉,重新编译,然后看一下相关的编译结果。

 

$gcc-O-Sexample1.c

$catexample1.s main:

 

push%ebp

 

movl   %esp,%ebp

movl   8(%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

 

.L3:

movl   (%eax),%eax

 

.L2:

popl   %ebp ret

 

 

由于内联汇编语句__asm__("":::"memory")GCC  声明,在此内联汇编语句出现的位置内存内容可能了改变,所以GCC  编译时就不能像刚才那样处理。这次,GCC  老老实实的将if语句生成了汇编代码。

 

可能有人会质疑为什么要使用__asm__("":::"memory")GCC声明内存发生了变化?明明“InstructionList是空的,没有任何对内存的操作,这样做只会增GCC生成汇编代码的数量。

 

那条内联汇编语句没有对内存作任何操作事实上它确实什么都没有做但影响内存内容的不仅仅是你当前正在运行的程序比如如果你现在正在操作的内存是一块内存映射映射的内容是外I/O设备寄存那么操作这块内存的就不仅仅是当前的程序I/O 设备也会去操作这块内存既然两者都会去操作同一块内存那么任何一方在任何时候都不对这块内存的内容想当然。所以当你使用高级语言C/C++这类程序的时候,你必须让编译器也能够明白这一点,毕竟高级语言最终要被编译为汇编代码。

 

你可能已经注意到了这次输出的汇编结果中有两个符号#APP#NO_APPGCC

 

将内联汇编语句"Instruction List"所列出的指令放#APP  #NO_APP  间,

__asm__("":::"memory")“InstructionList为空所以#APP#NO_APP间也没有任何内容。但我们以后的例子会更加清楚的表现这一点。

 

关于为什么内联汇编__asm__("":::"memory")是一条声明内存改变的语句,我们后面会详细讨论。

 

刚才我们花了大量的内容来讨论"InstructionList"为空是的情况,但在实际的编程中,

 

"InstructionList"绝大多数情况下都不是空的。它可以有1条或任意多条汇编指令。

 

当在"InstructionList"中有多条指令的时候,你可以在一对引号中列出全部指令,也可以将一条或几条指令放在一对引号中所有指令放在多对引号中如果是前者你可以将每一条指令放在一行,如果要将多条指令放在一行,则必须用分号()或换行符(/n,大多数情况下/n还要跟一个/t其中/n为了换行/t为了空出一个tab宽度的空格)们分开。下面的例子都是合法的写法。

 

__asm__("movl %eax, %ebx sti

popl%edi

 

subl%ecx,%ebx");

 

 

__asm__("movl%eax,%ebx;stipopl%edi;subl%ecx,%ebx");

 

 

__asm__("movl%eax,%ebx;sti/n/tpopl%edi subl%ecx,%ebx");

 

 

如果你将指令放在多对引号中则除了最后一对引号之外前面的所有引号里的最后一条指令之后都要有一个分号()(/n)(/n/t)。比如:

 

 

__asm__("movl %eax, %ebx sti/n"

 

"popl%edi;"

 

"subl%ecx,%ebx");

 

 

__asm__("movl%eax,%ebx;sti/n/t"

"popl%edi;subl%ecx,%ebx");

 

 

__asm__("movl%eax,%ebx;sti/n/tpopl%edi/n"

 

"subl%ecx,%ebx");

 

 

__asm__("movl%eax,%ebx;sti/n/tpopl%edi;"

 

"subl%ecx,%ebx");

 

 

 

上述原则可以归结为:

 

     任意两个指令间要么被分号()分开,要么被放在两行;

 

     放在两行的方法既可以从通/n的方法来实现,也可以真正的放在两行;

 

     可以使用1对或多对引号1对引号里可以放任一多条指令所有的指令都要被放到引号中。

 

在基本内联汇编中“InstructionList书写的格式和你直接在汇编文件中写非内联汇编没有什么不同,你可以在其中定义Label,定义对(.alignn),定义段(.section name )例如:

 

 

__asm__(".align2/n/t"

 

"movl%eax,%ebx/n/t"

 

"test%ebx,%ecx/n/t"

 

"jneerror/n/t"

"sti/n/t"

 

"error:popl%edi/n/t"

 

"subl%ecx,%ebx");

 

 

上面例子的格式是Linux  联代码常用的格式,非常整齐。也建议大家都使用这种格式来写内联汇编代码。

 

 

 

3.__volatile__

 

 

 

__volatile__GCCvolatile的宏定义:

 

#define__volatile__volatile

 

__volatile__volatil是可选的,你可以用它也可以不用它。如果你用了它,则是向GCC声明不要动我所写的InstructionList,我需要原封不动的保留每一条指令,否则当你使用了优化选项(-O)进行编译时,GCC  将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。

 

那么GCC  判断的原则是什么?我不知道(如果有哪位朋友清楚的话,请告诉我。我试验了一下,发现一条内联汇编语句如果是基本内联汇编的话(即只有“InstructionList没有Input/Output/Clobber 的内联汇我们后面将会讨论这一无论你是否使

 

__volatile__来修饰,GCC 2.9 优化编译时,都会原封不动的保留内联汇编中

“InstructionList。但或许我的试验的例子并不充分,所以这一点并不能够得到保证。为了保险起见如果你不想让GCC优化影响你的内联汇编代码你最好在前面都加上__volatile__而不要依赖于编译器的原则,因为即使你非常了解当前编译器的优化原则,

 

你也无法保证这种原则将来不会发生变化。而__volatile__的含义却是恒定的。

 

 

2.2 InlineASMwithC/C++Expression

 

 

GCC  许你通过C/C++达式指定内联汇编中"Instrcuction List"中指令的输入和输出,你甚至可以不关心到底使用哪个寄存器被使用,完全靠GCC来安排和指这一点可以让程序员避免去考虑有限的寄存器的使用也可以提高目标代码的效率先来看几个例子:

 

 

__asm__("":::"memory"); //前面提到的

 

 

__asm__("mov%%eax,%%ebx"

:"=b"(rv)

 

:"a"(foo)

 

:"eax","ebx");

 

 

__asm____volatile__("lidt%0"

:"=m"(idt_descr));

 

 

__asm__("subl%2,%0/n/t"

 

"sbbl%3,%1"

 

:"=a"(endlow),"=d"(endhigh)

: "g" (startlow), "g" (starthigh),

 

"0"(endlow),"1"(endhigh));

 

 

 

怎么样,有点印象了吧,是不是也有点晕?没关系,下面讨论完之后你就不会再晕了。

 

(当然,也有可能更晕☺。讨论开始——

 

带有C/C++达式的内联汇编格式为:

 

 

__asm___volatile__("InstructionList"

 

:Output

 

:Input

 

:Clobber/Modify);

 

 

从中我们可以看出它和基本内联汇编的不同之处在于它多了3个部分(InputOutput

 

Clobber/Modify)在括号中4个部分通过冒(:)分开。

 

4个部分都不是必须的,任何一个部分都可以为空,其规则为:

 

     如果Clobber/Modif 为空,则其前面的冒(:)须省。比如__asm__("mov

 

%%eax, %%ebx" : "=b"(foo) : "a"(inp) : )就是非法的写法;而__asm__("mov

 

%%eax,%%ebx":"=b"(foo):"a"(inp))则是正确的。

 

     如果InstructionList为空InputOutputClobber/Modify可以不为空可以为空。比如__asm__("":::"memory");__asm__(""::);都是合法的写法。

 

     如果OutputInputClobber/Modify为空,OutputInput之前的冒号(:)可以省略也可以不省略如果都省略则此汇编退化为一个基本内联汇编否则,仍然是一个带有C/C++达式的内联汇编此时"InstructionList"中的寄存器写法要遵守相关规定,比如寄存器前必须使用两个百分(%%),而不是像基本汇编格式一样在寄存器前只使用一个百分(%)__asm__( " mov %%eax,

 

%%ebx"::)__asm__("mov%%eax,%%ebx":)__asm__("mov%eax,%ebx")

 

都是正确的写法,而__asm__("mov%eax,%ebx"::)__asm__("mov%eax,

 

%ebx":)__asm__("mov%%eax,%%ebx")都是错误的写法。

 

     如果InputClobber/Modify为空Output为空Input前的冒号(:)既可以省略,也可以不省略。比如__asm__( " mov %%eax, %%ebx" : "=b"(foo)  : )

 

__asm__("mov%%eax,%%ebx":"=b"(foo))都是正确的。

 

     如果后面的部分不为空而前面的部分为空则前面的冒号(:)都必须保留否则无说明不为空的部分究竟是第几部分。比如,Clobber/ModifyOutput  为空,Input为空Clobber/Modify前的冒号必须省(前面的规则Output 前的冒号必须为保留如果Clobber/Modify为空InputOutput为空,InputOutput 前的冒号都必须保留比如__asm__("mov%%eax,%%ebx"::

 

"a"(foo))__asm__("mov%%eax,%%ebx":::"ebx")

 

从上面的规则可以看到另外一个事实,区分一个内联汇编是基本格式的还是带有C/C++达式格式的,其规则在于在"InstructionList"后是否有冒号(:)的存,如果没有是基本格式的,否则,则是带有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从何而来呢?

 

答案是引号中的内容,被称作操作约束OperationConstraint),在这个例子中操约束为"=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文档中提供了加号(+)约束但在实际的编译中通不过;我不知道老版本会怎么样,我在GCC2.96对加(+)约束的使用非常正常。

 

 

们通过一个例子看一下,在一个输出操作中使用等号(=)约束和加号(+)约束的不同。

 

 

$catexample2.c

 

 

intmain(int__argc,char*__argv[])

 

{

 

intcr0=5;

__asm____volatile__("movl%%cr0,%0"

 

:"=a"(cr0));

 

 

return0;

 

}

 

 

$gcc-Sexample2.c

 

 

$catexample2.s main:

push%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中。

 

下面是使用加号(+)约束的情况:

 

 

$catexample3.c

 

intmain(int__argc,char*__argv[])

 

{

 

intcr0=5;

 

__asm____volatile__("movl%%cr0,%0"

:"+a"(cr0));

 

return0;

 

}

 

 

 

$gcc-Sexample3.c

$catexample3.s main:

 

push%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域中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。例如:

 

 

$catexample3.c

__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输入到当前内联汇编中。

 

们看一个例子:

 

 

$catexample4.c

 

intmain(int__argc,char*__argv[])

 

{

intcr0 =5;

 

__asm____volatile__("movl%0,%%cr0"::"a"(cr0));

 

return0;

 

}

 

$gcc-Sexample4.c

$catexample4.s main:

 

push%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

 

们从编译出的汇编代码可以看到"InstructionList"GCC按照我们的输入约"a",将变量cr0的内容装入了eax寄存器。

 

 

3.OperationConstraint

 

 

每一个InputOutput达式都必须指定自己的操作约Operation Constraint,我们这里来讨论在80386平台上所可能使用的操作约束。

 

16                                                                                                     Developing Your OwnUnix-LikeOS on IBM PC

 

 

 

3.1 Register Constraint

 

当你当前的输入或输入需要借助一个寄存器时你需要为其指定一个寄存器约束你可以直接指定一个寄存器的名字,比如:

 

 

__asm____volatile__("movl%0,%%cr0"::"eax"(cr0));

 

也可以指定一个缩写,比如:

 

__asm____volatile__("movl%0,%%cr0"::"a"(cr0));

 

如果你指定一个缩写比如字母aGCC将会根据当前操作表达式中C/C++达式宽度决定使用%eax还是%ax%al。比如:

 

 

unsignedshort__shrt;

__asm__("mov%0%%bx"::"a"(__shrt));

 

 

由于变量__shrt16-bitshort类型,则编译出来的汇编代码中,会让变量__shrt使用

 

%ex寄存器。编译结果为:

 

 

Movw   -2(%ebp),%ax #%ax=__shrt

#APP

 

movl   %ax,%bx

 

#NO_APP

 

 

论是Input还是Output操作表达式约束,都可以使用寄存器约束。

 

下表中列出了常用的寄存器约束的缩写。

 

 

约束      

 

r                    表示使用一个通用寄存器,由GCC  %eax/%ax/%al,%ebx/%bx/%bl,

 

%ecx/%cx/%cl,%edx/%dx/%dl选取一GCC认为合适的。

 

g                   表示使用任意一个寄存器GCC在所有的可以使用的寄存器中选取一个

 

GCC认为合适的。

 

q                   表示使用一个通用寄存器,和约束r的意义相同。

 

a                   表示使用%eax/%ax/%al

 

b                   表示使用%ebx/%bx/%bl

 

c                    表示使用%ecx/%cx/%cl

 

d                   表示使用%edx/%dx/%dl

 

D                  表示使用%edi/%di

 

S                   表示使用%esi/%si

 

f                    表示使用浮点寄存器

 

t                    表示使用第一个浮点寄存器

 

u                   表示使用第二个浮点寄存器

 

 

3.2 Memory Constraint

 

如果一个Input/Output  操作表达式的C/C++达式表现为一个内存地址,不想借助于任何寄存器,则可以使用内存约束。比如:

 

 

__asm__("lidt%0":"=m"(__idt_addr));

__asm__("lidt%0"::"m"(__idt_addr));

 

 

们看一下它们分别被放在一个C源文件中,然后被GCC编译后的结果:

 

 

$catexample5.c

/*本例中,变量__sh被作为一个内存输入*/

 

intmain(int__argc,char*__argv[])

 

{

 

char*__sh=(char*)&__argc;

 

 

__asm____volatile__(

 

"lidt%0"

 

:/*nooutput*/

 

:"m"(__sh)

 

);

 

 

return0;

 

}

 

 

$gcc-Sexample5.c

 

 

$catexample5.s main:

 

push%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

 

 

 

$catexample6.c

/*本例中,变量__sh被作为一个内存输出*/

intmain(int__argc,char*__argv[])

 

{

 

char*__sh=(char*)&__argc;

 

 

__asm____volatile__(

"lidt%0"

 

:"=m"(__sh)

 

);

 

 

return0;

}

 

 

$gcc-Sexample6.c

 

 

$catexample6.s main:

 

push%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++达式而言是输还是输出,完全依赖与你写在"InstructionList"中的指令对其操作的指令。

 

由于上例中,对其操作的指令为lidtlidt指令的操作数是一个输入型的操作数,所以实上对变量sh的操作是一个输入操作,即使你把它放在Output  域也不会改变这一点。所以,对此例而言,完全符合语意的写法应该是将sh放在Input域,尽管放Output也会有正确的执行结果。

 

 

所以对于内存约束类型的操作表达式而言放在Input还是放在Output对编译结果是没有任何影响的因为本来我们将一个操作表达式放在Input域或放在Output是希望GCC为我们自动通过寄存器将表达式的值输入或输出。既然对于内存约束类型的操作表达式来说,GCC  不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序员的角度而言,为了增强代码的可读性,最好能够把它放在符合实际情况的地方。

 

 

约束      

 

M                  表示使用系统所支持的任何一种内存方式,不需要借助寄存器。

 

3.3 ImmediatelyNumberConstraint

 

如果一个Input/Output  操作表达式的C/C++达式是一个数字常数,不想借助于任何寄存器,则可以使用立即数约束。

 

由于立即数在C/C++中只能作为右值,所以对于使用立即数约束的表达式而言,只能放在Input域。比如:

 

 

__asm____volatile__("movl%0,%%eax"::"i"(100));

 

立即数约束很简单,也很容易理解,我们在这里就不再赘述。

 

 

约束      

 

i                    表示输入表达式是一个立即数(整数),不需要借助任何寄存器。

 

F                   表示输入表达式是一个立即数(浮点数),不需要借助任何寄存器。

 

 

 

 

3.4 GenericConstraint

 

 

约束       输入/输出      

g                   I,O                         表示可以使用通用寄存器内存立即数等任何一种处理方式。

 

0-9                I                             表示和第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__argc,char*__argv[])

 

{

 

JUST_MOV(100);

 

return0;

}

 

 

编译后生成的代码为:

 

 

main:

push%ebp

 

movl   %esp,%ebp

 

#APP

 

movl$100,%eax

 

#NO_APP

movl   $0,%eax pop%ebp

 

ret

 

很明显这是立即数方式。而下一个例子:

 

 

intmain(int__argc,char*__argv[])

 

{

JUST_MOV(__argc);

 

return0;

 

}

 

 

经编译后生成的代码为:

 

 

main:

 

push%ebp

 

movl   %esp,%ebp

 

#APP

movl   8(%ebp),%eax

 

#NO_APP

movl   $0,%eax popl   %ebp

 

ret

 

 

这个例子是使用内存方式。

 

一个带有C/C++达式的内联汇编,其操作表达式被按照被列出的顺序编号,第一个0,第21,依次类推,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操作表达式使用数字09中的一个数(假设为1为它的操作约则等于向GCC声明我要使用和编号为1Output操作表达式相同的寄存(如果Output操作表达式1使用的是寄存器或相同的内存地Output操作表达式1使用的是内存)。上面的描述包含两个限定:数字0到数字9为操作约束只能用在Input 操作表达式中被指定的操作表达(比如某个Input操作表达式使用数字1为约束么被指定的就是编号为1的操作表达式)只能是Output操作表达式。

 

由于GCC规定最多只能10Input/Output操作表达式所以事实上数字9为操约束永远也用不到因为Output操作表达式排在Input操作表达式的前面那么如果有一个Input操作表达式指定了数字9为操作约束的话那么说明Output操作表达式的数量已经至少为10个了,那么再加上这个Input  操作表达式,则至少为11个了,以及超出GCC的限制。

 

5ModifierCharacters

 

等号(=)和加号(+)用于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%0,%%eax;addl%%eax,%0":"+b"(foo))

 

但如果使用后一种写法,"InstructionList"中的别名也要相应的改动。关于别名,我们

 

 

后面会讨论。

 

像等号(=)和加号(+)饰符一样符号(&)也只能用于Output操作表达式的修饰使用它进行修饰时等于向GCC声明"GCC不得为任何Input操作表达式分配与此Output 操作表达式相同的寄存器"其原因是&饰符意味着被其修饰的Output操作表达式要在所有的Input操作表达式被输入前输出。我们看下面这个例子:

 

 

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

 

 

return0;

 

}

 

此例中,%0  对应的就Outpu 操作表式,它被指定的寄存器%eaxInstructionList的第一条指令popl%0编译后就成popl%eax这时%eax的内容已被修改,随后在InstructionListGCC会通movl%eax,address_of_out  这条指令将

%eax的内容放置到Output变量__out对于本例中的两Input操作表达式而言它们的寄存器约束为"r",即要求GCC为其指定合适的寄存器,然后在Instruction List之前将

__in1__in2的内容放入被选出的寄存器中如果它们中的一个选择了已经被__out  指定的寄存%eax假如__in1GCC  Instruction Lis 之前会插入指movl address_of_in1,%eax那么随后popl%eax指令就修改%eax此时%eax中存放的经不是Input变量__in1值了,那么随后的movl%1,%%esi指令,将不会按照我们的本意— — 即将__in1值放入%esi— — 而是将__out值放入%esi中了。

 

下面就是本例的编译结果,很明显,GCC__in2选择了和__out相同的寄存%eax这与我们的初衷不符。

 

main:

 

push%ebp

movl   %esp,%ebp subl   $12,%espmovl       $8, -4(%ebp) mov$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 mov$0,%eax

 

leave

 

为了避免这种情况,我们必须向GCC声明这一点,要求GCC为所有的Input操作表达式指定别的寄存器方法就是在Output操作表达式"=a"(__out)的操作约束中加入&约束,由于GCC规定等(=)约束必须放在第一个,所以我们写作"=&a"(__out)

 

下面是我们将&约束加入之后编译的结果:

 

main:

 

push%ebp

movl   %esp,%ebp subl $12,%espmovl       $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++达式互换这个修饰符号一般用于符合交换律运算,比如加(+),乘(*),与(&),或(|)等等。我们看一个例子:

 

 

 

intmain(int__argc,char*__argv[])

 

{

 

int__in1=8,__in2=4,__out=3;

 

 

__asm__("addl%1,%0/n/t"

 

:"=r"(__out)

 

:"%r"(__in1),"0"(__in2));

 

 

return0;

}

 

 

在此例中,由于指令是一个加法运算,相当于等式__out=__in1+__in2,而它与等式

__out=__in2+__in1没有什么不。所以使用百分号修饰,让GCC知道__in1__in2以互换,也就是说GCC可以自动将本例的内联汇编改变为:

 

 

__asm__("addl%1,%0/n/t"

:"=r"(__out)

 

:"%r"(__in2),"0"(__in1));

 

 

 

下表总结了各种修饰符的意义:

 

 

饰符     输入/输出     

 

=                   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规定一个内联汇编语句最多可以有10Input/Output操作表达式然后按照它们被列出的顺序依次赋予编号09对于占位符中的数字而言和这些编号是对应的。

 

由于占位符前面使用一个百分(%)为了区别占位符和寄存器,GCC  规定在带

 

 

C/C++达式的内联汇编中"InstructionList"中直接写出的寄存器前必须使用两个百分号

 

(%%)

 

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的寄存器约束由ab时,那么你也必须将addl令中的%%eax%%ebx,也就是说你需要同时修改两个地方,而如果你使用占位,你只需要修改一次就够了。另外,如果你不使用占位符,将不利于代码的清晰性。在上例中,如果你使用占位符,那么你一眼就可以得知,addl  指令的第二个操作数内容最终会输出到变量__out如果你不用占位符而是直接addl指令的2个操作数写%%eax那么你需要考虑一下才知道它最终需要输出到变量__out这是占位符最粗浅的意义竟在这种情况下,你完全可以不用。

 

对于这些情况来说,不用占位符就完全不行了:

 

首先们看一看上例中的第1Input操作表达式"m"(__in1),它GCC换之后,现为addladdress_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 为其选择的,同时此寄存器被"InstructionList"中的指令修改,而这个寄存器只是供当前内联汇编临时使用的情况。比如:

 

 

__asm__("movl%0,%%ebx"::"a"(__foo):"bx");

 

寄存%ebx  "Instruction Lis "并且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"

 

:/*nooutput*/

 

:"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内容发生了改变:

 

$catexample7.c

 

intmain(int__argc,char*__argv[])

 

{

intin=8;

 

__asm__("addl%0,%%ebx"

 

:/*nooutput*/

 

:"a"(in):"bx");

 

 

return0;

 

}

 

$gcc-O-Sexample7.c

$catexample7.s main:

push%ebp

 

movl   %esp,%ebp

push%ebx       #%ebx内容被保存

 

movl   $8,%eax

 

#APP

addl   %eax,%ebx

 

#NO_APP

 

movl   $0,%eax

movl   (%esp),%ebx     #%ebx内容被恢复

leave ret

 

下面这个例子的C码与上一个例子除了没有声明%ebx寄存器发生了改变之外其它都相同。

 

 

$catexample8.c

 

intmain(int__argc,char*__argv[])

 

{

 

intin=8;

 

 

__asm__("addl%0,%%ebx"

 

:/*nooutput*/

 

:"a"(in));

 

 

return0;

}

 

 

 

$ gcc-O-Sexample8.c

$catexample8.s main:

 

push%ebp

movl   %esp,%ebp movl   $8,%eax

 

#APP

 

addl%eax,%ebx

 

#NO_APP

movl   $0,%eax pop%ebp

 

ret

 

细对比一下example7.sexample8.s,你就会明白在Clobber/Modify域声明一个寄存器的意义。

 

另外需要注意的是如果你在Clobber/Modify域声明了一个寄存那么这个寄存器将不能再被用做当前内联汇编语句Input/Outpu 操作表达式的寄存器约束,Input/Output  操作表达式的寄存器约束被指定为"r""g"GCC也不会选择已经被声明在Clobber/Modify中的寄存器。比如:

 

__asm__("movl%0,%%ebx"

::"a"(__foo):"ax","bx");

 

 

此例中由于Output操作表达式"a"(__foo)的寄存器约束已经指定%eax寄存器么再在Clobber/Modify域中指定"ax"就是非法的。编译时,GCC给出编译错误。

 

除了寄存器的内容会变,内存的内容也可以被修改。如果一个内联汇编语

"InstructionList"中的指令对内存进行了修改,或者在此内联汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其Output  操作表达式使用"m"约束,这种情况下你需要使用在Clobber/Modify域使用字符串"memory"GCC声明:这里,内存发生了,或可能发生了改变。例如:

 

 

void*memset(void*__s,char__c,size_t__count)

{

 

__asm__("cld/n/t"

 

"rep/n/t"

 

"stosb"

 

:/*nooutput*/

:"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  声明内存发生了变化,而内存发生变化带来的影响并不止这一点。比如我们在前面讲到的例子:

 

 

intmain(int__argc,char*__argv[])

 

{

 

int* __p = (int*)__argc;

 

(*__p)=9999;

 

__asm__("":::"memory");

if((*__p)==9999)

 

return5;

 

 

return(*__p);

 

}

 

 

本例中如果没有那条内联汇编语句那个if语句的判断条件就完全是一句废话GCC 优化时会意识到这一点,而直接只生成return  5汇编代码,而不会再生成if语句的相关代而不会生成return(*__p)的相关代但你加上了这条内联汇编语句它除了声明内存变化之外,什么都没有做。但GCC时就不能简单的认为它不需要判断都知道(*__p) 一定与9999相等它只有老老实实生成这if语句的汇编代一起相关的两个return句相关代码。

 

当一个内联汇编指令中包含影响eflags寄存器中的条件(也就是那些Jxx等跳转指令要参考的标志位比如进位标志0标志等那么需要Clobber/Modify域中使用"cc" 来声明这一点。这些指令包括adc,divpopflbtrbts等等,另外,当包call指令时,由于你不知道你所call的函数是否会修改条件标志,为了稳妥起见,最好也使用"cc"

 

我很少在相关资料中看到有关"cc"的确切用法只有一份文档提到了它还不是i386 平台的,只是"cc"处理器平台相关的,并非所有的平台都支持它,但即使在不支持它的平台上,使用它也不会造成编译错误。我做了一些实验,但发现使"cc"和不使用"cc"所生成的代码没有任何不同Linux2.4的相关代码中用到了它如果谁知道在i386平台上"cc" 细节,请和我联系。

 

另外,还可以在Clobber/Modify域指定数字09以声明nInput/Output作表达式所使用的寄存器发生了变化,但正如我们在前面所的,如果你为某Input/Output  操作表达式指定了寄存器,或使用"g","r"约束让GCC  为其选择寄存器,GCC  经知道哪个寄存器内容发生了变化,所以这么做没有什么意义;我也作了相关的试验,没有发现使用它会对GCC  生成的汇编代码有任何影响,至少在i386  平台上是这样。Linux 2.4的所有i386平台相关内联汇编代码中都没有使用这一点,但S390平台相关代中有用到,但由于我S390汇编没有任何概念,所以,也不知道这么做的意义何在。

posted @ 2012-06-22 14:16  星泥  阅读(1586)  评论(0编辑  收藏  举报