[原创教程]LabWindows/CVI入门之第四章:库文件
导航目录
4.1 静态库与动态库
4.1.1 简介
通过前几章的学习,大家已经掌握了利用CVI开发涉及到UI、硬件、软件组件的程序。但在现实生活中,前几章示例程序那样的几十行几百行的代码的项目几乎不存在,大家将来遇到更多的是几万行乃至几千万行代码的软件开发项目。这种中型、大型的软件项目一般需要多人进行合作开发,此时就有必要将项目分为一个个小的功能模块,以方便其他程序员在应用程序中调用。在Windows中,常常采用目标文件(*.obj)、静态库(*.lib)或动态链接库文件(*.dll)进行功能模块的分割。
目标文件和静态库文件比较类似,函数和数据被编译为机器码之后存入一个二进制文件中,在使用目标文件或者静态库文件时,链接器(Linker)从目标文件或静态库中找到这些代表函数和数据的二进制代码并把它们复制到exe应用程序文件中,和其他模块组合起来并生成最终可被执行的exe文件。因此,目标文件和静态库文件只是起到一个“二进制数据源”的作用,当最终版的exe发布时,由于exe文件中已经包含了目标文件或静态库中的数据,因此无需随exe一起发布。目标文件与静态库文件不同的是,目标文件一般是C文件或其他编程语言文件编译得到的二进制代码,不会用来发布;而静态库文件一般发布以供其他程序员或者其他开发环境使用。
动态链接库(DLL,Dynamic Link Library)文件是从Microsoft推出第一个版本的Windows操作系统以来就有的一种可供多个程序调用的功能模块。跟目标文件、静态链接库一样,大部分的DLL中存放的也是可以直接被执行的机器码(之所以用“大部分”而不是“全部”的原因将会在“4.4 CVI调用.Net平台下的DLL”中提到)。跟静态库文件不同的是,应用程序运行时才链接到DLL中的功能模块中,也就是说,应用程序发布时其链接的DLL文件需要同时发布。在程序设计时,一般更倾向于使用DLL而不是静态库,有以下原因:
·DLL支持任何其他Windows下的编程语言,避免了编译器的兼容问题
·只要DLL中导出函数的接口不变,修改动态链接库的功能模块时就不必修改与之相互依存的其他模块的代码
·在同一个Windows操作系统下,不同的应用程序可以共享使用相同的DLL,可以减少应用程序可执行文件的大小,节省空间
·以DLL发布的程序可以给用户提供一个方便的二次开发平台而又不必担心自身源代码的泄漏
4.1.2 使用静态库与动态库
由于静态库文件实际上就是编译过的代码块,在一个工程中添加了一个静态库,就如同添加了“加密过的”普通的编程语言文件一样。当需要调用静态库文件时,只需要将静态库文件(*.lib)和相应的头文件加入到工程中,在其他文件中引用库的头文件之后,调用静态库中的函数或变量即可。
在使用动态库文件的时候,往往会用到另外两个文件文件:一个引入库(*.lib)文件和一个头(*.h)文件。虽然引入库文件的后缀名也是.lib,但是动态链接库的引入库文件和静态库文件有着本质上的区别。一般而言,静态库文件中包含了所有变量和函数以及函数执行的机器码,而DLL的引入库文件只包含了该DLL导出的函数以及变量的名称,而真正的可执行的机器码在DLL文件中。
一般情况下,编译生成DLL的时候导入库(.lib)文件会一同被创建。但若导入库文件丢失,在CVI中可以通过点击菜单Options-Generate DLL Import Library…来生成一个导入库文件。
4.1.3 静态库生成和使用的例子
为了方便大家充分的理解静态库的概念与功能,我们不妨举个例子。动态库的例子将会在“4.2 CVI生成DLL”和“4.3 CVI调用DLL”中给出,此小节不再给出具体的实例。
我们新建一个名为“StaticLib”的工程,工程下新建Main.c、StaticLib.c以及StaticLib.h三个文件。
其中,在StaticLib.c文件中添加了如下代码:
在StaticLib.h头文件中添加如下代码:
在Main.c中添加如下代码:
整个工程建立完毕之后,CVI下的工程目录如图 4‑1所示。
从以上代码我们可以看出,在StaticLib.c文件中,我们写了一个AddTest函数,输入两个int型变量,返回两个变量的和。在StaticLib.h文件中,我们提供了AddTest函数的声明。在Main.c文件中,我们调用了AddTest函数。以上工程运行的结果如图 4‑2所示。
若此时我们右击StaticLib工程中的Main.c文件,选择Exclude File from Build,如图 4‑3所示,然后点击CVI菜单-Build-Target Type-Static Library,将工程的输出由exe文件改为静态库文件,则编译之后,CVI将会编译未被排除在编译列表的StaticLib.c文件,并在工程目录下会生成一个名为“StaticLib.lib”的静态库文件。
生成静态库文件之后,将StaticLib.c文件排除在编译列表之外,将刚生成的StaticLib.lib文件添加进工程中,并恢复Main.c文件到编译列表中,恢复工程编译生成的目标类型为可执行文件(菜单Build-Target Type-Executable)。此时工程目录如图 4‑4所示。
重新编译工程,运行后将会得到跟刚才完全一样的结果。如图 4‑5所示。
4.2 CVI生成DLL
4.2.1 CVI下生成DLL文件
在上一节中,我们通过一个实例了解了在CVI下生成静态库文件的过程。生成DLL的步骤跟生成静态库的步骤基本相同,除了配置生成文件类型的时候选择“Dynamic Link Library”而不是“Static Library”之外。
设置生成文件类型为DLL后,在工程中只保留StaticLib.c文件与StaticLib.h文件在编译列表中,将其他文件排除到编译列表之外,继续Build上一节的工程。此时CVI弹出如所示的对话框,提示用户没有任何函数被导出。
图 4‑6 CVI提示没有任何函数被导出
若此时点击OK,DLL文件仍然可以被生成,但是DLL文件里不包含任何函数的任何信息。此时若使用CVI安装目录sdk\bin下的DEPENDS.EXE程序查看生成的DLL中包含的函数,则可以知道,在生成的DLL文件中确实不包含任何的函数的信息。如图 4‑7所示。
图 4‑7 使用Depends程序查看DLL中发现不包含任何函数信息
出现以上问题的原因在于,我们没有定义导出的函数库列表。点击CVI菜单-Build-Target Settings…之后,弹出如图 4‑8所示的窗口,在窗口中可以对生成的DLL进行设置。
在上面的对话框中,点击Exports中的Change…按钮,将Export what设置为Include file symbols,并点击下面的StaticLib.h使得前面打勾,点击OK即可。
图 4‑9 设置工程的导出内容
图 4‑8对话框各种设置说明如下:
(1) DLL file 设置DLL文件的类型(Debug/Release)以及位置、文件名。Debug类型的DLL文件可以方便的进行调试,而Release类型的DLL文件中去除了调试信息,文件体积较小,运行速度相对较快,适合发布。
(2)Import library base name 设置DLL导入库文件名。
(3)Where to copy DLL 设置是否将生成的DLL文件拷贝到System目录下或驱动目录下
(4)Run-time support 是否提供CVI运行引擎的支持
(5)Embed project .UIRs 是否将*.uir文件编译到DLL文件中
(6)Generate map file 是否生成map文件
(7)Version Info设置DLL文件的版本信息
(8)Import Library Choices… 设置生成DLL导入库的方式,是只与当前编译器兼容还是与CVI支持的四种编译器兼容。
(9)Type Library 在CVI中很多库文件拥有自己独特的数据类型,如VISA中整数类型为ViInt32,在这个选项中可以定义是否将这些类型库和帮助文档连接到DLL文件。
(10)LoadExternalModule Options 是否将外部的库文件编译到DLL中
(11)Exports 设置输出的方式,是将头文件定义的内容全部导出,还是只导出具有导出标志的内容。
设置导出列表之后,Build工程,则CVI发出如图 4‑10所示的提示,告诉用户DLL已经成功生成。
其中.dll(Dynamic Link Library)文件是动态链接库文件,.cdb(CVI Debug)是供调试用的参数文件,.lib(Library)文件是DLL文件的导入库文件。需要注意的是,此时的.lib导入库文件跟之前生成的.lib静态库文件内容与大小均不一样。这再次提醒我们,静态库文件与DLL的导入库文件虽然都是*.lib,但是内容不同,不能混淆。
若需将此次编译出来的DLL发布以供其他编译程序或者其他程序员使用,只需要将StaticLib_dbg.dll、StaticLib_dbg.lib以及StaticLib.h这三个文件打包发布即可。
4.2.2 在CVI下调用CVI生成的DLL
发布DLL文件时,一般会连同导入库(*.lib)文件、头(*.h)文件一起发布。有了导入库(*.lib)文件的帮助,在CVI下调用动态链接库DLL的方法跟调用静态链接库(*.lib)的方法大同小异。
调用静态库时,我们需要将静态库(*.lib)文件加入工程,并将必要的头文件加入工程之后,在工程的其他文件中即可调用静态库文件中的函数或者参数。
而在调用动态链接库时,我们需要把动态链接库的导入库(*.lib)文件加入工程,并将必要的头文件加入工程之后,在工程的其他文件中即可调用动态链接库中的函数或者参数。
需要额外注意的是,因为动态链接库(*.dll)文件中的二进制机器码并不会被连接器(Linker)复制到exe文件中,所以在运行、发布时,需要确保DLL文件已经在系统的system目录下或者工程当前目录下存在。
在下面的例子中,我们将使用上一节生成并发布的StaticLib_dbg.dll、StaticLib_dbg.lib以及StaticLib.h。
我们在工程中添加Main.c源文件、StaticLib.h头文件以及StaticLib_dbg.lib导入库文件,将StaticLib_dbg.dll文件复制到工程所在目录下。此时工程目录如图 4‑11所示。
其中Main.c文件与上一节中的内容完全一致。如下所示:
将工程的生成文件类型设置为Executable,编译、运行程序,程序运行结果如图 4‑12所示。
4.2.3 在VC下调用CVI生成的DLL
在VC中调用DLL跟在CVI中调用DLL的整体思路相同。将DLL的导入库文件以及库文件的头文件加入工程中,即可在其他编程语言文件中调用该DLL中的函数以及参数。
下面我们将以一个实例来调用上一节发布的StaticLib_dbg.dll、StaticLib_dbg.lib以及StaticLib.h文件。
建立VC工程
打开VC6.0,在VC6.0中建立一个名为TestDll的控制台工程。如图 4‑13所示,将StaticLib.h文件、StaticLib_dbg.lib导入库文件以及StaticLib_dbg.dll动态链接库文件复制到工程所在目录下,并将头文件及导入库文件加入到工程中。
编写C语言文件
新建TestDll.cpp文件,并在cpp文件中输入以下代码,并将其加入到工程。
编译、调试
点击组建按钮,我们注意到,编译器此时并没有成功的找到AddTest函数,而是报以下错误:
在上一节我们使用CVI调用该DLL时,完全正常,但是为什么此时会出现问题呢?
问题的原因
我们注意到,上述错误是一个连接器错误。而且我们考虑到,从CVI迁移到VC,无非是换了一个编译器。而编译器理论上应该都支持ANSI C标准,理论上这种改变不会导致出现此类问题。两个集成开发环境最大的不同在于其环境变量不同。
问题就出在这儿。由于每个C语言(*.c)或C++语言(*.cpp)文件都是被编译器编译为目标文件之后再交由连接器(Linker)进行连接的。每个编译器在生成目标文件的过程中,编程语言文件中的函数名会被重命名,而对于C++编译器来说,环境变量不同,重命名的方法也就不同。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器在不同的环境中可能生成的名字还略有区别)。
在上述例子中,C++编译器对TestDll.cpp进行编译时,AddTest函数在目标文件中被重命名为_Add_Test_int_int,而CVI中C编译器对StaticLib.c进行编译时AddTest函数在导入库文件中被重命名为_Add_Test。两个函数在目标文件中的名字不相同,连接器自然也就无法成功的将两个文件组建成一个exe文件。
解决C++编译器兼容问题
C++语言的创建初衷是“a better C”,因此C++之父在设计C++之时就考虑到C++里面的重载会引起C库与C++库的不兼容。为了在C++中尽可能的支持C,extern “C”{}就是其中的一个策略。extern “C”{}常用在头文件中,使用方法如下:
为了让编译器得知StaticLib.h文件中所有的函数及参数都是由C编译器编译产生的,我们只需要把头文件的所有内容加入到extern “C”内即可。
但是将extern “C”{}加入头文件以使得C与C++编译器兼容存在另外一个问题。extern “C”{}本身并不是C语言的关键字,extern “C”加在C语言中会引起编译错误。为了避免C语言编译错误,我们只需要在extern “C”的声明前后分别加入#if defined (__cplusplus)以及#endif即可。
修改之后的StaticLib.h文件如下所示:
再次编译、运行
再次编译,运行,则原先的连接错误不再出现,运行结果如图 4‑14所示。
4.3 CVI调用DLL
上节我们已经初步接触到了在CVI中调用DLL的方法。在本节中,我们将系统的介绍CVI调用其他编译器生成的DLL的方法。
调用DLL的方法分为‘显示调用’和‘隐式调用’两种方法。
隐式调用方法需要被调用DLL的头文件,采用创建DLL导入库.lib文件的方法调用。该方式使用Options选项卡中的Generate DLL Import Library向导创建DLL导入库.lib(若提供DLL同时也提供了.lib则不需要此步),将导入库.lib添加至工程项目后,就可以通过函数名方便灵活的使用被调用DLL中的函数,需要注意的是,头文件中需要对所调用的函数进行申明。
显式调用方法不需要被调用DLL的头文件以及导入库文件,使用windows.h提供的Loadlibrary、GetProcAddress、Freelibrary函数,直接根据指针访问DLL中的函数。该方式适用于没有DLL的头文件以及导入库文件,但知道被调用函数的原型的场合。
4.3.1 显式调用方法的例子
下面我们创建一个名为LoadLibrary的工程,并只将上节发布的StaticLib_dbg.dll文件复制到工程目录下,使用显示调用的方法使用StaticLib_dbg.dll文件中的AddTest函数。
我们在LoadLibrary工程中创建一个LoadLibrary.c的C语言源文件,创建完毕之后,LoadLibrary工程目录如所示。
图 4‑15 LoadLibrary工程目录
向LoadLibrary.c文件中添加如下内容:
在上述文件中,我们先通过加载DLL文件获取DLL文件的句柄,然后从DLL文件中查得AddTest函数的指针地址,然后赋值给一个函数指针,随即执行函数指针所指向的函数,即可显示最终执行的结果。
上述工程不需要使用头文件也不需要使用导入库文件。运行结果如图 4‑16所示。
对大家来说,上述代码让人不解的地方可能在于函数指针的使用。其实函数指针是内核编程等常用到的一种指针,通过函数指针的使用大大增加了灵活度,使程序变得更加清晰和简洁。如果大家的C语言功底足够强,应该可以发现,上述C语言代码中,main函数内的代码可以精简为以下四行代码:
对函数指针感兴趣的同学可以深入研究可以这样精简的原因,此处尽起一个抛砖引玉的作用,具体内容不再深究。
4.3.2 VC生成DLL文件
在VC下编写一个简单的DLL程序也并不复杂。下面我们将以一个实例来说明在VC下编写一个简单的DLL程序的方法。
组建VC下的DLL工程
打开VC,点击菜单-文件-新建,在工程页面中选择Win32 Dynamic-Link Library,选择工程路径与工程名称之后,点击确定,创建一个空白的DLL工程。
图 4‑17 VC下新建一DLL工程
新建一个C++源文件(*.cpp)与头文件(*.h),添加到工程中。其中cpp文件的源代码如下:
头文件的源代码如下:
点击VC组建按钮,则VC发出如下提示,告诉大家组建成功,DLL文件已经生成。
我们使用“4.2.1 CVI下生成DLL文件”中提到的DEPENDS工具查看生成的DLL文件内部是否已经包含了我们所需要的SubTest函数。结果如图 4‑18所示。
我们并没有在生成的DLL文件中发现任何函数。同时,我们也没有在DLL的目录下发现有导入库(*.lib)文件生成!
问题的原因
跟在CVI下生成DLL时遇到的问题相似,我们并没有定义要导出的函数。在VC中,我们需要使用__declspec关键词来定义需要导出的函数。
__declspec 是MFC提供的修饰符号。在要输出的函数、类、数据的声明前加上__declspec(dllexport)的修饰符,表示该函数、类、数据需要被输出到DLL文件中。
因此,我们若把头文件中SubTest的声明中添加__declspec(dllexport),即告诉编译器我们需要将SubTest函数导出到DLL文件中。
添加__declspec(dllexport)关键词之后,我们重新编译,组建,则在Debug目录下生成了我们所需要的导入库文件、DLL文件。用DEPENDS工具查看生成的DLL,可以发现SubTest函数已经在DLL文件中了。如图 4‑19所示。
4.3.3 CVI下调用VC生成的DLL
在CVI下调用VC生成的DLL,跟调用CVI生成的DLL并无区别。
我们新建一CVI工程,将上一小节生成的DLL文件、导入库(*.lib)文件以及头文件复制到CVI工程目录下,将导入库(*.lib)文件添加到工程中,在CVI工程中添加一C语言源文件,代码如下:
编译、连接、运行之后,测试结果如图 4‑20所示。
通过上面的示例程序我们可以发现,使用CVI调用VC生成的DLL的步骤跟调用CVI生成的DLL步骤几乎完全一样。
需要额外注意的是,倘若VC生成的DLL运行时也依赖与其他的非系统system下的DLL(运行依赖的DLL可以使用DEPENDS工具查看),那么也需要将对应的DLL文件复制到CVI工程所在文件夹下或者复制到系统的system目录下。
4.4 CVI调用.Net平台下的DLL
常用的DLL包括托管DLL和非托管DLL两种。托管DLL为中间代码,完全依赖于.NET平台运行,而非托管DLL是机器代码,不依赖于.NET平台运行。C#、VB.NET和F#编程采用纯.NET语言开发,生成的DLL属于托管DLL。而平时接触到的其它语言编写的DLL,包括C/C++、CVI、LabVIEW编写的DLL都属于非托管DLL。托管的应用程序和DLL可以直接调用非托管的DLL,而非托管的应用程序和DLL必须通过.NET Runtime才能调用托管的DLL。
前两节介绍的调用的DLL与生成的DLL都是非托管的DLL,是机器代码,不依赖于.Net平台运行。但是若在CVI下调用.Net平台下的DLL,那么则需要借助CVI的.Net工具。
CVI提供了帮助用户调用.Net程序集的.Net库。用户可以通过菜单-Tools-Create .Net Controller…向导生成可在CVI下使用的.Net库。通过使用CVI以及.Net库,我们可以完成以下工作:
·注册.Net控件并且加载控件
·创建.Net对象并且调用.Net对象
·管理系统资源
·创建.Net数组,并从.Net数据中获取元素
·获取.Net的错误与异常信息
·提供.Net组件的基本信息
·同COM组件交互操作
但是CVI对.Net的支持也有一些限制。CVI不支持.Net事件和委托。并且CVI也不是一个.Net控件容器。因此,在CVI中不能使用.Net的用户界面对象。
CVI调用C#等.NET语言编写的DLL需要使用工具选项卡中的‘Creat .NET controller’。通过它生成一个调用.NET汇编代码的包装器(wrapper),该包装器包含对应的仪器驱动、源文件和头文件。包装器生成具体步骤如下:
(1) 选择Tools选项卡中的‘Creat .NET controller’
(2) 一般第三方开发的DLL都不在Global Assembly Cache中,因此弹出对话框中勾选‘Specify Assembly by Path’,选择需要调用的 DLL
(3) 在‘Target Instrument’中指定一个仪器驱动.fp文件,点击OK,CVI程序便生成一个可以调用所选DLL的仪器驱动
(4) 将生成的.fp文件添加至工程项目
包装器自动生成的仪器驱动中会封装好一些调用函数,函数的命名方式为 [命名空间]_[类名称] ,命名空间和类名称都是在编写.NET程序时定义好的,命名空间也就是DLL的名称。如图 4‑21,ClassLibrary1为命名空间,Class1为类名称,一个DLL中包括一个命名空间,一个命名空间下可以包含一个或多个类,一个类下又可以包含多个函数。图 4‑21中的ClassLibrary1_Class1__add就是一个DLL中的加法函数。
一般如 Initialize_[命名空间] 和 Close_[命名空间] 两个函数分别调用 CDotNetLoadAssembly 和CDotNetDiscardAssemblyHandle 两个函数,[命名空间]_[类名称]__Create 调用CDotNetCreateGenericInstance 函数, [命名空间]_[类名称]_[函数名称] 调用CDotNetInvokeGenericStaticMember 函数等。
特别注意的是,由于被调用的DLL没有在全局程序集缓存(GAC,Global Assembly Cache)中,调用前需要使用CDotNetRegisterAssemblyPath函数先注册.NET的DLL。如果没有注册将出现如图4所提示的调用失败的错误。全局程序集缓存所在的文件夹为C:\WINDOWS\assembly。
图 4‑22 .NET的DLL加载失败错误提示
另外,C#编程中无需关心垃圾内存的回收(Garbage Collector),而回到C环境中被调用函数涉及的变量最后需要通过手动调用释放内存的函数来释放变量的内存空间。
编程的主要流程如下:
(1) 声明“[DLL名称]_[类名称]”类型的句柄;
(2) 调用CDotNetRegisterAssemblyPath(“[DLL名称] , Version=x.x.x.x, Culture=xx , PublicKeyToken=xx” , ”Full Path of DLL”)注册.NET的DLL,DLL的路径分隔符用”\\”,如D:\\CVI\\Projects\\C#net DLL call;
(3) 调用“Initialize_[DLL名称]”函数初始化.NET controller;
(4) 根据句柄,调用“[DLL名称]_[类名称]__Create”创建被调用DLL的实例;
(5) 调用“[DLL名称]_[类名称]_[函数名称]”等具体函数,编写相应代码;
(6) 调用CDotNetDiscardHandle释放.NET DLL实例句柄;
(7) 调用CDotNetFreeMemory释放变量内存;
(8) 调用“Close_[DLL名称]”卸载.NET DLL;
需要额外注意数据类型转换的问题,.NET语言中的Enum、Rectangular Array、String、System.Decimal和System.Boolean可以自动完成转换,而COM Run-Time Callable Wrapper (RCW) types、Jagged arrays和Boxed data types需要手动调用库函数转换。
在接下来的示例中,我们将在C#中写一个基于.Net的DLL,并在系统的全局程序集缓存(GAC)中注册,并在CVI中调用此DLL。
新建C#类库工程
打开Visual Studio 2008,新建一个C#类库(Class Library)工程。如图 4‑23所示。
为工程添加密钥文件
点击开始菜单-程序- Microsoft Visual Studio 2008 SDK- Tools- System Definition Model Command Prompt,敲入sn.exe –k c:\1.snk,为C#的工程生成一个密钥文件。生成的密钥文件将会保存在指定的c:\1.snk下。
打开C#工程的AssemblyInfo.cs文件,在其中插入以下代码:
在工程的Class1.cs文件中,输入以下代码:
点击Visual Studio 2008菜单Build-Build Solution,编译C#工程。在工程目录的bin\Debug目录下会生成ClassLibrary1.dll文件。
把DLL文件注册到系统全局程序集缓存中
点击开始菜单-程序-管理工具-Microsoft .NET Framework 2.0 配置,如所示,在程序集缓存中,将生成的DLL文件加入全局程序集缓存(GAC)中。
图 4‑24 Microsoft .NET Framework 2.0 配置
添加.Net控件
新建CVI工程,在CVI中点击菜单Tools-Create .Net Controller…,添加生成的ClassLibrary1.dll文件,并添加生成的.fp文件的路径。点击确定之后,在CVI左下角的函数库窗口的Instruments文件夹下多出了ClassLibrary1库,在当前工程下多出了刚才指定的.fp文件。
编写代码,编译运行
新建一C文件,在C文件中添加如下代码,并添加到当前工程中:
CVI程序目的是调用基于C#.Net的DLL的函数实现计算1+2的功能。编译运行后,程序运行结果如图 4‑25所示。
4.5 示例:CVI获取计算机CPU、硬盘、网卡ID
在实际的软件项目中,当一个收费软件发布时,常常需要用户购买“注册码”,输入正确的注册码之后才可以正常使用。然而,若该注册码一旦被人公开,整个收费软件的注册码形同虚设。为了避免注册码共享后即可被所有人使用的问题,我们必须针对每一台计算机生成独一无二的注册号。
对于一台计算机而言,软件环境经常会被改变,所以靠检测软件使用环境来识别一台计算机显然不够严密。一般情况下,最常用的方法是获取计算机的CPU序列号、网卡号或者硬盘序列号后来计算得知该计算机的“注册码”。
然而,在Intel的CPU中,获取计算机序列号是靠一条叫做cpuid的汇编指令来完成的。而在CVI下,笔者尚未发现嵌入汇编语言的方法。倘若一个软件整体框架是采用CVI写的,那么我们可以通过将使用VC来获取CPU序列号的代码封装成一个DLL,以提供给CVI使用。
软件运行时,首先调用这个DLL获取CPU序列号与网卡号,经过某种算法计算得到“注册码”之后与用户输入的注册码比较,若相同则软件继续运行,若不相同,则用户软件提示相应的注册信息并退出。
在本小节例子中,我们将只演示获取CPU序列号以及网卡物理地址的内容。获取硬盘序列号的方法略复杂,步骤与获取网卡物理地址类似,此处不再给出具体代码,具体代码可以从例程中获得。
获取CPU序列号(Intel)
在Intel的处理器中,获取CPU序列号需要用到汇编指令CPUID。由于入口参数存放在EAX寄存器中,执行前,往EAX寄存器赋值,再执行CPUID指令,即可从EAX、EBX、ECX以及EDX中获取CPUID的返回值。具体EAX输入以及四个寄存器输出的对应关系参见表 4‑1。
从上面的表格可以看出,往EAX中赋值0x00,则运行CPUID后可以得到字符GenuineIntel。往EAX中赋值0x01,运行CPUID后从EAX与EBX中获得处理器签名以及一些特性值。往EAX中赋值0x03,运行CPUID后从ECX以及EDX后即可得到CPU的序列号。
获取网卡号
获取网卡号通过Windows IP辅助API库(IPHlpApi.h)来完成,通过GetAdaptersInfo函数可以获取网卡的信息,并且将PIP_ADAPTER_INFO结构体中的Address等成员变量进行处理之后显示出来即可。
运行GetAdaptersInfo函数需要库文件Iphlpapi.lib的支持。Iphlpapi.lib在VC6.0的SDK(可能需要单独安装)的lib文件夹下,若添加
语句之后还不能找到Iphlpapi.lib文件可能是因为没有将Microsoft SDK加入VC的连接目录中导致的。此时需要点击VC菜单-工具-选项-目录-Library files,将Microsoft SDK目录加入其中即可。
VC最终代码
了解如何使用VC获取计算机的网卡号与CPU序列号之后,我们就可以着手实现具体程序了。
首先新建一个空的DLL工程,具体步骤参见“4.3.2 VC生成DLL文件”,并在工程中创建、添加一个cpp文件、一个头文件。CPP文件代码如下:
头文件代码如下:
在上述代码中,我们通过__asm指令完成了C++代码与汇编代码的嵌套。我们利用Windows IP辅助库的GetAdaptersInfo函数实现了网卡物理地址的获取。通过接口函数GetSerialNum,我们可以在CVI中方便的调用该DLL,实现获取CPU的序列号以及网卡的物理地址的功能。
CVI调用DLL
在CVI中,我们创建一个名为GetPhyNum的工程,将VC创建生成的DLL文件、lib文件以及头文件复制到工程目录下。在工程中新建一名为GetPhyNum.c的C语言源文件并将SerialNum.lib导入库文件添加进工程中。添加完毕后,工程目录如所示。
图 4‑26 CVI获取CPU、网卡序列号的工程目录
GetPhyNum.c源代码如下所示:
在上述C语言文件中,我们通过定义一个字符串并将字符串指针传给GetSerialNum函数。GetSerialNum函数运行时会完成将指针指向的字符串赋值的功能。最后我们调用printf函数显示获取得到的含有CPU的序列号以及网卡物理地址的字符串。
调试、运行
当CVI运行时,屏幕上会显示出CPU序列号以及网卡物理地址。程序运行结果如所示。
图 4‑27 CVI获取CPU序列号、网卡物理地址运行结果
运行计算机硬件检测程序Everest查看计算机的CPU序列号与网卡物理地址,可以发现以上程序检测得到的CPU序列号与网卡物理地址均准确无误。
图 4‑28 用Everest软件检测得到的CPU序列号与网卡物理地址
4.6 探索与实验
4.6.1 实验
使用CVI实时获取USB摄像头中的图像,显示在界面中,并使用算法检测一张白纸上的黑色方块。检测到的方块用红色方框实时标注出来。
4.6.2 探索
探索利用Matlab生成DLL文件并在CVI中调用Matlab生成的DLL的方法。