调试实战 | 使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(中)

前言

上一篇文章 中,我们总结了使用 windbgIDA 找出 cvtres.exe 报错的根本原因,但是留下了几个细节问题。本篇文章就来把这几个细节问题“格”干净。

几个值得深究的问题

  1. 为什么链接的时候需要调用 cvtres.exe 呢?

    微软官方描述 摘录如下:

    You can specify a .res file when linking a program. The .res file is created by the resource compiler (RC). LINK automatically converts .res files to COFF. The CVTRES.exe tool must be in the same directory as LINK.exe or in a directory specified in the PATH environment variable.

    stackoverflow 上有一个更加详细的描述,摘录如下:

    Input files must have the Common Object File Format (COFF) format. If an input file is not COFF, the linker automatically tries to convert 32-bit OMF objects to COFF, or runs CVTRES.EXE to convert resource files. This message indicates that the linker could not convert the file. This can also occur when using an incompatible version of CVTRES.EXE from another installation of Visual Studio, the Windows Development Kit, or .NET Framework.

  1. 有没有更好的设置条件断点的方式?目前的语法实在是太难用了。

    可以使用 dx 提供的语法来设置条件断点。先定义一个辅助函数 @$get_wfs_open_file_param 用来获取 file 参数的值。

    1
    dx @$get_wfs_open_file_param = (esp_value => ((wchar_t*)*((int*)esp_value+1)).ToDisplayString("sub"))

    设置条件断点

    1
    bp /w "@$get_wfs_open_file_param(@esp).EndsWith(\"510.res\")" MSVCR120!_wfsopen

    是不是比传统的方式好理解的多?如果不想定义辅助函数,可以直接像下面这样设置条件断点。

    1
    bp /w "((wchar_t*)*((int*)@esp+1)).ToDisplayString(\"sub\").EndsWith(\"510.res\")" MSVCR120!_wfsopen
  2. 有什么简单的办法可以查看 __piob 数组中元素的内容吗?

    在前一篇文章中,只是通过逻辑推理确定了当尝试打开 510.res 时,__piob 数组中的每个元素都被占用了。并没有实际查看其内容。其实,可以通过 dx 命令非常方便的查看想要查看的内容。比如,可以只查看那些可用的记录项(值为 NULL 或者不满足 inuse()str_locked() ),也可以只查看那些不可用的记录项。

    为了让代码更容易理解,定义了几个辅助变量及函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $$ 以下变量定义在 VC\crt\src\stdio.h 中
    dx @$ioReadFlag = 0x1
    dx @$ioWriteFlag = 0x2
    dx @$ioRWFlag = 0x80
    dx @$ioLockedFlag = 0x8000

    $$ inuse() 和 str_locked() 定义在 VC\crt\src\file2.h 中
    dx -r0 @$IsInUse = (f => f->_flag & (@$ioReadFlag | @$ioWriteFlag | @$ioRWFlag))
    dx -r0 @$IsLocked = (f => f->_flag & @$ioLockedFlag)

    $$ 使用自定义变量 my_piob 指向 __piob
    dx -r0 @$my_piob = (FILE*[512])MSVCR120!__piob

    $$ 找到所有可用的记录的数量
    dx @$my_piob.Where(f => f == 0 || (@$IsInUse(f) && @$IsLocked(f) ) ).Count()

    $$ 找到所有不可用的记录的数量
    dx @$my_piob.Where(f => !(f == 0 || (@$IsInUse(f) && @$IsLocked(f) ) ) ).Count()

    当打开 510.res 中断下来时,可以使用以上脚本找出所有不可用记录,发现没找到任何记录,说明所有记录都被占用了。

    view-free-and-nonfree-item-count

  1. 为什么在打开 510.res 的时候就报错了?应该可以打开 512 个文件才对?

    猜测应该是已经打开了一些文件。为了验证这个猜测,再次调试。

    cvtres.exe 中断到 windbg 后,执行 x MSVCR120!__piob 查看 __piob 的值。

    view-piob-in-windbg

    可以看到此时 __piob 的地址是 0x6250fe00 ,值是 0。说明此时还没被赋值。通过 ba w4 0x6250fe00 设置内存写断点设置好后,执行 g 命令恢复运行。几乎立刻中断到 windbg 中。

    view-init-piob-callstack

    可以看到 __piob 会在 __initstdio() 函数中被初始化。而且前 _IOB_ENTRIES 项(值为 20)的值会被 _iob 中对应的值赋值。而 _iob 的前三项是被占用的。如下图:

    view-iob-first-3-items

所以,cvtres.exe 最多只能通过 _wfsopen() 函数打开 512-3 = 509 个文件。也就是说,只能打开 1.res ~ 509.res,打开 510.res 的时候会报错。

注意: _iob 的前三项分别指向了 stdin, stdout, stderror。我是怎么知道的?看代码呗。
view-iob-init-code

除了通过 _iob 初始化代码的注释可以看出来前三项的意义,也可以通过 stdio.h 中的宏定义看出来。

view-stdin-stdout-stderror

More

还有一个小问题没解决 —— 为什么设置 PATH 环境变量后,直接通过文件名启动程序,windbg 会报错?下一篇文章再“格”吧。

总结

  • 命令行程序默认会打开三个文件 stdin、stdout、stderror。所以,crt 默认能同时打开的文件数是 509 个。
  • dx 是真的香。不仅强悍,而且还易用,符合直觉。

参考资料

https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/setting-a-conditional-breakpoint

https://docs.microsoft.com/en-us/cpp/build/reference/dot-res-files-as-linker-input?view=msvc-170

https://stackoverflow.com/questions/61581826/visual-studio-2019-cvt1101-lnk1123-fatal-error

vs2013 自带的 crt 源码

BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%