暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

cnblogs 浅析 C# Console 控制台为什么也会卡死

原创 xiyun 2023-10-24
217

浅析 C# Console 控制台为什么也会卡死
一:背景

  1. 讲故事
    在分析旅程中,总会有几例控制台的意外卡死导致的生产事故,有经验的朋友都知道,控制台卡死一般是动了 快速编辑窗口 的缘故,截图如下:

虽然知道缘由,但一直没有时间探究底层原理,市面上也没有对这块的底层原理介绍,昨天花了点时间简单探究了下,算是记录分享吧。

二:几个疑问解答

  1. 界面为什么会卡死
    相信有很多朋友会有这么一个疑问?控制台程序明明没有 message loop 机制,为什么还能响应 窗口事件 呢?

说实话这是一个好问题,其实 Console 之所以能响应 窗口事件,是因为它开了一个配套的 conhost 窗口子进程,用它来承接 UI 事件,为了方便阐述,上一段定时向控制台输出的测试代码。

    static void Main(string[] args)
    {
        for (int i = 0; i < int.MaxValue; i++)
        {
            Console.WriteLine($"i={i}");
            Thread.Sleep(1000);
        }
    }

将程序跑起来,再用 process explorer 观察进程树即可。

接下来用 windbg 附加到 conshost 进程上,观察下有没有 GetMessageW。

0:005> ~* k
0 Id: 3ec8.2c20 Suspend: 1 Teb: 000000d2`92014000 Unfrozen

Child-SP RetAddr Call Site

00 000000d2922ff798 00007fffa3e45746 ntdll!NtWaitForSingleObject+0x14
01 000000d2922ff7a0 00007fffa60b5bf1 KERNELBASE!DeviceIoControl+0x86
02 000000d2922ff810 00007ff69087a790 KERNEL32!DeviceIoControlImplementation+0x81
03 000000d2922ff860 00007fffa60b7614 conhost!ConsoleIoThread+0xd0
04 000000d2922ff9e0 00007fffa66a26a1 KERNEL32!BaseThreadInitThunk+0x14
05 000000d2922ffa10 0000000000000000 ntdll!RtlUserThreadStart+0x21

2 Id: 3ec8.1b70 Suspend: 1 Teb: 000000d2`9201c000 Unfrozen

Child-SP RetAddr Call Site

00 000000d29227f858 00007fffa4891b9e win32u!NtUserGetMessage+0x14
01 000000d29227f860 00007ff6908735c5 user32!GetMessageW+0x2e
02 000000d29227f8c0 00007fffa60b7614 conhost!ConsoleInputThreadProcWin32+0x75
03 000000d29227f920 00007fffa66a26a1 KERNEL32!BaseThreadInitThunk+0x14
04 000000d29227f950 0000000000000000 ntdll!RtlUserThreadStart+0x21

  1. 进程间如何通讯
    这个问题再细化一点就是Client 端通过 Console.WriteLine($“i={i}”); 写入的内容是如何被 Server 端的conhost!ConsoleIoThread 方法接收到的。

熟悉 Windows 编程的朋友都知道:Console.WriteLine 的底层调用逻辑是 ntdll!NtWriteFile -> nt!IopSynchronousServiceTail ,前者是用户态进入到内核态的网关函数,后者是用户将irp丢到线程的请求包队列后进入休眠(KeWaitForSingleObject),直到驱动提取并处理完之后唤醒。

说了这么多,怎么去验证呢?

客户端下断点

0: kd> !process 0 0 ConsoleApp2.exe
PROCESS ffffe001b5e51840
SessionId: 1 Cid: 0e8c Peb: 7ff7ab226000 ParentCid: 09d4
DirBase: 18079000 ObjectTable: ffffc00036965200 HandleCount:
Image: ConsoleApp2.exe

0: kd> bp /p ffffe001b5e51840 nt!IopSynchronousServiceTail
0: kd> g
Breakpoint 0 hit
nt!IopSynchronousServiceTail:
fffff802`a94f3410 48895c2420 mov qword ptr [rsp+20h],rbx
3: kd> k

Child-SP RetAddr Call Site

00 ffffd000f6477988 fffff802a94f2e80 nt!IopSynchronousServiceTail
01 ffffd000f6477990 fffff802a916db63 nt!NtWriteFile+0x680
02 ffffd000f6477a90 00007ffc2fed38aa nt!KiSystemServiceCopyEnd+0x13
03 0000009f0743dbd8 00007ffc2cd1d478 ntdll!NtWriteFile+0xa
04 0000009f0743dbe0 0000000000000005 0x00007ffc2cd1d478 05 0000009f0743dbe8 0000009f0743dcf0 0x5 06 0000009f0743dbf0 0000009f0978c9b8 0x0000009f0743dcf0
07 0000009f0743dbf8 00007ffc2986e442 0x0000009f0978c9b8 08 0000009f0743dc00 0000009f0743dc30 0x00007ffc2986e442
09 0000009f0743dc08 0000009f0743de00 0x0000009f0743dc30 0a 0000009f0743dc10 0000000000000005 0x0000009f0743de00
0b 0000009f0743dc18 0000000000000000 0x5

3: kd> tc
nt!IopSynchronousServiceTail+0x70:
fffff802a94f3480 e8ebf1b5ff call nt!IopQueueThreadIrp (fffff802a9052670)

服务端下断点
conhost端的提取逻辑是在 conhost!ConsoleIoThread 方法中,它的内部调用的是 kernelbase!DeviceIoControl 函数,这个方法挺有意思,可以直接给驱动程序下达命令,方法签名如下:

BOOL DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped
);

提取完了之后会通过 conhost!DoWriteConsole 向控制台输出,接下来可以下个断点验证下。

0:000> bp conhost!DoWriteConsole
0:000> g
Breakpoint 0 hit
conhost!DoWriteConsole:
00007ff690876ec0 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000095d627f738=0000000000000000
0:000> r
rax=000000000000000c rbx=00000095d627f7b0 rcx=000002370df76cc0
rdx=00000095d627f768 rsi=00000095d627f7c0 rdi=00000095d627f7f0
rip=00007ff690876ec0 rsp=00000095d627f728 rbp=00000095d627f8f9
r8=000002370bedf010 r9=00000095d627f7b0 r10=000002370df76cc0
r11=000002370e0c9d00 r12=00000095d627f970 r13=000002370bedf010
r14=000002370bedf010 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
conhost!DoWriteConsole:
00007ff690876ec0 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000095d627f738=0000000000000000
0:000> du 000002370df76cc0
00000237`0df76cc0 “i=18…”

可以看到果然有一个 i=18,这里要提醒一下,要想看方法的顺序逻辑,可以借助 perfview。

  1. 为什么快捷编辑之后就卡死
    conhost 的源码不是公开的,不过可以感官上推测出来。

快速编辑窗口 被用户启用后, GetMessage 会感知到这个自定义的 MSG 消息。

这个消息的逻辑会让 server 处理Client消息的流程一直处于等待中,导致 Client 的 IopSynchronousServiceTail 不能被唤醒,导致一直处于阻塞中,类似 Task 的完成状态一直不被设置。

接下来可以验证下 快速编辑窗口 的处理消息码是多少,只要在控制台点一下鼠标。参考脚本如下:

0:004> bp win32u!NtUserGetMessage “dp ebp-30 L2 ; g”
0:004> g
00000095d61ffae0 0000000000130e6e 0000000000000404 00000095d61ffae0 0000000000130e6e 0000000000000404
00000095d61ffae0 0000000000130e6e 0000000000000201 00000095d61ffae0 0000000000130e6e 0000000000000405
00000095d61ffae0 0000000000130e6e 0000000000000202 00000095d61ffae0 0000000000130e6e 0000000000000200

从 chaggpt 中对每个消息码的介绍,可以看到会有一个 405 的自定义消息,这个就是和 快速编辑窗口 有关的。

三:总结
这篇就是我个人对窗口卡死的推测和记录,高级调试不易,如果大家感兴趣,欢迎补充细节。

转自: https://www.cnblogs.com/huangxincheng/p/17782167.html

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论