11

Windbg should understand the MS exception protocol used to pass thread names to a debugger.

I can't get this to work. Looking on the net there are many examples showing "~" thread lists with no thread names, and that's what I see. I'm debugging a .NET x86 process, and I've tried the WDK 8.1 x86 and x64 versions of Windbg.

Does anyone know if this feature is still available? What am I missing?

x29a
  • 1,761
  • 1
  • 24
  • 43
Nick Westgate
  • 3,088
  • 2
  • 34
  • 41
  • I'm using WinDbg since 2010 and I've never seen thread names like that – Thomas Weller Jul 22 '15 at 09:48
  • The post is from 2005, so you might try with WinDbg 6.4.0007.0 (find it in [Windows 2003 SDK](http://download.cnet.com/Windows-Server-2003-R2-Platform-SDK-ISO-Download/3000-10248_4-10731094.html)) – Thomas Weller Jul 22 '15 at 09:57
  • Tangentially, [2018 might be the year](https://learn.microsoft.com/en-us/visualstudio/productinfo/vs2018-roadmap) .NET supports [a new thread naming mechanism](https://visualstudio.uservoice.com/forums/121579-visual-studio-ide/suggestions/17608120-properly-support-native-thread-naming-via-the-sett). – Nick Westgate May 10 '18 at 03:47

1 Answers1

16

For .NET threads, the following works for "normal" Threads (manually created threads, since I don't know a way to name threadpool threads):

A Thread is a class and thus can be found in the .NET managed heap:

0:000>.loadby sos clr
0:000> !dumpheap -stat -type Thread
      MT    Count    TotalSize Class Name
...
725e4960       11          572 System.Threading.Thread

Note that there is other output as well, since !dumpheap looks for parts of class names. The Method Table (MT) however, identifies a class uniquely, so that's what we use from now on:

0:000> !dumpheap -short -mt 725e4960
023123d0
02312464
02313c80
...

These are the addresses of Thread objects. Since it is clean output, we can use it in a loop:

0:000> .foreach (address {!dumpheap -short -mt 725e4960}) {.echo ${address} }
023123d0
02312464
02313c80    
...

Inside the loop, we can use the address to get more information about the thread. First, let's find out how a Thread looks like internally:

0:000> !do 023123d0
Name:        System.Threading.Thread
...
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
...
725e3e18  400076e        c        System.String  0 instance 02313c0c m_Name
...

At offset +0xC (depending on the bitness!), there's the m_Name member. That's a string. Let's find out how a string looks like:

0:000> !do poi(023123d0+c)
Name:        System.String
...
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
...
725e4810  40000ac        8          System.Char  1 instance       4d m_firstChar

So, the first character of the string is at offset +0x08. Strings in .NET are Unicode, so we can view it with du:

0:000> du poi(023123d0+c)+8
02313c14  "My named thread 0"

Combine all this knowledge into a single command:

.foreach (address {!dumpheap -short -mt 725e4960})
{
    du poi(${address}+c)+8
}

(formatted for readability, put it all in one line)

If you try that, you'll find that it may output something like

00000008  "????????????????????????????????"

This happens when m_Name is null. If you care about that, you can add a check for null:

.foreach (address {!dumpheap -short -mt 725e4960})
{
    .if (poi(${address}+c) != 0) {
        du poi(${address}+c)+8
    }
}

(formatted for readability, put it all in one line)

Other improvements:

  • do the same for the thread ID
  • prettify output (use .printf instead of dd and du)

Final result:

.foreach (address {!dumpheap -short -mt 725e4960}) 
{
    .if (poi(${address}+c) != 0) 
    {
        .printf "%d ",poi(${address}+28);
        .printf "%mu\r\n", poi(${address}+c)+8
    }
}
Thomas Weller
  • 55,411
  • 20
  • 125
  • 222
  • Thread pool threads are no different from regular threads, but naming them is usually pointless since they are reused. – Brian Rasmussen Jul 22 '15 at 20:58
  • It seems .NET doesn't use the first-chance exception protocol for thread names, though I haven't had time to verify that. Thanks for the tutorial. I've added your script to my rarely-used WinDbg cheat sheet! – Nick Westgate Jul 28 '15 at 22:54
  • I'm now working at a firm where I'm doing .Net development. I have just used the `.foreach` loop and it is working fine. I just have one question: the number `725e4960` obviously will change for every application. Is there a way to get this inside a function, something like `.foreach ... (!dumpheap -sort -mt $((get_ID(System.Threading.Thread)))`? (I know, I'm mixing Windbg with bash calculation notation and it looks ugly) – Dominique Nov 02 '21 at 08:25
  • 1
    @Dominique: I think this would make up it's own good question. You can assign a pseudo register `.foreach /pS 7 /ps 4 (token {!name2ee mscorlib.dll System.Threading.Thread}) { r $t0 = ${token}}` and you can then use it like `!dumpheap -short -mt $t0` – Thomas Weller Nov 02 '21 at 11:30
  • 1
    This kind of detailed answers make me feel happier. Thanks @ThomasWeller – Chuck Norris Sep 06 '22 at 11:08
  • 1
    Getting such a comment from @ChuckNorris is an honor. – Thomas Weller Sep 06 '22 at 11:18
  • @thomasweller this is nicely detailed! it works for me up to the point where I'm using the `!do poi(...)` command - I get an error saying `Invalid parameter poi(000002245c304028+18)` (for me the m_name is at offset 18). I'm using a fairly recent version of WinDbg (1.2303.30001.0). Is there any help you can provide? – Michael Bray Jun 03 '23 at 20:30
  • @MichaelBray please check if it is 0n18 (decimal) or 0x18 (hex) – Thomas Weller Jun 03 '23 at 23:09
  • 1
    @thomasweller it's 18 hex, because some of the other offsets listed are hex values. However even if it was decimal, using `poi(000002245c304028+12)` doesn't work either, nor does `poi(000002245c304028+24)` nor `poi(000002245c304040)`. – Michael Bray Jun 03 '23 at 23:19
  • @MichaelBray: sorry, at the moment I have no other idea. Feel free to ask a new question. Reference this answer, and explain that it is not a duplicate. Be specific about your versions. There are differences from .NET Framework to .NET Core. Ideally, provide a [mre], even if it's just trivial 5 lines of code. I'm getting tired doing that myself. If you like, leave me a link to the question and I'll gladly upvote it, if it is fine. – Thomas Weller Jun 04 '23 at 11:16