A brief analysis of how Ctrl+C in the C# console works

One: Background

1. Tell stories

In the last article, we talked about why the Console is stuck. Friends who have read that article believe that they have a general understanding of conhost.exe. This article goes further and talks about the special events of the window Ctrl + C What is the underlying flow like? To facilitate the description, let chagtgpt generate a Ctrl + C business code for me.

class Program
    {
        static void Main(string[] args)
        {
            Console.CancelKeyPress + = new ConsoleCancelEventHandler(CancelKeyPressHandler);

            Console.WriteLine("Press Ctrl + C and try it!");

            while(true)
            {
                //Write your code here
            }
        }

        static void CancelKeyPressHandler(object sender, ConsoleCancelEventArgs e)
        {
            Console.WriteLine("You pressed Ctrl + C!");
            //Here you can add the code you want to execute
            e.Cancel = true;
        }
    }

Then simply run it to confirm that there is nothing wrong with the code.

6dd2cb02cde66dcfc8e98399040ed679.png

Two: Publish and subscribe model

1. Where are the subscription events

I believe many friends have read this code and know that this is a publish-subscribe model. The code subscribes to the Console.CancelKeyPress event. Once there is Ctrl + C or Ctrl + Break will automatically execute the corresponding subscription logic when it occurs, but there is a problem here. Ctrl + C is a window event, so Win32 API must be involved. With this idea, you can decompile and take a lookConsole.CancelKeyPress Does the bottom layer use the Win32 API? The reference code is as follows:

private unsafe static PosixSignalRegistration Register(PosixSignal signal, Action<PosixSignalContext> handler)
{
    Token token = new Token(signal, handler);
    PosixSignalRegistration result = new PosixSignalRegistration(token);
    lock (s_registrations)
    {
        if (s_registrations.Count == 0 & amp; & amp; !Interop.Kernel32.SetConsoleCtrlHandler((delegate* unmanaged<int, Interop.BOOL>)(delegate*<int, Interop.BOOL>)( & amp;HandlerRoutine) , Add: true))
        {
            throw Win32Marshal.GetExceptionForLastWin32Error();
        }
        s_registrations.Add(token);
        return result;
    }
}

Judging from the hexagram, the handler is registered through the Kernel32.SetConsoleCtrlHandler method. If you don’t believe it, you can also use windbg to verify it.

0:000> bp KERNEL32!SetConsoleCtrlHandler
breakpoint 0 redefined
0:000> g

0:000> k 6
 # Child-SP RetAddr Call Site
00 00000064`e7b7e948 00007ff9`8162b797 KERNEL32!SetConsoleCtrlHandler
01 00000064`e7b7e950 00007ff9`817ac3cd System_Private_CoreLib + 0x1ab797
02 00000064`e7b7ea20 00007ff9`817ac270 System_Private_CoreLib!System.Runtime.InteropServices.PosixSignalRegistration.Register + 0xdd
03 00000064`e7b7ea90 00007ffa`144f8437 System_Private_CoreLib!System.Runtime.InteropServices.PosixSignalRegistration.Create + 0x10
04 00000064`e7b7eac0 00007ff9`226f29a1 System_Console!System.Console.add_CancelKeyPress + 0x97
05 00000064`e7b7eb20 00007ff9`8229af33 ConsoleApp1!ConsoleApp1.Program.Main + 0x61
...

2. Where to publish events

Friends who are familiar with Win32 programming believe that they already understand it very well. It turns out that C#’s Ctrl + C is a set of gameplay based on win32api encapsulation, that is, use KERNEL32!SetConsoleCtrlHandler to register, use KERNELBASE!CtrlRoutine to publish. To verify it is very simple, you can add a Debugger.Break(); in the callback function.

static void CancelKeyPressHandler(object sender, ConsoleCancelEventArgs e)
        {
            Console.WriteLine("You pressed Ctrl + C!");
            //Here you can add the code you want to execute
            e.Cancel = true;
            Debugger.Break();
        }

After running the program, use the Ctrl + C shortcut key on the console. One thing to note here is that this event will trigger an interrupt. We use gn (Go with Exception Not Handled) to handle it, otherwise windbg will swallow this exception notification.

0:000> g
(224c.51c8): Control-C exception - code 40010005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
KERNELBASE!CtrlRoutine + 0x369ff:
00007ffa`4ab9169f 0f1f440000 nop dword ptr [rax + rax]
0:008> dp rax + rax L1
00000456`73806260 ?`?
0:008>gn
(224c.51c8): Break instruction exception - code 80000003 (first chance)
KERNELBASE!wil::details::DebugBreak + 0x2:
00007ffa`4ab9d962 cc int 3

0:008> k
 # Child-SP RetAddr Call Site
00 0000003f`63b7f848 00007ff9`82343ed9 KERNELBASE!wil::details::DebugBreak + 0x2
01 0000003f`63b7f850 00007ff9`8184a66a coreclr!DebugDebugger::Break + 0x149 [D:\a\_work\1\s\src\coreclr\vm\debugdebugger.cpp @ 148]
02 0000003f`63b7f9d0 00007ff9`227198ca System_Private_CoreLib!System.Diagnostics.Debugger.Break + 0xa [/_/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs @ 18]
03 0000003f`63b7fa00 00007ffa`1448922d ConsoleApp1!ConsoleApp1.Program.CancelKeyPressHandler + 0x4a [D:\code\MyApplication\ConsoleApp1\Program.cs @ 32]
04 0000003f`63b7fa30 00007ff9`817ac6c3 System_Console!System.Console.HandlePosixSignal + 0x7d [/_/src/libraries/System.Console/src/System/Console.cs @ 952]
05 0000003f`63b7fa80 00007ffa`4ab91704 System_Private_CoreLib!System.Runtime.InteropServices.PosixSignalRegistration.HandlerRoutine + 0x193 [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRe gistration.Windows.cs@106]
06 0000003f`63b7fb20 00007ffa`4b667614 KERNELBASE!CtrlRoutine + 0x36a64
07 0000003f`63b7fc10 00007ffa`4cdc26a1 KERNEL32!BaseThreadInitThunk + 0x14
08 0000003f`63b7fc40 00000000`00000000 ntdll!RtlUserThreadStart + 0x21

Judging from the hexagram information above, it turns out that the KERNELBASE!CtrlRoutine logic triggered our custom callback function. I believe that careful friends will definitely have a question? How did this thread get new? It was not created in my code!

3. How does the CtrlRoutine thread come from

To answer this question, you need to have an understanding of some important processes in the Windows operating system. For example, what does csrss.exe do? You can consult chatgpt.

e6edc3c9bce68040a38e2dfd6fbb7546.png

Its words have implicitly told us that the thread may be remotely injected by the csrss process. Is that so? You can set a breakpoint to observe.

0: kd> !process 0 0 csrss.exe
PROCESS ffffe001d350f300
    SessionId: 0 Cid: 0180 Peb: 7ff755c16000 ParentCid: 0178
    DirBase: 1037cd000 ObjectTable: ffffc000a88e0480 HandleCount: <Data Not Accessible>
    Image: csrss.exe

PROCESS ffffe001d351c080
    SessionId: 1 Cid: 01d8 Peb: 7ff75628d000 ParentCid: 01c8
    DirBase: 10471f000 ObjectTable: ffffc000a907a140 HandleCount: <Data Not Accessible>
    Image: csrss.exe
0: kd> bp /p ffffe001d351c080 ntdll!NtCreateThreadEx
0: kd> g
3: kd> .reload /user
3: kd> k
 # Child-SP RetAddr Call Site
00 000000ff`917ff238 00007fff`9e9d6720 ntdll!NtCreateThreadEx
01 000000ff`917ff240 00007fff`9e9d6602 ntdll!RtlpCreateUserThreadEx + 0x110
02 000000ff`917ff380 00007fff`9b310290 ntdll!RtlCreateUserThread + 0x62
03 000000ff`917ff3f0 00007fff`9b30efb9 winsrv!InternalCreateCallbackThread + 0x114
04 000000ff`917ff4b0 00007fff`9b3109e9 winsrv!CreateCtrlThread + 0xe9
05 000000ff`917ff610 00007fff`9b365837 winsrv!SrvEndTask + 0xa9
06 000000ff`917ff880 00007fff`9e969f75 CSRSRV!CsrApiRequestThread + 0x4d7
07 000000ff`917ffd10 00000000`00000000 ntdll!RtlUserThreadStart + 0x45

Judging from the CreateCtrlThread and ntdll!NtCreateThreadEx in the hexagram, it seems that a remote thread related to Ctrl + C is really created. Is that true? The method signature is as follows:

NTSTATUS NtCreateThreadEx(
  PHANDLE ThreadHandle,
  ACCESS_MASK DesiredAccess,
  POBJECT_ATTRIBUTES ObjectAttributes,
  HANDLE ProcessHandle,
  PVOID StartRoutine,
  PVOID Argument,
  ULONGCreateFlags,
  ULONG_PTR ZeroBits,
  SIZE_T StackSize,
  SIZE_T MaximumStackSize,
  PVOID AttributeList
);

Then use windbg to extract the fifth parameter (StartRoutine) and verify it. The screenshot is as follows:

1b5d0b2518fc8781c8b3e1ef414a4cb4.png

From the information in the hexagram, it can be seen that kernelbase!CtrlRoutine is indeed used as the thread entry point, which has been verified.

At this point, some friends may have raised a new question. In the previous article, didn’t you say that GetMessageW of conhost.exe is used to receive window events? For example, this Ctrl + C event, how does it notify csrss.exe to inject a remote thread into ConsoleApp1.exe?

4. How conhost notifies csrss

This question sounds quite confusing. In fact, friends who are familiar with Windows programming know that there is a mechanism called ALPC in the Windows kernel that is specifically used to implement inter-process communication. The implication is conhost and csrss have an ALPC connection, which can be observed with windbg’s !alpc command.

1: kd> !process 0n4028 0
Searching for Process with Cid == fbc
PROCESS ffffe001d26a3080
    SessionId: 1 Cid: 0fbc Peb: 7ff680afc000 ParentCid: 05e4
    DirBase: 8b8de000 ObjectTable: ffffc000ab252cc0 HandleCount: <Data Not Accessible>
    Image: conhost.exe

1: kd> !alpc /lpp ffffe001d26a3080

Ports the process ffffe001d26a3080 is connected to:

 ffffe001d28a25c0 0 -> ffffe001d1749090 ('ApiPort') 1 ffffe001d351c080 ('csrss.exe')
    ...

1: kd> !process ffffe001d26a3080
...
        THREAD ffffe001d2250080 Cid 0fbc.0f94 Teb: 00007ff680afe000 Win32Thread: ffffe001d4559fa0 WAIT: (WrLpcReply) UserMode Non-Alertable
            ffffe001d22506b8 Semaphore Limit 0x1
        Waiting for reply to ALPC Message ffffc000b1c6a7a0 : queued at port ffffe001d1749090 : owned by process ffffe001d351c080
        Not impersonating
        DeviceMap ffffc000a9ea6f50
        Owning Process ffffe001d26a3080 Image: conhost.exe
        Attached Process N/A Image: N/A
        Wait Start TickCount 48619 Ticks: 1 (0:00:00:00.015)
        Context Switch Count 1406 IdealProcessor: 1
        UserTime 00:00:00.031
        KernelTime 00:00:00.687
        Win32 Start Address 0x00007fff8e601c90
        Stack Init ffffd00030df4c90 Current ffffd00030df4500
        Base ffffd00030df5000 Limit ffffd00030def000 Call 0000000000000000
        Priority 12 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
        Child-SP RetAddr Call Site
        ffffd000`30df4540 fffff802`792c0090 nt!KiSwapContext + 0x76
        ffffd000`30df4680 fffff802`792bfaa8 nt!KiSwapThread + 0x160
        ffffd000`30df4730 fffff802`792b86e5 nt!KiCommitThreadWait + 0x148
        ffffd000`30df47c0 fffff802`792bc643 nt!KeWaitForSingleObject + 0x385
        ffffd000`30df4870 fffff802`7969e70b nt!AlpcpSignalAndWait + 0x213
        ffffd000`30df4910 fffff802`7969f32a nt!AlpcpReceiveSynchronousReply + 0x5b
        ffffd000`30df4970 fffff802`7978f1fd nt!AlpcpProcessSynchronousRequest + 0x34a
        ffffd000`30df4a60 fffff802`7978f142 nt!LpcpRequestWaitReplyPort + 0x95
        ffffd000`30df4ac0 fffff802`793cdb63 nt!NtRequestWaitReplyPort + 0x6e
        ffffd000`30df4b00 00007fff`9e9f3a4a nt!KiSystemServiceCopyEnd + 0x13 (TrapFrame @ ffffd000`30df4b00)
        00000062`3d2bf5d8 00000000`00000000 ntdll!NtRequestWaitReplyPort + 0xa

If you carefully observe the information in the hexagram, you can see that there is indeed an ALPC connection communicating with csrss.exe, and conhost is waiting for the csrss message to return, that is, Waiting for reply to ALPC Message.

At this point, I feel that all the questions have been cleared up. Drawing a picture probably looks like this.

18038ea0ce7513d473b2da33650f3882.png

Three: Summary

This article explains in detail the underlying gameplay of Ctrl + C. I think the more important thing is how to use windbg. Seeing is believing. This is the value of debugging. I hope it can help friends who have doubts about this issue. Bring a little value.