解析C# Console 控制臺為什么也會卡死(原因分析)
一:背景
1. 講故事
在分析旅程中,總會有幾例控制臺的意外卡死導(dǎo)致的生產(chǎn)事故,有經(jīng)驗的朋友都知道,控制臺卡死一般是動了 快速編輯窗口 的緣故,截圖如下:

雖然知道緣由,但一直沒有時間探究底層原理,市面上也沒有對這塊的底層原理介紹,昨天花了點時間簡單探究了下,算是記錄分享吧。
二:幾個疑問解答
1. 界面為什么會卡死
相信有很多朋友會有這么一個疑問?控制臺程序明明沒有 message loop 機制,為什么還能響應(yīng) 窗口事件 呢?
說實話這是一個好問題,其實 Console 之所以能響應(yīng) 窗口事件,是因為它開了一個配套的 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. 進程間如何通訊
這個問題再細(xì)化一點就是Client 端通過 Console.WriteLine($"i={i}"); 寫入的內(nèi)容是如何被 Server 端的conhost!ConsoleIoThread 方法接收到的。
熟悉 Windows 編程的朋友都知道:Console.WriteLine 的底層調(diào)用邏輯是 ntdll!NtWriteFile -> nt!IopSynchronousServiceTail ,前者是用戶態(tài)進入到內(nèi)核態(tài)的網(wǎng)關(guān)函數(shù),后者是用戶將irp丟到線程的請求包隊列后進入休眠(KeWaitForSingleObject),直到驅(qū)動提取并處理完之后喚醒。
說了這么多,怎么去驗證呢?
客戶端下斷點
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)服務(wù)端下斷點
conhost端的提取邏輯是在 conhost!ConsoleIoThread 方法中,它的內(nèi)部調(diào)用的是 kernelbase!DeviceIoControl 函數(shù),這個方法挺有意思,可以直接給驅(qū)動程序下達(dá)命令,方法簽名如下:
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 的源碼不是公開的,不過可以感官上推測出來。
- 快速編輯窗口 被用戶啟用后, GetMessage 會感知到這個自定義的 MSG 消息。
這個消息的邏輯會讓 server 處理Client消息的流程一直處于等待中,導(dǎo)致 Client 的 IopSynchronousServiceTail 不能被喚醒,導(dǎo)致一直處于阻塞中,類似 Task 的完成狀態(tài)一直不被設(shè)置。
接下來可以驗證下 快速編輯窗口 的處理消息碼是多少,只要在控制臺點一下鼠標(biāo)。參考腳本如下:
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 的自定義消息,這個就是和 快速編輯窗口 有關(guān)的。
三:總結(jié)
這篇就是我個人對窗口卡死的推測和記錄,高級調(diào)試不易,如果大家感興趣,歡迎補充細(xì)節(jié)。
到此這篇關(guān)于C# Console 控制臺為什么也會卡死的文章就介紹到這了,更多相關(guān)C# Console 控制臺內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
輕松學(xué)習(xí)C#的預(yù)定義數(shù)據(jù)類型
輕松學(xué)習(xí)C#的預(yù)定義數(shù)據(jù)類型,C#的預(yù)定義數(shù)據(jù)類型包括兩種,一種是值類型,一種是引用類型,需要的朋友可以參考下2015-11-11
C# dump系統(tǒng)lsass內(nèi)存和sam注冊表詳細(xì)
這篇文章主要介紹了C# dump系統(tǒng)lsass內(nèi)存和sam注冊表,在這里選擇 C# 的好處是體積小,結(jié)合 loadAssembly 方便免殺,希望對讀者們有所幫助2021-09-09
C#使用Dns類實現(xiàn)查詢主機名對應(yīng)IP地址
C#中的Dns類能夠與默認(rèn)的DNS服務(wù)器進行通信,以檢索IP地址,這篇文章主要介紹了C#如何使用Dns類解析出主機對應(yīng)的IP地址信息,需要的可以參考下2024-02-02
C#如何使用Bogus創(chuàng)建模擬數(shù)據(jù)示例代碼
這篇文章主要給大家介紹了關(guān)于C#如何使用Bogus創(chuàng)建模擬數(shù)據(jù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用C#具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

