gcc内联汇编

来源:互联网 发布:linux系统服务器配置 编辑:程序博客网 时间:2024/05/21 03:29

GCC Inline ASMGCC支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作 GCC Inline ASM——GCC内联汇编。这是一个非常有用的功能,有利于我们将一些C/C++语法无法表达的指令直接潜入C/C++代码中,另外也允许我们直接写C /C++代码中使用汇编编写简洁高效的代码。1.基本内联汇编基本内联汇编的格式是__asm__ __volatile__("Instruction List");1、__asm____asm__是GCC关键字asm的宏定义:#define __asm__ asm__asm__或asm用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它开头的,是必不可少的。2、Instruction ListInstruction List是汇编指令序列。它可以是空的,比如:__asm__ __volatile__(""); 或__asm__ ("");都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。但并非所有Instruction List为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory"); 就非常有意义,它向GCC声明:“我对内存作了改动”,GCC在编译的时候,会将此因素考虑进去。我们看一看下面这个例子:$ cat example1.cint main(int __argc, char* __argv[]) { int* __p = (int*)__argc; (*__p) = 9999; //__asm__("":::"memory"); if((*__p) == 9999) return 5; return (*__p); }在这段代码中,那条内联汇编是被注释掉的。在这条内联汇编之前,内存指针__p所指向的内存被赋值为9999,随即在内联汇编之后,一条if语句判断__p 所指向的内存与9999是否相等。很明显,它们是相等的。GCC在优化编译的时候能够很聪明的发现这一点。我们使用下面的命令行对其进行编译:$ gcc -O -S example1.c选项-O表示优化编译,我们还可以指定优化等级,比如-O2表示优化等级为2;选项-S表示将C/C++源文件编译为汇编文件,文件名和C/C++文件一样,只不过扩展名由.c变为.s。我们来查看一下被放在example1.s中的编译结果,我们这里仅仅列出了使用gcc 2.96在redhat 7.3上编译后的相关函数部分汇编代码。为了保持清晰性,无关的其它代码未被列出。$ cat example1.smain: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax # int* __p = (int*)__argcmovl $9999, (%eax) # (*__p) = 9999 movl $5, %eax # return 5popl %ebp ret参照一下C源码和编译出的汇编代码,我们会发现汇编代码中,没有if语句相关的代码,而是在赋值语句(*__p)=9999后直接return 5;这是因为GCC认为在(*__p)被赋值之后,在if语句之前没有任何改变(*__p)内容的操作,所以那条if语句的判断条件(*__p) == 9999肯定是为true的,所以GCC就不再生成相关代码,而是直接根据为true的条件生成return 5的汇编代码(GCC使用eax作为保存返回值的寄存器)。我们现在将example1.c中内联汇编的注释去掉,重新编译,由于内联汇编语句__asm__("":::"memory")向GCC声明,在此内联汇编语句出现的位置内存内容可能了改变,所以GCC在编译时就不能像刚才那样处理。这次,GCC老老实实的将if语句生成了汇编代码。可能有人会质疑:为什么要使用__asm__("":::"memory")向GCC声明内存发生了变化?明明“Instruction List”是空的,没有任何对内存的操作,这样做只会增加GCC生成汇编代码的数量。确实,那条内联汇编语句没有对内存作任何操作,事实上它确实什么都没有做。但影响内存内容的不仅仅是你当前正在运行的程序。比如,如果你现在正在操作的内存是一块内存映射,映射的内容是外围I/O设备寄存器。那么操作这块内存的就不仅仅是当前的程序,I/O设备也会去操作这块内存。既然两者都会去操作同一块内存,那么任何一方在任何时候都不能对这块内存的内容想当然。所以当你使用高级语言C/C++写这类程序的时候,你必须让编译器也能够明白这一点,毕竟高级语言最终要被编译为汇编代码。你可能已经注意到了,这次输出的汇编结果中,有两个符号:#APP和#NO_APP,GCC将内联汇编语句中"Instruction List"所列出的指令放在#APP和#NO_APP之间,由于__asm__("":::"memory")中“Instruction List”为空,所以#APP和#NO_APP中间也没有任何内容。但我们以后的例子会更加清楚的表现这一点。关于为什么内联汇编__asm__("":::"memory")是一条声明内存改变的语句,我们后面会详细讨论。刚才我们花了大量的内容来讨论"Instruction List"为空是的情况,但在实际的编程中,"Instruction List"绝大多数情况下都不是空的。它可以有1条或任意多条汇编指令。当在"Instruction List"中有多条指令的时候,任意两个指令间要么被分号(;)分开,要么被放在两行; 放在两行的方法既可以从通过/n的方法来实现,也可以真正的放在两行; 可以使用1对或多对引号,每1对引号里可以放任一多条指令,所有的指令都要被放到引号中。在基本内联汇编中,“Instruction List”的书写的格式和你直接在汇编文件中写非内联汇编没有什么不同,你可以在其中定义Label,定义对齐(.align n ),定义段(.section name )。例如:__asm__(".align 2/n/t" "movl %eax, %ebx/n/t" "test %ebx, %ecx/n/t" "jne error/n/t" "sti/n/t" "error: popl %edi/n/t" "subl %ecx, %ebx");上面例子的格式是Linux内联代码常用的格式,非常整齐。也建议大家都使用这种格式来写内联汇编代码。3、__volatile____volatile__是GCC关键字volatile的宏定义:#define __volatile__ volatile__volatile__ 或volatile是可选的,你可以用它也可以不用它。如果你用了它,则是向GCC声明“不要动我所写的Instruction List,我需要原封不动的保留每一条指令”,否则当你使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。那么GCC判断的原则是什么?我不知道(如果有哪位朋友清楚的话,请告诉我)。我试验了一下,发现一条内联汇编语句如果是基本内联汇编的话(即只有“Instruction List”,没有Input/Output/Clobber的内联汇编,我们后面将会讨论这一点),无论你是否使用__volatile__来修饰,GCC 2.96在优化编译时,都会原封不动的保留内联汇编中的“Instruction List”。但或许我的试验的例子并不充分,所以这一点并不能够得到保证。为了保险起见,如果你不想让GCC的优化影响你的内联汇编代码,你最好在前面都加上__volatile__,而不要依赖于编译器的原则,因为即使你非常了解当前编译器的优化原则,你也无法保证这种原则将来不会发生变化。而__volatile__的含义却是恒定的。2、带有C/C++表达式的内联汇编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__("Instruction List" : Output : Input : Clobber/Modify);从中我们可以看出它和基本内联汇编的不同之处在于:它多了3个部分(Input,Output,Clobber/Modify)。在括号中的4个部分通过冒号(:)分开。这4个部分都不是必须的,任何一个部分都可以为空,其规则为:如果Clobber/Modify为空,则其前面的冒号(:)必须省略。比如__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) : )就是非法的写法;而__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) )则是正确的。 如果Instruction List为空,则Input,Output,Clobber/Modify可以不为空,也可以为空。比如__asm__ ( " " : : : "memory" );和__asm__(" " : : );都是合法的写法。 如果Output,Input,Clobber/Modify都为空,Output,Input之前的冒号(:)既可以省略,也可以不省略。如果都省略,则此汇编退化为一个基本内联汇编,否则,仍然是一个带有C/C++表达式的内联汇编,此时"Instruction List"中的寄存器写法要遵守相关规定,比如寄存器前必须使用两个百分号(%%),而不是像基本汇编格式一样在寄存器前只使用一个百分号(%)。比如__asm__( " mov %%eax, %%ebx" : : );__asm__( " mov %%eax, %%ebx" : )和__asm__( " mov %eax, %ebx" )都是正确的写法,而__asm__( " mov %eax, %ebx" : : );__asm__( " mov %eax, %ebx" : )和__asm__( " mov %%eax, %%ebx" )都是错误的写法。 如果Input,Clobber/Modify为空,但 Output不为空,Input前的冒号(:)既可以省略,也可以不省略。比如__asm__( " mov %%eax, %%ebx" : "=b"(foo) : );__asm__( " mov %%eax, %%ebx" : "=b"(foo) )都是正确的。 如果后面的部分不为空,而前面的部分为空,则前面的冒号(:)都必须保留,否则无法说明不为空的部分究竟是第几部分。比如, Clobber/Modify,Output为空,而Input不为空,则Clobber/Modify前的冒号必须省略(前面的规则),而Output 前的冒号必须为保留。如果Clobber/Modify不为空,而Input和Output都为空,则Input和Output前的冒号都必须保留。比如 __asm__( " mov %%eax, %%ebx" : : "a"(foo) )和__asm__( " mov %%eax, %%ebx" : : : "ebx" )。从上面的规则可以看到另外一个事实,区分一个内联汇编是基本格式的还是带有C/C++表达式格式的,其规则在于在"Instruction List"后是否有冒号(:)的存在,如果没有则是基本格式的,否则,则是带有C/C++表达式格式的。两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个百分号(%),这一点和非内联汇编相同;而带有C/C++表达式格式则要求寄存器前必须使用两个百分号(%%),其原因我们会在后面讨论。1. OutputOutput用来指定当前内联汇编语句的输出。我们看一看这个例子:__asm__("movl %%cr0, %0": "=a" (cr0));输出部分"=a"(cr0)是个“操作表达式”。括号内是C/C++表达式,用来保存内联汇编的一个输出值,它只能是一个可以合法的放在C/C++赋值操作中等号(=)左边的表达式。引号中是“操作约束 ”(Operation Constraint),=说明cr0是Write-Only,不能作为输入。a是寄存器EAX / AX / AL的简写,说明cr0的值要从eax寄存器中获取,也就是说cr0 = eax。[另:=和+都表示可写,+同时也表示可读]在Output域中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。例如:__asm__( "movl %%eax, %0 /n/t" "pushl %%ebx /n/t" "popl %1 /n/t" "movl %1, %2" : "+a"(cr0), "=b"(cr1), "=c"(cr2));2、InputInput域的内容用来指定当前内联汇编语句的输入。例子:__asm__("movl %0, %%db7" : : "a" (cpu->db7));例中Input域的内容为一个表达式"a"[cpu->db7),被称作“输入表达式”,用来表示一个对当前内联汇编的输入。两部分对于一个内联汇编输入表达式来说也是必不可少的: 括号中的表达式cpu->db7是一个C/C++语言的表达式,它可以是一个变量,一个数字,还可以是一个复杂的表达式(比如a+b/c*d)。引号号中的部分是约束部分,它不允许指定加号(+)约束和等号(=)约束,也就是说它只能是默认的Read-Only的。[约束中必须指定一个寄存器约束,例中的字母a表示当前输入变量cpu->db7要通过寄存器eax输入到当前内联汇编中]。我们看一个例子:$ cat example4.cint main(int __argc, char* __argv[]) { int cr0 = 5; __asm__ __volatile__("movl %0, %%cr0"::"a" (cr0)); return 0; }$ gcc -S example4.c$ cat example4.smain: pushl %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 我们从编译出的汇编代码可以看到,在"Instruction List"之前,GCC按照我们的输入约束"a",将变量cr0的内容装入了eax寄存器。3. Operation Constraint每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint,我们这里来讨论在80386平台上所可能使用的操作约束。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__ ("mov %0,%%bx" : : "a"(__shrt));由于变量__shrt是16-bit short类型,则编译出来的汇编代码中,则会让此变量使用%ex寄存器。编译结果为:movw -2(%ebp), %ax # %ax = __shrt#APPmovl %ax, %bx#NO_APP无论是Input,还是Output操作表达式约束,都可以使用寄存器约束。下表中列出了常用的寄存器约束的缩写。约束 Input/Output 意义 r I,O 表示使用一个通用寄存器,由GCC在%eax/%ax/%al, %ebx/%bx/%bl, %ecx/%cx/%cl, %edx/%dx/%dl中选取一个GCC认为合适的。 q I,O 表示使用一个通用寄存器,和r的意义相同。 a I,O 表示使用%eax / %ax / %al b I,O 表示使用%ebx / %bx / %bl c I,O 表示使用%ecx / %cx / %cl d I,O 表示使用%edx / %dx / %dl D I,O 表示使用%edi / %di S I,O 表示使用%esi / %si f I,O 表示使用浮点寄存器 t I,O 表示使用第一个浮点寄存器 u I,O 表示使用第二个浮点寄存器 2、内存约束 如果一个Input/Output操作表达式的C/C++表达式表现为一个内存地址,不想借助于任何寄存器,则可以使用内存约束。比如:__asm__ ("lidt %0" : "=m"(__idt_addr)); 或 __asm__ ("lidt %0" : :"m"(__idt_addr));我们看一下它们分别被放在一个C源文件中,然后被GCC编译后的结果:$ cat example5.c// 本例中,变量sh被作为一个内存输入int main(int __argc, char* __argv[]) { char* sh = (char*)&__argc; __asm__ __volatile__("lidt %0" : : "m" (sh)); return 0; } $ gcc -S example5.c$ cat example5.smain: pushl %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 $ cat example6.c// 本例中,变量sh被作为一个内存输出int main(int __argc, char* __argv[]) { char* sh = (char*)&__argc; __asm__ __volatile__("lidt %0" : "=m" (sh)); return 0; } $ gcc -S example6.c$ cat example6.smain:pushl %ebpmovl %esp, %ebpsubl $4, %espleal 8(%ebp), %eaxmovl %eax, -4(%ebp) # sh = (char*) &__argc#APPlidt -4(%ebp)#NO_APPmovl $0, %eaxleaveret首先,你会注意到,在这两个例子中,变量sh没有借助任何寄存器,而是直接参与了指令lidt的操作。其次,通过仔细观察,你会发现一个惊人的事实,两个例子编译出来的汇编代码是一样的!虽然,一个例子中变量sh作为输入,而另一个例子中变量sh作为输出。这是怎么回事?原来,使用内存方式进行输入输出时,由于不借助寄存器,所以GCC不会按照你的声明对其作任何的输入输出处理。GCC只会直接拿来用,究竟对这个C/C++表达式而言是输入还是输出,完全依赖与你写在"Instruction List"中的指令对其操作的指令。由于上例中,对其操作的指令为lidt,lidt指令的操作数是一个输入型的操作数,所以事实上对变量sh的操作是一个输入操作,即使你把它放在 Output域也不会改变这一点。所以,对此例而言,完全符合语意的写法应该是将sh放在Input域,尽管放在Output域也会有正确的执行结果。所以,对于内存约束类型的操作表达式而言,放在Input域还是放在Output域,对编译结果是没有任何影响的,因为本来我们将一个操作表达式放在 Input域或放在Output域是希望GCC能为我们自动通过寄存器将表达式的值输入或输出。既然对于内存约束类型的操作表达式来说,GCC不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序员的角度而言,为了增强代码的可读性,最好能够把它放在符合实际情况的地方。约束 Input/Output 意义 m I,O 表示使用系统所支持的任何一种内存方式,不需要借助寄存器 3、立即数约束如果一个Input/Output操作表达式的C/C++表达式是一个数字常数,不想借助于任何寄存器,则可以使用立即数约束。由于立即数在C/C++中只能作为右值,所以对于使用立即数约束的表达式而言,只能放在Input域。比如:__asm__ __volatile__("movl %0, %%eax" : : "i" (100) ); 立即数约束很简单,也很容易理解,我们在这里就不再赘述。约束 Input/Output 意义 i I 表示输入表达式是一个立即数(整数),不需要借助任何寄存器 F I 表示输入表达式是一个立即数(浮点数),不需要借助任何寄存器

原创粉丝点击