VC ASM

vc程序中嵌入汇编的语句的说明
2008年10月08日 星期三 08:47

为了加速游戏,一提起汇编语言,大家也许会感到很神秘。其实如果你学起来就会发现,它并非想象中那样难。特别是内嵌汇编,由于它和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( )
{
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
clc CF=0
stc CF=1
cmc CF=1-CF
add a,b a=a+b (结果过大可能会有古怪的结果,且置CF 1)
adc a,b a=a+b+CF (加上进位)
sub a,b a=a-b (如结果小于0会加上2的16或32次方,且置CF 1)
sbb a,b a=a-b-CF (减去退位)
inc a a++
dec a a- -
neg a a=-a
mul a eax=eax*a后的低32位, edx=高32位例: mov eax,234723 mov edx, 12912189 mul edx; 则eax=2835794967 edx=705
div a eax=(edx eax)/a的商, edx=余数例: mov eax,12121 mov 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 PF AX或BX
mov ax, 7896 ? ? ? ? ? 7896
add al, ah 1 0 0 0 0 780e
add ah, al 0 0 1 1 0 860e
add al, f2 1 1 0 0 1 8600
add al, 1234 0 0 1 0 0 9834

mov bx, 9048 ? ? ? ? ? 9048
sub bh, bl 0 0 0 1 1 4848
sub bl, bh 0 1 0 0 1 4800
sub bl, 5 1 0 1 0 0 48fb
sub bx, 8f34 1 0 1 1 0 b9c7  

6.4 逻辑与移位指令
逻辑指令会将标志寄存器中的OF和CF清零。
表6.3
not a a=~a(注意not与neg不同!)
and a, b a=a&b
or a, b a=a|b
xor a, b a=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 / JZ ZF=1时转移
JNE / JNZ ZF=0时转移

JS SF=1时转移
JNS SF=0时转移
JO OF=1时转移
JNO OF=0时转移
JP / JPE PF=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)

特殊转移语句:
JECXZ CX=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) a = b; else a = 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]<<" ";
}  

 

如何用VC6.0集成开发环境来构建MASM32汇编的编程环境(原创)

作者:maxzhou88(周哥)

   开发高性能的程序少不了用汇编编程,限于C语言中内嵌汇编语言的局限(如在C代码中混合汇编语言编程时,很难实现跳表),一些代码必须书写在独立的 汇编源文件中。遗憾的是,在调试时,只能看到裸的汇编码,而不能到懂标号,变量等,现在能这样C与ASM混合编程就方便多了。

编程环境:VC6.0,Masm32v8

优点:
[1] 使用VC6.0集成开发环境可以利用其强大的Debug功能来实现源代码级调试(Source Code Debug),(看变量、设置断点、查看MASM32的高级命令展开......)
[2] 使用VC6.0的资源编辑器,可视化编辑资源。
[3] 实现C/C++与ASM的混合编程。
[4] 在窗口下比在CMD下工作符合一般人的习惯,再也不需要写makefile文件或xxx.bat文件了。

方法:
[1] 安装VC6.0,这个我就说了,大家都会的哦,一般我将它安装在 C:/Program Files/ Microsoft Visual Studio。
[2] 安装MASMv8.exe,一般我将它安装在 D:/MASM32 下。
[3] 将Win32ASM(masm32)中的ml.exe和ml.err拷贝到C:/Program Files/ Microsoft Visual Studio/VC98/Bin
[4] 使用VC6.0新建一个空工程,如:hello(类型为:win32 console、win32 application、DLL等均可)。
[5] 把汇编和资源文件拷至新建工程目录下(hello.asm,hello.rc),并把这些文件加入工程中(将*.asm添加到Source Files, 将*.rc添加到Resource Files)。
[6] 配置IDE(这就配置一次就搞定):在VC的菜单tools/option…/paths(include files)中添加一个路径d:/MASM32/INCLUDE 
[7] 配置工程(每个工程都要这样配置):打开工程设置(project settings),点击*.asm文件选择(Custom Build),
                                      命令中加上: ml /c /coff /Zi /Fo$(TargetDir)/$(InputName).obj $(InputPath)
                                      输出中加上: $(TargetDir)/$(InputName).obj
    注意选Settings for: Win32 Debug 和 Win32 release,将上面的两条都加上,而且每个*.ASM文件(如有多个ASM时)都这样设置。

[8] 点击VC的编译(build)就可以运行了哦。

    至此就可以使用VC调试器源码级调试汇编程序了,可以设置断点,察看变量、内存、寄存器等,masm出现语法错误时可以双击output window中的错误行定位到程序中的指定行。
    为了不在debug和release目录中产生超大的预编译头文件xxxx.pch,在工程设置(project settings)中的C/C++ -> Precompiled Headers的选项上选择"不使用预编译头",其实就现在的电脑而言,不使用预编译头也能快速地编译C代码的哦.

    资源编译/编辑器蛮好用的.只是还有个问题我也没有搞懂:VC6.0的资源编译/编辑器不支持16进制的资源ID,非要用10进制的,我是用两种方法解决 的:一是将资源文件*.rc中的ID改成10进制,这样就可以编辑修改了;二是直接用老罗书中的rc文件导入,但在VC6.0中不要打开编辑它就可以了. 有人能告诉我VC6.0的资源编辑器用16进制ID的方法嘛,可能这很简单,我没有去深究罢了.

有空来踩踩我的空间哦:http://hi.baidu.com/maxzhou88
该文在我的百度空间:http://hi.baidu.com/maxzhou88/blog/item/7c7b4b09181ce186d1581b8f.html
Win32 C++/ASM 混合编程的Demo下载: http://maxzhou88.ys168.com/pc

    我在学老罗的MASM32程序时都是象这样在VC6.0的集成环境下工作的,他书中的例子我基本都试验过,比在DOS(CMD窗口)下方便多了,也不要什 么makefile文件,建个proj项目就搞定,最重要的是用VC的资源编辑器来编辑资源比原先手工编辑方便多了,如果有什么问题欢迎大家交流哦!

                                                         周哥(maxzhou88) 写于 2009-01-21

为了方便大家配置VC6.0 ,我在这里贴两张图:

这是配置项目中每个ASM文件的Custom Build 注意:Debug 和 Release 版本都要设置)


这是配置VC6.0的IDE,就设置一遍就搞定了。

不过这两张图是我百度空间的,但百度是不提供图片外链服务的哦!别急,先分别右击这两张图的图框,在弹出菜单的“属性”中COPY图片的URL地址,然后 将地址在IE中打开,这时图片就下载你机器的IE缓冲区中,再将这个看不到图的页面按F5刷新一下,哈哈!IE就直接从缓冲区中取图了,你也就看得见了。 没有办法啊,对不能外链的图就只能这样麻烦得做了哦。

    另外,我在调试MASM32程序时还经常开CMD窗来实时查看变量,这也是种很好的Debug方法,其实在Win32下无所谓 windows窗口程序和console 控制台程序,也就是说在建立工程时可以选console 控制台程序类型来写windows窗口程序,反之亦然。我一般是这样做的,先将工程以windows窗口程序类型来建立新工程,当要用CMD窗口来调试输 出变量时,我就将工程的link选项卡中的Project options中的subsystem:windows 换成 subsystem:console就可以了,这时候编译连接后程序运行就会出CMD窗口,当调试完后再改回subsystem:windows即可。


CMD控制台输出的代码片段如下:

.data?
szBuffer   db   'hello the world' ,0dh, 0ah ;要调试的输出内容
hStdOut   dd   ? ;控制台标准输出句柄,在CMD中是默认打开的
dwBytesWrite dd   ?
;用下列语句就可以实现CMD的输出
invoke WriteConsole, hStdOut, addr szBuffer, sizeof szBuffer, addr dwBytesWrite, NULL

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值