GCC内嵌汇编.docx
《GCC内嵌汇编.docx》由会员分享,可在线阅读,更多相关《GCC内嵌汇编.docx(19页珍藏版)》请在冰豆网上搜索。
GCC内嵌汇编
内核代码绝大部分使用C语言编写,只有一小部分使用汇编语言编写,例如与特定体系结构相关的代码和对性能影响很大的代码。
GCC提供了内嵌汇编的功能,可以在C代码中直接内嵌汇编语言语句,大大方便了程序设计。
一、基本内嵌汇编
GCC提供了很好的内嵌汇编支持,最基本的格式是:
__asm____volatile__(汇编语句模板);
1、__asm__
__asm__是GCC关键字asm的宏定义:
#define__asm__asm
__asm__或asm用来声明一个内嵌汇编表达式,所以任何一个内嵌汇编表达式都是以它开头的,是必不可少的。
2、汇编语句模板
“汇编语句模板”是一组插入到C程序中的汇编指令(可以是单个指令,也可以是一组指令)。
每条指令都应该由双引号括起,或者整组指令应该由双引号括起。
每条指令还应该用一个定界符结尾。
有效的定界符为换行符(\n)和分号(;)。
\n后可以跟一个制表符(\t)作为格式化符号,增加GCC在汇编文件中生成的指令的可读性。
上述原则可以归结为:
①任意两个指令间要么被分号(;)分开,要么被放在两行;
②放在两行的方法既可以通过\n的方法来实现,也可以真正的放在两行;
③可以使用一对或多对双引号,每对双引号里可以放任意多条指令,所有的指令都必须放到双引号中。
在基本内嵌汇编中,“汇编语句模板”的书写的格式和你直接在汇编文件中使用汇编语言编程没有什么不同,你可以在其中定义标号(Label),定义对齐(.alignn),定义段(.sectionname)。
例如:
__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");
建议大家都使用这种格式来写内嵌汇编代码。
3、__volatile__
__volatile__是GCC关键字volatile的宏定义:
#define__volatile__volatile
__volatile__或volatile是可选的。
如果不想让GCC的优化改动你的内嵌汇编代码,你最好在前面都加上__volatile__。
二、带C语言表达式的内嵌汇编
在内嵌汇编中,可以将C语言表达式指定为汇编指令的操作数,而且不用去管如何将C语言表达式的值读入哪个寄存器,以及如何将计算结果写回C变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可,GCC会自动插入代码完成必要的操作。
通常嵌入到C代码中的汇编语句很难做到与其它部分没有任何关系,因此更多时候需要用到扩展的内嵌汇编格式:
__asm____volatile__(汇编语句模板:
输出部分:
输入部分:
破坏描述部分);
内嵌汇编表达式包含4个部分,各部分由“:
”分隔。
这4个部分都不是必须的,任何一个部分都可以为空,其规则为:
①如果“破坏描述部分”为空,则其前面的“:
”必须省略。
比如:
__asm__("mov%%eax,%%ebx":
:
);。
②如果“汇编语句模板”为空,则“输出部分”,“输入部分”以及“破坏描述部分”可以不为空,也可以为空。
比如:
__asm__("":
:
:
"memory");。
③如果“输出部分”,“输入部分”以及“破坏描述部分”都为空,“输出部分”和“输入部分”之前的“:
”既可以省略,也可以不省略。
如果都省略,则此汇编退化为一个基本内嵌汇编,否则,仍然是一个带有C语言表达式的内嵌汇编。
④如果“输入部分”和“破坏描述部分”为空,但“输出部分”不为空,“输入部分”前的“:
”既可以省略,也可以不省略。
⑤如果后面的部分不为空,而前面的部分为空,则前面的“:
”都必须保留,否则无法说明不为空的部分究竟是第几部分。
⑥如果“破坏描述部分”不为空,而“输出部分”和“输入部分”都为空,则“输出部分”和“输入部分”前的“:
”都必须保留。
从上面的规则可以看到另外一个事实,区分一个内嵌汇编是基本格式的还是扩展格式的,其规则在于在“汇编语句模板”后面是否有“:
”的存在,如果没有则是基本格式的,否则,就是扩展格式的。
这两种格式对寄存器语法的要求不同:
基本格式要求寄存器前只能使用一个%,这一点和原生汇编相同;而扩展格式则要求寄存器前必须使用两个%%。
比如:
__asm__("mov%%eax,%%ebx":
)
和
__asm__("mov%eax,%ebx")
都是正确的写法,而
__asm__("mov%eax,%ebx":
)
和
__asm__("mov%%eax,%%ebx")
都是错误的写法。
任何只带一个“%”的标识符都看成是操作数,而不是寄存器。
1、内嵌汇编举例
使用内嵌汇编,要先编写汇编语句模板,然后将C语言表达式与指令的操作数相关联,并告诉GCC对这些操作有哪些约束条件。
例如在下面的汇编语句:
__asm__("movl%1,%0":
"=r"(result):
"m"(input));
“movl%1,%0”是指令模板;“%0”和“%1”代表指令的操作数,称为占位符,内嵌汇编靠它们将C语言表达式与指令操作数相对应。
指令模板后面用圆括号括起来的是C语言表达式,本例中只有两个:
“result”和“input”,他们按照在输出部分和输入部分出现的顺序分别与指令操作数“%0”,“%1”对应;注意对应顺序:
第一个C语言表达式对应“%0”;第二个表达式对应“%1”,依次类推。
在每个操作数前面有一个用双引号括起来的字符串,字符串的内容是对该操作数的约束或者说要求。
“result”前面的约束字符串是“=r”,其中“=”表示“result”在指令中是只写的(输出操作数),“r”表示需要将“result”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后在指令中使用相应寄存器,而不是“result”本身,当然指令执行完后需要将寄存器中的值存入变量“result”,从表面上看好像是指令直接对“result”进行操作,实际上GCC做了隐式处理,这样我们可以少写一些指令。
“input”前面的“r”表示该表达式需要先放入某个寄存器,然后在指令中使用该寄存器参加运算。
由此可见,C语言表达式或者变量与寄存器的关系由GCC自动处理,我们只需使用约束字符串指导GCC如何处理即可。
内联汇编的重要性体现在它能够灵活操作,而且可以使其输出通过C变量显示出来。
因为它具有这种能力,所以__asm__可以用作汇编指令和包含它的C程序之间的接口。
2、汇编语句模板
◆操作数
C语言表达式可用作内嵌汇编中的汇编指令的操作数。
在汇编指令通过对C语言表达式进行操作来执行有意义的作业的情况下,操作数是扩展格式的内嵌汇编的主要特性。
每个操作数都由操作数约束字符串指定,后面跟着用圆括号括起来的C语言表达式,例如:
"constraint"(Cexpression)
操作数约束的主要功能是确定操作数的寻址方式。
◆占位符
在扩展格式的内嵌汇编的“汇编语句模板”中,操作数由占位符引用。
如果总共有n个操作数(包括输入和输出),那么第一个输出操作数的编号为0,逐项递增,总操作数的数目限制在10个(%0、%1、…、%9)。
如果要处理很多输入和输出操作,数字型的占位符很快就会变得混乱。
为了使条理清晰,GNU编译器(从版本3.1开始)允许声明替换的名称作为占位符。
替换的名称在“输入部分”和“输出部分”中声明。
格式如下:
[name]"constraint"(Cexpression)
声明name后,使用%[name]的形式替换内嵌汇编代码中相应的数字型占位符。
如下面所示:
__asm__("cmoveq%1,%2,%[result]"
:
[result]"=r"(result)
:
"r"(test),"r"(new),"[result]"(old));
在内嵌汇编中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。
对字节操作可以显式的指明是低字节还是高字节。
方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:
%h1。
必须使用占位符的情况:
我们看一看下面这个例子:
__asm__("addl%1,%0"
:
"=a"(out)
:
"m"(in1),"a"(in2));
①首先,我们看一看上例中的第1个输入操作表达式"m"(in1),它被GCC替换之后,表现为addladdress_of_in1,%%eax,in1的地址是什么?
编译时才知道。
所以我们完全无法直接在指令中去写出in1的地址,这时使用占位符,交给GCC在编译时进行替代,就可以解决这个问题。
所以这种情况下,我们必须使用占位符。
②其次,如果上例中的输出操作表达式"=a"(out)改为"=r"(out),那么out究竟会使用哪个寄存器只有到编译时才能通过GCC来决定,既然在我们写代码的时候,我们不知道究竟哪个寄存器被选择,我们也就不能直接在指令中写出寄存器的名称,而只能通过占位符替代来解决。
3、输出部分
“输出部分”用来指定当前内嵌汇编语句的输出。
我们看一看这个例子:
__asm__("movl%%cr0,%0":
"=a"(cr0));
这个内嵌汇编语句的输出部分为"=r"(cr0),它是一个“操作表达式”,更具体地在这里叫作“输出操作表达式”,指定了一个输出操作。
“输出操作表达式”由两部分组成,这两部分都是必不可少的:
①圆括号括起来的部分是一个C语言表达式,用来保存内嵌汇编的一个输出值,其操作就等于C的赋值表达式cr0=output_value,因此,圆括号中的输出表达式只能是C的左值表达式。
那么右值output_value从何而来呢?
②答案是双引号中的内容,被称作“操作约束”(OperationConstraint),在这个例子中操作约束为"=a",它包含两个约束:
等号(=)和字母a,其中等号(=)说明圆括号中左值表达式cr0是Write-Only的,只能够被作为当前内嵌汇编的输出,而不能作为输入。
而字母a是寄存器EAX/AX/AL的简写,说明cr0的值要从EAX寄存器中获取,也就是说cr0=%eax,最终这一点被转化成汇编语句就是movl%eax,address_of_cr0。
另外,需要特别说明的是,很多文档都声明,所有输出操作的操作约束必须包含一个等号(=),但GCC的文档中却很清楚的声明,并非如此。
因为等号(=)约束说明当前的表达式是Write-Only的,但另外还有一个符号——加号(+)用来说明当前表达式是Read-Write的,如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是Read-Only的。
因为对于输出操作来说,肯定是必须是可写的,而等号(=)和加号(+)都表示可写,只不过加号(+)同时也表示是可读的。
所以对于一个输出操作来说,其操作约束只需要有等号(=)或加号(+)中的任意一个就可以了。
二者的区别是:
等号(=)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则表示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。
但无论是等号(=)约束还是加号(+)约束所约束的操作表达式都只能放在“输出部分”中,而不能被用在“输入部分”中。
在“输出部分”中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。
4、输入部分
“输入部分”的内容用来指定当前内嵌汇编语句的输入。
我们看一看这个例子:
__asm__("movl%0,%%db7":
:
"a"(cpu->db7));
例中“输入部分”的内容为一个表达式"a"(cpu->db7),被称作“输入操作表达式”,用来表示一个对当前内嵌汇编的输入。
像输出操作表达式一样,一个输入操作表达式也分为两部分:
带圆括号的部分(cpu->db7)和带双引号的部分"a"。
这两部分对于一个内嵌汇编输入操作表达式来说也是必不可少的。
圆括号中的表达式cpu->db7是一个C语言的表达式,它不必是一个左值表达式,也就是说它不仅可以是放在C赋值操作左边的表达式,还可以是放在C赋值操作右边的表达式。
所以它可以是一个变量,一个数字,还可以是一个复杂的表达式。
比如上例可以改为:
__asm__("movl%0,%%db7":
:
"a"(foo));
__asm__("movl%0,%%db7":
:
"a"(0x1000));
__asm__("movl%0,%%db7":
:
"a"(x*y/z));
双引号中的部分是约束部分,和输出操作表达式约束不同的是,它不允许指定加号(+)约束和等号(=)约束,也就是说它只能是默认的Read-Only的。
约束中必须指定一个寄存器约束,例中的"a"表示当前输入变量cpu->db7要通过寄存器%eax输入到当前内嵌汇编中。
在“输入部分”中可以有多个输入操作表达式,多个操作表达式中间必须用逗号(,)分开。
5、操作约束
前面提到过,在内嵌汇编中的每个操作数都应该由操作数约束字符串描述,后面跟着用圆括号括起来的C语言表达式。
操作数约束主要是确定指令中操作数的寻址方式。
约束也可以指定:
①是否允许操作数位于寄存器中,以及它可以包括在哪些类型的寄存器中
②操作数是否可以是内存引用,以及在这种情况下使用哪些类型的寻址方式
③操作数是否可以是立即数
约束字符必须与指令对操作数的要求相匹配,否则产生的汇编代码将会有错,在这个例子中:
__asm__("movl%1,%0":
"=r"(result):
"r"(input));
如果将那两个"r",都改为"m"(“m”表示操作数是内存引用)编译后得到的结果是:
movlinput,result
很明显这是一条非法指令(mov不允许内存到内存的操作)。
每一个输入和输出操作表达式都必须指定自己的操作约束,下面是在80x86平台上可能使用的操作约束:
◆寄存器约束
当你当前的输入或输出需要借助一个寄存器时,你需要为其指定一个寄存器约束。
你可以直接指定一个寄存器的名字,比如:
__asm__("movl%0,%%cr0":
:
"eax"(cr0));
也可以指定一个缩写,比如:
__asm__("movl%0,%%cr0":
:
"a"(cr0));
如果你指定一个缩写,比如“a”,则GCC将会根据当前操作表达式中C语言表达式的类型决定使用%eax,还是%ax或%al。
比如:
unsignedshortshrt;
__asm__("mov%0,%%bx":
:
"a"(shrt));
由于变量shrt是16-bitshort类型,则编译出来的汇编代码中,会让此变量使用%ax寄存器。
无论是输入还是输出的操作表达式,都可以使用寄存器约束。
◆内存约束
如果一个输入或输出操作表达式的C语言表达式表现为一个内存地址,并且不想借助于任何寄存器,则可以使用内存约束。
比如:
__asm__("lidt%0":
"=m"(idt_addr));
使用内存方式进行输入输出时,由于不借助寄存器,所以GCC不会按照你的声明对其作任何的输入输出处理。
GCC只会直接拿来用,究竟对这个C语言表达式而言是输入还是输出,完全依赖与你写在“汇编语句模板”中的指令对其操作的指令。
当操作数位于内存中时,任何对它们执行的操作都将在内存位置中直接发生,所以,对于内存约束类型的操作表达式而言,放在“输入部分”还是放在“输出部分”,对编译结果是没有任何影响的,既然对于内存约束类型的操作表达式来说,GCC不会自动为它做任何事情,那么放在哪儿也就无所谓了。
但从程序员的角度而言,为了增强代码的可读性,最好能够把它放在符合实际情况的地方。
◆立即数约束
如果一个输入或输出操作表达式的C语言表达式是一个数字常数,并且不想借助于任何寄存器,则可以使用立即数约束。
由于立即数在C中只能作为右值,所以对于使用立即数约束的操作表达式而言,只能放在“输入部分”。
比如:
__asm__("movl%0,%%eax":
:
"i"(100));
◆匹配约束
匹配约束符是一位数字:
“0”,“1”,…,“9”,表示它约束的C表达式分别与占位符%0,%1,…,%9相对应的C变量匹配。
例如使用“0”作为%1的约束字符,那么%0和%1表示同一个C变量。
在某些情况下,一个变量既要充当输入操作数,也要充当输出操作数。
可以通过使用匹配约束在内嵌汇编中的“输入部分”指定这种情况。
__asm__("incl%0":
"=a"(var):
"0"(var));
在上面的示例中,寄存器%eax既用作输入变量,也用作输出变量。
将输入变量var读取到%eax,执行inc指令后将更新了值的%eax再次存储在var中。
这里的"0"指定与第0个输出变量相同的约束。
即,它指定var的输出实例只应该存储在%eax中。
该约束可以用于以下情况:
①输入从变量中读取,或者变量被修改后,修改写回到同一变量中
②不需要将输入操作数和输出操作数的实例分开
使用匹配约束最重要的意义在于它们可以导致有效地使用可用寄存器。
i386指令集中许多指令的操作数是读写型的,例如:
addl%1,%0
它先读取%0与%1原来的值然后将两者的值相加,并把结果存回%0,因此操作数%0是读写型操作数。
老版本的GCC对这种类型操作数的支持不是很好,它将操作数严格分为输入和输出两种,分别放在输入部分和输出部分,而没有一个单独部分描述读写型操作数。
__asm__("addl%1,%0":
"=r"(result):
"r"(input));
上例使用“r”约束的输出变量,GCC会分配一个寄存器,然后用该寄存器替换占位符,但是在使用该寄存器之前并不将result变量的值先读入寄存器,GCC认为所有输出变量以前的值都没有用处,也就没有必要将其读入寄存器(这可能是因为AT&T汇编源于RISC架构处理器的原故,在RISC处理器中大部分指令的输入输出明显分开,而不像CISC那样一个操作数既做输入又做输出,例如:
addr0,r1,r2
r0和r1是输入,r2是输出,输入和输出分开,不使用输入输出型操作数。
这种情况下GCC理所当然认为所有输出变量以前的值都没有用处,也就没有必要先将输出操作数的值读入寄存器r2了)。
上面的内嵌汇编指令不能奏效,因为需要在执行addl之前把result的值入寄存器。
因此在GCC中读写型的操作数需要在输入和输出部分分别描述,靠匹配约束符将两者关联到一起。
注意仅在输入和输出部分使用相同的C变量,但是不用匹配约束符,例如:
__asm__("addl%2,%0":
"=r"(result):
"r"(result),"m"(input));
产生的代码很可能不对。
看上去上面的代码可以正常工作,因为我们知道%0和%1都和result相关,应该使用同一个寄存器,但是GCC并不去判断%0和%1是否和同一个C语言表达式或变量相关联(这样易于产生与内嵌汇编相应的汇编代码),因此%0和%1使用的寄存器可能不同。
使用匹配约束符后,GCC知道应将对应的操作数放在同一个位置(同一个寄存器或者同一个内存变量)。
使用匹配约束字符的代码如下:
__asm__("addl%2,%0":
"=r"(result):
"0"(result),"m"(input));
相应的汇编代码为:
movl$0,_result
movl$1,_input
movl_result,%edx
movl%edx,%eax
#APP
addl_input,%eax
#NO_APP
movl%eax,%edx
movl%edx,_result
可以看到与result相关的寄存器是%edx,在执行指令addl之前先从%edx将result读入%eax,执行之后需要将结果从%eax读入%edx,最后存入result中。
这里我们可以看出GCC处理内嵌汇编中输出操作数的一点点信息:
addl并没有使用%edx,可见它不是简单的用result对应的寄存器%edx去替换%0,而是先分配一个寄存器,执行运算,最后才将运算结果存入对应的变量,因此GCC是先看该占位符对应的变量的约束符,发现是一个输出型寄存器变量,就为它分配一个寄存器,此时没有去管对应的C变量,最后GCC知道还要将寄存器的值写回变量,与此同时,它发现该变量与%edx关联,因此先存入%edx,再存入变量。
在新版本的GCC中增加了一个约束字符“+”,它表示操作数是读写型的,GCC知道应将变量值先读入寄存器,然后计算,最后写回变量,而无需在输入部分再去描述该变量。
__asm__("addl%1,%0":
"+r"(result):
"m"(input));
产生的汇编代码如下:
movl$0,_result
movl$1,_input
movl_result,%eax
#APP
addl_input,%eax
#NO_APP
movl%eax,_result
L2:
movl%ebp,%esp
处理的比使用匹配约束符的情况还要好,省去了好几条汇编代码。
◆修饰符
等号(=)和加号(+)用于对输出操作表达式的修饰,一个输出操作表达式要么被等号(=)修饰,要么被加号(+)修饰,二者必居其一。
使用等号(=)说明此输出操作表达式是Write-Only的,使用加号(+)说明此输出操作表达式是Read-Write的。
它们必须是输出操作表达式约束字符串中的第一个字符。
比如:
"a="(var)是非法的,而"+g"(var)则是合法的。
当使用加号(+)的时候,此输出操作表达式等价于使用等号(=)约束再加上一个输入操作表达式。
比如:
__asm__("incl%0":
"+a"(var));
等价于
__asm__("incl%0":
"=a"(var):
"0"(var));
像等号(=)和加号(+)修饰符一样,符号(&)也只能用于对输出操作表达式的修饰。
约束符“&”表示输入和输出操作数不能使用相同的寄存器,这样可以避免很多错误。
举一个例子,下面代码的作用是将函数foo的返回值存入变量ret中:
__asm__("callfoo;movl%%edx,%1":
"=a"(ret):
"r"(bar));
我们知道函数的int型返回值存放在%eax中,但是GCC编译的结果是输入和输出同时使用了寄存器%eax,如下:
movlbar,%eax
#APP
callfoo
movl%ebx,%eax
#NO_APP
movl%eax,ret
结果显然不对,原因是GCC并不知道%eax中的值是我们所要的。
避免这种情况的方法是使用“&”修饰符,这样bar就不会再使用%eax寄存器,因为已被ret指定使用。
__asm__("callfoo;movl%%edx,%1":
"=&a"(ret):
"r"(bar));
6、破坏描述部分
有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间