使用 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
,于是我在微软官方论坛上发了一个帖子,希望能得到一些回复。
目前只有一位网友回复(另外一个是我自己),为了方便大家阅读,截图如下:
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
版权所有,转载请保留原文链接:)