随笔-89  评论-944  文章-0  trackbacks-0

突然想到个问题,EXE可否像DLL一样导出函数呢?于是就起来做试验——

 

静态链接调用

嗯,先建立一个EXE,内容很简单:

 

#include "stdafx.h"

#define EXE_LIBRARY

#include "ExeLibrary.h"

 

EXE_LIBRARY_API int Sum(int a, int b)

{

    return a + b;

}

 

int APIENTRY _tWinMain(_In_ HINSTANCE     hInstance,

                       _In_opt_ HINSTANCE hPrevInstance,

                       _In_ LPTSTR        lpCmdLine,

                       _In_ int           nCmdShow)

{

    return 0;

}

 

Sum函数是等下用来测试的。其中ExeLibrary.h 中模仿系统生成的DLL头文件进行了宏定义:

 

#ifdef EXE_LIBRARY

#define EXE_LIBRARY_API __declspec(dllexport)

#else

#define EXE_LIBRARY_API __declspec(dllimport)

#endif

 

EXE_LIBRARY_API int Sum(int a, int b);

 

 

然后建立另一个EXE

 

#include "../ExeLibrary/ExeLibrary.h"

#pragma comment(lib, "ExeLibrary.lib")

 

int main()

{

    int s = Sum(1, 2);

 

    return s;

}

 

然后运行:

clip_image001

 

能跑,运行结果也正确,说明函数被正确执行。

 

查了下进程列表,木有ExeLibrary.exe

再查调试环境的模块列表:

clip_image002

ExeLibrary赫然在目。

 

整个运行过程中,WinMain 函数木有进入。尝试

 

动态加载调用

先为刚才的ExeLibrary添加一个def文件:

 

LIBRARY

 

EXPORTS

    Sum

 

然后新建另一个EXE

 

#include <Windows.h>

#include <tchar.h>

 

int main()

{

    HMODULE hModule = LoadLibrary(_T("ExeLibrary.exe"));

 

    if (hModule == nullptr)

    {

        return 0;

    }

 

    typedef int (*FnSum)(int a, int b);

    FnSum Sum = (FnSum)GetProcAddress(hModule, "Sum");

 

    int s = 0;

 

    if (Sum != nullptr)

    {

        s = Sum(1, 2);

    }

 

    FreeLibrary(hModule);

 

    return s;

}

 

运行结果:

 

clip_image003

 

同刚才一样,一切正常。

 

结论

可以像DLL一样,在EXE里导出函数。调用时可以静态链接也可以动态加载。EXE只作为一个进程内的模块被加载,不会新起一个进程。加载过程中EXE中的WinMain函数不会被调用。

 

例程下载:http://pan.baidu.com/s/1mgqTYBI

posted on 2012-12-01 11:41 溪流 阅读(6370) 评论(15)  编辑 收藏 引用 所属分类: Windows

评论:
# re: EXE导出函数 2012-12-01 13:15 | OwnWaterloo
exe和dll还是有很多区别的。
首先entrypoint肯定就被忽略了。
其次重定位和依赖加载好像也会有问题。

比如试试这个?
int error_code;
__declspec(dllexport) int get_error(void) { return error_code; }
__declspec(dllimport) void set_error(int x) { error_code = x; }

get和set产生的指令里会有error_code的地址。
如果是dll,加载时指令中的地址会被正确地重定位。
而exe不行,即使保留重定位信息也不行。

exe可以被加载应该是为了里面的资源而不是代码。
  回复  更多评论
  
# re: EXE导出函数 2012-12-02 12:09 | 溪流
@OwnWaterloo
不懂,为啥例子中一个是export一个是import?  回复  更多评论
  
# re: EXE导出函数 2012-12-02 16:36 | OwnWaterloo
@溪流
手误。。。我的错。。。  回复  更多评论
  
# re: EXE导出函数 2012-12-02 16:48 | OwnWaterloo
@溪流
两个都是dllexport。两个函数产生的代码都会用上error_code的地址。

链接产生这个dll/exe的时候,是很难确切地知道加载后该dll/exe的地址。同样也很难确切知道error_code的地址,因为它和dll/exe被加载后的基地址之间的偏移是链接后就固定了的。

于是链接器只能先假设dll/exe会被加载到某个位置(首选基地址),然后根据它产生代码。

比如一种极端情况,将dll/exe复制一份本地文件(首选基地址相同),然后loadlibrary它俩。
那么,至少有一个dll/exe是无法被加载到首选基地址的,也就是set/get的指令中使用的地址是不正确的。

如果是dll,没有被加载到首选基地址的话,就会发生重定项。set/get的指令会相应的修改。
而exe,我记得loader就不会做这个工作,于是就。。。
  回复  更多评论
  
# re: EXE导出函数 2012-12-02 16:50 | OwnWaterloo
@溪流
另外,load是不会根据dll/exe后缀名来判断是否是dll还是exe。
它是根据pe格式中的一个域来判断的。具体位置我忘了。。。不过dumpbin好像能显示出来。

也就是说。。。很多那些后缀是exe(甚至是ocx什么的),而且加载后也能成功调用里面函数的文件,其实按pe格式来说都是dll文件,只是后缀名没有用dll而已。
  回复  更多评论
  
# re: EXE导出函数 2012-12-02 16:58 | 溪流
@OwnWaterloo
懂了~就是说EXE当DLL用,不会被重定向咯?改天试下哈  回复  更多评论
  
# re: EXE导出函数 2012-12-02 23:47 | 朱峰everettjf
当时我也做了个类似的研究,
http://www.cppblog.com/everett/archive/2012/05/25/176073.aspx
交流交流  回复  更多评论
  
# re: EXE导出函数 2012-12-04 10:58 | zuhd
@OwnWaterloo
我做了个实验,exe的导出函数中调用全局变量也是没问题的,没出现楼上所说的崩溃现象,如果按照pe格式来理解的话,即使是exe应该也会被重定向吧?  回复  更多评论
  
# re: EXE导出函数 2012-12-04 11:00 | zuhd
楼主的例子我没下载成功,用的是朱峰everettjf提供看雪里的例子,我新增加了两个函数,调用全局变量。如果需要提供现场的话,留个邮箱。  回复  更多评论
  
# re: EXE导出函数 2012-12-04 15:12 | OwnWaterloo
@zuhd
没有崩溃=没有问题=程序正确?

*(int*)随便写个什么地址 = 12; // 只要运气好,同样不会立即崩溃。  回复  更多评论
  
# re: EXE导出函数 2012-12-04 16:13 | OwnWaterloo
本来应该用高级评论将重点高亮的,但不知道cppblog出了什么问题用不了。只能眼睛尖点了。。。

1. test files

module.c 导出1个变量与3个函数

__declspec(dllexport) int error_code;
__declspec(dllexport) int get(void) { return error_code; }
__declspec(dllexport) void set(int x) { error_code = x; }
int main(void) { return 0; }

main.c 输出变量地址、函数地址以及函数包含的指令

#include <stdio.h>

__declspec(dllimport) int error_code;
__declspec(dllimport) int get(void);
__declspec(dllimport) void set(int x);

int main(void)
{
int i;
unsigned char const* p;

printf("%p\n", (void*)&error_code);

p = (unsigned char*)get;
printf("%p:", p);
for (i=0; i<12; ++i) printf(" %02X", p[i]);
printf("\n");

p = (unsigned char*)set;
printf("%p:", p);
for (i=0; i<12; ++i) printf(" %02X", p[i]);
printf("\n");

return 0;
}


2. dll

编译
cl /LD /O1 module.c /link /noentry

查看首选基地址
dumpbin /all module.dll | find /i "image base"
10000000 image base (10000000 to 10004FFF)

查看反汇编与重定项
dumpbin /disasm /relocations module.dll

File Type: DLL

10001000: A1 00 30 00 10 mov eax,dword ptr ds:[10003000h]
10001005: C3 ret
10001006: 8B 44 24 04 mov eax,dword ptr [esp+4]
1000100A: A3 00 30 00 10 mov dword ptr ds:[10003000h],eax
1000100F: C3 ret

BASE RELOCATIONS #4
1000 RVA, C SizeOfBlock
1 HIGHLOW 10003000
B HIGHLOW 10003000

10001000 处(get的第1条)指令的操作数(地址在10001001)是 10003000
1000100A 处(set的第2条)指令的操作数(地址在1000100B)也是 10003000
注意"File Type: DLL",这是根据PE的域来的。

编译并运行得到的输出是
cl main.c module.lib && main.exe
10003000
10001000: A1 00 30 00 10 C3 8B 44 24 04 A3 00
10001006: 8B 44 24 04 A3 00 30 00 10 C3 00 00

error_code的地址和指令中使用的地址是相同的。


3. dll relocation

上面 module.dll 恰好加载在首选基地址,所以没有发生重定项。
要演示重定项发生的情况, 可以将 module.dll 复制一份, 然后用 LoadLibrary 加载。
或者直接首选基地址为一个会冲突的, exe的默认基地址0x400000。

cl /LD /O1 module.c /link /noentry /base:0x400000

dumpbin /all module.dll | find /i "image base"
400000 image base (00400000 to 00404FFF)

dumpbin /disasm /relocations module.dll
00401000: A1 00 30 40 00 mov eax,dword ptr ds:[00403000h]
00401005: C3 ret
00401006: 8B 44 24 04 mov eax,dword ptr [esp+4]
0040100A: A3 00 30 40 00 mov dword ptr ds:[00403000h],eax
0040100F: C3 ret

BASE RELOCATIONS #4
1000 RVA, C SizeOfBlock
1 HIGHLOW 00403000
B HIGHLOW 00403000

cl main.c module.lib && main.exe
00393000
00391000: A1 00 30 39 00 C3 8B 44 24 04 A3 00
00391006: 8B 44 24 04 A3 00 30 39 00 C3 00 00

对比 dumpbin 得到的反汇编与 main.exe 的输出,可以发现指令中的操作数有相应的修改,以正确的使用00393000上的变量error_code。


4. dll fixed

如果链接时选择基地址固定
cl /LD /O1 module.c /link /noentry /base:0x400000 /fixed

产生的dll里就没有重定项信息
dumpbin /relocations module.dll

并且选择的是一个肯定会冲突的基地址,所以加载main.exe就会失败。

main.exe


5. exe export

默认exe是不会包含重定项信息的
cl /O1 module.c && dumpbin /relocations module.exe

File Type: EXECUTABLE IMAGE

注意"File Type: EXECUTABLE IMAGE",这是根据PE的域来的。

并且首选基地址也是冲突的。
dumpbin /all module.exe | find /i "image base"
400000 image base (00400000 to 0040BFFF)

但是让 main.c 链接到 module.exe 可以运行成功(之前dll fixed的情况是加载 main.exe 失败)
cl main.c module.lib & main.exe
0039B700
00391000: A1 00 B7 40 00 C3 8B 44 24 04 A3 00
00391006: 8B 44 24 04 A3 00 B7 40 00 C3 33 C0

注意指令里的操作码,并没有修改为error_code的地址:0039B700。
如果真的调用了get和set,也只是读写了其他的地址,而不是error_code。
bug已经产生了。 没崩只是运气, 那个地址恰好有读写权限。
而且实验代码一般都比较短,跑完马上就退出了,这种意外的写入产生的影响也不一定能发现。

6. exe export with relocation information

可以用 /fixed:no 附带上重定项信息
cl /O1 module.c /link /fixed:no

dumpbin /relocations module.exe 会产生很多输出,因为它还引用了libc。

而让 main.c 链接到 module.exe 并运行的同样不会发生重定项
cl main.c module.lib & main.exe
0039B700
00391000: A1 00 B7 40 00 C3 8B 44 24 04 A3 00
00391006: 8B 44 24 04 A3 00 B7 40 00 C3 33 C0
  回复  更多评论
  
# re: EXE导出函数 2012-12-05 00:11 | 溪流
@OwnWaterloo
呀,信息量好大,学习了~!本想到周末研究一番的~  回复  更多评论
  
# re: EXE导出函数 2012-12-05 09:44 | zuhd
@OwnWaterloo
可以这样理解吗?
1,用lib的方式加载exe的导出函数,
#pragma comment(linker,"/FIXED:NO") ,
这样exe就和dll一样,均不会有问题。
2,用loadlibrary的方式加载exe的到处函数,
即使是重定向,在操作全局变量,也不会定向到全局变量的正确地址。
如:
而让 main.c 链接到 module.exe 并运行的同样不会发生重定项
cl main.c module.lib & main.exe
0039B700
00391000: A1 00 B7 40 00 C3 8B 44 24 04 A3 00
00391006: 8B 44 24 04 A3 00 B7 40 00 C3 33 C0  回复  更多评论
  
# re: EXE导出函数 2012-12-05 14:28 | OwnWaterloo
@zuhd
我记得是只与文件类型(PE中的那个域)有关,与加载方式(隐式加载/显式加载)无关。

只要文件类型是exe,加载器就不会去处理重定项。
如果exe没有加载到首选基地址,里面的指令操作的就不是预想中的地址。

前面说loadlibrary的意思是:这是一种让dll/exe无法加载到首选基地址的方法。
如果将dll/exe文件复制一份(文件各种信息都是相同的),然后用loadlibrary加载这两者,那两者之一肯定之多有一个是被加载到首选基地址。于是就可以观察另一个的情况了。

但前面为了偷懒。。。 就没有用这个方法,而是用/base:0x400000 —— 这个是exe文件默认的基地址 —— 让dll无法加载到首选基地址。
  回复  更多评论
  
# re: EXE导出函数 2012-12-06 09:24 | zuhd
多谢OwnWaterloo
如果可以的话 希望能在cppblog单开一主题,探其究竟。  回复  更多评论
  
# re: EXE导出函数 2012-12-06 22:41 | OwnWaterloo
@zuhd
cppblog的排版功能太弱了。。。 写东西很费劲。。。  回复  更多评论
  

只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理