C指针原理教程之C内嵌汇编
内联汇编的重要性体现在它能够灵活操作,而且可以使其输出通过C变量显示出来。因为它具有这种能力,所以"asm"可以用作汇编指令和包含它的C程序之间的接口。简单得说,内联汇编,就是可以让程序员在C语言中直接嵌入汇编代码,并与汇编代码交互C程序中的C表达式,享受汇编的高运行效率。
内联汇编的格式是直接在C代码中插入以下格式:
asm( .... .... )
其中的"..."为汇编代码,比如下面例子中,在result=a*b和printf("%d\n",result)之间插入一段汇编,
下面的这段汇编什么都不做,每个nop指令占用一个指令的执行时间
result=a*b; asm("nop\n\t" "nop\n\t" "nop\n\t" "nop");//4个nop指令,\n\t表示换行,然后加上TAB行首空,因为每个汇编指令必须在单独一行,需要换行,加上制表符是为了适应某些编译器的要求。 printf("%d\n",result);
可以很明显地看到:
汇编代码之间用“\n\t”间隔,并且每条汇编代码单独占用一行,共有4个nop指令,每个指令后的“\n\t”表示换行,然后加上TAB行首空,因为每个汇编指令必须在单独一行,需要换行,加上制表符是为了适应某些编译器的要求。
下面是一个完整的例子,内嵌的汇编完成对2个C程序定义的全局变量c和d的相加,并将相加结果存入全局变量addresult中:
#includeintc=10; intd=20; intaddresult; intmain(void){ inta=6; intb=2; intresult; result=a*b; asm("nop\n\t" "nop\n\t" "nop\n\t" "nop");//4个nop指令,\n\t表示换行,然后加上TAB行首空,因为每个汇编指令必须在单独一行,需要换行,加上制表符是为了适应某些编译器的要求。 printf("%d\n",result); asm("pusha\n\t" "movlc,%eax\n\t" "movld,%ebx\n\t" "add%ebx,%eax\n\t" "movl%eax,addresult\n\t" "popa");//使用全局C变量c和d printf("%d\n",addresult); return0; }
编译上述代码
$gcc-otesttest.c $./test 12 30
在汇编代码中可以直接使用变量名称操作C程序定义的全局变量,比如c、d和addresult就是全局变量:
"movlc,%eax\n\t" "movld,%ebx\n\t" "movl%eax,addresult\n\t"
内联汇编部分如果不需要编译器优化(优化可能破坏汇编代码的内部结构,因为汇编代码直接操作寄存器,而寄存器使用优化是编译器提供的功能),可以在"asm"后使用关键字"volatile"。
asmvolatile( .... .... )
如果程序必须与ANSIC兼容,则应该使用asm和volatile。
__asm____volatile__( ......... ......... )
下面的代码和刚才代码功能一样,唯一的区别是不需要优化
#includeintc=10; intd=20; intaddresult; intmain(void){ inta=6; intb=2; intresult; result=a*b; //ansic标准的asm有其它用,所以用__asm__,__volatile__表示内联汇编部分不用优化(可以用volatile,但是ansic不行),以防优化破坏内联代码组织结构 __asm____volatile__("nop\n\t" "nop\n\t" "nop\n\t" "nop");//4个nop指令,\n\t表示换行,然后加上TAB行首空,因为每个汇编指令必须在单独一行,需要换行,加上制表符是为了适应某些编译器的要求。 printf("%d\n",result); __asm____volatile__("pusha\n\t" "movlc,%eax\n\t" "movld,%ebx\n\t" "add%ebx,%eax\n\t" "movl%eax,addresult\n\t" "popa");//使用全局C变量c和d printf("%d\n",addresult); return0; }
如何在内联汇编中访问C程序的局部变量呢,请看下面这段代码。
#includeintmain(void){ //不使用全局变量,必须使用扩展GNU的asm //格式为:asm("汇编代码":输出位置:输入位置:改动的寄存器列表) //a为eax,ax,al;b为ebx等;c为ecx等;d为edx等;S为esi或si;D为edi或di //+读和写;=写;%如果必要,操作数可以和下一个操作数切换;&在内联函数完成之前,可以删除或重新使用操作数 intxa=6; intxb=2; intresult; //ansic标准的asm有其它用,所以用__asm__,__volatile__表示内联汇编部分不用优化(可以用volatile,但是ansic不行),以防优化破坏内联代码组织结构 asmvolatile( "add%%ebx,%%eax\n\t" "movl$2,%%ecx\n\t" "mul%%ecx\n\t" "movl%%eax,%%edx" :"=d"(result):"a"(xa),"b"(xb):"%ecx");//注意扩展方式使用2个%表示 printf("%d\n",result); return0; }
这个例子完成这个计算:(xa+xb)2=(6+2)2=16
不使用全局变量与汇编代码交互,我们必须使用扩展GNU的asm,格式为:
asm("汇编代码":输出位置:输入位置:改动的寄存器列表)
汇编代码中涉及寄存器部分的使用2个“%”,如:使用%%eax表示eax寄存器
输出位置、输入位置的特殊命名规则为:
a为eax,ax,al;b为ebx等;c为ecx等;d为edx等;S为esi或si;D为edi或di
+读和写
=写
%如果必要,操作数可以和下一个操作数切换
&在内联函数完成之前,可以删除或重新使用操作数
上述代码中,汇编代码部分为
输出位置、输入位置、改动的寄存器列表部分为:
:"=d"(result):"a"(xa),"b"(xb):"%ecx"
先来看汇编代码部分,使用双%号表示寄存器,比如:
"add%%ebx,%%eax\n\t"
关于输出位置、输入位置部分,可以这么理解:将变量与寄存器绑定,绑定后,对寄存器的操作就是对变量的操作。
:"=d"(result):"a"(xa),"b"(xb)
将result与寄存器edx绑定,xa与寄存器eax绑定,xb与寄存器ebx绑定。
%ecx属于需要改动的寄存器
#includeintmain(void){ intxa=6; intxb=2; intresult; //使用占位符,由r表示,编译器自主选择使用哪些寄存器,%0,%1。。。表示第1、2。。。个变量 asmvolatile( "add%1,%2\n\t" "movl%2,%0" :"=r"(result):"r"(xa),"r"(xb)); printf("%d\n",result); return0; }
result、xa、xb绑定的寄存器由编译器决定,前面的例子中我们采用直接指定的方式,在这里我们改成由编译器
自主选择,"r"是占位符,表示由编译器自主选择使用哪些寄存器,不指定哪个变量绑定在哪个寄存器上,
:"=r"(result):"r"(xa),"r"(xb)
那我们如何知道这些变量绑定在哪些寄存器上呢,不知道绑定的寄存器,如何对变量进行操作呢,可以使用
%0,%1这样的符号来代替要操作的寄存器,%后的数字表示第几个变量,如:%0,%1。。。表示第1、2。。。个变量。
:"=r"(result):"r"(xa),"r"(xb)
上面这个输出和输入列表已经指定了变量的顺序,
result是第0个,xa是第1个,xb是第2个
下面的例子完成 xb=xb-xa的计算,问题出现了,可能会导致xb被分配了2个寄存器:
:"=r"(xb):"r"(xa),"r"(xb));
使用引用占位符能有效地使用可用寄存器,在这里我们指定xb使用第0个变量绑定的寄存器
:"=r"(xb):"r"(xa),"0"(xb));
第0个变量就是xb,即xb绑定的寄存器被修改后,结果仍写回原寄存器
下面是完整例子
#includeintmain(void){ intxa=2; intxb=6; asmvolatile( "subl%1,%0\n\t" :"=r"(xb):"r"(xa),"0"(xb)); printf("%d\n",xb); return0; }
我们编译运行一下
$gcc-otesttest.c $./test 4
用数字来表示变量的顺序也许很麻烦,我们可以使用更简单的方法,使用“[标识]”的格式标记绑定后的变量。 下面的代码完成xb=xb+xa的计算
#includeintmain(void){ intxa=6; intxb=2; asmvolatile( "add%[mya],%[myb]\n\t" :[myb]"=r"(xb):[mya]"r"(xa),"0"(xb)); printf("%d\n",xb); return0; }
我们使用m标记可以直接在内存中对数进行操作,前面的例子对变量进行操作时都需要将变量值存储在要修改的寄存器中,然后将它写回内存位置中.
#includeintmain(void){ intxa=2; intxb=6; asmvolatile( "subl%1,%0\n\t" :"=r"(xb):"m"(xa),"0"(xb)); printf("%d\n",xb); return0; }
我们直接从xa的内存地址中将xa取出,而不需要再将xa先存储在一个寄存器。
首先,我们看一下AT&T汇编各段的意义
节含义
.text已编译程序的机器代码
.rodata只读数据,如pintf和switch语句中的字符串和常量值
.data已初始化的全局变量
.bss未初始化的全局变量
.symtab符号表,存放在程序中被定义和引用的函数和全局变量的信息
.rel.text当链接器吧这个目标文件和其他文件结合时,.text节中的信息需修改
.rel.data被模块定义和引用的任何全局变量的信息
.debug一个调试符号表。
.line原始C程序的行号和.text节中机器指令之间的映射
.strtab一个字符串表,其内容包含.systab和.debug节中的符号表
上面列表也许比较抽象,我们从一个C程序生成的中间汇编代码分析:
#includevoidmain(){ char*x="xxxx"; chary[]="yy";//y的16进制ASCII码是97,9797的十进制为31097 printf("%s-----%s",x,y); exit(0); }
我们使用gcc-Stestcr.c,查看编译生成的汇编代码(为便于理解,将生成的汇编代码进行了注释)
.file"testcr.c" .section.rodata .LC0: .string"xxxx"#使用char*分配 .LC1: .string"%s-----%s" .text .globlmain .typemain,@function main: pushl%ebp movl%esp,%ebp andl$-16,%esp subl$32,%esp#分配32字节栈空间,根据变量情况分配 movl$.LC0,24(%esp)#x变量使用指针(4个字节大小),放入栈中,可以看到,变量分配靠近栈空间的尾部 movw$31097,29(%esp)#字符'yy'移到main程序的栈中,直接将y变量的值放入栈中 movb$0,31(%esp)#加上NULL标志,表示字符结束 movl$.LC1,%eax leal29(%esp),%edx movl%edx,8(%esp) movl24(%esp),%edx movl%edx,4(%esp) movl%eax,(%esp) callprintf movl$0,(%esp) callexit .sizemain,.-main .ident"GCC:(Ubuntu4.4.3-4ubuntu5)4.4.3" .section.note.GNU-stack,"",@progbits
在MAIN函数中char*分配在只读数据段中,实际使用时,只在程序栈中分配一个指针的空间。char[]在程序栈中分配空间,然后直接使用movl、movw之类的汇编直接把值放入栈中空间。那么在其它函数中声明的呢,可以从以下程序中看出,仍然如此。
#includevoidmyprinf(){ char*x="xxxx"; chary[]="yy";//y的16进制ASCII码是97,9797的十进制为31097 printf("%s-----%s",x,y); } voidmain(){ intnum=1; myprint(); exit(0); }
生成的中间汇编代码为:
.file"testcr.c" .section.rodata .LC0: .string"xxxx" .LC1: .string"%s-----%s" .text .globlmyprinf .typemyprinf,@function myprinf: pushl%ebp movl%esp,%ebp subl$40,%esp movl$.LC0,-16(%ebp) movw$31097,-11(%ebp) movb$0,-9(%ebp) movl$.LC1,%eax leal-11(%ebp),%edx movl%edx,8(%esp) movl-16(%ebp),%edx movl%edx,4(%esp) movl%eax,(%esp) callprintf leave ret .sizemyprinf,.-myprinf .globlmain .typemain,@function main: pushl%ebp movl%esp,%ebp andl$-16,%esp subl$32,%esp movl$1,28(%esp) callmyprint movl$0,(%esp) callexit .sizemain,.-main .ident"GCC:(Ubuntu4.4.3-4ubuntu5)4.4.3" .section.note.GNU-stack,"",@progbits
内存的常用分配方式有:
第一,静态分配,所有名字在编译时绑定某个存储位置。不能在运行时改变
第二,栈分配,活动时压入系统栈。
第三,堆分配,以任意次序分配