gnu内联汇编

来源:互联网 发布:python fabs abs 编辑:程序博客网 时间:2024/05/15 01:07
  首先不得不说的是,这同样是一篇copy过来的文章,也许你在internet上看过很多遍了,但这些人对里面出现的错误竟然视而不见,对此我既不表示惊讶,也不表示遗憾,而是表示淡定!
     GCC支持在C/C++代码中嵌入汇编代码,这些代码被称作是"GCC Inline ASM"(GCC内联汇编)。
一、基本内联汇编
     GCC中基本的内联汇编非常易懂,格式如下:
__asm__ [__volatile__] ("instruction list"); 
     其中:
1.__asm__:
     它是GCC定义的关键字asm的宏定义(#define __asm__ asm),它用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都以它开头,它是必不可少的。如果要编写符合ANSI C标准的代码(即与ANSI C兼容),那就要使用__asm__。
2.__volatile__:
     它是GCC关键字volatile的宏定义,这个选项是可选的,它向GCC声明"不要动我所写的instruction list,我需要原封不动地保留每一条指令",如果不使用__volatile__,则当你使用了优化选项-O进行优化编译时,GCC将会根据自己的判断来决定是否将这个内联汇编表达式中的指令优化掉。如果要编写符合ANSI C标准的代码(即与ANSI C兼容),那就要使用__volatile__。
3.instruction list:
     它是汇编指令列表,它可以是空列表,比如:__asm__ __volatile__("");或__asm__("");都是合法的内联汇编表达式,只不过这两条语句什么都不做,没有什么意义;但并非所 有"instruction list"为空的内联汇编表达式都是没意义的,比如:__asm__("":::"memory");就是非常有意义的,它向GCC声明:"我对内存做了改动",这样,GCC在编译的时候,就会将此因素考虑进去;
      instruction list的编写规则:当指令列表里面有多条指令时,可以在一对双引号中全部写出,也可将一条或多条指令放在一对双引号中,所有指令放在多对双引号中;如果是将所有指令写在一对双引号中,则相邻两条指令之间(一定注意:此时假定没有单独写在两行中)必须用分号";"或换行符(\n)隔开,如果使用换行符(\n),通常\n后面还要跟一个\t(主要是为了代码对齐,你可以在gcc -O -S 文件名.c后查看文件名.s的APP和NOAPP之间的指令列表,来证实这一点)。如果把所有指令写在一对双引号中,则相邻两条指令此时假定单独写在两行,那么gcc会报语法错误。
二、带有C/C++表达式的内联汇编
     GCC允许你通过C/C++表达式指定内联汇编中"instruction list"中的指令的输入和输出,你甚至可以不关心到底使用哪些寄存器,完全依靠GCC来安排和指定;这一点可以让程序员免去考虑有限的寄存器的使用,也可以提高目标代码的效率;
1.带有C/C++表达式的内联汇编语句的格式:
__asm__ [__volatile__]("instruction list":Output:Input:Clobber/Modify);
     圆括号中的内容被冒号":"分为四个部分:
A. 如果第四部分的"Clobber/Modify"可以为空;如果"Clobber/Modify"为空,则其前面的冒号(:)必须省略;比如:语句 __asm__("movl %%eax,%%ebx":"=b"(foo):"a"(inp):);是非法的,而语句__asm__("movl %%eax,%%ebx":"=b"(foo):"a"(inp));则是合法的;
B.如果第一部分的"instruction list"为空,则input、output、Clobber/Modify可以为空,也可以不为空;比如,语句__asm__("":::"memory");和语句__asm__(""::);都是合法的写法;
C. 如果Output、Input和Clobber/Modify都为空,那么,Output、Input之前的冒号(:)可以省略,也可以不省略(也就是说可以留下两个冒号,一个冒号,和零个冒号,但零个冒号表示的不是扩展汇编,而是基本汇编);如果都省略,则此汇编就退化为一个基本汇编,否则,仍然是一个带有C/C++表达式的内联汇编,此时"instruction list"中的寄存器的写法要遵循相关规定,比如:寄存器名称前面必须使用两个百分号(%%);基本内联汇编中的寄存器名称前面只有一个百分号(%);比如,语句__asm__("movl %%eax,%%ebx"::);__asm__("movl %%eax,%%ebx":);和语句__asm__("movl %%eax,%%ebx");都是正确的写法,而语句__asm__("movl %eax,%ebx"::);__asm__("movl %eax,%ebx":);和语句__asm__("movl %%eax,%%ebx");都是错误的写法;
D.如果Input、Clobber/Modify为空,但Output不为空,则,Input前 面的冒号(:)可以省略,也可以不省略;比如,语句__asm__("movl %%eax,%%ebx":"=b"(foo):);和语句__asm__("movl %%eax,%%ebx":"=b"(foo));都是正确的;
E. 如果后面的部分不为空,而前面的部分为空,则,前面的冒号(:)都必须保留,否则无法说明不为空的部分究竟是第几部分;比 如,Clobber/Modify、Output为空,而Input不为空,则Clobber/Modify前面的冒号必须省略,而Output前面的冒号必须保留;如果Clobber/Modify不为空,而Input和Output都为空,则Input和Output前面的冒号都必须保留;比如,语句 __asm__("movl %%eax,%%ebx"::"a"(foo));和__asm__("movl %%eax,%%ebx":::"ebx");
     注意:基本内联汇编中的寄存器名称前面只能有一个百分号(%),而带有C/C++表达式的内联汇编中的寄存器(即扩展汇编)名称前面必须有两个百分号(%%);
2.Output:
     Output部分用来指定当前内联汇编语句的输出,称为输出操作表达式;
格式为: "操作约束"(C/C++表达式)
例如:
__asm__("movl %%cr0,%1":"=a"(cr0)); //注意:后面的cr0是个变量,不是寄存器
     这个语句中的Output部分就是("=a"(cr0)),它是一个操作表达式,指定了一个内联汇编语句的输出部分;
     Output部分由两个部分组成:由双引号括起来的部分和由圆括号括起来的部分,这两个部分是一个Output部分所不可缺少的部分;
     用圆括号括起来的部分就是C/C++表达式,它用于保存当前内联汇编语句的一个输出值,其操作就是C/C++赋值语句"="的左值部分,因此,圆括号中指定的表达式只能是C /C++中赋值语句的左值表达式,即:放在等号=左边的表达式;也就是说,Output部分只能作为C/C++赋值操作左边的表达式使用;
     用双引号括起来的部分指定了C/C++中赋值表达式的右值来源;这个部分被称作是"操作约束"(Operation Constraint),也可以称为"输出约束";在这个例子中的操作约束是"=a",这个操作约束包含两个组成部分:等号(=)和字母a,其中,等号 (=)说明圆括号中的表达式cr0是一个只写的表达式,只能被用作当前内联汇编语句的输出,而不能作为输入;字母a是寄存器EAX/AX/AL的缩写,说明cr0的值要从寄存器EAX中获取,也就是说cr0(变量)=%eax,最终这一点被转化成汇编指令就是:movl %eax,address_of_cr0;
     注意:很多文档中都声明,所有输出操作的的操作约束都必须包含一个等号(=),但是GCC的文档中却明确地声明,并非如此;因为等号(=)约束说明当前的表达式是一个只写的,但是还有另外一个符号:加号(+),也可以用来说明当前表达式是可读可写的;如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是只读的;因此,对于输出操作来说,肯定必须是可写的,而等号(=)和加号(+)都可表示可写,只不过加号(+)同时也可以表示可读;所以, 对于一个输出操作来说,其操作约束中只要包含等号(=)或加号(+)中的任意一个就可以了;
     等号(=)与加号(+)的区别:等号(=)表示当前表达式是一个纯粹的输出操作,而加号(+)则表示当前表达式不仅仅是一个输出操作,还是一个输入操作;但无论是等号(=)还是加号(+),所表示的都是可写,只能用于输出,只能出现在Output部分,而不能出现在Input部分;
     在Output部分可以出现多个输出操作表达式,多个输出操作表达式之间必须用逗号(,)隔开;
3、Input:
     Input部分用来指定当前内联汇编语句的输入,称为输入操作表达式;
格式为: "操作约束"(C/C++表达式)
例如:
__asm__("movl %0,%%db7"::"a"(cpu->db7));
     其中,表达式"a"(cpu->db7)就称为输入操作表达式,用于表示一个对当前内联汇编的输入; Input同样也由两部分组成:由双引号括起来的部分和由圆括号括起来的部分;这两个部分对于当前内联汇编语句的输入来说也是必不可少的;
     在这个例子中,由双引号括起来的部分是"a",用圆括号括起来的部分是(cpu->db7);
     用双引号括起来的部分就是C/C++表达式,它为当前内联汇编语句提供一个输入值;在这里,圆括号中的表达式cpu->db7是一个C/C++语言的表达式,所以,Input可以是一个变量、一个数字,还可以是一个复杂的表达式(如:a+b/c*d);
例如:(注意:下面例子和上面那个例子不等价
__asm__("movl %0,%%db7"::"a"(foo));__asm__("movl %0,%%db7"::"a"(0x12345));__asm__("movl %0,%%db7"::"a"(va:vb/vc));
     用双引号括起来的部分就是C/C++中赋值表达式,用于约束当前内联汇编语句中的当前输入;这个部分也成为"操作约束",也可以成为是"输入约束";与输出表达式中的操作约束不同的是,输入表达式中的操作约束不允许指定等号(=)约束或加号(+)约束,也就是说,它只能是只读的;约束中必须指定一个寄存器约束;例子中的字母a表示当前输入变量cpu->db7要通过寄存器EAX输入到当前内联汇编语句中;
三、操作约束:Operation Constraint
     每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint;约束的类型有:寄存器约束、内存约束、立即数约束、通用约束;
操作表达式的格式:
"约束"(C/C++表达式)
1.寄存器约束:
     当你的输入或输出需要借助于一个寄存器时,你需要为其指定一个寄存器约束;
可以直接指定一个寄存器名字;
比如:
__asm__ __volatile__("movl %0,%%cr0"::"eax"(cr0));
也可以指定寄存器的缩写名称;
比如:
__asm__ __volatile__("movl %0,%%cr0"::"a"(cr0));
     如果指定的是寄存器的缩写名称,比如:字母a;那么,GCC将会根据当前操作表达式中C/C++表达式的宽度来决定使用%eax、%ax还是%al;
如:
unsigned short __shrt;
__asm__ __volatile__("movl %0,%%bx"::"a"(__shrt));
     由于变量__shrt是16位无符号类型,占两个字节,所以编译器编译出来的汇编代码中,则会让此变量使用寄存器%ax;
   无论是Input还是Output操作约束,都可以使用寄存器约束;特别注意本文中所说的通用寄存器是指eax,ebx,ecx,edx,esi,edi不包括esp,ebp,因为这两种分别作为栈顶和栈基址
+---+--------------------+
| r | Register(s) |
+---+--------------------+
| a | %eax, %ax, %al |
| b | %ebx, %bx, %bl |
| c | %ecx, %cx, %cl |
| d | %edx, %dx, %dl |
| S | %esi, %si |
| D | %edi, %di |
+---+--------------------+
Some other constraints used are:
  1. "m" : A memory operand is allowed, with any kind of address that the machine supports in general.
  2. "o" : A memory operand is allowed, but only if the address is offsettable. ie, adding a small offset to the address gives a valid address.
  3. "V" : A memory operand that is not offsettable. In other words, anything that would fit the `m’ constraint but not the `o’constraint.
  4. "i" : An immediate integer operand (one with constant value) is allowed. This includes symbolic constants whose values will be known only at assembly time.
  5. "n" : An immediate integer operand with a known numeric value is allowed. Many systems cannot support assembly-time constants for operands less than a word wide. Constraints for these operands should use ’n’ rather than ’i’.
  6. "g" : Any register, memory or immediate integer operand is allowed, except for registers that are not general registers.
Following constraints are x86 specific.
  1. "r" : Register operand constraint, look table given above.
  2. "q" : Registers a, b, c or d.
  3. "I" : Constant in range 0 to 31 (for 32-bit shifts).
  4. "J" : Constant in range 0 to 63 (for 64-bit shifts).
  5. "K" : 0xff.
  6. "L" : 0xffff.
  7. "M" : 0, 1, 2, or 3 (shifts for lea instruction).
  8. "N" : Constant in range 0 to 255 (for out instruction).
  9. "f" : Floating point register
  10. "t" : First (top of stack) floating point register
  11. "u" : Second floating point register
  12. "A" : Specifies the `a’ or `d’ registers. This is primarily useful for 64-bit integer values intended to be returned with the `d’ register holding the most significant bits and the `a’ register holding the least significant bits.
2.内存约束:
     如果一个Input/Output操作表达式的C/C++表达式表现为一个内存地址(指针变量),不想借助于任何寄存器,则可以使用内存约束;
比如:
__asm__("lidt %0":"=m"(__idt_addr));或__asm__("lidt %0"::"m"(__idt_addr));
     内存约束使用约束名"m",表示的是使用系统支持的任何一种内存方式,不需要借助于寄存器;
     使用内存约束方式进行输入输出时,由于不借助于寄存器,所以,GCC不会按照你的声明对其做任何的输入输出处理;GCC只会直接拿来使用,对这个C/C++ 表达式而言,究竟是输入还是输出,完全依赖于你写在"instruction list"中的指令对其操作的方式;所以,不管你把操作约束和操作表达式放在Input部分还是放在Output部分,GCC编译生成的汇编代码都是一样 的,程序的执行结果也都是正确的;本来我们将一个操作表达式放在Input或Output部分是希望GCC能为我们自动通过寄存器将表达式的值输入或输出;既然对于内存约束类型的操作表达式来说,GCC不会为它做任何事情,那么放在哪里就无所谓了;但是从程序员的角度来看,为了增强代码的可读性,最好能够把它放在符合实际情况的地方;
3.立即数约束:
      如果一个Input/Output操作表达式的C/C++表达式是一个数字常数,不想借助于任何寄存器或内存,则可以使用立即数约束;
     由于立即数在C/C++表达式中只能作为右值使用,所以,对于使用立即数约束的表达式而言,只能放在Input部分;比如:
__asm__ __volatile__("movl %0,%%eax"::"i"(100));
     立即数约束使用约束名"i"表示输入表达式是一个整数类型的立即数,不需要借助于任何寄存器,只能用于Input部分;使用约束名"f "表示输入表达式是一个浮点数类型的立即数,不需要借助于任何寄存器,只能用于Input部分;
4.通用约束:
     约束名"g"可以用于输入和输出,表示可以使用通用寄存器、内存、立即数等任何一种处理方式; 约束名"0,1,2,3,4,5,6,7,8,9"只能用于输入,表示与第n个操作表达式使用相同的寄存器/内存;
     通用约束"g"是一个非常灵活的约束,当程序员认为一个C/C++表达式在实际操作中,无论使用寄存器方式、内存方式还是立即数方式都无所谓时,或者程序员想实现一个灵活的模板,以让GCC可以根据不同的C/C++表达式生成不同的访问方式时,就可以使用通用约束g;
例如:
#define JUST_MOV(foo) __asm__("movl %0,%%eax"::"g"(foo))
则JUST_MOV(100)和JUST_MOV(var)就会让编译器产生不同的汇编代码;
对于JUST_MOV(100)的汇编代码为:
#APP
 movl $100,%eax      #立即数方式;
#NO_APP
对于JUST_MOV(var)的汇编代码为:
#APP
movl 8(%ebp),%eax #内存方式的"o"方式,参见上文
#NO_APP
像这样的效果,就是通用约束g的作用;
5.修饰符:
      等号(=)和加号(+)作为修饰符,只能用于Output部分;等号(=)表示当前输出表达式的属性为只写,加号(+)表示当前输出表达式的属性为可读可写;这两个修饰符用于约束对输出表达式的操作,它们俩被写在输出表达式的约束部分中,并且只能写在第一个字符的位置;
   看一下下面的代码就知道为什么要将读写型操作数,分别在输入和输出部分加以描述。该例功能是求input+result的和,然后存入result:
extern int input,result;
void test_at_t(){
   result= 0;
   input = 1;
   __asm__ __volatile__ ("addl %1,%0":"=r"(result): "r"(input));
 }
对应的汇编代码为:
movl $0,_result
movl $1,_input
movl _input,%edx //APP
addl %edx,%eax //NO_APP
movl %eax,%edx
movl %edx,_result
   input为输入型变量,而且需要放在寄存器中,GCC给他分配的寄存器是%edx,在执行addl之前%edx的内容已是input的值。可见对于使用“r”限制的输入型变量或表达式,在使用之前GCC会插入必要的代码将他们的值读到寄存器;“m”型变量则不必这一步。读入input后执行addl,显然%eax的值不对,需要先读入result的值才行。再往后看:movl %eax,%edx和movl %edx,_result的作用是将结果存回result,分配给result的寄存器和分配给input的相同,都是%edx。
   综上能总结出如下几点:
1. 使用“r”限制的输入变量,GCC先分配一个寄存器,然后将值读入寄存器,最后用该寄存器替换占位符;
2. 使用“r”限制的输出变量,GCC会分配一个寄存器,然后用该寄存器替换占位符,不过在使用该寄存器之前并不将变量值先读入寄存器,最后GCC插入代码,将寄存器的值写回变量;
3. 输入变量使用的寄存器在最后一处使用他的指令之后,就能挪做其他用处,因为已不再使用。例如上例中的%edx。在执行完addl之后就作为和result对应的寄存器。
    因为第二条,上面的内嵌汇编指令不能奏效,因此需要在执行addl之前把result的值读入寄存器,似乎再将result放入输入部分就行了(因为根据上述第一条会确保将result先读入寄存器)。修改后的指令如下(为了更容易说明问题将input限制符由“r,”改为“m”):
extern int input,result;
void test_at_t() {
     result = 0;
   input = 1;
   __asm__ __volatile__ ("addl %2,%0":"=r"(result):"r"(result),"m"(input));
}
   看上去上面的代码能正常工作,因为我们知道%0和%1都和result相关,应该使用同一个寄存器,不过GCC并不去判断%0和%1,是否和同一个C表达式或变量相关联(这样易于产生和内嵌汇编相应的汇编代码),因此%0和%1使用的寄存器可能不同。我们看一下汇编代码就知道了。
movl $0,_result
movl $1,_input
movl _result,%edx /APP
addl _input,%eax /NO_APP
movl %eax,%edx
movl %edx,_result
     目前在执行addl之前将result的值被读入了寄存器%edx,不过addl指令的操作数%0却成了%eax,而不是%edx,和预料的不同,这是因为GCC给输出和输入部分的变量分配了不同的寄存器,GCC没有去判断两者是否都和result相关,后面会讲GCC怎么翻译内嵌汇编,看完之后就不会惊奇啦。使用匹配限制符后,GCC知道应将对应的操作数放在同一个位置(同一个寄存器或同一个内存变量)。使用匹配限制字符的代码如下:
extern int input,result;
void test_at_t() {
   result = 0;
   input = 1;
   __asm__ __volatile__ ("addl %2,%0":"=r"(result):"0"(result),"m"(input));
}
     输入部分中的result用匹配限制符“0”限制,表示%1和%0,代表同一个变量,输入部分说明该变量的输入功能,输出部分说明该变量的输出功能,两者结合表示result是读写型。因为%0和%1,表示同一个C变量,所以放在相同的位置,无论是寄存器还是内存。
相应的汇编代码为:
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知道应将变量值先读入寄存器,然后计算,最后写回变量,而无需在输入部分再去描述该变量。
extern int input,result;
void test_at_t(){
   result = 0;
   input = 1;
   __asm__ __volatile__ ("addl %1,%0":"+r"(result):"m"(input));
}
此处用“+”替换了“=”,而且去掉了输入部分关于result的描述,产生的汇编代码如下:
movl $0,_result
movl $1,_input
movl _result,%eax //APP
addl _input,%eax //NO_APP
movl %eax,_result
L2:
movl %ebp,%esp
     显然,处理的比使用匹配限制符的情况还要好,省去了好几条汇编代码。
     修饰符&的作用就是要求GCC编译器为所有的Input操作表达式分配别的寄存器,而不会分配与被修饰符&修饰的Output操作表达式相同的寄存器;修饰符&也写在操作约束中,即:&约束;由于GCC已经规定加号(+)或等号(=)占据约束的第一个字符,那么&约束只能占用第二个 字符;
例如:
int __out, __in1, __in2;
__asm__("popl %0\n\t"
        "movl %1,%%esi\n\t"
        "movl %2,%%edi\n\t"
        :"=&a"(__out)
        :"r"(__in1),"r"(__in2));
     注意: 如果一个Output操作表达式的寄存器约束被指定为某个寄存器,只有当至少存在一个Input操作表达式的寄存器约束可选约束(意思是GCC可以从多个寄存器中选取一个,例如你选用"r"约束而不是"b"约束)时,比如"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被分别指定了使用了内存方式和固定的寄存器(ecx),GCC无从选择;
     如果你已经为某个Output操作表达式指定了修饰符&,并指定了固定的寄存器,那么,就不能再为任何Input操作表达式指定这个寄存器了(但是可以用约名"0,1,2,3,4,5,6,7,8,9",这些约束名只能用于输入,表示与第n个操作表达式使用相同的寄存器/内存),否则会出现编译报错;
   Unless an output operand has the '&' constraint modifier, GCC may allocate it in the same register as an unrelated input operand, on the assumption the inputs are consumed before the outputs are produced. This assumption may be false if the assembler code actually consists of more than one instruction. In such a case, use '&' for each output operand that may not overlap an input.

比如:
__asm__("popl %0; movl %1,%%esi; movl %2,%%edi;":"=&a"(__out):"a"(__in1),"c"(__in2));
 __asm__("popl %0; movl %1,%%esi; movl %2,%%edi;":"=&a"(__out):"0"(__in1),"c"(__in2)); 这样就不会导致编译错误。(你在输入操作数中用0约束符表达了你要使用和输出操作数同样的寄存器(此处是eax),相当与你向gcc表明了你知道这样做的后果,只有在这种情况下,才是合法的。)
     相反,你也可以为Output指定可选约束,比如"r"或"g"等,让GCC为此Output操作表达式选择合适的寄存器,或使用内存方式,GCC在选择的时候,会排除掉已经被Input操作表达式所使用过的所有寄存器,然后在剩下的寄存器中选择,或者干脆使用内存方式;
比如:
__asm__("popl %0; movl %1,%%esi; movl %2,%%edi;":"=&r"(__out):"a"(__in1),"c"(__in2));
      这三个修饰符只能用在Output操作表达式中,而修饰符%则恰恰相反,它只能用在Input操作表达式中;
      修饰符%用于向GCC声明:"当前Input操作表达式中的C/C++表达式可以与下一个Input操作表达式中的C/C++表达式互换";这个修饰符一般用于符合交换律运算的地方;比如:加、乘、按位与&、按位或|等等;
例如:
__asm__("addl %1,%0\n\t":"=r"(__out):"%r"(__in1),"0"(__in2));
     其中,"0"(__in2)表示使用与第一个Input操作表达式("r"(__in1))相同的寄存器或内存;
     由于使用符号%修饰__in1的寄存器方式r,那么就表示,__in1与__in2可以互换位置;加法的两个操作数交换位置之后,和不变;
修饰符  I/O  意义 (I表示输入操作式,O表示输出操作式)
=        O    表示此Output操作表达式是只写的
+        O    表示此Output操作表达式是可读可写的
&        O    表示此Output操作表达式独占为其指定的寄存器(但是可以用约束名"0,1,2,3,4,5,6,7,8,9"
%        I    表示此Input操作表达式中的C/C++表达式可以与下一个Input操作表达式中的C/C++表达式互换
四、占位符
     每一个占位符对应一个Input/Output操作表达式;
     带C/C++表达式的内联汇编中有两种占位符:序号占位符名称占位符;一般只涉及到序号占位符,故略去名称占位符的讨论。
序号占位符:
     GCC 规定:一个内联汇编语句中最多只能有10个Input/Output操作表达式,这些操作表达式按照他们被列出来的顺序依次赋予编号0到9;对于占位符中 的数字而言,与这些编号是对应的;比如:占位符%0对应编号为0的操作表达式,占位符%1对应编号为1的操作表达式,依次类推;
     由于占位符前面要有一个百分号%,为了去边占位符与寄存器,GCC规定:在带有C/C++表达式的内联汇编语句的指令列表里列出的寄存器名称前面必须使用两个百分号(%%),一区别于占位符语法;
     GCC对占位符进行编译的时候,会将每一个占位符替换为对应的Input/Output操作表达式所指定的寄存器/内存/立即数;
例如:
__asm__("addl %1,%0\n\t":"=a"(__out):"m"(__in1),"a"(__in2));
     这个语句中,%0对应Output操作表达式"=a"(__out),而"=a"(__out)指定的寄存器是%eax,所以,占位符%0被替换 为%eax;占位符%1对应Input操作表达式"m"(__in1),而"m"(__in1)被指定为内存,所以,占位符%1被替换位__in1的内存 地址;
     用一句话描述:序号占位符就是前面描述的%0、%1、%2、%3、%4、%5、%6、%7、%8、%9;其中,每一个占位符对应一个Input/Output的C/C++表达式;
五、寄存器/内存修改标示(Clobber/Modify)
     有时候,当你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望GCC在编译时能够将这一点考虑进去;那么你就可以在Clobber/Modify部分声明这些寄存器或内存;
1.寄存器修改通知:
     这种情况一般发生在一个寄存器出现在指令列表中,但又不是Input/Output操作表达式所指定的,也不是在一些Input/Output操作表达式中使用"r"或"g"约束时由GCC选择的,同时,此寄存器被指令列表中的指令所修改,而这个寄存器只供当前内联汇编语句使用的情况;比如:
__asm__("movl %0,%%ebx"::"a"(__foo):"bx");
      这个内联汇编语句中,%ebx出现在指令列表中,并且被指令修改了,但是却未被任何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");
       这个语句中,声明了bx和cx,告诉GCC:寄存器%ebx和%ecx可能会被修改,要求GCC考虑这个因素;
寄存器名称串:
"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"就可以了,譬如al或eax和ax是此处等价的;都代表eax。
     如果你在一个内联汇编语句的Clobber/Modify部分向GCC声明了某个寄存器内容发生了改变,GCC在编译时,例如发现这个被声明的寄存器的内容在此内联汇编之后还要继续使用,那么,GCC会首先将此寄存器的内容保存起来,然后在此内联汇编语句的相关代码生成之后,再将其内容恢复;只需要记住这一点,gcc对指令列表中的寄存器使用情况是不清楚的,你在clobber/modify部分声明某些寄存器,是为了给gcc编译器关于这些寄存器的信息,gcc得知这些寄存器的使用情况,才会正常地分配和利用寄存器。
     因为所有的代码都是用高级语言编写,编译器能识别各种语句的作用,在转换的过程中所有的寄存器都由编译器决定怎么分配使用,他有能力确保寄存器的使用不会冲突;也能利用寄存器作为变量的缓冲区,因为寄存器的访问速度比内存快非常多倍。如果全部使用汇编语言则由程序员去控制寄存器的使用,只能靠程式员去确保寄存器使用的正确性。不过如果两种语言混用情况就变复杂了,因为内嵌的汇编代码能直接使用寄存器,而编译器在转换的时候并不去检查内嵌的汇编代码使用了哪些寄存器(因为非常难检测汇编指令使用了哪些寄存器,例如有些指令隐式修改寄存器,有时内嵌的汇编代码会调用其他子过程,而子过程也会修改寄存器),因此需要一种机制通知编译器我们使用了哪些寄存器(程式员自己知道内嵌汇编代码中使用了哪些寄存器),否则对这些寄存器的使用就有可能导致错误,修改描述部分能起到这种作用。当然内嵌汇编的输入输出部分指明的寄存器或指定为“r”,“g”型由编译器去分配的寄存器就不必在破坏描述部分去描述,因为编译器已知道了。
      另外需要注意的是,如果你在Clobber/Modify部分声明了一个寄存器,那么这个寄存器将不能再被用作当前内联汇编语句的Input/Output操作表达式的寄存器约束,如果Input/Output操作表达式的寄存器约束被指定为"r"/"g",GCC也不会选择已经被声明在Clobber /Modify部分中的寄存器。
例如:
__asm__("movl %0,%%ebx"::"a"(__foo):"ax","bx");
      这条语句中的Input操作表达式"a"(__foo)中已经指定了寄存器%eax,那么在Clobber/Modify部分中个列出的"ax"就是非法的;编译时,GCC会报错。
2.内存修改通知:
      除了寄存器的内容会被修改之外,内存的内容也会被修改;如果一个内联汇编语句的指令列表中的指令对内存进行了修改,或者在此内联汇编出现的地方,内存内容可能发生改变,而被改变的内存地址你没有在其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"]);//因为Input操作表达式已经指定了eax,edi,ecx,在clobber/modify再列出他们中任意一个都是会导致编译错误的。这是memset在以前内核版本中的实现,可是现在这样写就是错误的,笔者认为是现在的gas语法和语义发生了改变。
  return s;
}
      如果一个内联汇编语句的Clobber/Modify部分存在"memory",那么GCC会保证在此内联汇编之前,如果某个内存的内容被装入了寄存器,那么,在这个内联汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝;因为这个时候寄存器中的拷贝很可能已经和内存处的内容不一致了;
3.标志寄存器修改通知:
      当一个内联汇编中包含影响标志寄存器eflags的条件,那么也需要在Clobber/Modify部分中使用"cc"来向GCC声明这一点。

附录1:gcc优化选项
代码优化指的是编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行性能。gcc提供的代码优化功能非常强大,它通过编译选项-On来控制优化代码的生成,其中n是一个代表优化级别的整数。对于不同版本的gcc来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。
编译时使用选项-O可以告诉gcc同时减小代码的长度和执行时间,其效果等价于-O1。在这一级别上能够进行的优化类型虽然取决于目标处理器,但一般都会包括线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。
选项-O2告诉gcc除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。
选项-O3则除了完成所有-O2级别的优化之外,还包括循环展开和其他一些与处理器特性相关的优化工作
通常来说,数字越大优化的等级越高,同时也就意味着程序的运行速度越快。许多Linux程序员都喜欢使用-O2选项,因为它在优化长度、编译时间和代码大小之间取得了一个比较理想的平衡点。
0 0
原创粉丝点击