缘起

前一段时间在折腾拆分 rc 的问题,现已把遇到的问题收拾成文了。感兴趣的小伙伴儿能够参考这儿,这儿 和 这儿。本以为不会有问题了,后续流程就请其它搭档协助处理了,没想到在拆分实际项目时遇到了一个十分古怪的链接问题。

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

初闻过错

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

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

听到这个问题的时分,我置疑是不是哪里操作有问题。从过错提示看是 无法翻开 xxx.res 进行读取,所以榜首感觉是文件途径不对。所以赶忙跟搭档聊了一下,搭档觉得是 vs 的约束,或许这个约束数量是 512

可是我从没听过同一个工程中的 .rc 文件有数量约束,不论怎样,还是建个简略的工程验证下吧。

测验重现

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

从过错提示看,处理 dialog_testmultiplerccompile_dialog507.rc 文件的时分报错了。按照搭档说的,删除若干个 .rc 文件,只保存 500 个,再次编译,没有报错。

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

开端深入查询前,先看看报错信息。

了解的过错

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

再看过错 error CVT1101: 无法翻开“dialog_testmultiplerccompile_dialog507.res”进行读取。猜想是在读取这个文件的时分发生了过错,能够在 process monitor 中检查相关事情。

过滤相关事情

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

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

请出 IDA

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

大约阅览后,根本了解了 main() 函数的整体流程。首先,解析传入的参数,承认榜首个文件在参数列表中的索引位置。然后,从此索引开端循环调用 ReadResFile() 读取每个文件,读取完一切的文件后一致调用 CvtRes() 函数进行转换。

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

其中的 CvtRes() 函数应该是转换的首要函数,十分值得置疑。刻不容缓的发动 windbg 预备调试,可是 cvtres.exe 是被 link.exe 调用的,该如何调试呢?

建立调试环境

假如 cvtres.exe 发动的时分,能够自动中止到调试器中,就能够便利的调试了。之前在 全局变量初始化顺序探求 中介绍过运用 gflags 进行设置的方法。

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

依据之前调试 cl.exe 的经历,假如长期中止到调试器中,调用者会重新发动 cl.exe。猜想这儿也会有类似的逻辑。为了防止这种问题,需求依据 link.exe 发动 cvtres.exe 的参数手动运转 cvtres.exe

能够经过 process monitor 很快找出 cvtres.exe 需求的参数。经过简略查询,发现传递给 cvtres.exe 的参数比较简略直接,而且依据 cvtres.exe /? 提供的协助信息,能够很快承认各个参数的意义。

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

没想到,双击脚本运转的时分,出现了如下过错:

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

阐明: 为了防止指令行参数过长,我特意简化了 .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,如下图:

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

上图中赤色高亮部分便是关键调用栈。从上图还能够得到一个十分有用的信息 —— exit code 的值是 1。能够猜想,link.exe 便是依据 cvtres.exe 的返回值来判别其是否履行成功的。

调用栈中的 OurFileOpen() 函数,应该是担任翻开文件的函数。在持续调试之前,先在 IDA 中看看 OurFileOpen() 函数的完成。

回到 IDA

双击 OurFileOpen,当然是直接检查 F5 的成果啦,有细节需求承认再看反汇编代码。

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

能够看到这个函数完成的十分简略,便是调用 _wfsopen(),假如失利(result == 0)那么调用 ErrorPrint() 打印过错信息。假如 open_mode(第二个参数)是 0,那么传递给 ErrorPrint() 的榜首个参数是 1101,否则是 1108

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

从上方赤色高亮语句 CVTRES: fatal error CVT%04u: 根本能够承认猜想是正确的。从上图底部的赤色高亮区域还能够知道该函数内部的确会调用 exit(1) 来结束进程。

接下来需求查询的问题是 _wfsopen 为什么失利了?

为什么 _wfsopen 会失利?

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

这个函数尽管很简略,加上注释不到 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;} };"

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

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

_getstream 错在哪里了?

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

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

能够看到 _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 找到了几个相关的网址。

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

尽管能够经过 _setmaxstdio() 调整 crt 的最大文件翻开数,可是如同不能经过修正配置文件或许修正注册表的方法调整。

发帖询问

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

帖子地址是 docs.microsoft.com/en-us/answe…

现在只要一位网友回复(别的一个是我自己),为了便利大家阅览,截图如下:

使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(上)

尽管到现在还没收到官方的承认回复,不过我仍然以为这是一个 bug,而不是 feature

解决方案

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

尽管问题现已查询清楚了,可是还有几个问题值得探求。

几个值得深究的问题

  1. 为什么链接的时分需求调用 cvtres.exe 呢?
  2. 有没有更好的设置条件断点的方法?现在的语法真实是太难用了。
  3. 有什么简略的办法能够检查 __piob 数组中元素的内容吗?
  4. 为什么在翻开 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 源码