使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)
原总结排错process monitorvsIDAwindbg调试rcCVT1101LNK1123
缘起
前一段时间在折腾拆分 rc 的问题,已经把遇到的问题整理成文了。感兴趣的小伙伴儿可以参考这里,这里 和 这里。本以为不会有问题了,后续流程就请其它同事帮忙处理了,没想到在拆分实际项目时遇到了一个非常奇怪的链接问题。本文总结了使用 process monitor 监听进程创建,查看进程参数、使用 gflags 设置 Image File Excution Options、使用 IDA 静态分析相关函数的业务逻辑以及使用 windbg 进行动态调试的整个过程。我认为这是一个由不良的编程习惯与 crt
初闻错误
前些日子,在家隔离办公的某日中午,收到同事发来的信息说 rc

尝试把 .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.exe 与 cvtres.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.exe,IDA 会提示是否查找符号(真是一个好消息),当然选择是。等待 IDA 分析完成后,在左侧的 Function window 中找到 _main,双击查看反汇编代码,直接在反汇编窗口按 F5,查看伪代码( IDA 的 F5大概浏览后,基本明白了 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
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.exe 在 main() 函数中会循环调用 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
目前只有一位网友回复(另外一个是我自己),为了方便大家阅读,截图如下:

talking-thread-on-microsoft-q&a
虽然到现在还没收到官方的确认回复,不过我依然认为这是一个 bug,而不是 feature。
解决方案
既然没有设置选项或者配置文件可以简单的调整最大文件打开数量,对 cvtres.exe 打补丁又不太现实(每台机器上都要做处理),等待微软修复这个问题也不现实(远水解不了近渴)。所以我们的解决方案是通过合并一些 .rc 以减少工程中的 .rc
虽然问题已经调查清楚了,但是还有几个问题值得探究。
几个值得深究的问题
- 为什么链接的时候需要调用 cvtres.exe 呢?
- 有没有更好的设置条件断点的方式?目前的语法实在是太难用了。
有什么简单的办法可以查看 __piob为什么在打开 510.res
由于本篇已经太长了,下一篇文章中继续把残留的这几个问题解答。
总结
crt
- 有最大打开文件数的限制,可以通过
_setmaxstdio()
- 在一个工程中最好不要同时包含太多
.rc
- 在不需要使用文件的时候,一定要及时关闭。
- 进程退出后,依然可以使用
k
参考资料
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
欢迎各位小伙伴指出不足,提出建议!感谢关注我的博客:)
作 者:编程难
码云博客:https://bianchengnan.gitee.io
github博客:https://bianchengnan.github.io
版权所有,转载请保留原文链接:)



