Visual Studio调试DLL项目

20/03/28 |
Visual Studio调试DLL项目

引言

因为易语言写多了, 被奇奇怪怪的坑点弄得总想换个语言开发, 摸索了很久, 选择了C++,
可能是对新事物不太习惯, 最开始找了一些IDE用不太懂, 特别是配置环境什么的,
加上英语不好, 就更搞不清楚, 所以一直搁在那.

后面发现VS这个IDE, 刚好赶上C++课设, 就试着用VS写课设, 踩了一些坑,
后面放假就开始愉快的用VS开发DLL项目, 还算是顺利吧.

不过一直搞不清楚怎么调试DLL项目, 编译完一开始运行就提示「 不是有效的应用程序 」,
一些博主说要创建一个EXE项目来调用, 但没说清楚怎么调用云云.

因为易语言可以直接运行调试DLL, 所以觉得这种方法有点麻烦,
加上可以用机器人程序挂载DLL, 做到运行时调试, 所以勉强过得去.

但是每次调试都要先卸载DLL后才能编译, 完了还要加载, 而且也要开着机器人程序,
有时候网不好或者其他原因, 登不上去, 那就没法调代码了, 特别坑.

所以还是想把这个问题解决了, 但能力有限, 找不到合适的方法解决.
加上中间几个月因为各种事情没有精力写DLL, 所以这个问题也就耽搁在那没有管.

最近又有点时间碰DLL, 但还是没有管这个问题, 只是稍微探索了下,
试了一些骚操作, 不过没有成功, 接着搁置.

今天修完bug后还是想摸索一下, 在半被喷的情况下和群里dalao讨论了一会, 算是摸出来了.

无界面调试

特点

  • 依赖Windows系统自带的rundll32.exe,
    省去额外创建项目调用DLL的操作.

步骤

  1. 创建一个可被rundll32.exe调用的特定函数
    根据MSDN的说明, (只能在一些博客找到)该函数原型:

    void CALLBACK EntryPoint(HWND hWnd, HINSTANCE hInst, LPSTR lpszCmdLine, int nCmdShow);
    • EntryPoint 为 可自定义函数名
    • lpszCmdLine 为 传入的命令行参数, char类型
    DLL中的实际代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    extern "C" __declspec(dllexport) 
    void __stdcall DllDebug(
    HWND hWnd, // 父窗口的句柄
    HINSTANCE hInst, // DLL的实例句柄
    LPCSTR lpszCmdLine, // DLL将要解析的字符串
    int nCmdShow // 显示状态
    )
    {
    if(!IsDebuggerPresent())
    return;
    OutputDebugString("Start Debugging\n");
    // TODO
    OutputDebugString("End Debugging\n");
    return;
    }
    • 添加了一些函数头, 使用「 DllDebug 」作为函数名,
    • 函数本身可为空, 这里为了防止非调试时调用, 添加了限定条件,
  2. 设置项目属性
    2.1 将「 调试 」-「 要启动的调试器 」改为「 本地 Windows 调试器 」,
    2.2 根据平台修改「 命令 」设置,

    • Win32:「 $(SystemRoot)\SysWOW64\rundll32.exe
    • x64:「 $(SystemRoot)\System32\rundll32.exe

    2.3 修改「 命令参数 」为「 "$(TargetPath)" DllDebug

  3. (可选) 如果使用了「 模块定义文件 」
    考虑到你可能不清楚这是什么:

    简单来说, 就是用于帮助编译程序更好声明DLL函数入口的一个文件.

    这么说肯定还是很抽象:

    看看「 项目属性 」-「 链接器 」-「 输入 」-「 模块定义文件 」是否被设置

    如果没有设置, 就跳过这步


    打开「 模块定义文件 」, 在最后加入一行「 DllDebug @ 99

  4. 点击「 调试 」-「 开始执行 」( 或按F5 ) 感受结果.
    至少不会提示「 xxx.dll 不是有效的应用程序 」了吧?
    如果会, 请检查你选择的 「 配置 」「 平台 」

附加说明

  1. 为什么接收的命令行参数lpszCmdLine不能是wchar_t类型?
    其实是可以的, 这与rundll32.exe的设定有关,
    rundll32会判断有没有W结尾的函数, 有的话会优先调用W结尾的函数.
    我们只需要在函数名后加上W就好, 不需要修改「 命令参数 」,
    如果用到「 模块定义文件 」, 也需要加W.

  2. 为什么会提示「 找不到指定的模块 」?

    因为项目中使用了lib文件, lib文件会在DLL被加载时主动加载其对应的DLL.

    「 如何解决? 」使用VS工具检查DLL依赖:
    2.1 复制编译生成文件的绝对路径, 如D:\a.dll
    2.2 打开「 Developer Command Prompt for VS 2019 」,
          其他版本名字类似, 也可使用「 Developer PowerShell
    2.3 使用「 dumpbin 」查看DLL的依赖项,

    `dumpbin /dependents "D:\a.dll"`
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ···

    Dump of file D:\a.dll

    File Type: DLL

    Image has the following dependencies:

    KERNEL32.dll
    USER32.dll
    QYOffer.dll

    ···

    2.4 根据命令输出, 可以发现存在非系统依赖项DLL「 QYOffer.dll
          这时候有两种做法:
          i. 将「 QYOffer.dll 」复制到和DLL文件同目录下,
           但这种做法不现实, 因为实际部署项目的时候, 并不可能把所有文件放在同目录下.
          ii. 设置延迟加载项
           在 项目属性「 链接器 」-「 输入 」-「 延迟加载的DLL 」的属性值中添加「 QYOffer.dll

带控制台调试

思路

和 无界面调试 略有不同, 此方法是将DLL项目手动改为控制台应用项目

步骤

  1. 添加main函数, 对, 就是你想的那样
    1
    2
    3
    4
    int main() 
    {
    return 0;
    }
  2. 修改「 项目属性 」-「 常规 」-「 配置类型 」, 改为「 应用程序 (.exe)
  3. 修改「 项目属性 」-「 链接器 」-「 系统 」-「 子系统 」, 改为「 控制台 (/SUBSYSTEM:CONSOLE)
  4. 点击「 调试 」-「 开始执行 」( 或按F5 ) 感受结果.
    如果出现编译问题, 则需要进行第5步.
  5. 修改「 项目属性 」-「 C/C++ 」-「 预处理器 」-「 预处理器定义 」, 删除_WINDOWS, 添加_CONSOLE.
  6. 再次进行第4步.

说明

其实这个方法还挺简单的, 但会影响最终生成的文件, 所以一般只在Debug配置.

参考链接

[1] EntryPoint原型
[2] Rundll32.exe 调用DLL自定义导出函数
[3] rundll32提示找不到指定的模块
[4] 用VS查看程序的dll依赖项
[5] 无法解析的外部符号 _WinMain@16