browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

DLL 导出函数的静态链接

Posted by on 2009 年 01 月 04 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

通常在编写 DLL 的时候,我们往往需要编写一个相应的 .def 文件,这个文件中用于定义 DLL 中导出的所有符号。
举个例子来说,我有一个名为 SayHello.dll 的动态库,这个 DLL 中导出了两个函数 SayHelloChinese 和 SayHelloEnglish,功能分别是用汉语和英语弹出“你好,世界!”和 "Hello, World" 的 MessageBox。那么,我的这个 SayHello.def 内容可以是下面这个样子:

1
2
3
4
5
6
LIBRARY "SayHello"
 
EXPORTS
 
SayHelloChinese    @1
SayHelloEnglish    @2

用来调用 SayHello.dll 的应用程序名为 Hello.exe,它之中只调用了 SayHelloChinese 函数。
接下来用 Dependency Walker 打开 Hello.exe,查看它对 SayHello.dll 的依赖情况,如下图。

我们会发现,Hello.exe 的确导入了 SayHelloChinese,但是更确切一些来说,它导入的是 SayHello.dll 所导出的第 1 个函数。为了验证这个猜测,我们把 SayHello.def 修改为下面这个样子:

1
2
3
4
5
6
LIBRARY "SayHello"
 
EXPORTS
 
SayHelloEnglish    @1
SayHelloChinese    @2

修改完毕后重新编译 SayHello.dll,但 Hello.exe 一定要保持原样不动。编译完成后,再次使用 Dependency Walker 打开 Hello.exe,如下图。

现在可以看到,Hello.exe 仍然导入的是 SayHello.dll 所导出的第 1 个函数,但这个函数已经变成 SayHelloEnglish 了。于是问题就来了——如何保证 Hello.exe 始终调用 SayHelloChinese 呢?
方法很简单,修改 SayHello.def,去掉导出函数名后面的编号就可以了,如下。

1
2
3
4
5
6
LIBRARY "SayHello"
 
EXPORTS
 
SayHelloChinese
SayHelloEnglish

重新编译 SayHello.dll 和 Hello.exe,Dependency Walker 的结果显示出,Hello.exe 按名称导入了 SayHelloChinese。

其实上面这点东西没什么技术含量,不过倒是相当有使用价值的:我们可以借助 .def 文件来静态链接没有 .lib 的 DLL,比如 ntdll.dll 中的原生 API。
Platform SDK 中并没有附带 ntdll.lib,所以想要调用原生 API 的话,必须使用 GetProcAddress 动态调用,或者从 DDK 中找到 ntdll.lib 来使用。其实,我们完全可以使用 .def 来自己生成这个 ntdll.lib。
举个例子来说,ntdll.dll 中所导出的 DbgPrint 要比 OutputDebugString 更好用。为了使用这个函数,我们可以新建一个 ntdll.def,内容如下。

1
2
3
4
5
LIBRARY "ntdll"
 
EXPORTS
 
DbgPrint

下面使用 VC 中自带的 lib.exe 工具,生成我们的 ntdll.lib 导入库。

1
lib /def:E:\ntdll.def /machine:x86 /out:E:\ntdll.lib

生成完毕之后,就可以直接静态链接并使用 DbgPrint 了,测试代码如下。

1
2
3
4
5
6
7
8
9
#pragma comment(lib, "E:\\ntdll.lib")
 
extern "C" void __cdecl DbgPrint(const char* Format, ...);
 
int main(int argc, char* argv[])
{
    DbgPrint("Hello, World!");
    return 0;
}

需要指出的是,这里一定不能使用序号来链接 DbgPrint,因为在不同版本的 ntdll.dll 之中,DbgPrint 的序号是不同的。

订阅本站

7 Comments

  • At 2009.01.05 09:22, 李马 said:

    自己坐沙发。需要补充一点,这里我挑 DbgPrint 是运气好遇到了个能用的,因为它是 cdecl 调用约定的,而 lib.exe 生成的导入库也会是按照这个约定生成的。
    因此,对于 stdcall 的原生 API 来说,仅仅用 lib.exe + .def 文件是不够的。目前我找到的办法是实现一份空的 ntdll.dll,然后取用其导入库。

    • At 2009.01.07 10:22, luther said:

      好文.
      其实想用自己定义def的办法, 是在于已经知道某个DLL里函数导出的原形. 对于很多想用DLL而只能Dependency 看到它的函数名,而不知参数.

      • At 2009.01.14 12:05, 王永龙 said:

        你写这个的用意是不是想在不重新编译Hello.exe的情况下,修改SayHello.dll的导出函数或者增加减少导出函数,而不至于Hello.exe在运行时不发生调用错误?

        按照正常理解,如果修改了def文件,不管你是否标上了导出函数的序号,那么输出的索引.lib一定要替换原来的,然后重新编译一下Hello.exe。

        不知理解是否恰当~ :)

        • At 2009.01.14 12:10, 李马 said:

          如果使用的是名称绑定,只要函数名称不改变就可以不重新编译 Hello.exe。
          事实上我的用意是利用名称绑定来为没有 .lib 的 DLL 文件生成导入库,仅此而已。

          • At 2009.04.20 17:43, starmate said:

            我觉得这个方法很好,只是我有个疑问,对于非系统的dll,我用这个方法,最后的时候弹出对话框提示找不到动态链接库,这不是静态编译的吗?
            如果不是,那么第一步中两个SayHello的函数又让我不理解了,hello.exe难道是动态加载了那个dll吗?

            • At 2009.04.20 17:47, 李马 said:

              不是“静态编译”,而是静态链接。
              静态链接是指利用 .lib 文件将 dll 中的导出函数链接到 exe 的导入表之中,这样在启动 exe 之后,目标 dll 会被 exe 自动加载,而不需要手工调用 LoadLibrary。

              • At 2009.04.20 18:02, starmate said:

                原来如此,我把两个概念弄混了,难怪了,我说呢,要不然动态库也可以静态编译那都不用静态库了,呵呵,动手还是弄清楚了很多东西,blog写的不错,很欣赏,对于你在《两个决定》提到的SSDT的图,我发现一些讲解SSDT的论文也都引用了你的图呢

                (Required)
                (Required, will not be published)