一:背景

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 000000d2`922ff798 00007fff`a3e45746 ntdll!NtWaitForSingleObject+0x14
01 000000d2`922ff7a0 00007fff`a60b5bf1 KERNELBASE!DeviceIoControl+0x86
02 000000d2`922ff810 00007ff6`9087a790 KERNEL32!DeviceIoControlImplementation+0x81
03 000000d2`922ff860 00007fff`a60b7614 conhost!ConsoleIoThread+0xd0
04 000000d2`922ff9e0 00007fff`a66a26a1 KERNEL32!BaseThreadInitThunk+0x14
05 000000d2`922ffa10 00000000`00000000 ntdll!RtlUserThreadStart+0x21
...
2 Id: 3ec8.1b70 Suspend: 1 Teb: 000000d2`9201c000 Unfrozen
# Child-SP RetAddr Call Site
00 000000d2`9227f858 00007fff`a4891b9e win32u!NtUserGetMessage+0x14
01 000000d2`9227f860 00007ff6`908735c5 user32!GetMessageW+0x2e
02 000000d2`9227f8c0 00007fff`a60b7614 conhost!ConsoleInputThreadProcWin32+0x75
03 000000d2`9227f920 00007fff`a66a26a1 KERNEL32!BaseThreadInitThunk+0x14
04 000000d2`9227f950 00000000`00000000 ntdll!RtlUserThreadStart+0x21
...

2. 进程间如何通讯

这个问题再细化一点就是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: <Data Not Accessible>
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 ffffd000`f6477988 fffff802`a94f2e80 nt!IopSynchronousServiceTail
01 ffffd000`f6477990 fffff802`a916db63 nt!NtWriteFile+0x680
02 ffffd000`f6477a90 00007ffc`2fed38aa nt!KiSystemServiceCopyEnd+0x13
03 0000009f`0743dbd8 00007ffc`2cd1d478 ntdll!NtWriteFile+0xa
04 0000009f`0743dbe0 00000000`00000005 0x00007ffc`2cd1d478
05 0000009f`0743dbe8 0000009f`0743dcf0 0x5
06 0000009f`0743dbf0 0000009f`0978c9b8 0x0000009f`0743dcf0
07 0000009f`0743dbf8 00007ffc`2986e442 0x0000009f`0978c9b8
08 0000009f`0743dc00 0000009f`0743dc30 0x00007ffc`2986e442
09 0000009f`0743dc08 0000009f`0743de00 0x0000009f`0743dc30
0a 0000009f`0743dc10 00000000`00000005 0x0000009f`0743de00
0b 0000009f`0743dc18 00000000`00000000 0x5 3: kd> tc
nt!IopSynchronousServiceTail+0x70:
fffff802`a94f3480 e8ebf1b5ff call nt!IopQueueThreadIrp (fffff802`a9052670)
  • 服务端下断点

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:
00007ff6`90876ec0 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000095`d627f738=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:
00007ff6`90876ec0 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000095`d627f738=0000000000000000
0:000> du 000002370df76cc0
00000237`0df76cc0 "i=18.."

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

3. 为什么快捷编辑之后就卡死

conhost 的源码不是公开的,不过可以感官上推测出来。

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

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

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


0:004> bp win32u!NtUserGetMessage "dp ebp-30 L2 ; g"
0:004> g
00000095`d61ffae0 00000000`00130e6e 00000000`00000404
00000095`d61ffae0 00000000`00130e6e 00000000`00000404
00000095`d61ffae0 00000000`00130e6e 00000000`00000201
00000095`d61ffae0 00000000`00130e6e 00000000`00000405
00000095`d61ffae0 00000000`00130e6e 00000000`00000202
00000095`d61ffae0 00000000`00130e6e 00000000`00000200

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

三:总结

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

浅析 C# Console 控制台为什么也会卡死的更多相关文章

  1. terminal(终端),shell,tty,console(控制台)区别

    原文地址  stackexchange:What is the exact difference between a 'terminal', a 'shell', a 'tty' and a 'con ...

  2. eclipse中的Console控制台视图脱离主窗口解决办法

    问题:Console控制台视图由于操作不当,跑出来了,脱离了主窗口 解决:在eclipse主窗口最上面的工具条选项中,找到Window,点击里面的Reset Perspective,即可,这样视图就重 ...

  3. phpStorm如何在Console控制台执行php文本,而不是浏览器中

    如何在Console控制台执行php文本 phpStorm默认会在浏览器中执行脚本 默认的配置 配置PHP脚本 扩展,配置项目运行

  4. console控制台的小问题

    第一个foo里面应该是123,但是当执行完下面的代码之后,console控制台会自动将里面的内容改成我们修改之后的

  5. Console控制台的正确打开方式

    Console控制台的正确打开方式 console对象提供了访问浏览器调试模式的信息到控制台 -- Console对象 |-- assert() 如果第一个参数断言为false,则在控制台输出错误信息 ...

  6. 浅析System.Console.WriteLine()

    浅析System.Console.WriteLine() Writeline()函数的功能向 StreamWriter 类写入指定字符串和一行字符,共有19个重载,其与Write()函数的主要区别在于 ...

  7. Spring Boot2解决idea console 控制台输出乱码

    Idea默认配置是采用GBK, 而项目工程文件采用的是UTF-8. 编码不一致,导致idea Console控制台输出乱码. 网上的解决方案,大都是直接修改Settings=>Editor=&g ...

  8. Python window console 控制台 实现最后一行输出 print 重写

    Python window console 控制台 实现最后一行输出 print 重写 # -*- coding: utf-8-*- from __future__ import print_func ...

  9. python subprocess popen 静默模式(不弹出console控制台)

    python subprocess popen 静默模式(不弹出console控制台) import subprocess,sys IS_WIN32 = 'win32' in str(sys.plat ...

  10. VS开发】如何给console控制台程序更换应用程序图标

    [VS开发]如何给console控制台程序更换应用程序图标 标签:[VS开发] 实际上非常简单,就是增加一个图标资源,在资源视图里,然后修改其ID为IDC_MAINFRAME,然后编译生成即可! 20 ...

随机推荐

  1. requests的基础使用

    爬虫介绍 # 爬虫:又称网络蜘蛛,spider,一堆程序,从互联网中抓取数据---->数据清洗---->入库 # 爬虫需要掌握的知识 -抓取数据:发送网络请求(http),获得响应(htt ...

  2. Java架构师之路:从Java码农到年薪八十万,最牛Java架构师进阶路线

    Java架构师之路:从Java码农到年薪八十万,最牛Java架构师进阶路线 摘要:本文将为Java开发工程师提供一条从Java码农到年薪八十万的进阶之路,探讨如何成为一名顶尖的Java架构师.我们将介 ...

  3. 巧用 nc 命令传输文件

    今天在业务上云的时候,遇到了些问题.最终发现问题的根源不好排查,于是-- 把生产环境的全量配置文件,还有日志全量打包下载到开发机器分析! 刚入职不是很久的整个运维团队,也不是很熟悉生产环境(有时候觉得 ...

  4. 2023-07-19:布尔表达式 是计算结果不是 true 就是 false 的表达式 有效的表达式需遵循以下约定: ‘t‘,运算结果为 true ‘f‘,运算结果为 false ‘!(subExpr

    2023-07-19:布尔表达式 是计算结果不是 true 就是 false 的表达式 有效的表达式需遵循以下约定: 't',运算结果为 true 'f',运算结果为 false '!(subExpr ...

  5. CSDN这么公然爬取(piao qie)cnblogs的文章,给钱了吗?

    在CSDN网站经常看到有博客转载cnblogs的文章,开始还以为是网友自行转载,后来才发现,这些所谓的转载应该都是机器爬取(piao qie)过去的.不知道cnblogs对此怎么看. 下面看看几个示例 ...

  6. 2021-8-5 Mysql个人练习题

    创建学校表格 CREATE TABLE `Student`( `s_id` VARCHAR(20), `s_name` VARCHAR(20) NOT NULL DEFAULT '', `s_birt ...

  7. 【AltWalker】模型驱动:轻松实现自动化测试用例的自动生成和组织执行

    模型驱动的自动化测试 模型驱动的自动化测试(Model-Based Testing, 后文中我们将简称为MBT)是一种软件测试方法,它将系统的行为表示为一个或多个模型,然后从模型中自动生成和执行测试用 ...

  8. Cilium系列-11-启用带宽管理器

    系列文章 Cilium 系列文章 前言 将 Kubernetes 的 CNI 从其他组件切换为 Cilium, 已经可以有效地提升网络的性能. 但是通过对 Cilium 不同模式的切换/功能的启用, ...

  9. 银河麒麟v10安装达梦数据库

    简介 达梦数据库是商业化的国产关系型数据库,体系架构比较像Oracle. 官方在线手册 原生安装 系统版本:银河麒麟V10服务器版 数据库版本:DM8 下载官方安装包,解压后有个ISO文件和包含sha ...

  10. 几种常用到的 Hybrid App 技术框架

    移动操作系统在经历了诸神混战之后,BlackBerry OS.Symbian OS.Windows Phone 等早期的移动操作系统逐渐因失去竞争力而退出.目前,市场上主要只剩下安卓和 iOS 两大阵 ...