首页
论坛
课程
招聘
[原创] 使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏
2022-2-6 19:37 25502

[原创] 使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏

2022-2-6 19:37
25502

缘起

前一段时间在折腾拆分 rc 的问题,已经把遇到的问题整理成文了。感兴趣的小伙伴儿可以参考这里这里这里。本以为不会有问题了,后续流程就请其它同事帮忙处理了,没想到在拆分实际项目时遇到了一个非常奇怪的链接问题。

 

本文总结了使用 process monitor 监听进程创建,查看进程参数、使用 gflags 设置 Image File Excution Options、使用 IDA 静态分析相关函数的业务逻辑以及使用 windbg 进行动态调试的整个过程。我认为这是一个由不良的编程习惯与 crt 的限制共同导致的问题。快来一起看看吧。

初闻错误

前些日子,在家隔离办公的某日中午,收到同事发来的信息说 rc 拆分的编译问题已经解决了,但是遇到了链接错误,还发送了链接错误的截图,并且给出了一个解决方案。

cvt1101-lnk1123-in-real-project

尝试把 .rc 文件排除几十个就链接过去了。

 

听到这个问题的时候,我怀疑是不是哪里操作有问题。从错误提示看是 无法打开 xxx.res 进行读取,所以第一感觉是文件路径不对。于是赶紧跟同事聊了一下,同事觉得是 vs 的限制,可能这个限制数量是 512

 

但是我从没听过同一个工程中的 .rc 文件有数量限制,不管怎样,还是建个简单的工程验证下吧。

尝试重现

带着怀疑 + 好奇的心态,我快速新建了一个 MFC 对话框工程。然后在 vs 中不断复制默认对话框(大概复制了600 个,已经比同事所说的 512 上限要多了,如果有问题应该能重现了),然后使用工具把每个对话框拆分成独立的 .rc 文件并添加到工程文件中。保存好工程后,开始编译。等待一段时间后,果然报错了,错误截图如下:

 

cvt1101-lnk1123-in-test-project

 

从错误提示看,处理 dialog_testmultiplerccompile_dialog507.rc 文件的时候报错了。按照同事说的,删除若干个 .rc 文件,只保留 500 个,再次编译,没有报错。

 

看来,在同一个工程中包含太多 .rc 文件真可能有问题。难道真有限制?为什么会做这种限制呢?不管为什么要做限制,我需要找到一个解决方案。

 

开始深入调查前,先看看报错信息。

熟悉的错误

之前遇到过错误 LINK : fatal error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏,是由于 link.execvtres.exe 的版本不一样导致的。这次报错不是这个原因。通过 process monitor 看,这两个程序的路径是一样的。

 

link-cvtres-same-path

 

再看错误 error CVT1101: 无法打开“dialog_testmultiplerccompile_dialog507.res”进行读取。猜测是在读取这个文件的时候发生了错误,可以在 process monitor 中查看相关事件。

过滤相关事件

process monitor 中根据路径名进行过滤。如果路径以 dialog_testmultiplerccompile_dialog507.res 结尾则包含,如下图:

 

include-path-end-with-dialog507

 

没想到一条记录都没有,一片空白。这是怎么回事?说实话,我有点不知所措,看来只能硬着头皮调试 + 用 IDA 逆向了。在调试之前,先用 IDA 看看有没有什么发现。

请出 IDA

使用 ida32 打开 cvtres.exeIDA 会提示是否查找符号(真是一个好消息),当然选择是。等待 IDA 分析完成后,在左侧的 Function window 中找到 _main,双击查看反汇编代码,直接在反汇编窗口按 F5,查看伪代码( IDAF5 真香!)。

 

大概浏览后,基本明白了 main() 函数的整体流程。首先,解析传入的参数,确定第一个文件在参数列表中的索引位置。然后,从此索引开始循环调用 ReadResFile() 读取每个文件,读取完所有的文件后统一调用 CvtRes() 函数进行转换。

 

下图是在 IDA 中对 main() 函数使用 F5 获得的伪代码的后半部分。

 

cvtres-main-function-logic

 

其中的 CvtRes() 函数应该是转换的主要函数,非常值得怀疑。迫不及待的启动 windbg 准备调试,但是 cvtres.exe 是被 link.exe 调用的,该如何调试呢?

搭建调试环境

如果 cvtres.exe 启动的时候,能够自动中断到调试器中,就可以方便的调试了。之前在 全局变量初始化顺序探究 中介绍过使用 gflags 进行设置的方法。

 

gflags-cvtres-setting

 

根据之前调试 cl.exe 的经验,如果长时间中断到调试器中,调用者会重新启动 cl.exe。猜想这里也会有类似的逻辑。为了避免这种问题,需要根据 link.exe 启动 cvtres.exe 的参数手动运行 cvtres.exe

 

可以通过 process monitor 很快找出 cvtres.exe 需要的参数。经过简单观察,发现传递给 cvtres.exe 的参数比较简单直接,而且根据 cvtres.exe /? 提供的帮助信息,可以很快确定各个参数的意义。

 

于是很快写出了一个批处理脚本,如下图:

 

cvtres-error-startup-bat

 

没想到,双击脚本运行的时候,出现了如下错误:

 

windbg-cannot-start-cvtres-error

 

提示找不到 cvtres.exe。看来需要使用完整路径。正确的脚本如下:

 

cvtres-startup-command

说明: 为了避免命令行参数过长,我特意简化了 .res 文件名,之前的名字太长了。而且经过测试,打开 510.res 的时候就能重现,没必要准备 600 多个 .res 进行测试,这里只准备了 511.res 文件进行测试。

猜错了

双击脚本启动 cvtres.exe,立刻就中断到了 windbg 中。

 

windbg 中执行 x cvtres!*main 即可找到入口函数,输入 bp cvtres!wmain 即可在 wmain() 函数入口处设置好断点。

 

同理,执行 x cvtres!*CvtRes 即可找到 cvtres!CvtRes() 函数,输入 bp cvtres!CvtRes 即可在 CvtRes() 函数入口处设置好断点。

 

设置好断点后,输入 g 让程序跑起来,可以发现 wmain() 函数内的断点命中了,但是 CvtRes() 函数内的断点并没有命中,进程直接退出了。

 

有些出乎意料,居然不是在 CvtRes() 函数里出的错。没(有)关(点)系(懵),继续挖掘有效信息。

继续努力

虽然进程退出了,但是依然可以通过 k 系列命令查看调用栈,在 windbg 中输入 kp,如下图:

 

cvtres-exit-call-stack

 

上图中红色高亮部分就是关键调用栈。从上图还可以得到一个非常有用的信息 —— exit code 的值是 1。可以猜测,link.exe 就是根据 cvtres.exe 的返回值来判断其是否执行成功的。

 

调用栈中的 OurFileOpen() 函数,应该是负责打开文件的函数。在继续调试之前,先在 IDA 中看看 OurFileOpen() 函数的实现。

回到 IDA

双击 OurFileOpen,当然是直接查看 F5 的结果啦,有细节需要确认再看反汇编代码。

 

view-ourfileopen-in-ida-using-f5

 

可以看到这个函数实现的非常简单,就是调用 _wfsopen(),如果失败(result == 0)那么调用 ErrorPrint() 打印错误信息。如果 open_mode(第二个参数)是 0,那么传递给 ErrorPrint() 的第一个参数是 1101,否则是 1108

 

而调用 OurFileOpen 时传递的第二个参数是通过 edx 传递的,对应的值是 0,所以如果出错,那么会传递 1101

 

view-ourfileopen-param

 

说实话,看到 OurOpenFile() 函数中的 1101 ,我太激动了,因为在vs 中看到的错误提示是 error CVT1101: 无法打开“xxx.res”进行读取。为了进一步确认猜想,在 IDA 中查看 ErrorPrint() 函数的反汇编代码,如下图:

 

view-ErrorPrint-in-ida

 

从上方红色高亮语句 CVTRES: fatal error CVT%04u: 基本可以确定猜测是正确的。从上图底部的红色高亮区域还可以知道该函数内部确实会调用 exit(1) 来结束进程。

 

接下来需要调查的问题是 _wfsopen 为什么失败了?

为什么 _wfsopen 会失败?

windbg 中输入 .restart 重启目标程序,输入 bp MSVCR120!_wfsopen,然后执行 g 命令。因为已经设置好了符号查找路径,所以 windbg 自动打开了对应的源码文件。

 

break-and-open-source-file

 

这个函数虽然很简单,加上注释不到 50 行。但是会被调用很多次,根据经验,前面的 500 多次调用都没有问题,在尝试打开 510.res 的时候会有问题,所以设置一个条件断点非常有必要。

 

简单查看反汇编代码发现,_wfsopen() 函数的第一个参数是通过 ecx 传递的,可以设置如下的条件断点(真是烧脑还不好理解,我不会告诉你,我尝试了很久才写出了下面这段蹩脚的脚本):

1
bp MSVCR120!_wfsopen "aS /mu $myFileName @ecx; .block {.echo $myFileName; r @$t0=$spat(@\"$myFileName\", @\"*510.res\"); .if(1==$t0){.echo **** bang ****} .else{ gc;} };"

耐心等待一会就中断下来了,如下图:

 

break-at-open-510res

 

单步走两步,发现是 _getstream() 出错了。

_getstream 错在哪里了?

输入 .restart 重启目标程序,并且设置好条件断点,重新运行程序,当中断到 _wfsopen() 函数后,单步步入到 _getstream() 函数中。

 

view-getstream-in-windbg

 

可以看到 _getstream() 函数逻辑也不复杂,根据注释可以很简单的理解此函数的逻辑 —— 从 __piob 中(大小是 _nstream,通过 dt _nstream 可知其大小是 512)找到一条可用的记录项。判断一条记录项是否可用的标准是 __piob[i] == NULL ,或者 !inuse( (FILE *)__piob[i] ) && !str_locked( (FILE *)__piob[i] )

 

直接在函数末尾加好断点,g 起来,发现确实没有找到一条可用的记录项。

 

至此,我大概明白了整个过程。cvtres.exemain() 函数中会循环调用 ReadResFile() 函数(内部会调用 _wfsopen())读取所有的 .res 文件,但是读取完一个 .res 文件后,并没有关闭,当打开一定数量的文件后会导致 __piob 被占满。再尝试打开一个文件的时候就报错了。

 

看来,crt 还有最大打开文件数的限制,赶紧 google 搜索是否有什么设置可以调整最大文件打开数量。

google 一下

google 中输入 crt max open file 找到了几个相关的网址。

 

search-crt-max-open-file-in-google

 

虽然可以通过 _setmaxstdio() 调整 crt 的最大文件打开数,但是好像不能通过修改配置文件或者修改注册表的方式调整。

发帖询问

说实话,第一次分析到这个结果的时候我是有些不信的。于是我再三确认了 ReadResFile() 函数内部确实没有关闭文件的操作。难道有什么特殊的理由不关闭打开的文件?但是我实在想不出有什么理由。所以我觉得这是一个 bug,于是我在微软官方论坛上发了一个帖子,希望能得到一些回复。

 

帖子地址是 https://docs.microsoft.com/en-us/answers/questions/709392/cvt1101-can39t-open-xxxres-for-reading.html

 

目前只有一位网友回复(另外一个是我自己),为了方便大家阅读,截图如下:

 

 

虽然到现在还没收到官方的确认回复,不过我依然认为这是一个 bug,而不是 feature

解决方案

既然没有设置选项或者配置文件可以简单的调整最大文件打开数量,对 cvtres.exe 打补丁又不太现实(每台机器上都要做处理),等待微软修复这个问题也不现实(远水解不了近渴)。所以我们的解决方案是通过合并一些 .rc 以减少工程中的 .rc 文件数量来规避这个问题。

 

虽然问题已经调查清楚了,但是还有几个问题值得探究。

几个值得深究的问题

  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.

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

    可以使用 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
  3. 有什么简单的办法可以查看 __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

  4. 为什么在打开 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

总结

  • crt 有最大打开文件数的限制,可以通过 _setmaxstdio() 进行调整。
  • 在一个工程中最好不要同时包含太多 .rc 文件,一般应该不会遇到我遇到的这种情况。
  • 在不需要使用文件的时候,一定要及时关闭。
  • 进程退出后,依然可以使用 k 系列命令查看调用栈,有时候可以快速定位进程退出的原因。

参考资料

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

 

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

 

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

 

https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-170

 

vs2013 自带的 crt 源码


看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

收藏
点赞3
打赏
分享
最新回复 (6)
雪    币: 9990
活跃值: 活跃值 (5700)
能力值: ( LV13,RANK:375 )
在线值:
发帖
回帖
粉丝
TkBinary 活跃值 5 2022-2-8 09:32
2
0
认真拜读了以下文章。楼主的钻研精神是真牛。纯逆向记录贴。特别是 浪费了很多时间写了windbg脚本哪里 幽默风趣一笔带过。是不是研究的同时还能学学脚本怎么写。
雪    币: 1125
活跃值: 活跃值 (1196)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 活跃值 1 2022-2-8 16:00
3
0
感谢分享!
雪    币: 6405
活跃值: 活跃值 (4407)
能力值: ( LV12,RANK:270 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-2-9 21:38
4
0
TkBinary 认真拜读了以下文章。楼主的钻研精神是真牛。纯逆向记录贴。特别是 浪费了很多时间写了windbg脚本哪里[em_13] 幽默风趣一笔带过。是不是研究的同时还能学学脚本怎么写。[em_13]
对啊大佬,我觉得最大的收获是解锁了 dx 新语法,以后写条件断点再也不用愁了
雪    币: 6405
活跃值: 活跃值 (4407)
能力值: ( LV12,RANK:270 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-2-9 21:39
5
0
fengyunabc 感谢分享!
感谢大佬阅读
雪    币: 107
活跃值: 活跃值 (290)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yangya 活跃值 2022-2-18 14:18
6
0
dx语法很强大啊。
)@esp+1这里为啥是esp,不应该是ecx?
雪    币: 6405
活跃值: 活跃值 (4407)
能力值: ( LV12,RANK:270 )
在线值:
发帖
回帖
粉丝
编程难 活跃值 3 2022-3-6 11:23
7
0
yangya dx语法很强大啊。 )@esp+1这里为啥是esp,不应该是ecx?
我也觉得 dx 太强大了,32 位的程序撒
游客
登录 | 注册 方可回帖
返回