缘起
前一段时间在折腾拆分 rc
的问题,现已把遇到的问题收拾成文了。感兴趣的小伙伴儿能够参考这儿,这儿 和 这儿。本以为不会有问题了,后续流程就请其它搭档协助处理了,没想到在拆分实际项目时遇到了一个十分古怪的链接问题。
本文总结了运用 process monitor
监听进程创建,检查进程参数、运用 gflags
设置 Image File Excution Options
、运用 IDA
静态剖析相关函数的事务逻辑以及运用 windbg
进行动态调试的整个进程。我以为这是一个由不良的编程习气与 crt
的约束一同导致的问题。快来一同看看吧。
初闻过错
前些日子,在家隔离办公的某日中午,收到搭档发来的信息说 rc
拆分的编译问题现已解决了,可是遇到了链接过错,还发送了链接过错的截图,并且给出了一个解决方案。
测验把
.rc
文件排除几十个就链接过去了。
听到这个问题的时分,我置疑是不是哪里操作有问题。从过错提示看是 无法翻开 xxx.res 进行读取
,所以榜首感觉是文件途径不对。所以赶忙跟搭档聊了一下,搭档觉得是 vs
的约束,或许这个约束数量是 512
。
可是我从没听过同一个工程中的 .rc
文件有数量约束,不论怎样,还是建个简略的工程验证下吧。
测验重现
带着置疑 + 好奇的心态,我快速新建了一个 MFC
对话框工程。然后在 vs
中不断复制默认对话框(大约复制了600
个,现已比搭档所说的 512
上限要多了,假如有问题应该能重现了),然后运用工具把每个对话框拆分红独立的 .rc
文件并添加到工程文件中。保存好工程后,开端编译。等待一段时间后,果然报错了,过错截图如下:
从过错提示看,处理 dialog_testmultiplerccompile_dialog507.rc
文件的时分报错了。按照搭档说的,删除若干个 .rc
文件,只保存 500
个,再次编译,没有报错。
看来,在同一个工程中包含太多 .rc
文件真或许有问题。难道真有约束?为什么会做这种约束呢?不论为什么要做约束,我需求找到一个解决方案。
开端深入查询前,先看看报错信息。
了解的过错
之前遇到过过错 LINK : fatal error LNK1123: 转换到 COFF 期间失利: 文件无效或损坏
,是由于 link.exe
与 cvtres.exe
的版别不一样导致的。这次报错不是这个原因。经过 process monitor
看,这两个程序的途径是一样的。
再看过错 error CVT1101: 无法翻开“dialog_testmultiplerccompile_dialog507.res”进行读取
。猜想是在读取这个文件的时分发生了过错,能够在 process monitor
中检查相关事情。
过滤相关事情
在 process monitor
中依据途径名进行过滤。假如途径以 dialog_testmultiplerccompile_dialog507.res
结尾则包含,如下图:
没想到一条记载都没有,一片空白。这是怎么回事?说实话,我有点手足无措,看来只能硬着头皮调试 + 用 IDA
逆向了。在调试之前,先用 IDA
看看有没有什么发现。
请出 IDA
运用 ida32
翻开 cvtres.exe
,IDA
会提示是否查找符号(真是一个好消息),当然挑选是。等待 IDA
剖析完成后,在左边的 Function window
中找到 _main
,双击检查反汇编代码,直接在反汇编窗口按 F5
,检查伪代码( IDA
的 F5
真香!)。
大约阅览后,根本了解了 main()
函数的整体流程。首先,解析传入的参数,承认榜首个文件在参数列表中的索引位置。然后,从此索引开端循环调用 ReadResFile()
读取每个文件,读取完一切的文件后一致调用 CvtRes()
函数进行转换。
下图是在 IDA
中对 main()
函数运用 F5
取得的伪代码的后半部分。
其中的 CvtRes()
函数应该是转换的首要函数,十分值得置疑。刻不容缓的发动 windbg
预备调试,可是 cvtres.exe
是被 link.exe
调用的,该如何调试呢?
建立调试环境
假如 cvtres.exe
发动的时分,能够自动中止到调试器中,就能够便利的调试了。之前在 全局变量初始化顺序探求 中介绍过运用 gflags
进行设置的方法。
依据之前调试 cl.exe
的经历,假如长期中止到调试器中,调用者会重新发动 cl.exe
。猜想这儿也会有类似的逻辑。为了防止这种问题,需求依据 link.exe
发动 cvtres.exe
的参数手动运转 cvtres.exe
。
能够经过 process monitor
很快找出 cvtres.exe
需求的参数。经过简略查询,发现传递给 cvtres.exe
的参数比较简略直接,而且依据 cvtres.exe /?
提供的协助信息,能够很快承认各个参数的意义。
所以很快写出了一个批处理脚本,如下图:
没想到,双击脚本运转的时分,出现了如下过错:
提示找不到 cvtres.exe
。看来需求运用完整途径。正确的脚本如下:
阐明: 为了防止指令行参数过长,我特意简化了
.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
,如下图:
上图中赤色高亮部分便是关键调用栈。从上图还能够得到一个十分有用的信息 —— exit code
的值是 1
。能够猜想,link.exe
便是依据 cvtres.exe
的返回值来判别其是否履行成功的。
调用栈中的 OurFileOpen()
函数,应该是担任翻开文件的函数。在持续调试之前,先在 IDA
中看看 OurFileOpen()
函数的完成。
回到 IDA
双击 OurFileOpen
,当然是直接检查 F5
的成果啦,有细节需求承认再看反汇编代码。
能够看到这个函数完成的十分简略,便是调用 _wfsopen()
,假如失利(result == 0
)那么调用 ErrorPrint()
打印过错信息。假如 open_mode
(第二个参数)是 0
,那么传递给 ErrorPrint()
的榜首个参数是 1101
,否则是 1108
。
而调用 OurFileOpen
时传递的第二个参数是经过 edx
传递的,对应的值是 0
,所以假如犯错,那么会传递 1101
。
说实话,看到 OurOpenFile()
函数中的 1101
,我太激动了,因为在vs
中看到的过错提示是 error CVT1101: 无法翻开“xxx.res”进行读取
。为了进一步承认猜想,在 IDA
中检查 ErrorPrint()
函数的反汇编代码,如下图:
从上方赤色高亮语句 CVTRES: fatal error CVT%04u:
根本能够承认猜想是正确的。从上图底部的赤色高亮区域还能够知道该函数内部的确会调用 exit(1)
来结束进程。
接下来需求查询的问题是 _wfsopen
为什么失利了?
为什么 _wfsopen 会失利?
在 windbg
中输入 .restart
重启目标程序,输入 bp MSVCR120!_wfsopen
,然后履行 g
指令。因为现已设置好了符号查找途径,所以 windbg
自动翻开了对应的源码文件。
这个函数尽管很简略,加上注释不到 50
行。可是会被调用很多次,依据经历,前面的 500
多次调用都没有问题,在测验翻开 510.res
的时分会有问题,所以设置一个条件断点十分有必要。
简略检查反汇编代码发现,_wfsopen()
函数的榜首个参数是经过 ecx
传递的,能够设置如下的条件断点(真是烧脑还不好了解,我不会告诉你,我测验了很久才写出了下面这段蹩脚的脚本):
1
copy
bp MSVCR120!_wfsopen "aS /mu $myFileName @ecx; .block {.echo $myFileName; r @$t0=$spat(@\"$myFileName\", @\"*510.res\"); .if(1==$t0){.echo **** bang ****} .else{ gc;} };"
耐心等待一会就中止下来了,如下图:
单步走两步,发现是 _getstream()
犯错了。
_getstream 错在哪里了?
输入 .restart
重启目标程序,并且设置好条件断点,重新运转程序,当中止到 _wfsopen()
函数后,单步步入到 _getstream()
函数中。
能够看到 _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
找到了几个相关的网址。
尽管能够经过 _setmaxstdio() 调整 crt
的最大文件翻开数,可是如同不能经过修正配置文件或许修正注册表的方法调整。
发帖询问
说实话,榜首次剖析到这个成果的时分我是有些不信的。所以我再三承认了 ReadResFile()
函数内部的确没有封闭文件的操作。难道有什么特殊的理由不封闭翻开的文件?可是我真实想不出有什么理由。所以我觉得这是一个 bug
,所以我在微软官方论坛上发了一个帖子,希望能得到一些回复。
帖子地址是 docs.microsoft.com/en-us/answe…
现在只要一位网友回复(别的一个是我自己),为了便利大家阅览,截图如下:
尽管到现在还没收到官方的承认回复,不过我仍然以为这是一个 bug
,而不是 feature
。
解决方案
已然没有设置选项或许配置文件能够简略的调整最大文件翻开数量,对 cvtres.exe
打补丁又不太实际(每台机器上都要做处理),等待微软修正这个问题也不实际(远水解不了近渴)。所以我们的解决方案是经过合并一些 .rc
以削减工程中的 .rc
文件数量来规避这个问题。
尽管问题现已查询清楚了,可是还有几个问题值得探求。
几个值得深究的问题
- 为什么链接的时分需求调用 cvtres.exe 呢?
- 有没有更好的设置条件断点的方法?现在的语法真实是太难用了。
- 有什么简略的办法能够检查
__piob
数组中元素的内容吗? - 为什么在翻开
510.res
的时分就报错了?应该能够翻开 512 个文件才对?
由于本篇现已太长了,下一篇文章中持续把残留的这几个问题解答。
总结
-
crt
有最大翻开文件数的约束,能够经过_setmaxstdio()
进行调整。 - 在一个工程中最好不要一起包含太多
.rc
文件,一般应该不会遇到我遇到的这种状况。 - 在不需求运用文件的时分,必定要及时封闭。
- 进程退出后,仍然能够运用
k
系列指令检查调用栈,有时分能够快速定位进程退出的原因。
参考资料
stackoverflow.com/questions/6…
docs.microsoft.com/en-us/cpp/b…
docs.microsoft.com/en-us/cpp/c…
vs2013
自带的 crt
源码