四、高级篇
说是高级篇,其实应该算提高篇,我也没有达到高级的水平,更不可能写出高级水平的代码,我希望一些大牛能够对这篇文章进行增补,以达到真正的高级。写这节内容实在是不好写,不知道该写些什么,所以随便找了些内容填充。谁叫你没事找事干,既然干了,就得把它继续下去。本节内容,注册机中引入密码学算法源码、自定义函数的方法和调用API函数方法、嵌入汇编。
现在密码学已经被应用到很多的软件中去,在算法中应用一些密码学知识可以增加软件被调试的难度,论坛上一些前辈已经把一些密码学知识摸的很透了,不知道什么时候能达到那水平,幸运的是现在网络上到处都可以找到一些常用算法的加密或解密源码,这些是题外话,我们这里只讨论如何做这种注册机。我们以最简单的MD5算法为实例(写教程找简单的来做例子多省事,呵呵),了解如何在注册机总引入MD5算法源码。
一、 VB
先讲讲VB的模块,VB代码存储在模块中,模块有三种类型:窗体模块,标准模块,类模块.窗体模块。标准模块(.bas)是应用程序内其它模块访问的过程和声明的容器。类模块(.cls)是面向对象编程的基础。可在类模块中编写代码建立新对象。我们来看看VB中怎么引入这些模块。
用菜单中的“工程”-“添加模块”,这时你可以把MD5源码贴在这个类中(或者直接导入已有的MD5.BAS),引用时只要MD5(STRING,32)就可以了,具体代码看附件。假如是类模块怎么引用呢,同样添加一个类模块,贴上相关代码,引用时需要初始化类模块,Set C1 = New Class1,C1.Md5_String_Calc(STRING)(因为类模块中声名的全局函数名称是Md5_String_Calc),看附件。
二、 DELPHI
DELPHI中引用MD5需要添加PAS单元。具体操作:FILE-NEW-UNIT添加新的
单元,贴入相关代码,引用时注意单元中的函数声明,具体代码看附件。
三、 VC
相对来说,VC中引入MD5代码麻烦多了。需要添加新类,引入global.h 头文
件,md5.h 头文件,md5c.c 算法的核心部分,具体看附件中的调用实例。
我们知道,有些函数的功能用软件本身的自带函数是实现不了的,这时候就需要自己写函数或者调用WIN32的相关API函数才能实现某些功能,毕竟软件本身的内部函数实在是太少了,下面我们来讲讲如何写自定义函数和如何引用WINDOWS API函数。
一、 VB
l VB的自定义函数格式:
Private Function 函数名(变量名 数值类型)数值类型
....中间写过程
End Function结束
在窗口的代码区,随便找个地方,但不要和其它过程相交就行,如一个简单的进制转换例子:自定义一个将十进制转为二进制的函数DEC_to_BIN。
Public Function DEC_to_BIN(Dec As Long) As String
DEC_to_BIN = ""
Do While Dec > 0
DEC_to_BIN = Dec Mod 2 & DEC_to_BIN
Dec = Dec \ 2
Loop
End Function
l VB中使用API函数:
在VB中声明API函数有两种方法:如果我们只在某个窗体中使用API函数,我们可以在窗体代码的General部分声明它:
声明的语法是:
Private Declare Function ...
Private Declare Sub.....
这里必须采用Private声明,因为这个API函数只能被一个窗体内的程序所调用。
如果我们的程序有多个窗体构成,而且我们需要在多个窗体中使用同一个API函数,就需要在模块中声明了。先添加一个模块,然后采用如下语法声明:
Public Declare Function....
Public Declare Sub....
Public声明的含义是把API函数作为一个公共函数或过程,在一个工程中的任何位置(包括所有的窗体和模块)都能直接调用它。 声明完毕我们就能在程序中使用此API函数了。
可采用以下几种方式使用API函数,以SetWindowPos函数为例:
(1)忽略函数返回值的调用:
SetWindowPos Form1.hWnd, -2 ,0 ,0 ,0, 0, 3
注意此时函数的参数是不加括号的。
(2)Call方法调用:
Call SetWindowPos(Form1.hWnd, -2, 0, 0, 0, 3)
注意这里需要加上括号,但我们不取回函数的返回值。
(3)取得函数返回值的调用:
MyLng = SetWindowPos(Form1.hWnd, -2, 0, 0, 0, 3)
此时需要加上括号,而且我们必须事先定义一个变量(变量的类型与函数返回值类型相同)来存储API函数的返回值。
一个典型的例子,取硬盘序列号:
在模块中加入下列声明:
Public Declare Function GetVolumeInformation Lib "kernel32" _
Alias "GetVolumeInformationA" (ByVal lpRootPathName As String, _
ByVal lpVolumeNameBuffer As String, ByVal nVolumeNameSize As Long, _
lpVolumeSerialNumber As Long, lpMaximumComponentLength As Long, _
lpFileSystemFlags As Long, ByVal lpFileSystemNameBuffer As String, _
ByVal nFileSystemNameSize As Long) As Long
'得到某一磁盘分区的信息,如C:
窗体代码如下:
Option Explicit
Private Regid, Localid As Long
Private Sub CmdLocalID_Click()
'根据C盘序列号得到原ID
Dim Driver, VolName, Fsys As String
Dim volNumber, MCM, FSF As Long
Driver = "c:\"
Dim res As Long
res = GetVolumeInformation(Driver, VolName, 127, volNumber, MCM, FSF, Fsys, 127)
'volNumber是C盘序列号
End Sub
二、DELPHI
l 自定义函数
Procedure 函数名( 变量名,数值类型):数值类型 ;
var
//加入定义
Begin
//加入程序语句
End;
例:十进制转二进制
Function DecTobin(Value :Integer) : string;
Var
ST:String;
N:Integer;
Begin
ST:='';
n:=value;
While n>=2 Do
Begin
st:=st+IntToStr(mod_num(n,2));
n:=n div 2;
End;
st:=st+IntToStr(n);
Result:=reverse(st);
End;
l 调用API
在Delphi中调用Windows API 函数十分方便,只需在单元的uses段加入Windows 单元名即可(对于由Delphi自动创建的单元,该项工作已经完成)。单元Windows.pas 已经由Delphi编写并直接提供开发者引用。
取硬盘序列号:
function GetHDNumber(Drv : String): DWORD; //得到硬盘序列号
var
VolumeSerialNumber : DWORD;
MaximumComponentLength : DWORD;
FileSystemFlags : DWORD;
begin
if Drv[Length(Drv)] =':' then Drv := Drv + '';
GetVolumeInformation(pChar(Drv),
nil,
0,
@VolumeSerialNumber,
MaximumComponentLength,
FileSystemFlags,
nil,
0);
Result:= (VolumeSerialNumber);
end;
三、VC++
l 自定义函数
自定义函数可以与程序的main()写在同一个文件中,也可以写在另一个文件中,这时你可能还另写自己的头文件或者写extern....,告诉编译器,main中用到的某某函数是"外部函数". (一般时,想调用外部文件中的函数,一定要先#include "文件名",如果不在一个路径的话还要加上路径名,调用几个文件就得加几个上面的函数。)
例如,main()在a.c中,自定义函数my_func()在a2.c中
a.c内容:
#include <stdio.h>
extern float my_func(float a);
main()
{
printf("result=%f\n",my_func(2.0));
}
a2.c 内容:
float my_func(float a)
{
return a;
}
编译:
cl -c a.c [得到a.obj]
cl -c a2.c [得到a2.obj]
cl a.obj a2.obj [链接成a.exe]
运行:
a.exe
得
result=2.000000
extern float my_func() 是外部说明,告诉编译,main()里的my_func是外部函数,要通过链接(.obj)得到.
如果把my_func写在a.c里:
#include <stdio.h>
float my_func(float a){
retun a;
}
main()
{
printf("result=%f\n",my_func(2.0));
}
my_func() 就不是外部函数.
编译:
cl a.c [得a.exe]
运行:
a.exe
得
result=2.000000
l 调用API
在VC++(非MFC,MFC可以直接调用)中,一般只要先包含Windows.h文件就可以了,不需要额外的步骤,因为所有函数都是现成的。个别函数可能需要包含其他头文件或库文件,如果VC++报告找不到某个函数的定义,可以看一下MSDN Library,在每个API函数的说明的Requirements 部分列出了所需要的头文件、库文件,以及支持的操作系统。如荌nitCommonControlEx的相应说明如下:
Requirements
Version 4.70 and later of Comctl32.dll
Windows NT/2000: Requires Windows 2000 (or Windows NT 4.0 with Internet Explorer 3.0 and later).
Windows 95/98/Me: Requires Windows 98 (or Windows 95 with Internet Explorer 3.0 or later).
Header: Declared in commctrl.h.
Import Library: comctl32.lib.
这说明调用该函数需要包含commctrl.h,并加入comctl32.lib。
例:
char m_Volume[256];//卷标名
char m_FileSysName[256];
DWORD m_SerialNum;//序列号
DWORD m_FileNameLength;
DWORD m_FileSysFlag;
::GetVolumeInformation("c:\\", m_Volume, 256, &m_SerialNum, &m_FileNameLength, &m_FileSysFlag, m_FileSysName, 256);
在代码中直接引入汇编有时候可以省了不少事情,有的语句甚至可以直接抄原程序,只要稍做修改就行。而且执行速度比纯代码快了不少。
一、VB
VB不能直接引入汇编代码,不象VC,Delphi可以直接在程序中写汇编代码,这是非常可惜的事情。很多VB程序员通过各种转换的方法使VB支持直接写入汇编代码,效果都不是很好,比较实用的是用VbInLineASM插件和ThunderVB(可以让VB支持汇编和C代码)。本人也曾经发过相关的文章,所以这里就不再重复了。我用VbInLineASM有测试成功,ThunderVB由于要用英文版VB,没有做测试。很多人说用VbInLineASM都没有成功,这是因为这个插件有BUG,你只要注意几点就一定会成功的:一定要与MASM的ML.EXE连接上。另外,VB工程必须置于盘符根目录,比如C盘的PROJECT文件夹,文件名不能用中文(这点非常重要)。另外只有编译后汇编代码才能实现功能。不能用P-CODE编译。每次更改VbInLineASM设置后最好重新启动工程一次。
一个典型的例子:获取CPU主频。
Private Sub Command1_Click()
D0im myLong(2) As Long
Dim I As Long
Dim lngPointer1 As Long, lngPointer2 As Long, lngPointer3 As Long
Dim lngCPUFeatures(4) As Long, lngCacheInfo(4) As Long
lngPointer1 = VarPtr(myLong(0))
lngPointer2 = VarPtr(lngCPUFeatures(0))
lngPointer3 = VarPtr(lngCacheInfo(0))
Text1.Text = asmCPUID(lngPointer1, lngPointer2, lngPointer3)
End Sub
=================ASM Module================
Public Function asmCPUID(ByVal lngPointer1 As Long, ByVal lngPointer2 As Long, ByVal lngPointer3 As Long) As Long
'#ASM_START
'.686
' push ebp
' mov ebp, esp
' push ecx
' push edx
' push edi
' mov eax, 0
' CPUID
' mov edi, DWORD PTR [ebp+8]
' mov [edi], ebx
' mov edi, DWORD PTR [ebp+8]
' Add edi, 4
' mov [edi], edx
' mov edi, DWORD PTR [ebp+8]
' Add edi, 8
' mov [edi], ecx
' cmp eax, 0
' je ExitASM
' mov eax, 1
' CPUID
' mov edi, [ebp+12] ;//Pointer to array2
' mov [edi], eax
' Add edi, 4
' mov [edi], ebx
' Add edi, 4
' mov [edi], ecx
' Add edi, 4
' mov [edi], edx
' mov eax, 0
' CPUID
' cmp eax, 1
' jng ExitASM
' mov eax, 2
' CPUID
' mov edi, [ebp+16]
' mov [edi], eax
' Add edi, 4
' mov [edi], ebx
' Add edi, 4
' mov [edi], ecx
' Add edi, 4
' mov [edi], edx
'ExitASM:
' pop edi
' pop edx
' pop ecx
' mov esp, ebp
' pop ebp
' ret 12
'#ASM_END
End Function
二、 DELPHI
Delphi调用汇编,有两种方式。(引用Delphi+汇编例子)
一如---
procedure TForm1.DrawRain;
var
x1,y1,x2,y2,d,i:integer;
begin//delphi程序开始
for i:=0 to 100 do
begin
x1:=random(537);
y1:=random(280);
d:=random(7);
asm//内嵌汇编开始
push eax
mov eax,x1
sub eax,d
mov x2,eax
mov eax,y1
add eax,d
mov y2,eax
pop eax
end;;//内嵌汇编结束
times:=times+1;
drawLine2(x1,y1,x2,y2,clmedGray);
wait();
if (i div 2)=0 then drawLine2(x1,y1,x2,y2,clwindow);
end;
end;//delphi程序结束
还有一种,把外层的begin...end去掉,通过asm...end直接进入汇编。
procedure TForm1.Wait();
asm//内嵌汇编开始
push eax
mov eax,0
@loop:
add eax,1
cmp eax,1000000
jnz @loop
pop eax
end;//内嵌汇编结束
Delphi的嵌入式汇编:(转自Delphi与汇编)
Delphi中提供了几乎全部常用汇编指令的支持:MOV、JE、JMP、CMP、SHL、SHR、SAL、SAR、POP、PUSH、HLT……自己去查吧。至于INT也能识别,不过非法操作或死机可别找我(在最早的Windows95中用Delphi 3似乎可以正确运行中断,但Windows 95 OEM、Windows 98就不对了,大概是16位模块的问题,还搞不清楚)。
* 嵌入式汇编的格式
Delphi是使用ASM……END来标志汇编语句
如:ASM
mov al,1
mov bl,al
END;
* 可操作的寄存器
Delphi可用汇编管理以下寄存器:
32位寄存器EAX EBX ECX EDX ESP EBP ESI EDI
16位寄存器AX BX CX DX SP BP SI DI
8位寄存器AL BL CL DL AH BH CH DH
16位段寄存器CS DS SS ES
以及协处理器寄存器堆栈 ST
* 使用汇编前的工作
教汇编的老师一再强调使用汇编要保存寄存器现场(保存使用前的寄存器状态,使用Push压栈和Pop从栈中弹出),不过这一切对于Delphi的嵌入式汇编是没有必要的(除非你自己要使用Push和Pop),因为Delphi已经帮你做了,不必担心会使数据丢掉。
* Delphi嵌入式汇编的使用方式
1.在一般函数过程中使用汇编
汇编程序段可以嵌套于其它过程中:如:
procedure TForm1.Button1Click(Sender: TObject);
var i:smallint;
begin
i:=1;
asm
mov ax,i
sal ax,1
mov &i,ax
end;
showmessage(inttostr(i));
end;
这个程序段是把16位的变量I进行左移,然后把结果用Mov &I,ax语句放入I变量所在地址返回值。最后显示I 的值是2。
2.独立的汇编程序段
汇编程序段也可以单独写成函数或过程。这就涉及到参数的传递与结果的返回。首先Delphi对于函数的返回有一个约定:
即:整型数据:8位的用AL返回,16位的用AX返回,32位的用EAX返回;
实型:用ST(0)返回
指针:用EAX返回
长字符串:用EAX返回其所在地址
变量:可用@Result返回
例如:一个用汇编的求和函数
function _Sum(X, Y: Integer): Integer;
asm
MOV EAX,X //把32位的数放入EAX
ADD EAX,Y //进行加法运算
MOV @Result,EAX //返回X+Y
end;
一个把字符转化为大写的函数例子
function _UpCase( ch : Char ) : Char;
asm
CMP AL,`a'
JB @@exit
CMP AL,`z'
JA @@exit
SUB AL,`a' -`A'
@@exit:
end;
值得注意的是第二个例子中,没有象第一个那样把参数用语句放到寄存器中,这是由于Delphi中默认的把Byte(Char)类型放在AL中,不需要用Mov语句,但是这种函数不能是类的成员,否则结果会出错。
3.在汇编中调用其它过程
汇编语句中的Call语句,可以用于调用其它过程,既可以是其它汇编程序段也可以是Delphi中的标准过程:
例如:假设新建一个窗体并在上面加了一个按钮,在Click事件中写入以下代码
procedure TForm1.Button1Click(Sender: TObject);
begin
showmessage(`ok');
end;
再写一个过程_X
function TForm1._x(var i:smallint):integer;
asm
call button1click
end;
执行_x的结果就可以显示消息框。
* 汇编的调试
编好了程序,没错,还好,如果有错,就得用到调试工具:如变量的跟踪、断点、堆栈查看……对于汇编还可以用View菜单的Debug Windows的CPU窗口跟踪。
三、VC
在VC中内联汇编非常方便,只需要按照如下格式
__asm{
//汇编语句
}
转一篇不错的文章:在 Visual C++ 中使用内联汇编
一、 优点
使用内联汇编可以在 C/C++ 代码中嵌入汇编语言指令,而且不需要额外的汇编和连接步骤。在 Visual C++ 中,内联汇编是内置的编译器,因此不需要配置诸如 MASM 一类的独立汇编工具。这里,我们就以 Visual Studio .NET 2003 为背景,介绍在 Visual C++ 中使用内联汇的相关知识(如果是早期的版本,可能会有些许出入)。
内联汇编代码可以使用 C/C++ 变量和函数,因此它能非常容易地整合到 C/C++ 代码中。它能做一些对于单独使用 C/C++ 来说非常笨重或不可能完成的任务。
内联汇编的用途包括:
* 使用汇编语言编写特定的函数;
* 编写对速度要求非常较高的代码;
* 在设备驱动程序中直接访问硬件;
* 编写 naked 函数的初始化和结束代码。
二、 关键字
使用内联汇编要用到 __asm 关键字,它可以出现在任何允许 C/C++ 语句出现的地方。我们来看一些例子:
* 简单的 __asm 块:
__asm
{
MOV AL, 2
MOV DX, 0xD007
OUT AL, DX
}
* 在每条汇编指令之前加 __asm 关键字:
__asm MOV AL, 2
__asm MOV DX, 0xD007
__asm OUT AL, DX
* 因为 __asm 关键字是语句分隔符,所以可以把多条汇编指令放在同一行:
__asm MOV AL, 2 __asm MOV DX, 0XD007 __asm OUT AL, DX
显然,第一种方法与 C/C++ 的风格很一致,并且把汇编代码和 C/C++ 代码清楚地分开,还避免了重复输入 __asm 关键字,因此推荐使用第一种方法。
不像在 C/C++ 中的“{}”,__asm 块的“{}”不会影响 C/C++ 变量的作用范围。同时,__asm 块可以嵌套,而且嵌套也不会影响变量的作用范围。
为了与低版本的 Visual C++ 兼容,_asm 和 __asm 具有相同的意义。另外,Visual C++ 支持标准 C++ 的 asm 关键字,但是它不会生成任何指令,它的作用仅限于使编译器不会出现编译错误。要使用内联汇编,必须使用 __asm 而不是 asm 关键字。
三、 汇编语言
1. 指令集
内联汇编支持 Intel Pentium 4 和 AMD Athlon 的所有指令。更多其它处理器的指令可以通过 _EMIT 伪指令来创建(_EMIT 伪指令说明见下文)。
2. MASM 表达式
在内联汇编代码中,可以使用所有的 MASM 表达式(MASM 表达式是指用来计算一个数值或一个地址的操作符和操作数的组合)。
3. 数据指示符和操作符
虽然 __asm 块中允许使用 C/C++ 的数据类型和对象,但它不能使用 MASM 指示符和操作符来定义数据对象。这里特别指出,__asm 块中不允许 MASM 中的定义指示符(DB、DW、DD、DQ、DT 和 DF),也不允许使用 DUP 和 THIS 操作符。MASM 中的结构和记录也不再有效,内联汇编不接受 STRUC、RECORD、WIDTH 或者 MASK。
4. EVEN 和 ALIGN 指示符
尽管内联汇编不支持大多数 MASM 指示符,但它支持 EVEN 和 ALIGN。当需要的时候,这些指示符在汇编代码里面加入 NOP 指令(空操作)使标号对齐到特定边界。这样可以使某些处理器取指令时具有更高的效率。
5. MASM 宏指示符
内联汇编不是宏汇编,不能使用 MASM 宏指示符(MACRO、REPT、IRC、IRP 和 ENDM)和宏操作符(<>、!、&、% 和 .TYPE)。
6. 段
必须使用寄存器而不是名称来指明段(段名称“_TEXT”是无效的)。并且,段跨越必须显式地说明,如 ES:[EBX]。
7. 类型和变量大小
在内联汇编中,可以用 LENGTH、SIZE 和 TYPE 来获取 C/C++ 变量和类型的大? ?
* LENGTH 操作符用来取得 C/C++ 中数组的元素个数(如果不是一个数组,则结果为 1)。
* SIZE 操作符可以获取 C/C++ 变量的大小(一个变量的大小是 LENGTH 和 TYPE 的乘积)。
* TYPE 操作符可以返回 C/C++ 类型和变量的大小(如果变量是一个数组,它得到的是数组中单个元素的大小)。
例如,程序中定义了一个 8 维的整数型变量:
int iArray[8];
下面是 C 和汇编表达式中得到的 iArray 及其元素的相关值:
__asm C Size
LENGTH iArray sizeof(iArray)/sizeof(iArray[0]) 8
SIZE iArray sizeof(iArray) 32
TYPE iArray sizeof(iArray[0]) 4
8. 注释
内联汇编中可以使用汇编语言的注释,即“;”。例如:
__asm MOV EAX, OFFSET pbBuff ; Load address of pbBuff
因为 C/C++ 宏将会展开到一个逻辑行中,为了避免在宏中使用汇编语言注释带来的混乱,内联汇编也允许使用 C/C++ 风格的注释。
9. _EMIT 伪指令
_EMIT 伪指令相当于 MASM 中的 DB,但是 _EMIT 一次只能在当前代码段(.text 段)中定义一个字节。例如:
__asm
{
JMP _CodeLabel
_EMIT 0x00 ; 定义混合在代码段的数据
_EMIT 0x01
_CodeLabel: ; 这里是代码
_EMIT 0x90 ; NOP指令
}
10. 寄存器使用
一般来说,不能假定某个寄存器在 __asm 块开始的时候有已知的值。寄存器的值将不能保证会从 __asm 块保留到另外一个 __asm 块中。
如果一个函数声明为 __fastcall 调用方式,则其参数将通过寄存器而不是堆栈来传递。这将会使 __asm 块产生问题,因为函数无法被告知哪个参数在哪个寄存器中。如果函数接收了 EAX 中的参数并立即储存一个值到 EAX 中的话,原来的参数将丢失掉。另外,在所有声明为 __fastcall 的函数中,ECX 寄存器是必须一直保留的。为了避免以上的冲突,包含 __asm 块的函数不要声明为 __fastcall 调用方式。
* 提示:如果使用 EAX、EBX、ECX、EDX、ESI 和 EDI 寄存器,你不需要保存它。但如果你用到了 DS、SS、SP、BP 和标志寄存器,那就应该用 PUSH 保存这些寄存器。
* 提示:如果程序中改变了用于 STD 和 CLD 的方向标志,必须将其恢复到原来的值。
四、 使用 C/C++ 元素
1. 可用的 C/C++ 元素
C/C++ 与汇编语言可以混合使用,在内联汇编中可以使用 C/C++ 变量以及很多其它的 C/C++ 元素,包括:
* 符号,包括标号、变量和函数名;
* 常量,包括符号常量和枚举型成员;
* 宏定义和预处理指示符;
* 注释,包括“/**/”和“//”;
* 类型名,包括所有 MASM 中合法的类型;
* typedef 名称,通常使用 PTR 和 TYPE 操作符,或者使用指定的的结构或枚举成员。
在内联汇编中,可以使用 C/C++ 或汇编语言的基数计数法。例如,0x100 和 100H 是相等的。
2. 操作符使用
内联汇编中不能使用诸如“<<”一类的 C/C++ 操作符。但是,C/C++ 和 MASM 共有的操作符(比如“*”和“[]”操作符),都被认为是汇编语言的操作符,是可以使用的。举个例子:
int iArray[10];
__asm MOV iArray[6], BX ; Store BX at iArray + 6 (Not scaled)
iArray[6] = 0; // Store 0 at iArray+12 (Scaled)
* 提示:在内联汇编中,可以使用 TYPE 操作符使其与 C/C++ 一致。比如,下面两条语句是一样的:
__asm MOV iArray[6 * TYPE int], 0 ; Store 0 at iArray + 12
iArray[6] = 0; // Store 0 at iArray + 12
3. C/C++ 符号使用
在 __asm 块中可以引用所有在作用范围内的 C/C++ 符号,包括变量名称、函数名称和标号。但是不能访问 C++ 类的成员函数。
下面是在内联汇编中使用 C/C++ 符号的一些限制:
* 每条汇编语句只能包含一个 C/C++ 符号。在一条汇编指令中,多个符号只能出现在 LENGTH、TYPE 或 SIZE 表达式中。
* 在 __asm 块中引用函数必须先声明。否则,编译器将不能区别 __asm 块中的函数名和标号。
* 在 __asm 块中不能使用对于 MASM 来说是保留字的 C/C++ 符号(不区分大小写)。MASM 保留字包含指令名称(如 PUSH)和寄存器名称(如 ESI)等。
* 在 __asm 块中不能识别结构和联合标签。
4. 访问 C/C++ 中的数据
内联汇编的一个非常大的方便之处是它可以使用名称来引用 C/C++ 变量。例如,如果 C/C++ 变量 iVar 在作用范围内:
__asm MOV EAX, iVar ; Stores the value of iVar in EAX
如果 C/C++ 中的类、结构或者枚举成员具有唯一的名称,则在 __asm 块中可以只通过成员名称来访问(省略“.”操作符之前的变量名或 typedef 名称)。然而,如果成员不是唯一的,你必须在“.”操作符之前加上变量名或 typedef 名称。例如,下面的两个结构都具有 SameName 这个成员变量:
struct FIRST_TYPE
{
char *pszWeasel;
int SameName;
};
struct SECOND_TYPE
{
int iWonton;
long SameName;
};
5. 用内联汇编写函数
如果用内联汇编写函数的话,要传递参数和返回一个值都是非常容易的。看下面的例子,比较一下用独立汇编和内联汇编写的函数:
; PowerAsm.asm
; Compute the power of an integer
PUBLIC GetPowerAsm
_TEXT SEGMENT WORD PUBLIC 'CODE'
GetPowerAsm PROC
PUSH EBP ; Save EBP
MOV EBP, ESP ; Move ESP into EBP so we can refer
; to arguments on the stack
MOV EAX, [EBP+4] ; Get first argument
MOV ECX, [EBP+6] ; Get second argument
SHL EAX, CL ; EAX = EAX * (2 ^ CL)
POP EBP ; Restore EBP
RET ; Return with sum in EAX
GetPowerAsm ENDP
_TEXT ENDS
END
C/C++ 函数一般用堆栈来传递参数,所以上面的函数中需要通过堆栈位置来访问它的参数(在 MASM 或其它一些汇编工具中,也允许通过名称来访问堆栈参数和局部堆栈变量)。
下面的程序是使用内联汇编写的:
// PowerC.c
#include <Stdio.h>
int GetPowerC(int iNum, int iPower);
int main()
{
printf("3 times 2 to the power of 5 is %d\n", GetPowerC( 3, 5));
}
int GetPowerC(int iNum, int iPower)
{
__asm
{
MOV EAX, iNum ; Get first argument
MOV ECX, iPower ; Get second argument
SHL EAX, CL ; EAX = EAX * (2 to the power of CL)
}
// Return with result in EAX
}
使用内联汇编写的 GetPowerC 函数可以通过参数名称来引用它的参数。由于 GetPowerC 函数没有执行 C 的 return 语句,所以编译器会给出一个警告信息,我们可以通过 #pragma warning 禁止生成这个警告。
内联汇编的其中一个用途是编写 naked 函数的初始化和结束代码。对于一般的函数,编译器会自动帮我们生成函数的初始化(构建参数指针和分配局部变量等)和结束代码(平衡堆栈和返回一个值等)。使用内联汇编,我们可以自己编写干干净净的函数。当然,此时我们必须自己动手做一些有关函数初始化和扫尾的工作。例如:
void __declspec(naked) MyNakedFunction()
{
// Naked functions must provide their own prolog.
__asm
{
PUSH EBP
MOV ESP, EBP
SUB ESP, __LOCAL_SIZE
}
// And we must provide epilog.
__asm
{
POP EBP
RET
}
}
6. 调用 C/C++ 函数
内联汇编中调用声明为 __cdecl 方式(默认)的 C/C++ 函数必须由调用者清除参数堆栈,下面是一个调用 C/C++ 函数例子:
#include <Stdio.h>
char szFormat[] = "%s %s\n";
char szHello[] = "Hello";
char szWorld[] = " world";
void main()
{
__asm
{
MOV EAX, OFFSET szWorld
PUSH EAX
MOV EAX, OFFSET szHello
PUSH EAX
MOV EAX, OFFSET szFormat
PUSH EAX
CALL printf
// 压入了 3 个参数在堆栈中,调用函数之后要调整堆栈
ADD ESP, 12
}
}
* 提示:参数是按从右往左的顺序压入堆栈的。
如果调用 __stdcall 方式的函数,则不需要自己清除堆栈。因为这种函数的返回指令是 RET n,会自动清除堆栈。大多数 Windows API 函数均为 __stdcall 调用方式(仅除 wsprintf 等几个之外),下面是一个调用 MessageBox 函数的例子:
#include <Windows.h>
TCHAR g_tszAppName[] = TEXT("API Test");
void main()
{
TCHAR tszHello[] = TEXT("Hello, world!");
__asm
{
PUSH MB_OK OR MB_ICONINFORMATION
PUSH OFFSET g_tszAppName ; 全局变量用 OFFSET
LEA EAX, tszHello ; 局部变量用 LEA
PUSH EAX
PUSH 0
CALL DWORD PTR [MessageBox] ; 注意这里不是 CALL MessageBox,而是调用重定位过的函数地址
}
}
* 提示:可以不受限制地访问 C++ 成员变量,但是不能访问 C++ 的成员函数。
7. 定义 __asm 块为 C/C++ 宏
使用 C/C++ 宏可以方便地把汇编代码插入到源代码中。但是这其中需要额外地注意,因为宏将会扩展到一个逻辑行中。
为了不会出现问题,请按以下规则编写宏:
* 使用括号把 __asm 块包围住;
* 把 __asm 关键字放在每条汇编指令之前;
* 使用经典 C 风格的注释(“/* comment */”),不要使用汇编风格的注释(“; comment”)或单行的 C/C++ 注释(“// comment”);
举个例子,下面定义了一个简单的宏:
#define PORTIO __asm \
/* Port output */ \
{ \
__asm MOV AL, 2 \
__asm MOV DX, 0xD007 \
__asm OUT DX, AL \
}
乍一看来,后面的三个 __asm 关键字好像是多余的。其实它们是需要的,因为宏将被扩展到一个单行中:
__asm /* Port output */ {__asm MOV AL, 2 __asm MOV DX, 0xD007 __asm OUT DX, AL}
从扩展后的代码中可以看出,第三个和第四个 __asm 关键字是必须的(作为语句分隔符)。在 __asm 块中,只有 __asm 关键字和换行符会被认为是语句分隔符,又因为定义为宏的一个语句块会被认为是一个逻辑行,所以必须在每条指令之前使用 __asm 关键字。
括号也是需要的,如果省略了它,编译器将不知道汇编代码在哪里结束,__asm 块后面的 C/C++ 语句看起来会被认为是汇编指令。
同样是由于宏展开的原因,汇编风格的注释(“; comment”)和单行的 C/C++ 注释(“// commen”)也可能会出现错误。为了避免这些错误,在定义 __asm 块为宏时请使用经典 C 风格的注释(“/* comment */”)。
和 C/C++ 宏一样 __asm 块写的宏也可以拥有参数。和 C/C++ 宏不一样的是,__asm 宏不能返回一个值,因此,不能使用这种宏作为 C/C++ 表达式。
不要不加选择地调用这种类型的宏。比如,在声明为 __fastcall 的函数中调用汇编语言宏可能会导致不可预料的结果(请参看前文的说明)。
8. 转跳
可以在 C/C++ 里面使用 goto 转跳到 __asm 块中的标号处,也可以在 __asm 块中转跳到 __asm 块里面或外面的标号处。__asm 块内的标号是不区分大小写的(指令、指示符等也是不区分大小写的)。例如:
void MyFunction()
{
goto C_Dest; /* 正确 */
goto c_dest; /* 错误 */
goto A_Dest; /* 正确 */
goto a_dest; /* 正确 */
__asm
{
JMP C_Dest ; 正确
JMP c_dest ; 错误
JMP A_Dest ; 正确
JMP a_dest ; 正确
a_dest: ; __asm 标号
}
C_Dest: /* C/C++ 标号 */
return;
}
不要使用函数名称当作标号,否则将转跳到函数中执行,而不是标号处。例如,由于 exit 是 C/C++ 的函数,下面的转跳将不会到 exit 标号处:
; 错误:使用函数名作为标号
JNE exit
exit:
美元符号“$”用于指定当前指令位置,常用于条件跳转中,例如:
JNE $+5 ; 下面这条指令的长度是 5 个字节
JMP _Label
NOP ; $+5,转跳到了这里
_Label:
五、在 Visual C++ 工程中使用独立汇编
内联汇编代码不易于移植,如果你的程序打算在不同类型的机器(比如 x86 和 Alpha)上运行,你可能需要在不同的模块中使用特定的机器代码。这时候你可以使用 MASM(Microsoft Macro Assembler),因为 MASM 支持更多方便的宏指令和数据指示符。
这里简单介绍一下在 Visual Studio .NET 2003 中调用 MASM 编译独立汇编文件的步骤。
在 Visual C++ 工程中,添加按 MASM 的要求编写的 .asm 文件。在解决方案资源管理器中,右击这个文件,选择“属性”菜单项,在属性对话框中,点击“自定义生成步骤”,设置如下项目:
命令行:ML.exe /nologo /c /coff "-Fo$(IntDir)\$(InputName).obj" "$(InputPath)"
输出:$(IntDir)\$(InputName).obj
如果要生成调试信息,可以在命令行中加入“/Zi”参数,还可以根据需要生成 .lst 和 .sbr 文件。
如果要在汇编文件中调用 Windows API,可以从网上下载 MASM32 包(包含了 MASM 汇编工具、非常完整的 Windows API 头文件/库文件、实用宏以及大量的 Win32 汇编例子等)。相应地,应该在命令行中加入“/I X:\MASM32\INCLUDE”参数指定 Windows API 汇编头文件(.inc)的路径。MASM32 的主页是:http://www.masm32.com,里面可以下载最新版本的 MASM32 包。
单击此处附件下载:keygen3.rar
【版权声明】: 本文由langxang原创于看雪论坛,转载请注明作者并保持文章的完整, 谢谢!