使用 IDA 和 windbg 调试 LNK1123 转换到 COFF 期间失败:文件无效或损坏(下)
原总结排错process monitorvsIDAwindbg调试rcCVT1101LNK1123
前言
在前面两篇文章中(这里,这里)总结了使用 windbg
和 IDA
找出 cvtres.exe
报错的根本原因,并把一些细节问题弄清楚了。但是还剩下一个小细节没有深究 —— 如果启动 cvtres.exe
的时候没有指定全路径,windbg
说明: 写完本文,我犹豫了很久要不要发表出来。因为这个问题其实很简单(在设置 PATH 环境变量时,路径多加了双引号)。但是当时的我真的是当局者迷,完全没意识到这个问题,导致花费了很长时间。process monitor,gflag,IDA,windbg,轮番上场,甚至都调试起 windbg 来了(嗯,你没看错,不是用 windbg 调试,而是调试 windbg),好一阵忙活,中间还走了很多弯路(自以为是的在错误的函数中下断)。最后幡然醒悟,原来真理就在那躺着,静静的等着被发现。
之所以决定发出来有几点原因:
- 介绍了让进程自动中断到调试器中的方法,甚至是让一个调试器自动中断到另外一个调试器中的方法。
- 介绍了只对特定线程设置断点的方法、使用
wt
- 提醒自己,在调试过程中一定要保持清醒的头脑。
没耐心的朋友直接跳到最后即可。
问题回顾
相信小伙伴儿们都已经知道可以通过 gflags
设置 Image File Execution Option
来让指定程序启动时自动中断到调试器。但是当启动 cvtres.exe
的时候,如果只指定文件名,windbg
windbg-cannot-start-cvtres-error
猜测应该是 windbg
我依稀记得创建进程时,CreateProcess()
会到 PATH
环境变量指定的路径中查找文件。所以我在脚本开始的地方把 cvtres.exe
所在的路径添加到 PATH
环境变量。使用 process explorer
查看 windbg
对应的 PATH
环境变量的值,发现 PATH
环境变量已经包含了 cvtres.exe
所在的路径。因为 PATH
view-windbg-path-variable
提示: 注意上图中高亮部分。一切皆因它而起。
难道 windbg
不是通过 CreateProcess()
创建的新进程?看来需要调试一下 windbg
了。但是 windbg
螳螂捕蝉,黄雀在后
还记得我们是怎么调试 cvtres.exe
的吗?可以用同样的方法调试 windbg
—— 通过 gflags
为 windbg
设置 Image File Execution Option
。但是有一个细节需要注意:Debugger
cdb
与 windbg
同源,大多数命令与 windbg
use-cdb-debugging-windbg-with-gflags
按上图设置后,windbg.exe
在启动时会自动中断到 cdb.exe
双击启动脚本,cvtres.exe
会自动中断到 windbg
中,而 windbg
会自动中断到 cdb
中。下图是 windbg
中断到 cdb
cdb-auto-attach-to-windbg
小提示: 可以使用 |
windbg
断错函数了?
创建进程应该会走 CreateProcess()
系列中的一个,猜测 windbg
应该也是调用 CreateProcess()
创建的子进程,与创建普通进程不同的是,windbg
需要调试新启动的进程。当为 CreateProcess()
函数的 dwCreationFlags
参数设置一个特殊的标志位 DEBUG_PROCESS
时,在进程创建过程中就会建立相关的调试对象,windbg
按照上面的理论,在 cdb
中输入 x *!*CreateProcess*
查看相关函数,发现了一堆相关函数。想着最终肯定会调用 ntdll
模块中的函数,于是在 cdb
中输入 bp ntdll!NtCreateProcessEx
,然后执行 g
命令重新运行 windbg
。 但是居然没断下来。难道 windbg
不是通过 NtCreateProcessEx()
启动新进程的吗?还是在调用 NtCreateProcessEx()
对比正常流程
通过全路径启动 cvtres.exe
的时候,windbg
可以正常启动对应的程序。可以对比查看正常情况下 windbg
通过 process monitor
监听相关事件,只查看进程相关的事件,可以看到启动 cvtres.exe
view-windbg-create-cvtres-call-stack
看来 windbg
是通过 kernelbase!CreateProcessW()
启动进程的,该函数内部最终会调用 ntdll!NtCreateUserProcess()
。看来,前面草率了,在错误的函数中设置了断点。赶紧在 ntdll!NtCreateUserProcess()
看来,应该是在前面就出错了。尝试在 kernelbase!CreateProcessW()
重新下断
在 kernelbase!CreateProcessW()
中设置断点,再次运行程序,这次终于断下来了。执行 gu
退出 kernelbase!CreateProcessW()
,然后查看返回值,也就是查看 eax
的值,发现 eax
的值是 0
,根据 msdn CreateProcess 文档 中的描述可知,返回 0
表示出错了。执行 !gle
查看错误码,发现 Last Error
的值是 2
,也就是 ERROR_PATH_NOT_FOUND。System Error Codes (0-499) 中对应的描述如下:The system cannot find the file specified.
,翻译过来就是 系统找不到指定的文件,与 windbg
run-createprocess-and-view-return-value-and-last-error
看来问题出在 kernelbase!CreateProcessW()
函数内部 ,用 IDA
看了下 kernelbase!CreateProcessW()
的反汇编代码,发现该函数只是直接调用了 kernelbase!CreateProcessInternalW()
。而 kernelbase!CreateProcessInternalW()
的反汇编代码超级多,即使用 IDA
的 F5
得到的伪代码都非常多,实在没耐心看完。那该怎么办呢?脑子中突然想起一个很久之前就知道但是一直没真正使用过的命令 —— wt
。
wt 立功了
重新运行程序,当 windbg
中断到 cdb
后,输入 bp kernelbase!CreateProcessInternalW
设置好断点,输入 g
重新运行 windbg
。中断下来后,输入 wt -l1
提示: 因为 wt 的输出结果很可能会有很多,所以最好先执行 .logopen d:\windbg.txt 来打开日志文件,追踪完成后再执行 .logclose
查看日志文件中的调用记录,很快就发现了让我头脑瞬间清醒的几个函数调用。一个是 SearchPathW
一个是 RtlSetLastWin32Error
。
view-wt-createprocessinternalw
一看到 RtlSetLastWin32Error
,瞬间清醒了。GetLastError
一看到 SearchPathW
,立刻就联想到很可能是在通过 PATH
环境变量在找文件。后悔最开始的时候没用 process monitor
再次运行程序,使用 process monitor
观察一下文件访问情况。看到下图的访问记录(红色高亮的路径,居然带着双引号),我瞬间就明白了,一定是我设置的 PATH
view-cvtres-in-process-monitor
修复
既然是指定的路径出问题了(不能带双引号),修改脚本内容如下:
set path=%path%;C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\
cvtres.exe /machine:x86 /nologo /out:"d:\test-rc-compile-error\test.tmp" /readonly 1.res 2.res 3.res 4.res 5.res
再次运行脚本,果然一切正常了。
有趣的访问方式
当我在 process monitor
interesting-file-access-pattern
有没有发现什么规律?注意对照脚本代码看,每次访问失败后都会多取一部分再次进行尝试。
cvtres.exe /machine:x86 /nologo /out:"d:\test-rc-compile-error\test.tmp" /readonly 1.res 2.res 3.res 4.res 5.res
进一步调试确认后,上面提到的对 SearchPathW
的调用,并不是在对 PATH
环境变量中的每个路径做循环。而是在对脚本中的每个部分进行循环。SearchPathW
会在内部处理环境变量中的值。根据 MSDN SearchPathW 的文档,这个函数的参数如下:
DWORD SearchPathW(
[in, optional] LPCWSTR lpPath,
[in] LPCWSTR lpFileName,
[in, optional] LPCWSTR lpExtension,
[in] DWORD nBufferLength,
[out] LPWSTR lpBuffer,
[out, optional] LPWSTR *lpFilePart
);
其中,PATH
环境变量的值会传给 lpPath
参数。上面提到的有规律的调用是通过参数 lpFileName
~4 bp KERNELBASE!SearchPathW ".echo **************** bang ****************; du /cA0 poi(esp+4) LA0; du /cA0 poi(esp+8) LA0; kv2; gc;"
~4
表示只对 4
号线程设置断点,其它线程调用 KERNELBASE!SearchPathW
/cA0
表示显示指定显示列宽为 0xA0
,如果不指定,则按默认列宽进行显示。LA0
表示只显示前 0xA0
运行结果如下图:
auto-display-param-and-callstack
反思
其实这个问题本来应该很快就能解决的。
首先,在调试之前就应该使用 process monitor
查看文件访问事件。只不过当时脑子不清醒,只是用 process monitor
其次,当时陷入了思维误区 —— 想当然的认为带空格的路径一定要加上双引号。整个排查过程中都没有怀疑是多加了双引号导致的问题。反过头来看这个问题,觉得自己太傻了,查看 PATH
收获
- 长记性了,设置
PATH
- 再次熟悉了使用
gflags
- 设置
Image File Execution Option
- 熟悉了
wt
- 的用法。很早之前就知道
wt
- 熟悉了为指定线程设置断点的方法。语法很简单,很容易记忆。在
bp
- 之前添加
~tid
- ,即可为指定的线程设置断点。
- 知道了突破默认输出列宽的方法 —— 通过
/c
- 熟悉了
CreateProcess
参考资料
https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-searchpathw
欢迎各位小伙伴指出不足,提出建议!感谢关注我的博客:)
作 者:编程难
码云博客:https://bianchengnan.gitee.io
github博客:https://bianchengnan.github.io
版权所有,转载请保留原文链接:)