• Windows编程dll基本知识点


    前言

    本篇博客主要是记录windows系统下dll开发的相关基本知识点,并使用相关分析工具分析,有利于初学者学习,更是为开发者查缺补漏;

    使用dumpbin查看dll,lib,exe相关信息

    VS编译器提供了查看链接库相关的工具,安装后的VS编译器的安装目录内可以找到dumpbin.exe,也可以在工具里直接打开dumpbin
    在这里插入图片描述

    打开VS2015 x86 x64兼容工具命令提示符,输入dumpbin指令,/exports是显示所有函数的指令,后面是要查看的dll文件

    C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC>dumpbin /exports G:\bin\CustomWidget.dll
    
    • 1

    执行之后便可以看到dll内相关函数信息,红色框内便是函数名
    在这里插入图片描述
    也可以用dumpbin查看lib和exe信息,只不过指令不是/exports,具体的指令有:

    查看a.dll库中包含哪些函数,可以使用:dumpbin /exports a.dll 
    查看b.exe中加载了哪些动态库,可以使用:dumpbin /imports b.exe
    查看c.lib中包含哪些函数,可以使用:dumpbin /all /rawdata:none c.lib 
    查看d.obj中包含哪些函数,可以使用:dumpbin /all /rawdata:none d.obj 
    
    • 1
    • 2
    • 3
    • 4

    dll输出调试信息与debugview调试

    在编写dll程序的过程中,不像exe可以输出信息到控制台,可以将dll中信息输出到调试信息窗口内;只需要调用OutputDebugString函数;别人调用此函数时,调试信息也会显示在调试窗口内;

    int __stdcall fun1(int a, int b)
    {
    	OutputDebugString(L"Camera::Run Fun");
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    调用dll时,可以在调试窗口内看到:
    在这里插入图片描述

    dll发布后,三方调用时,如何查看dll内输出的信息,微软提供了debugview软件,可以跟踪dll内信息;
    在这里插入图片描述
    在这里插入图片描述
    点击漏斗型按钮,打开过滤器设置,在Include中输入Camera::,在Exclude中输入WAIT_TIMEOUT;这样就只显示带字符串“TRACE”的debug信息,不显示带“WAIT_TIMEOUT”的调试信息;不设置过滤器的话,会捕获所有dll的调试信息,我们只关注所监控的信息,在编写dll时,设置好唯一的标识符字符;
    在这里插入图片描述

    在这里插入图片描述

    exe是如何找到dll的?

    .dll 是动态链接库文件,里面存储着函数和数据;
    .lib是静态数据连接库文件,存储着函数名和文件位置;
    也就是说在执行程序时,exe文件可通过lib文件找到dll文件,并执行在程序中调用的函数。
    Windows在查找dll文件会按照以下几种方式顺序查找:
    1.exe文件所在的目录下;
    2.进程当前的工作目录;
    3.Windows系统目录;
    4.Windows目录;
    5.环境变量Path下的一系列目录

    C语言编译成dll,lib和C++编译成dll,lib的区别

    C语言编译动态链接库

    首先使用VS建立一个win32的dll空项目,再添加SelfDll.h文件和SelfDll.c文件(编译器会根据.c文件格式默认为C的编译器)
    SelfDll.h文件代码如下:

    /**
    @file       SelfDll.h
    @brief      C语言导出dll
    */
    #pragma once 
    __declspec(dllexport) int fun1(int a,int b);
    __declspec(dllexport) double fun2(double a, double b);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里注意的是

    _declspec(dllexport) int fun1(int a,int b);    //加上_declspec(dllexport)是导出lib文件,如果不加上,则只有dll文件
    int fun1(int a,int b);     //只导出dll文件
    
    • 1
    • 2

    SelfDll.c文件代码如下:

    #include "SelfDll.h"
    int fun1(int a, int b) 
    {
    	return a + b;
    }
    
    double fun2(double a, double b)
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    最终编译出来的文件如图
    在这里插入图片描述
    使用dumpbin查看c编译的dll
    在这里插入图片描述

    C++编译动态链接库

    VS建立一个win32的dll空项目,添加SelfDll.h文件和SelfDll.cpp文件(注意不是.c文件)
    SelfDll.h文件代码如下:

    /**
    @file       SelfDll.h
    @brief      C++导出dll
    */
    #pragma once 
    __declspec(dllexport) int fun1(int a,int b);
    __declspec(dllexport) double fun2(double a, double b);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    SelfDll.cpp文件代码如下:

    #include "SelfDll.h"
    int fun1(int a, int b) 
    {
    	return a + b;
    }
    
    double fun2(double a, double b)
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    编译出的dll,lib与C语言相同名称,使用dumpbin查看c++编译的dll
    在这里插入图片描述

    C++如何使用C语言编译的链接库

    从上面的图可以看出,虽然函数声明与定义一样,但是编译出来的结果不一样。
    一个是_fun1,另一个是?fun1@@YAHHH@Z,可以看出存在差异。
    如果C++程序链接C编译的动态链接库,则会报错:
    在这里插入图片描述
    如果C++可以使用C编译的链接库,需要在原先C的头文件内增加extern "C"关键字,这样编译器便会将此处函数按照C链接编译;
    C++项目中重新修改原先C的头文件

    /**
    @file       SelfDll.h
    @brief      C语言导出dll
    */
    #pragma once 
    
    extern "C"
    {
    	__declspec(dllexport) int fun1(int a, int b);
    	__declspec(dllexport) double fun2(double a, double b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    当C++编译到C这段代码时,会将这段按照C编译;

    C++导出类的动态链接库

    同样的,需要增加__declspec(dllexport),在.h文件内代码如下:

    __cdecl与__stdcall的区别

    Visual Studio默认是__cdecl,如果使用这个关键字,以后栈的销毁是调用者来做,VS自己编译的dll,再用VS调用会自动销毁栈,但是给其他编译器使用时,就会出问题。
    因此标准调用,最好是自己编写__stdcall,其他编译器和其他语言,比如C#,VB都会识别出这个dll,并自动清理栈;

    __cdecl是VS默认加上的,因此VS不需要添加写,__stdcall这个关键字需要在函数声明和定义的时候都带上,不然编译器会认为是两个函数;

    分别定义一个带_stdcall的dll文件和不带_stdcall的dll文件,用dumpbin查看下区别:
    带_stdcall的.h源码如下:

    /**
    @file       SelfDll.h
    @brief      __cdecl与__stdcall的区别
    */
    
    #pragma once 
    
    extern "C" {
    __declspec(dllexport) int __stdcall fun1(int a, int b);
    __declspec(dllexport) double __stdcall fun2(double a, double b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    带_stdcall的.cpp源码如下:

    #include "SelfDll.h"
    
    int __stdcall fun1(int a, int b)
    {
    	return a + b;
    }
    
    double __stdcall fun2(double a, double b)
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    用dumpbin查看下区别
    带__stdcall的
    在这里插入图片描述
    不带__stdcall的
    在这里插入图片描述
    带__stdcall的函数_fun1@8,这里8代表8个字节,源代码fun1的形参是两个int,占8个字节,带__stdcall的函数编译出的dll附带形参栈信息,因此可以被其他语言或者编译器调用。

    综上所述,对于dll开发者而言,应该考虑到二次开发者是使用C++开发还是C开发,最好添加一个判断C++的宏,因此标准的dll文件一般都有这样开头的宏定义

    #ifndef SDK_API
    #if (defined(_WIN32) || defined(_WIN64))
    #if  defined(SDK_API)
    #define SDK_API __declspec(dllexport)
    #else
    #define SDK_API __declspec(dllimport)
    #endif
    #else
    #ifndef __stdcall
    #define __stdcall
    #endif
    #ifndef SDK_API
    #define SDK_API
    #endif
    #endif
    #endif
    
    #ifdef __cplusplus
        extern "C"  // 使用extern "C"
        {
    #endif
            // 设置业务消息回调接口
            SDK_API void __stdcall SetMsgCallBack(int a);
            // 初始化SDK库
            SDK_API void __stdcall InitNetSDK();
            // 登录
            SDK_API void __stdcall LoginServer(const int a);
     
    #ifdef __cplusplus
        }
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    对于二次开发者来说,如果加载dll进项目中后发现编译错误。在发现配置文件没有出错的情况下,很可能开发者忘记了添加extern “C”,这时候二次开发者就需要手动在原来的.h文件内添加extern "C"了;

    def文件规范导出符号

    如果为了其他语言或者编译器能够使用dll,我们就需要在每一个函数签名加上"extern “C” _declspec(dllexport)"这一长串声明。如果需要导出的函数较多则显得非常繁琐,也非常难看。为了简化这一过程,VS引入了 def文件方便我们操作。
    头文件便可以简化为:

    /**
    @file       SelfDll.h
    @brief      使用def文件导出dll
    */
    
    #pragma once 
    
    int __stdcall fun1(int a, int b);
    double __stdcall fun2(double a, double b);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在项目中加上一个.def文件,内容如图所示。
    在这里插入图片描述

    在VS编译器内添加上:
    在这里插入图片描述

    模块定义文件是用来描述 dll 文件的文本格式的文件,其格式如下:

    LIBRARY libdll.dll      ;dll 文件的文件名
    DESCRIPTION "描述信息"  ;描述信息,此行可以不要
    EXPORTS
    lib_add @1        ;函数描述
    lib_sub @2        ;函数描述
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第一行:在 LIBRARY 后面填 dll 文件的名字,分号后面是注释。
    第二行:DESCRIPTION,描述信息,此行可以忽略
    第三行:EXPORTS
    第四行开始,是 dll 文件中函数的描述,可以使用 dumpbin /EXPORTS libdll.dll 命令查看,(其中,libdll.dll 是目标 dll 的文件路径)
    注意,def只生成dll文件,是没有lib文件的,如果想要生成对应的lib文件,需要在dumpbin那里使用以下命令:

    lib /out:F:\sqlite3.lib /MACHINE:X64 /def:F:\sqlite3.def
    
    • 1

    F:\sqlite3.lib是要生成的lib文件
    F:\sqlite3.def是项目生成dll的def文件

    __declspec(dllimport)和__declspec(dllexport)的区别

    这篇博主详细分析了之间的区别,结论就是:
    dllimport是为了更好的处理类中的静态成员变量的,如果没有静态成员变量,那么这个__declspec(dllimport)无所谓。
    链接如下:
    dllimport与dllexport作用与区别
    因此,很多三方库的.h文件内都是这样定义

    #ifdef SIMPLEDLL_EXPORT
    #define DLL_EXPORT __declspec(dllexport)
    #else
    #define DLL_EXPORT __declspec(dllimport)
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5

    C#使用C++导出的dll

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Runtime.InteropServices;  //引入三方库
    
    namespace LearnDll
    {
        class Program
        {
            [DllImport("E:/14_learnC++/LearnDll/SelfDll(C++)/SelfDll/Debug/SelfDll.dll", CharSet = CharSet.Ansi)]
            //声明函数
            static extern int fun1(int a, int b);
    
            static void Main(string[] args)
            {
                int a = 1;
                int b = 2;
                int c = fun1(a,b);
                Console.WriteLine("结果是:"+ c );
                Console.ReadKey();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    C++编译的dll库通过def文件导出,C#可以直接调用;

  • 相关阅读:
    如何破解压缩包密码,CTF压缩包处理
    Pytorch分布式训练/多卡训练DDP——模型初始化(torch.distribute 与 DDP的区别)
    【Git】推送Github失败:remote: Permission to xxx/*.git denied to xxx
    C#复杂XML反序列化为实体对象两种方式
    php 目录访问控制
    oracle存储过程实现定时备份表和处理重复数据
    学习-Java数组之foreach遍历数组之正负数数量统计
    400 The plain HTTP request was sent to HTTPS port
    Android学习笔记 61. 使用布局编辑器进行线性布局
    C# 设置PDF表单不可编辑、或提取PDF表单数据
  • 原文地址:https://blog.csdn.net/qq_43376782/article/details/132548150