内嵌汇编(C++中嵌入汇编语句)

原文地址:http://blog.sina.com.cn/s/blog_622bd1660100pdqm.html


为了加速游戏,一提起汇编语言,大家也许会感到很神秘。其实如果你学起来就会发现,它并非想象中那样难。特别是内嵌汇编,由于它和C++紧密结合,使你不必考虑很多烦琐的细节(例如输入输出函数的写法),学习起来比较容易。使用内嵌汇编,特别是使用MMX指令,可以大大提高各种游戏中常见特效的速度,对于编出一个漂亮的游戏非常重要。学好汇编语言还有一个特别有趣的用处:可以观察和看懂VC++生成的汇编代码,从而更好地了解C++语言本身和优化代码。  
   
  6.1  内嵌汇编简介  
 在高级语言中,我们可以无所顾忌地使用各种语句,再由编译器将语句经过非常复杂的编译过程将其转换为机器指令后运行。事实上,处理器本身所能处理的指令不多;更糟糕的是,大部分指令不能直接施用在内存中的变量上,要借助寄存器这个中间存储单元(你可以把寄存器看做是一个变量)。Pentium级处理器的寄存器不多,只有8个32位通用寄存器,分别被称为EAX,  EBX,  ECX,  EDX,  EBP,  ESP,  EDI   ESI。每一个通用寄存器的低16位又分别被称为AX,  BX,  CX,  DX,  BP,  SP,  DI   SI。其中AX,  BX,  CX,  DX的高8位被称为AH,  BH,  CH,  DH;低8位被称为AL,  BL,  CL,  DL。注意在内嵌汇编中不应使用EBP和ESP,它们存储着重要的堆栈信息。  
 还有一个非常重要的寄存器,叫做标志寄存器(EFLAGS),标明了运算结果的各个属性,你不能直接读取或修改它。这些属性有:不溢出/溢出(OF)、正/负(SF)、非零/零(ZF)、偶/奇(PF)、不进位/进位(CF)等。  
 汇编语言中若要表示有符号整数,需先写出该整数的绝对值的二进制形式,若此数为正数或零则已得到结果,否则将其取反(0->1,1->0)后再加上一即为结果。所以一个8位寄存器可表示的有符号整数范围为从-128到127。  
 与C++类似,汇编语言提供了得到指针所指内存的方法,这被称为"寻址"。用法很简单,象这样:[寄存器+寄存器*1/2/4/8+32位立即数]就可以得到这个位置的数了。举一个例子,如果有一个数组unsigned  short  A[100],且EAX中存储着A[0]的地址,那么[EAX+58]即为A[29]的值;如果此时EBX=9,那么[EAX+EBX*2+4]将是A[11]的值。  
 那么又怎么把一个变量的地址装载进寄存器呢?后面将会介绍。  
 内嵌汇编的使用方法是:  
  _asm  
  
  语句  //后面可加可不加分号  
  
 你可以把它插入程序中的任何位置,非常灵活。  
6.2  基本指令  
 基本指令均不影响标志寄存器。  
 第一条指令是传送指令:MOV  DEST,  SRC。其作用为将DEST赋以值SRC。其中DEST和SRC可为整数(称为立即数)、变量或[地址](存储器),寄存器。需注意的是有的操作是不允许的:在汇编语言中你永远不能将存储器或寄存器内容赋给立即数(你见过5=a这样的语句吗?);也不能将存储器内容直接赋给另一存储器,必须借助寄存器作为中间变量来实现。关于MOV还有一点要注意的是DEST和SRC必须都为32位/16位/8位,即同一大小。值得特别注意的是,数据在内存中的存储方式是以字节为单位颠倒的,即:如果内存地址0000存储的字节是5F,地址0001存储的字节是34,地址0002存储的字节是6A,地址0003存储的字节是C4,那么地址0000处存储的字(WORD,16位)为345F,双字(DWORD,32位)为C46A345F。  
 第二条指令是地址装载指令:LEA  A,  B。其作用为将B变量的地址装载进A寄存器(A需为32位)。要注意的是不能像LEA  EAX,  Temp[5]这样直接调数组中某个元素的地址。这个指令还可以用来进行简单的运算,考虑下面的语句:LEA  EAX,  [EBX+ECX*4+8],此语句可将EBX+ECX*4+8的值赋给EAX。  
 OK,让我们看一个可以将两个正整数相加的程序:  
 #include  <iostream>  
  using  namespace  std;  
 //此程序也展示了内嵌汇编应如何使用C++中的指针  
  void  main(   
   
  int  a,b;  //unsigned  int  a,b;  
 cin>>a;  
 cin>>b;  
  int  *c   &a;  
  __asm  //下面是内嵌汇编...  
  
  mov  eax,  c;  //c中存储的a的地址->eax    
  mov  eax,  [eax];  //a的值->eax   
  //注意直接mov  eax,  [c]是错误的   
  mov  ebx,  b;  //可以像这样直接对ebx赋值  
  lea  eax,  [eax+ebx];  
  mov  a,  eax;  //可以直接将eax的值->a  
  //内嵌汇编部分结束...  
  cout<<a;  
 }
  
 第三条指令是交换指令,形式为XCHG  A,  B。A和B中至少有一个须为寄存器。如果你想交换两处内存中的数据则要使用寄存器作为中间人。  
 接着是扩展传送指令,共有两条,为MOVSX  DEST,  SRC和MOVZX  DEST,  SRC,它们的用处分别是将SRC中的有符号数或无符号数赋给DEST。这时你就可以将字长较短的寄存器的内容赋给字长较长的寄存器,反之则不行。  
 大家会发现,8个通用寄存器实在无法满足编程的要求。为了解决这一矛盾,引入了堆栈这一聪明的设想。你可以把堆栈想象为一块放箱子的区域,用入栈(PUSH)可将一个箱子放在现有箱子的最顶端,而出栈(POP)可将现有箱子最顶端的那个箱子取出。看看下面的指令吧:  
  push  eax  //eax进栈,  堆栈为eax  
  push  ebx  //eax进栈,  堆栈为eax  ebx  
  push  ecx  //eax进栈,  堆栈为eax  ebx  ecx  
  pop  ebx    //ebx=ecx,  堆栈为eax  ebx  
  pop  eax    //eax=ebx,  堆栈为eax  
  pop  ecx    //ecx=eax,  堆栈空  
 可以看到,堆栈不仅可以方便地暂时存储数据而且还可以调整他们的次序。  
   
  6.3  算术指令  
 算术指令大都影响标志寄存器。这些指令比较容易明白,现在将其列出:  
  表6.1  
  clcCF=0  
  stcCF=1  
  cmcCF=1-CF  
  add   a,ba=a+b    (结果过大可能会有古怪的结果,且置CF  1)  
  adc   a,ba=a+b+CF  (加上进位)  
  sub   a,ba=a-b      (如结果小于0会加上2的16或32次方,且置CF  1)  
  sbb   a,ba=a-b-CF    (减去退位)  
  inc   aa++  
  dec   aa-   
  neg   aa=-a  
  mul   aeax=eax*a后的低32位,   edx=高32位例:mov  eax,234723                mov  edx,  12912189              mul  edx;                则eax=2835794967      edx=705  
  div   aeax=(edx  eax)/a的商,   edx=余数例:mov   eax,12121mov  edx,2                此时(edx  eax)=8589946713                mov  ebx,121                div  ebx;                  则eax=70991295              edx=18  
  imul   idiv  dest,   src有符号数乘   除法,dest=dest乘   除src  
  imul   idiv  dest,  s1,   s2有符号数乘   除法,dest=s1乘   除s2  
   
 为了让大家弄懂标志,请看两段程序(出现的数都为十六进制数):  
  表6.2  
  指令 CF ZF SF OF PFAX或BX  
  mov  ax,   7896 ? ? ? ? ?7896  
  add  al,   ah 1 0 0 0 0780e  
  add  ah,   al 0 0 1 1 0860e  
  add  al,   f2 1 1 0 0 18600  
  add  al,   1234 0 0 1 0 09834  
   
  mov  bx,   9048 ? ? ? ? ?9048  
  sub  bh,   bl 0 0 0 1 14848  
  sub  bl,   bh 0 1 0 0 14800  
  sub  bl,   5 1 0 1 0 048fb  
  sub  bx,   8f34 1 0 1 1 0b9c7  

6.4  逻辑与移位指令  
 逻辑指令会将标志寄存器中的OF和CF清零。  
  表6.3  
  not   aa=~a(注意not与neg不同!)  
  and  a,   ba=a&b  
  or  a,   ba=a|b  
  xor  a,   ba=a^b  
   
 下面是移位指令,其中x可为8位立即数或CL寄存器。  
  表6.4  
  sal(也可写成shl)  a,   x将a左移x位,CF=移出的那一位数空位用0补足  
  sar  a,   x将有符号a右移x位,CF=移出的那一位数空位按a的符号用0/1补足  
  shr  a,   x将无符号a右移x位,CF=移出的那一位数空位用0补足  
  rol  a,   x将a循环左移(左边出去的数又从最右边回来)  
  ror  a,   x将a循环右移(右边出去的数又从最左边回来)  
  rcl   rcr  a,   x把CF放在目标最左边然后循环左/右移  
  shld  a,  b,   x将a左移x位,  空出位用b高端m位填充例:shld  edx,  eax,  16可将eax的高16位  放入dx中。  
  shrd  a,  b,   x将a右移x位,  空出位用b低端m位填充  
   
  6.5  比较、测试、转移与循环指令  
 比较与测试指令基本上总是与转移指令相配合使用,其形式分别为CMP  a,  b和TEST  a,  b。CMP实际上是根据a-b的值改变标志寄存器但不改变a和b,可以检测两个数的大小关系。TEST则是根据a&b的值改变标志寄存器,同样不改变a和b。这条指令可以用来测试a中哪些位为1。执行完这些指令后,立刻用转移指令就可实现条件转移,因为条件转移语句会根据标志寄存器决定是否转移。转移指令的使用方法就像这样:  
  __asm{  
 _addax:    add  ax,1;  //_addax是标号  
     jmp  _addax;  
      
  转移指令有:  
  JMP无条件转移                                                      
  JE    JZZF=1时转移  
  JNE    JNZZF=0时转移  
   
  JSSF=1时转移  
  JNSSF=0时转移  
  JOOF=1时转移  
  JNO  OF=0时转移  
  JP    JPEPF=1时转移  
  JNP   JPO  PF=0时转移  
   
 根据两无符号数关系转移:  
  JA    JNBE大于时转移  (CF或ZF=0)  
  JBE    JNA不大于时转移  (CF或ZF=1)  
  JB   JNAE    JC小于时转移  (CF=1)  
  JNB   JAE   JNC  不小于时转移  (CF=0)  
   
 根据两有符号数关系转移:  
  JNLE    JG大于时转移  ((SF异或OF)或ZF)=0  )  
  JLE    JNG不大于时转移  ((SF异或OF)或ZF)=1  )  
  JL   JNGE          小于时转移  (SF异或OF=1)  
  JNL    JGE不小于时转移  (SF异或OF=0)  
   
 特殊转移语句:  
  JECXZCX=0时转移  
   
 为了记住这么多条指令,你只需知道一点,就是无符号数之间的关系分别被称为Above,Equal,Below,分别代表大于,等于,小于;有符号数之间相应的关系则分别被称为Great,Equal,Less。  
 事实上,有些转移是可以避免的。举个例子,要算一个数的绝对值是否要用转移呢?请看一段程序:  
  MOV  EDX,EAX    
  SAR  EDX,31      //EDX现在全为EAX的符号位  
  XOR  EAX,EDX  
  SUB  EAX,EDX  
   
 找出两个数中较大的一个应该要用转移吧?不过也可以象下面的解决方案那样利用标志,真是绝了:  
  SUB  EBX,EAX  
  SBB  ECX,ECX    //如果EBX≥EAX,现在ECX=0,否则ECX=FFFFFFFF  
  AND  ECX,EBX  
  ADD  EAX,ECX  
   
  下面的一段程序实现了if  (a  !=  0)    b;  else    c;  
  CMP  EAX,1  
  SBB  EAX,EAX  
  XOR  ECX,EBX  
  AND  EAX,ECX  
  XOR  EAX,EBX  
   
 循环语句常用的是LOOP,它等价于DEC  CX加上JNZ。  
   
 下面看一个汇编的综合运用:冒泡排序。  
#include<iostream>  
using namespace std;  

#define array_size 10  

int a[array_size]={42, 73, 65, 97, 23, 59,18, 84, 36, 6};  

void main()  
 
 int  *p;  
 p=&a[0];  
 p--;  

 __asm  
  
  mov  esi,p;  
  mov  ecx,array_size;  
_outloop:  
  mov  edx,ecx;  
_inloop:  
  mov  eax,   esi+ecx*4  ];  //一个int占4字节  
  mov  ebx,   esi+edx*4  ];  
  cmp  eax,  ebx;  
  jnb  _noxchg;  //不交换  
  mov   esi+ecx*4  ],  ebx;  
  mov   esi+edx*4  ],  eax;  
_noxchg:  
  dec  edx;  
  jnz  _inloop;  
  loop  _outloop;  
  

  for(int i=0; i<10;i++)  
    cout<<a[i]<<"";  

//附加例子===================================================================================

#include <iostream>
#include <ctime>
using  namespace  std;  
 
void  main(   
 
 inta,b,sum;  
 cin>>a;  
 cin>>b;
 double cost;
 clock_t start, finish;

   start = clock();
 for(int i=0; i<1000000000;i++)
 {
  __asm  //下面是内嵌汇编...  
   
   mov  eax,  a;  //c中存储的a的地址->eax    
   mov  ebx,  b;  //可以像这样直接对ebx赋值  
   add  eax, ebx
   mov  sum,   eax; //可以直接将eax的值->a  
      //内嵌汇编部分结束... 
 }
 finish  = clock();
 cost = (double)(finish - start) /CLOCKS_PER_SEC;
 cout<<"汇编耗时:"<<cost<<endl;

 start = clock();
 for(int i=0; i<1000000000;i++)
 {
  sum = a+b;
 }
 finish  = clock();
 cost = (double)(finish - start) /CLOCKS_PER_SEC; 
 cout<<"C++耗时:"<<cost<<endl;

 cout<<a<<"+"<<b<<"="<<sum<<endl; 
 
//字节 AH\AL
//字   AX
//双字 EAX

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值