C/C++程序入口函数的隐藏方法

引言

我们都知道,在C/C++程序中都有一个入口函数,即主函数main。而在WIN 32程序设计中,有入口函数WinMain,如果是平地建高楼,从头开始编写一个C/C++程序,都必须包含这两个入口函数中的其中一个。但是有时候,通过IDE提供的项目模板创建的项目,在自动生成的源文件(指实现文件.c文件和.cpp文件,以及头文件.h文件)中却找不到上述的入口函数,这让我们一时摸不着头脑。其实,入口函数被封装起来了,它通过调用源文件中的形式上的入口函数来完成工作。

介绍

本文提供了C/C++程序入口函数的封装方法,希望对您有所帮助。其中包含两种封装方式,一种是将入口函数封装到动态链接库中,另一种是将其封装到静态链接库中。

文中的代码均已在Visual Studio 2008上编译通过,如果您使用的IDE与本文不同,可根据实际情况进行相应调整。其中叙述的相关概念多来自网络,由于个人能力有限,不能保证完全准确无误,如果有不当之处请劳烦指出。

封装方法

为了叙述方便,下面将通过实例来进行讲解。

创建解决方案

首先我们在Visual Studio 2008中创建一个名为DLLEntryWIN32项目(Project),该项目包含在名为EntryEncapsulation的解决方案中(在Visual C++ 6.0中称为工作空间,即Workspace)。为了便于区分,我们的解决方案不想与DLLEntry同名。DLLEntry项目用于将实际入口函数(mainWinMain)封装到DLL中。如果你是VS2008老手的话,应该知道怎么创建DDL项目和静态库项目,我只是个新手。接着,我们在EntryEncapsulation解决方案中再添加一个名为LIBEntry的静态库项目,这个项目用于将实际入口函数封装到LIB中。另外,为了在完成封装后可以检验一下我们的劳动成果,还需预先创建一个用于测试DLLLIB的空项目NewEntry。顾名思义,我们将在这个项目里编写一个名为NewEntry的函数作为形式上的入口函数。由于我建的NewEntry项目是个WIN 32窗口程序项目,所以它实际入口函数为WinMain。如果您不喜欢的话,可以另外创建一个入口函数为mainWIN 32控制台应用程序项目。

好的,一切准备就绪,出发!期待激动人心的时刻到来吧!

现在先来搞定LIBEntry项目。

入口函数的静态库封装方法

首先在LIBEntry项目中添加源文件LIBEntry.cpp,并在其中编写实际入口函数WinMain。要编写的代码估计你已经很熟悉了,不过为了讲解方便,我得把它再简化些。LIBEntry.cpp中的代码如下:

#include "stdafx.h"    // 标准头文件

< xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" /> 

int NewEntry(void);    // 声明形式入口函数NewEntry

 

// [--

// 在实际入口函数中调用形式入口函数,并把实际入口函数封装到LIB

 

int WINAPI WinMain(HINSTANCE hInstance,

                               HINSTANCE hPrevInstance,

                   LPSTR lpCmdLine,

                   int iShowCmd)

{

    // 提示先由实际入口函数进入

       MessageBox(NULL, TEXT("我是实际入口函数,我封装在LIB里,")

                              TEXT("下面将调用形式入口函数!"),

                        TEXT("WinMain"), MB_OK);

      

       // -- TODO:

    // 可以通过GetCommandLine获取命令行参数,

    // 并通过CommandLineToArgvW

    // 将其转换成argv风格的字符串数组,

    // 来调用int NewEntry(int argc, char * argv[])

    // 是不是很像int main(int argc, char * argv[])

    // -- ENDTOD

 

       NewEntry();   // 调用形式入口函数,这个函数不在LIB

 

       return 0;

}

 

// WinMain

// --]

 

好了,在LIB中封装WinMain就这么简单。看到NewEntry了吧,我把它作为新的形式上的入口函数,只要在WinMain里调用它就可以了。封装之后,在外面就只能看到NewEntry。调用NewEntry前先做下声明,否则会提示找不到。LIBEntry项目中虽然声明了NewEntry函数,但却不存在它的函数实现,不过这照样能编译通过,因为LIB编译过程不检查函数是否实际存在,这样做是有好处的。在主程序链接时,LIB中的代码将被全部导入到EXE文件中。当程序运行时,不管是原LIB中的代码,还是链接前源文件中自带的代码,都会载入到同一个地址空间中。这样我们就可以假想,WinMain函数和NewEntry函数是被定义在同一个项目的不同源文件中的。入口函数的LIB封装方法就是根据这个原理实现的。事实上,检查函数存在性的过程被放在LIB链接阶段进行。如果在NewEntry项目中不存在NewEntry函数实现,那么项目就无法链接通过。

接下来我们来完成DLLEntry项目。

动态链接库封装方法

首先在DLLEntry项目的DLLEntry.cpp中编写C/C++实际入口函数,在这同样还是WinMain,你可以自己再添上mainDLLEntry.cpp中的代码如下:

#include "StdAfx.h"            // 标准头文件

 

int (* NewEntry)(void);       // 定义形式入口函数指针NewEntry

 

// [--

// 在实际入口函数中调用形式入口函数,并把实际入口函数封装到DLL

 

int WINAPI WinMain(HINSTANCE hInstance,

                   HINSTANCE hPrevInstance,

                   LPSTR lpCmdLine,

                   int iShowCmd)

{

    // 提示先由实际入口函数进入

    MessageBox(NULL, TEXT("我是实际入口函数,我被封装在DLL里,")

                     TEXT("下面将调用形式入口函数!"),

               TEXT("WinMain"), MB_OK);

 

    // 获取形式入口函数指针

    NewEntry = (int (*)())GetProcAddress(hInstance, "NewEntry"); 

    if(!NewEntry)

{

        // 提示获取形式入口函数地址出错

        MessageBox(NULL, TEXT("找不到形式入口函数,即将退出程序!"),

                   TEXT("WinMain"), MB_OK);

 

        ExitProcess(1);  // 结束进程

    }

    else

    {

        // -- TODO:

        // 可以通过GetCommandLine获取命令行参数,

        // 并通过CommandLineToArgvW

        // 将其转换成argv风格的字符串数组,

        // 来调用int NewEntry(int argc, char * argv[])

        // 是不是很像int main(int argc, char * argv[])

        // -- ENDTOD

 

        NewEntry();    // 调用形式入口函数,这个函数不在DLL

    }

 

       return 0;

}

 

// WinMain

// --]

 

LIB封装不同,不能在DLL中直接调用不存在的函数,只能通过GetProcAdress来获取其在地址空间中的指针,再通过指针调用。这是因为DLL编译时要检查函数存在性,但程序在运行之前,GetProcAdress无法判断指针的有效性。因此,为了防止NewEntry项目中不存在NewEntry函数实现而使程序在运行时出错,有必要在调用NewEntry之前判断其函数指针的有效性。

做了这些还不够,还有一些细节上的东西需要注意,否则会有苦头吃——调用的时候报错,告诉你没找到WinMain函数。如果你做过DLL封装的话,这对你应该不成问题。好了,先来分析一下吧。

DLL的链接方式有两种,分别是载入时动态链接(Load-Time Dynamic Linking)和运行时动态链接(Run-Time Dynamic Linking)。载入时动态链接是在链接时将函数所在DLL的导入库链接到可执行文件中。导入库向系统提供了载入DLL时所需的信息及用于定位DLL函数的地址符号。因此,采用载入时动态链接,在链接时能够检查到WinMain函数的所在。而运行时动态链接是在程序运行之后再载入DLL,并在需要的时候才调用DLL中的导出函数,只有调用的时候才能知道函数是否存在。由于WinMain函数是实际上的入口函数,程序最早从它开始执行,这在DLL载入之前,所以运行时动态链接无法检查它的存在性。

综上,NewEntry项目链接时只能采用载入时动态链接。

问题是,该怎么使导入库中含有WinMain函数的的地址符号。也许你会想到添加__declspec(dllexport)修饰,因为以前我们就是这么做的,但在这里这办法行不通。如果你尝试在WinMain前添加__declspec(dllexport)修饰的话,编译器会告诉你WinMain重定义,因为它跟WinBase.h里的声明有冲突。我想你不会去修改WinBase.h里面的声明吧,至少我不会。所以我们只能另辟蹊径,那就是使用老办法,创建模块定义文件。添加一个名为NewEntry.def的模块定义文件,内容如下:

LIBRARY "DLLEntry"

EXPORTS

    WinMain

 

同时你得确保在“项目设置”的“链接器”中“模块定义文件”一项已经填写上了我们创建的NewEntry.def。如果没问题的话,编译时就可以在导入库DLLEntry.lib中生成WinMain的地址符号。

OK,激动人心的时刻快要到来了,赶快完成我们的NewEntry项目吧。

形式入口函数的使用

NewEntry项目中添加一个名为NewEntry.cpp的源文件,源文件中的代码如下:

#include <windows.h>

 

int NewEntry(void)

{

       MessageBox(NULL, TEXT("我是形式入口函数,实际入口函数调用了我!"),

                        TEXT("main"), MB_OK);

       return 0;

}

 

看到了吧!除了提示消息框外,没法再简单了。然后我们依次使用LIBEntry.libDLLEntry.dll构建NewEntry项目。最方便的方法是设置NewEntry项目的“项目依赖项”,首先依赖LIBEntry项目,再对NewEntry项目进行构建,生成可执行文件NewEntry.exe,运行一下,弹出消息框,内容显示:

WinMain:“我是实际入口函数,我封装在LIB里,下面将调用形式入口函数!”

单击“确定”按钮,再次弹出一个消息框,显示:

NewEntry:“我是形式入口函数,实际入口函数调用了我!”

再次单击“确定”按钮,程序正常退出。

好的,LIB封装任务圆满完成。重新设置NewEntry项目的“项目依赖项”,这次依赖DLLEntry项目,再重新构建NewEntry项目,生成可执行文件NewEntry.exe,运行一下,弹出消息框,内容显示:

WinMain:“我是实际入口函数,我封装在DLL里,下面将调用形式入口函数!”

单击“确定”按钮,再次弹出一个消息框,显示:

NewEntry:“我是形式入口函数,实际入口函数调用了我!”

再次单击“确定”按钮,程序正常退出。

OK,大功告成。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页