使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效...
共 7151字,需浏览 15分钟
·
2022-09-06 09:01
缘起
前一段时间在折腾拆分 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
文件并添加到工程文件中。保存好工程后,开始编译。等待一段时间后,果然报错了,错误截图如下:
![186055b0c2a4507530d1ad81772610ea.webp](https://filescdn.proginn.com/0eabfd77f0675cc99dfc21c11f97ae90/186055b0c2a4507530d1ad81772610ea.webp)
从错误提示看,处理 dialog_testmultiplerccompile_dialog507.rc
文件的时候报错了。按照同事说的,删除若干个 .rc
文件,只保留 500
个,再次编译,没有报错。
看来,在同一个工程中包含太多 .rc
文件真可能有问题。难道真有限制?为什么会做这种限制呢?不管为什么要做限制,我需要找到一个解决方案。
开始深入调查前,先看看报错信息。
熟悉的错误
之前遇到过错误 LINK : fatal error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏
,是由于 link.exe
与 cvtres.exe
的版本不一样导致的。这次报错不是这个原因。通过 process monitor
看,这两个程序的路径是一样的。
![57a87364298339df2d18a74cd4ffa428.webp](https://filescdn.proginn.com/2e69caf1e6bc717279c92f23c85366b0/57a87364298339df2d18a74cd4ffa428.webp)
再看错误 error CVT1101: 无法打开“dialog_testmultiplerccompile_dialog507.res”进行读取
。猜测是在读取这个文件的时候发生了错误,可以在 process monitor
中查看相关事件。
过滤相关事件
在 process monitor
中根据路径名进行过滤。如果路径以 dialog_testmultiplerccompile_dialog507.res
结尾则包含,如下图:
![d2b88ddd7e3a594ae5900740449beffc.webp](https://filescdn.proginn.com/f9f08f6cee94e6b695ef7cd0127dd9ae/d2b88ddd7e3a594ae5900740449beffc.webp)
没想到一条记录都没有,一片空白。这是怎么回事?说实话,我有点不知所措,看来只能硬着头皮调试 + 用 IDA
逆向了。在调试之前,先用 IDA
看看有没有什么发现。
请出 IDA
使用 ida32
打开 cvtres.exe
,IDA
会提示是否查找符号(真是一个好消息),当然选择是。等待 IDA
分析完成后,在左侧的 Function window
中找到 _main
,双击查看反汇编代码,直接在反汇编窗口按 F5
,查看伪代码( IDA
的 F5
真香!)。
大概浏览后,基本明白了 main()
函数的整体流程。首先,解析传入的参数,确定第一个文件在参数列表中的索引位置。然后,从此索引开始循环调用 ReadResFile()
读取每个文件,读取完所有的文件后统一调用 CvtRes()
函数进行转换。
下图是在 IDA
中对 main()
函数使用 F5
获得的伪代码的后半部分。
![7dcd66a14d7c0786213f28fe19d4ba88.webp](https://filescdn.proginn.com/0a691e2af0c6f09e8e93bbee1fe0b540/7dcd66a14d7c0786213f28fe19d4ba88.webp)
其中的 CvtRes()
函数应该是转换的主要函数,非常值得怀疑。迫不及待的启动 windbg
准备调试,但是 cvtres.exe
是被 link.exe
调用的,该如何调试呢?
搭建调试环境
如果 cvtres.exe
启动的时候,能够自动中断到调试器中,就可以方便的调试了。之前在 全局变量初始化顺序探究 中介绍过使用 gflags
进行设置的方法。
![d4b5f99b84e11d3f6f1945df2bfbd905.webp](https://filescdn.proginn.com/ceaa604b52f498f2d2fd9f12a8885829/d4b5f99b84e11d3f6f1945df2bfbd905.webp)
根据之前调试 cl.exe
的经验,如果长时间中断到调试器中,调用者会重新启动 cl.exe
。猜想这里也会有类似的逻辑。为了避免这种问题,需要根据 link.exe
启动 cvtres.exe
的参数手动运行 cvtres.exe
。
可以通过 process monitor
很快找出 cvtres.exe
需要的参数。经过简单观察,发现传递给 cvtres.exe
的参数比较简单直接,而且根据 cvtres.exe /?
提供的帮助信息,可以很快确定各个参数的意义。
于是很快写出了一个批处理脚本,如下图:
![04cd0624492df4c05621279b32b18e6f.webp](https://filescdn.proginn.com/5e7e2c10c605e6d8ecbd0123c9b169a1/04cd0624492df4c05621279b32b18e6f.webp)
没想到,双击脚本运行的时候,出现了如下错误:
![0b1f004d5df3d460050b4f82fd428226.webp](https://filescdn.proginn.com/744ceb7e96adb93503b90e4e108a67e4/0b1f004d5df3d460050b4f82fd428226.webp)
提示找不到 cvtres.exe
。看来需要使用完整路径。正确的脚本如下:
![48e60a703bd63e4df9a57420a39ab3b8.webp](https://filescdn.proginn.com/80df0ec21ffab82f94510ff35a09dd2c/48e60a703bd63e4df9a57420a39ab3b8.webp)
说明: 为了避免命令行参数过长,我特意简化了
.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
,如下图:
![5fd8d03468c500177ff8af62636ee56d.webp](https://filescdn.proginn.com/68100e9826219a58cae8bc2441ae7527/5fd8d03468c500177ff8af62636ee56d.webp)
上图中红色高亮部分就是关键调用栈。从上图还可以得到一个非常有用的信息 —— exit code
的值是 1
。可以猜测,link.exe
就是根据 cvtres.exe
的返回值来判断其是否执行成功的。
调用栈中的 OurFileOpen()
函数,应该是负责打开文件的函数。在继续调试之前,先在 IDA
中看看 OurFileOpen()
函数的实现。
回到 IDA
双击 OurFileOpen
,当然是直接查看 F5
的结果啦,有细节需要确认再看反汇编代码。
![886969ea8bb6c828428fde7a55385ef7.webp](https://filescdn.proginn.com/683caf53bb198d06ab6846e1636139d2/886969ea8bb6c828428fde7a55385ef7.webp)
可以看到这个函数实现的非常简单,就是调用 _wfsopen()
,如果失败(result == 0
)那么调用 ErrorPrint()
打印错误信息。如果 open_mode
(第二个参数)是 0
,那么传递给 ErrorPrint()
的第一个参数是 1101
,否则是 1108
。
而调用 OurFileOpen
时传递的第二个参数是通过 edx
传递的,对应的值是 0
,所以如果出错,那么会传递 1101
。
![6e38ee3eaf6a9d05b62c59c0720f8103.webp](https://filescdn.proginn.com/af2fa9e606465072c7f3dded7a1ad093/6e38ee3eaf6a9d05b62c59c0720f8103.webp)
说实话,看到 OurOpenFile()
函数中的 1101
,我太激动了,因为在vs
中看到的错误提示是 error CVT1101: 无法打开“xxx.res”进行读取
。为了进一步确认猜想,在 IDA
中查看 ErrorPrint()
函数的反汇编代码,如下图:
![d8aba7482ba883ebdfeeb2d4f2b8e484.webp](https://filescdn.proginn.com/7130719e2f6751b656719dc6bda7bb18/d8aba7482ba883ebdfeeb2d4f2b8e484.webp)
从上方红色高亮语句 CVTRES: fatal error CVT%04u:
基本可以确定猜测是正确的。从上图底部的红色高亮区域还可以知道该函数内部确实会调用 exit(1)
来结束进程。
接下来需要调查的问题是 _wfsopen
为什么失败了?
为什么 _wfsopen 会失败?
在 windbg
中输入 .restart
重启目标程序,输入 bp MSVCR120!_wfsopen
,然后执行 g
命令。因为已经设置好了符号查找路径,所以 windbg
自动打开了对应的源码文件。
![c94949ab22979a8ecbbf7af136ff0d54.webp](https://filescdn.proginn.com/47ebf5f960571f00000e423b3b1b68d9/c94949ab22979a8ecbbf7af136ff0d54.webp)
这个函数虽然很简单,加上注释不到 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;} };"
耐心等待一会就中断下来了,如下图:
![86317bf72f842feba579a000a619e251.webp](https://filescdn.proginn.com/ca3f73caaddd2a2bd5caae21d90f0402/86317bf72f842feba579a000a619e251.webp)
单步走两步,发现是 _getstream()
出错了。
_getstream 错在哪里了?
输入 .restart
重启目标程序,并且设置好条件断点,重新运行程序,当中断到 _wfsopen()
函数后,单步步入到 _getstream()
函数中。
![42bf3e4890826396224cd2d5f11361e4.webp](https://filescdn.proginn.com/e540bbb066fa3fddc1c73f9ffc04254b/42bf3e4890826396224cd2d5f11361e4.webp)
可以看到 _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
找到了几个相关的网址。
![02f81f62eb510f1a1f200fedc331306d.webp](https://filescdn.proginn.com/a152145a4c8013c584c3fcab5e774463/02f81f62eb510f1a1f200fedc331306d.webp)
虽然可以通过 _setmaxstdio() 调整 crt
的最大文件打开数,但是好像不能通过修改配置文件或者修改注册表的方式调整。
发帖询问
说实话,第一次分析到这个结果的时候我是有些不信的。于是我再三确认了 ReadResFile()
函数内部确实没有关闭文件的操作。难道有什么特殊的理由不关闭打开的文件?但是我实在想不出有什么理由。所以我觉得这是一个 bug
,于是我在微软官方论坛上发了一个帖子,希望能得到一些回复。
帖子地址是 https://docs.microsoft.com/en-us/answers/questions/709392/cvt1101-can39t-open-xxxres-for-reading.html
目前只有一位网友回复(另外一个是我自己),为了方便大家阅读,截图如下:
![ab96ef388e05c71becaafae6b38aab2b.webp](https://filescdn.proginn.com/3a340c48ff80556bbba8c34e8e30d50c/ab96ef388e05c71becaafae6b38aab2b.webp)
虽然到现在还没收到官方的确认回复,不过我依然认为这是一个 bug
,而不是 feature
。
解决方案
既然没有设置选项或者配置文件可以简单的调整最大文件打开数量,对 cvtres.exe
打补丁又不太现实(每台机器上都要做处理),等待微软修复这个问题也不现实(远水解不了近渴)。所以我们的解决方案是通过合并一些 .rc
以减少工程中的 .rc
文件数量来规避这个问题。
虽然问题已经调查清楚了,但是还有几个问题值得探究。
几个值得深究的问题
- 为什么链接的时候需要调用 cvtres.exe 呢?
- 有没有更好的设置条件断点的方式?目前的语法实在是太难用了。
-
有什么简单的办法可以查看
__piob
数组中元素的内容吗? -
为什么在打开
510.res
的时候就报错了?应该可以打开 512 个文件才对?
由于本篇已经太长了,下一篇文章中继续把残留的这几个问题解答。
总结
-
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
源码