[PowerShell Script] PowerDbg v3.1 - Using PowerShell to Control WinDbg

 

This new version has one more parser for !PrintException and a killer feature that my colleagues and myself have wanted since the beginning: PowerDbg, more specifically Send-PowerDbgCommand, which now has the ability to wait until a command finished its execution. It means no more delays to wait for a command to be processed and no more exceptions when the command takes longer than the delay to finish its execution.

To know how it’s a great improvement over the previous version, let me explain the PowerDbg concept.

If you are new to PowerDbg, here’s a great explanation about it.

Basically it’s a library that interacts with WinDbg using the Shell object from Windows Script Host.

With PowerDbg it’s possible to send commands to WinDbg and receive data from WinDbg.

The basic flow from a script using PowerDbg is below. As you can see, it’s very easy to interact with WinDbg and to create more parsers or scripts that use PowerDbg:

1- Change ( .wtitle PowerDbg) WinDbg title to PowerDbg, so the cmdlets can find the WinDbg instance.

2- Send-PowerDbgCommand “command” ß Sends command to the debugger.

3- Parse-PowerDbg<command>       ß Parses the output and sends it to a CSV file.

4- Convert-PowerDbgCSVToHashTable  ß Converts the CSV to a Hash Table.

Then you can use the hash table in the way you want.

It works fine, however, when Send-PowerDbgCommand sends a command to WinDbg; it returns, doing an asynchronous call. However, when the cmdlet returns the WinDbg could be still struggling to process the command. At this point, if a Parse cmdlet is called from a script, an exception will happen. To work around this timing issue, the scripts using PowerDbg and even some cmdlets used a sleep call to give WinDbg time to process the command.

The problems with this approach are:

a) If the command is quick and returns the result it called upon, the scripts don’t know that, and they need to use an idle time just in case, thus, impacting the performance.

b) If the developer using PowerDbg forgets to add Start-Sleep after using Send-PowerDbgCommand, it is almost sure an error will happen when another PowerDbg command is called. It happens because of the timing issue.

c) If you use a long timeout, it may not be enough for specific cases, so your script will break, throwing an exception. For example, try to use it from PowerShell, using the previous PowerDbg library:

Send-PowerDbgCommand "~* kpn 1000"; Parse-PowerDbgK; $ht = @{}; $ht = Convert-PowerDbgCSVToHashTable

Or test using:

Analyze-PowerDbgThreads

If you have a dump or process with too many threads or if the symbols are still being solved, chances are you’ll get an exception. The reason is related to timing. After sending the command to the debugger, PowerShell will try to parse and create a Hash Table from an empty csv file while WinDbg is still processing the command.

To solve it you can use something like Start-Sleep 8, for example. However, what if the command took just 3 seconds? Or what if it took 1 minute?

SOLVING THE TIMING ISSUE

To solve the timing situation exemplified above, the latest PowerDbg version only returns from Send-PowerDbgCommand when WinDbg has finished processing the command!

It means no more idle time, better performance, and no more exceptions after a forced delay.

To do that I considered several approaches and ended up using the approach below:

Send-PowerDbgCommand uses “ .pcmd” to send a message to the debugger when any command that affects execution runs (for more, see the WinDbg documentation). So, the user command is sent to the debugger and, just after it, the “r” command is sent to the debugger, too. Because the first command may be still be processing, the second command doesn’t run and stays in the queue.

The “r” command is considered a command the affects the execution, so the message configured with “ .pcmd” is triggered when “r” is used. Got it? After executing the user command, the “r” command that is queued to be processed, is processed. Then our custom message is sent to the debugger. In the meantime Send-PowerDbgCommand is monitoring the log file that has the raw output and looking for the message that signals the end of command execution. The default timeout is 3 minutes and can be easily changed. When it finds the message, it removes the garbage from the output file so that the parsed file won’t have any differences when compared to previous versions of PowerDbg. (in other words, it won’t break your scripts J)

The only drawback is a message and registers that appear in the WinDbg window whenever you send a command, causing a bit of visual pollution, like:

#### IGNORE REGISTERS BELOW ####

eax=7ff24000 ebx=00000000 ecx=00000000 edx=76e7f06d esi=00000000 edi=00000000

eip=76e32ea8 esp=0b9fff74 ebp=0b9fffa0 iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

ntdll!DbgBreakPoint:

76e32ea8 cc int 3

#### IGNORE REGISTERS ABOVE ####

Anyway, for the benefit of this approach it’s just a minor drawback, and it doesn’t affect the Parsers from PowerShell either in the way you use the library to create your own cmdlets or in scripts.

Note: The command .pcmd is not going to be in the article series Special Commands because, although it’s a special command, it’s very unlikely you end up using it during your debugging. At the same time, it’s very simple to use it.

Below, are examples of using PowerDbg, so you can use it as a reference when building scripts.

Verifying if a specified Module is in the process/dump:

a) Send Command, parse, and convert CSV file to Hash Table:

Send-PowerDbgCommand "lm1m"; Parse-PowerDbgLM1M; $ht = @{}; $ht = Convert-PowerDbgCSVToHashTable  

b) Now we have the Hash Table. You can play the way you want! J For instance, let’s see if a specific module exists:

$ht[“Test”]    ß Doesn’t return anything, so the module is not there.

$ht[“mscorwks”] ß Returns the module, so it’s there.

 

Playing with the call stack:

a) Again the same sequence: command -> parser -> convert csv to Hash table:

Send-PowerDbgCommand "~* kpn 1000"; Parse-PowerDbgK; $ht = @{}; $ht = Convert-PowerDbgCSVToHashTable

b) This is the hash table:

$ht

Pay attention to this: Because we have a csv file, we have to use another way to put the stack as the value and the thread number as the key. To do that the cmdlet removed the newline and replaced it with a special character sequence that you can get from:

$global:g_frameDelimiter

It’s documented in the header file from Parse-PowerDbgK in case were wondering. J

c) Now, let’s see the call stack for thread 20. Note how I replace the $global:g_frameDelimiter for new line:

write-Host $ht["20"].Replace($global:g_frameDelimiter, "`n") -foreground Green -background Black

 It’s a piece of cake to see what each thread is doing, sorted by CPU time. Imagine the time you would spend by hand to understand what each thread is doing.

Here it is:

Analyze-PowerDbgThreads

 

By the way it’s super easy to improve the accuracy of Analyze-PowerDbgThreads, for example, adding symbols for your specific application!

OK, now, let’s suppose you want to use DML. Just one small detail: my WinDbg has customized colors, so it may look different on your screen:

a) Let’s get the symbol to look for from the keyboard:

$symbol = Read-Host

b) Sends a comment to WinDbg:

Send-PowerDbgComment "Click the hyperlink below"

c) Builds the DML command:

Send-PowerDbgDML "Find Stacks that Match Symbol" "!findstack $symbol 2"

 

To learn more see the PowerShell Scripts that use PowerDbg and read the headers from each cmdlet.

This is the source code for PowerDbg v3.1. I like to save it into my $profile (Microsoft.PowerShell_profile.ps1), so I can use it from anywhere, but it's up to you.

########################################################################################################

# PowerDbg v 3.1

#

# New Features:

#

# - Parse-PowerDbgPRINTEXCEPTION()

# - No more Sleep calls to wait until a command is finished. Now, whenenever a command is sent to the debugger

# the cmdlet Send-PowerDbgCommand waits until the command finish. Using this approach there won't happen

# errors when you send a command that may take a long time to execute. At the same time the scripts using

# PowerDbg don't need to implement delays anymore. It's a big performance improvement.

# - The script were revised and changed to be compatible with this new PowerDbg. To compatibilize I've

# removed start-sleep and the regular "r" command (without arguments). Get the newest version.

# - Fixed bug that appeared when using WinDbg from the default path or when using a path that has

# spaces, like: c:\Program Files

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

# Global variables.

########################################################################################################

$global:g_instance = $null

$global:g_fileParsedOutput = "POWERDBG-PARSED.LOG"

$global:g_fileCommandOutput = "POWERDBG-OUTPUT.LOG"

$global:g_CSVDelimiter = ","

$global:g_frameDelimiter = "#$#@"

# It's not possible to create an enum like C++/C# using a PowerShell keyword.

# The PowerShell blog has a solution for it, but here I'm going to use local variables.

$global:g_unknownSymbol = 0

$global:g_waitingForCriticalSection = 1

$global:g_doingIO = 2

$global:g_threadWaiting = 3

$global:g_GCthread = 4

$global:g_waitUntilGCComplete = 5

$global:g_suspendForGC = 6

$global:g_waitForFinalize = 7

$global:g_tryingGetLock = 8

$global:g_winSockReceivingData = 9

$global:g_finalizerThread = 10

$global:g_CLRDebuggerThread = 11

$global:g_w3wpMainThread = 12

$global:g_threadPool = 13

$global:g_CompressionThreaad = 14

$global:g_COMcall = 15

$global:g_CLRWorkerThread = 16

$global:g_completionPortIOThread = 17

$global:g_gateThread = 18

$global:g_timerThread = 19

$global:g_unloadAppDomainThread = 20

$global:g_RPCWorkerThread = 21

$global:g_LPRCWorkerThread = 22

$global:g_hostSTAThread = 23

# The array below has the meaning of each constant above. It can be used to display high level information

# to the user.

$global:g_whatThreadIsDoing =

@(

"Thread working and doing unknown activity.", # 0

"Thread waiting for Critical Section.", # 1

"Thread doing I/O operation.", # 2

"Thread in wait state.", # 3

"Thread from Garbage Collector.", # 4

"Thread waiting for the Garbage Collector to complete.", # 5

"Thread is being suspended to perform a Garbage Collector." # 6

"Thread waiting for the Finalizer event. The Finalizer thread might be blocked.", # 7

"Thread trying to get a managed lock.", # 8

"Thread receiving or trying to receive data. The data might be from the database.", # 9

"Thread is the Finalizer Thread.", # 10

"Thread is the CLR Debugger Thread.", # 11

"Thread is the W3WP.EXE main thread.", # 12

"Thread is from the W3WP.EXE pool of threads.", # 13

"Thread is a Compression Thread from W3WP.EXE.", # 14

"Thread doing a COM call.", # 15

"Thread is a CLR Worker Thread.", # 16

"Thread is a Completion Port I/O Thread.", # 17

"Thread is a CLR Gate Thread.", # 18

"Thread is a CLR Timer Thread.", # 19

"Thread is an Unload Application Domain Thread.", # 20

"Thread is a RPC Worker Thread.", # 21

"Thread is an LRPC Worker Thread.", # 22

"Thread is the Host STA Thread." # 23

)

########################################################################################################

# Function: Start-PowerDbgWinDbg

#

# Parameters: [string] <$debuggerPathExe>

# Path and executable where is located your WinDbg.

#

# [string] <$nameOfDumpOrProcess>

# Name of dump file or process to debug. The process must be running.$#

#

# [string] <$symbolPath>

# Specifies the symbol file search path. Separate multiple paths with a semicolon (;).

#

# Return: Global variable $debuggerInstance that has the debugger instance.

#

# Purpose: Start an WinDbg $g_instance and open a dump file or attach to a running process.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Start-PowerDbgWinDbg(

[string] $debuggerPathExe = $(throw "Error! You must provide the Windbg path."),

[string] $nameOfDumpOrProcess = $(throw "Error! You must provide dump file or process name."),

[string] $symbolPath = $(throw "Error! You must provide the symbol path.")

)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

$debugger = $debuggerPathExe

$isExe = $false

# Check if the argument is a dump file or process name and use the corresponding WinDbg command.

if($nameOfDumpOrProcess -ilike "*.dmp")

{

$debugger += " -z " + $nameOfDumpOrProcess

}

elseif($nameOfDumpOrProcess -ilike "*.exe")

{

$isExe = $true

}

else

{

throw "Error! You must provide a valid dump file or executing process."

}

if($isExe)

{

$debugger += " -Q -QSY -QY -v -y " + "`"$symbolPath`"" + " -c `".symfix;.reload`"" + " -T PowerDbg" + " " + $nameOfDumpOrProcess

}

else

{

$debugger += " -Q -QSY -QY -v -y " + "`"$symbolPath`"" + " -c `".symfix;.reload`"" + " -T PowerDbg"

}

# Now we can start a new debugger instance.

$global:g_instance = new-object -comobject WScript.Shell

$output = $global:g_instance.Run($debugger, 3)

# Maximize window. It's not necessary to use the full name.

$output = $global:g_instance.AppActivate("PowerDbg")

}

########################################################################################################

# Function: Send-PowerDbgCommand

#

# Parameters: [string] <$command>

# Command from Windbg. Avoid mixing more than one command at the same line to be easier to parse the output.

#

# Return: Nothing.

#

# Purpose: Sends a command to the Windbg instance that has the PowerDbg title and saves the command and its output

# into a log file named POWERDBG.LOG.

# If there's no instance that has the PowerDbg title you need to use the .wtitle command from WinDbg

# and change the WinDbg window in order to start with the PowerDbg string.

# The command output will be into the POWERDBG-OUTPUT.LOG.

# Your parser functions should use POWERDBG-OUTPUT.LOG.

#

# Attention! The timeout before raising an error while waiting for a command to be executed is 3 minutes (180 seconds)

# If you want to increase this number, change the $timeoutInSeconds local variable.

# Also, DO NOT use the "r" command, but you can use something like "r @$tid". Using the regular "r"

# will break the execution if you have something like: "~* r;kL"

#

# Changes History: 12/31/2007 - In a few specific scenarios the .logopen command may not create the log file.

# Now the POWERDBG.LOG is tested. If it was not created, the function tries to

# create it for a limited number of times.

#

# 03/28/2008 - No more delays while waiting for a command to be executed. Now whenever a

# command is sent to the debugger, the debugger sends a message and close

# the log file when it's done. The cmdlet verifies the log to see if the command

# finished its execution. If yes it returns, otherwise it keeps waiting.

#

# 04/06/2008 - .logopen was being used without ", so failing when the Debugger was installed

# in a folder that has spaces, like: : c:\Program Files.

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Send-PowerDbgCommand([string] $command = $(throw "Error! You must provide a Windbg command."))

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap

{

# If the log is open it may cause problems if another command is sent from PowerDbg.

$output = $global:g_instance.SendKeys(".logclose")

$output = $global:g_instance.SendKeys("{ENTER}")

"Error message: $_"

}

# If 180 seconds is not enough for you, just increase this number.

$timeoutInSeconds = 180

# First let's locate if Start-PowerDbgWinDbg had created an WinDbg instance.

# If not, let's try to use one running instance.

if($global:g_instance -eq $null)

{

$global:g_instance = new-object -comobject WScript.Shell

}

# Set focus to Windbg instance.

$return = $global:g_instance.AppActivate("PowerDbg")

start-sleep 1

# Set focus to Command window.

$return = $global:g_instance.AppActivate("Command")

start-sleep 1

# Get the directory where the log will be created.

$aux = get-Process windbg

# We may have several instances. That's why I use element 0.

if($aux.Count -gt 0)

{

$aux = $aux[0].MainModule.Filename

}

else

{

$aux = $aux.MainModule.Filename

}

$aux = [System.IO.Path]::GetDirectoryName($aux)

$logFile = "$aux\POWERDBG.LOG"

$fileExists = test-Path $logFile

# Remove log file, so we can know if it was created.

if($fileExists -eq $true)

{

remove-Item $logFile

}

$message = "#### IGNORE REGISTERS ABOVE ####"

# Makes the debugger close the file and send a message when the command finishes its execution.

# NOTE: Commands that are related to execution, like p, t, r, will close the log and trigger the message.

$output = $global:g_instance.SendKeys(".pcmd -s `" .echo $message; .logclose `" ")

$output = $global:g_instance.SendKeys("{ENTER}")

# Create log.

$output = $global:g_instance.SendKeys(".logopen `"$logFile `" ")

$output = $global:g_instance.SendKeys("{ENTER}")

# If POWERDBG.LOG was not created, try it again for 5 times.

for([System.Int32] $count = 0; $count -lt 5; $count++)

{

start-Sleep 1

$fileExists = test-Path $logFile

# If the log file was created leaves the loop.

if($fileExists -eq $true)

{

break

}

else # If not, tries to create it again, for 5 more times.

{

# Tries to create log again.

$output = $global:g_instance.SendKeys(".logopen `"$logFile `"")

$output = $global:g_instance.SendKeys("{ENTER}")

}

}

# Adjust specific commands.

$command = $command.Replace("~", "{~}")

$command = $command.Replace("%", "{%}")

$command = $command.Replace("+", "{+}")

# Send command.

$output = $global:g_instance.SendKeys($command)

$output = $global:g_instance.SendKeys("{ENTER}")

$garbageMessage = "#### IGNORE REGISTERS BELOW ####"

# To force the WinDbg to close the log and display the message when the command is done,

# we have to use a command that is affects the execution without changing the execution.

# The best command is "r" that display the registers.

$output = $global:g_instance.SendKeys(".echo $garbageMessage;r")

$output = $global:g_instance.SendKeys("{ENTER}")

[System.Int32] $count = 0

# Verify if command finished its execution. If it's done we should find the specific message,

# otherwise the variable will be null.

# If trying for more than 3 minutes, raise an exception.

for(;;)

{

Start-Sleep 1 # Wait one second then try again.

$messageFound = (get-content "$aux\POWERDBG.LOG" | where-Object {$_ -like $message})

# If null the command is still executing.

if($messageFound -eq $null)

{

$count++;

write-host "." -Foreground Green -Background Black -nonewline

# Is it trying for more than our threshold?

if($count -gt $timeoutInSeconds)

{

throw "Timeout error! A command did not finish its execution after 3 minutes!"

}

}

else

{

break # We can exit the loop. The command was executed.

}

}

write-host "" -Foreground Green -Background Black # Just a new empty line.

# A delay is required here, just to be sure the file was flushed and closed.

start-sleep 4

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

$isDone = $false

# Remove commands and content that is specific for PowerDbg, include garbage created by the "r" command.

get-content "$aux\POWERDBG.LOG" | `

foreach-object {

if($_ -imatch $garbageMessage)

{

# After a while debugging the script I found the break instruction doesn't work with foreach-object. Use a break here makes the script

# finish the execution. As a workaround I'm using a flag.

$isDone = $true

}

elseif($isDone -eq $false -and (($_ -notmatch "^.:...>") -and ($_ -notmatch "^Opened log file") -and ($_ -notmatch "^$message") -and ($_ -notmatch "^Closing open log file")))

{

$builder = $builder.AppendLine([string] $_)

}

}

# Save the output into a file. The location is the same you are executing PowerDbg.

out-file -filepath $global:g_fileCommandOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgDT

#

# Parameters: [switch] [$useFieldNames]

# Switch flag. If $useFieldNames is present then the function saves the field

# names from struct/classes and their values. Otherwise, it creates saves the offsets

# and their values.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "dt" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgDT([switch] $useFieldNames)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

if($useFieldNames)

{

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "0x\S+\s+(?<key>\w+).+\:\s+(?<value>.+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

}

}

}

else

{

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>0x\S+).+\:\s+(?<value>.+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

}

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Convert-PowerDbgCSVtoHashTable

#

# Parameters: None.

#

# Return: Hash table.

#

# Purpose: Sometimes the Parse-PowerDbg#() functions return a CSV file. This function

# loads the data using a hash table.

# However, it works just when the CSV file has two fields: key and value.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Convert-PowerDbgCSVtoHashTable()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

$hashTable = @{}

import-csv -path $global:g_fileParsedOutput | foreach {$hashTable[$_.key] = $_.value}

return $hashTable

}

########################################################################################################

# Function: Send-PowerDbgDML

#

# Parameters: [string] <$hyperlinkDML>

# Hyperlink for the DML command.

#

# [string] <$commandDML>

# Command to execute when the hyperlink is clicked.

#

# Return: Nothing.

#

# Purpose: Creates a DML command and send it to Windbg.

# DML stands for Debug Markup Language. Using DML you can create hyperlinks that

# run a command when the user click on them.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Send-PowerDbgDML(

[string] $hyperlinkDML = $(throw "Error! You must provide the hyperlink for DML."),

[string] $commandDML = $(throw "Error! You must provide the command for DML.")

)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

Send-PowerDbgCommand ".printf /D `"<link cmd=\`"$commandDML\`"><b>$hyperlinkDML</b></link>\n\`"`""

}

########################################################################################################

# Function: Parse-PowerDbgNAME2EE

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!name2ee" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgNAME2EE()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# Attention! The Name: doesn't map to the right value, however, it should be the same method name provide as argument.

if($line -match "(?<key>\w+\:)\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgDUMPMD

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!dumpmd" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgDUMPMD()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>((^Method Name :)|(^MethodTable)|(^Module:)|(^mdToken:)|(^Flags :)|(^Method VA :)))\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgDUMPMODULE

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!dumpmodule" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgDUMPMODULE()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

[int] $countFields = 0

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# Fields for .NET Framework 2.0

if($line -match "(?<key>((^dwFlags)|(^Assembly:)|(^LoaderHeap:)|(^TypeDefToMethodTableMap:)|(^TypeRefToMethodTableMap:)|(^MethodDefToDescMap:)|(^FieldDefToDescMap:)|(^MemberRefToDescMap:)|(^FileReferencesMap:)|(^AssemblyReferencesMap:)|(^MetaData start address:)))\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

$countFields++

}

}

# If nothing was found, let's try to use the .NET Framework 1.1 fields.

if($countFields -lt 3)

{

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# Fields for .NET Framework 2.0

if($line -match "(?<key>((^dwFlags)|(^Assembly\*)|(^LoaderHeap\*)|(^TypeDefToMethodTableMap\*)|(^TypeRefToMethodTableMap\*)|(^MethodDefToDescMap\*)|(^FieldDefToDescMap\*)|(^MemberRefToDescMap\*)|(^FileReferencesMap\*)|(^AssemblyReferencesMap\*)|(^MetaData starts at)))\s+(?<value>\S+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

$hasFound = $true

}

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgLMI

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!lmi" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgLMI()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>((^.+\:)))\s+(?<value>\S+)")

{

$strNoLeftSpaces = $matches["key"]

$strNoLeftSpaces = $strNoLeftSpaces.TrimStart()

$builder = $builder.AppendLine($strNoLeftSpaces + $global:g_CSVDelimiter + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Has-PowerDbgCommandSucceeded

#

# Parameters: None.

#

# Return: Return $true if the last command succeeded or $false if not.

#

# Purpose: Return $true if the last command succeeded or $false if not.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Has-PowerDbgCommandSucceeded

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -imatch "(Fail) | (Failed) | (Error) | (Invalid) | (Unable to get) | (Exception)")

{

return $false

}

}

return $true

}

########################################################################################################

# Function: Send-PowerDbgComment

#

# Parameters: [string] $comment

# Comment to be sent to the debugger.

#

# Return: Nothing.

#

# Purpose: Sends a bold comment to the debugger. Uses DML.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Send-PowerDbgComment(

[string] $comment = $(throw "Error! You must provide a comment.")

)

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

Send-PowerDbgCommand ".printf /D `"\n<b>$comment</b>\n\n\`"`""

}

########################################################################################################

# Function: Parse-PowerDbgVERTARGET

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output of Kernel time and User time from "vertarget" command, using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# The number of days is ignored in this version.

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgVERTARGET()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>((Kernel time:)|(User time:)))\s+\d+\s+\S+\s+(?<value>\d+\:\d+\:\d+\.\d+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgRUNAWAY

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output of "!runaway 1" or "!runaway 2" command, using a hash table.

# For this version the number of days is not being considered.

# The output is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Attention! If you need to know the top threads consuming CPU time use the Convert-PowerDbgRUNAWAYtoArray

# instead of this command. With Convert-PowerDbgRUNAWAYtoArray, the array has the exact same order of the

# original command.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgRUNAWAY()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>(\d+))\:\S+\s+\d+\s+\S+\s+(?<value>\d+\:\d+\:\d+\.\d+)")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Convert-PowerDbgRUNAWAYtoArray

#

# Parameters: None.

#

# Return: Two dimensions array.

#

# Purpose: After executing the !runaway 1 or !runaway 2, use this command to put the information into

# an array.

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Convert-PowerDbgRUNAWAYtoArray()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# We need to count line numbers to be able to create the array.

# This particular command extracts the information from the command output file.

$num = get-Content $global:g_fileCommandOutput

# Now, we create a multidimensional array.

# We need to discard 3 lines that corresponds to:

# User Mode Time

# Thread Time

# and one extra white line.

$arrayFromRUNAWAY = new-Object 'object[,]' ($num.Length - 3),2

[System.Int32] $i = 0 # Counter.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>(\d+))\:\S+\s+\d+\s+\S+\s+(?<value>\d+\:\d+\:\d+\.\d+)")

{

$arrayFromRUNAWAY[$i, 0] = $matches["key"]

$arrayFromRUNAWAY[$i, 1] = $matches["value"]

$i++

}

}

# The cmoma below is very important, otherwise the function will return a single dimension array.

return ,$arrayFromRUNAWAY

}

########################################################################################################

# Function: Parse-PowerDbgK

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output of "k" command and variations like "kv, kbn, kpn", etc..., using a hash table.

# It doesn't work with "kPn".

# The key is the thread number if you use something like "~* kbn" or the key is "#" if you use

# something like "kb" just to show the stack from the current thread.

# Frame are separated by '$global:g_frameDelimiter'. So, to display the frames using newline you need to

# replace before displaying.

#

# The output is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Attention!

# 1- It doesn't work with "kPn".

# 2- It replaces "," by ";" to avoid conflict with CSV delimiter.

#

# Changes History: 12/21/2007 - The number of threads couldn't exceed 2 digits. Now it works until 999 threads.

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgK()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

$key = ""

# [ #] --> One space or #

# \s --> Just one space.

# (d+) --> Returns the decimal digits.

# \s+ --> 1 or more spaces.

# | --> or

# \s --> One space.

# (#) --> Returns #.

# \s --> One space.

# | --> or

# (\w+\s\w+\s.+) --> Returns two blocks of words and the remaining string.

# | --> or

# (\d+\s\w+\s\w+\s.+) --> Returns one block of digits + two blocks of words + remaining string.

# [ #]\s(\d+)\s+|\s(#)\s|(\w+\s\w+\s.+)|(\d+\s\w+\s\w+\s.+) <-- The actual implementation differs a little bit.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# For each call stack we have the key that is the thread number or # and the value that are a set

# of lines, including all frames, etc...

# Here we first identify the thread number if it was not identified before.

if($line -match "([ #]\s*(?<key>(\d+))\s+Id)")

{

# If key changed append a new line. Do not consider one single thread, that is represented by "#"

if($key -ne $matches["key"])

{

# The string assignment is a small tricky to avoid displaying contents in PS window.

$builder = $builder.AppendLine("")

$key = $matches["key"]

}

# Just add the key. The stack is the value and it's going to be added below.

# The string assignment is a small tricky to avoid displaying contents in PS window.

$builder = $builder.Append($matches["key"] + $global:g_CSVDelimiter)

}

#elseif($line -match "(?<value>(\w+\s\w+\s.+))") # Gets the stack.

elseif($line -match "(?<value>(\w+\s\w+\s.+))") # Gets the stack.

{

# If there is just one thread the thread number doesn't appear. For this case the thread number

# will be "#".

if($key -eq "")

{

$key = "#"

# Just add the key. The stack is the value and it's going to be added below.

# The string assignment is a small tricky to avoid displaying contents in PS window.

$builder = $builder.Append($key + $global:g_CSVDelimiter)

}

# Append each frame replacing any commas by ";".

# The value part of the hash table is a long string with all frames. At the end of each frame there is

# a delimiter. When you want to show the stack you know you can replace the delimiter by `r`n.

# Using a delimiter is easy to do that.

$builder = $builder.Append($matches["value"].Replace(",",";") + $global:g_frameDelimiter)

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgSymbolsFromK

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps just the symbols of "k" command and variations like "kv, kbn, kpn", etc..., using a hash table.

# It doesn't work with "kPn".

# The key is the thread number if you use something like "~* kbn" or the key is "#" if you use

# something like "kb" just to show the stack from the current thread.

# Frame are separated by '$global:g_frameDelimiter'. So, to display the frames using newline you need to

# replace before displaying.

#

# The output is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Attention!

# 1- It doesn't work with "kPn".

# 2- It replaces "," by ";" to avoid conflict with CSV delimiter.

#

# Changes History: 12/21/2007 - The number of threads couldn't exceed 2 digits. Now it works until 999 threads.

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgSymbolsFromK()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

$key = ""

# [ #] --> One space or #

# \s --> Just one space.

# (d+) --> Returns the decimal digits.

# \s+ --> 1 or more spaces.

# | --> or

# \s --> One space.

# (#) --> Returns #.

# \s --> One space.

# | --> or

# (\w+\s\w+\s.+) --> Returns two blocks of words and the remaining string.

# | --> or

# (\d+\s\w+\s\w+\s.+) --> Returns one block of digits + two blocks of words + remaining string.

# [ #]\s(\d+)\s+|\s(#)\s|(\w+\s\w+\s.+)|(\d+\s\w+\s\w+\s.+) <-- The actual implementation differs a little bit.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

# For each call stack we have the key that is the thread number or # and the value that are a set

# of lines, including all frames, etc...

# Here we first identify the thread number if it was not identified before.

if($line -match "([ #]\s*(?<key>(\d+))\s+Id)")

{

# If key changed append a new line. Do not consider one single thread, that is represented by "#"

if($key -ne $matches["key"])

{

# The string assignment is a small tricky to avoid displaying contents in PS window.

$builder = $builder.AppendLine("")

$key = $matches["key"]

}

# Just add the key. The stack is the value and it's going to be added below.

# The string assignment is a small tricky to avoid displaying contents in PS window.

$builder = $builder.Append($matches["key"] + $global:g_CSVDelimiter)

}

elseif($line -match "\s(?<value>(\w+!\w+::\w+))|(?<value>(\w+!\w+)|(?<value>(\w+_ni)))") # Gets the symbols from the stack.

{

# \s(\w+!\w+::\w+)|(\w+!\w+)

# (\w+!\w+::\w+) <-- Find possible C++ methods.

# (\w+!\w+) <-- Find regular symbols.

# The order is important here because the "or" is not going to evaluate the second expression if the

# first expression returns true.

# If there is just one thread the thread number doesn't appear. For this case the thread number

# will be "#".

if($key -eq "")

{

$key = "#"

# Just add the key. The stack is the value and it's going to be added below.

# The string assignment is a small tricky to avoid displaying contents in PS window.

$builder = $builder.Append($key + $global:g_CSVDelimiter)

}

# Append each symbol frame found plus the delimiter.

$builder = $builder.Append($matches["value"] + $global:g_frameDelimiter)

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Parse-PowerDbgLM1M

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output of "lm1m".

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgLM1M()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "(?<key>(\w+))")

{

# Value and key has the same value for this particular parser.

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["key"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}

########################################################################################################

# Function: Classify-PowerDbgThreads

#

# Parameters: None.

#

# Return: Array where the index is the thread number and the element is one of these values:

# 0 UNKNOWN_SYMBOL

# 1 WAITING_FOR_CRITICAL_SECTION

# 2 DOING_IO

# 3 WAITING

# 4 GC_THREAD

# 5 WAIT_UNTIL_GC_COMPLETE

# 6 SUSPEND_FOR_GC

# 7 WAIT_FOR_FINALIZE

# 8 TRYING_MANAGED_LOCK

# 9 DATA_FROM_WINSOCK

#

# The constants above are stored in global variables.

#

# Purpose: Returns an array which the index corresponds to thread numbers and the content is a value represented

# by the constants above. This cmdlet gives you an idea of what the threads are doing.

# Notice that is very easy to add more symbols and more constants to get a more granular analysis.

#

# Changes History: 01/25/08 - More symbols added to the hash table. The analysis became more granular.

#

# Mike McIntyre

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Classify-PowerDbgThreads()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

$symbolsForEachThread = @{}

# Let's save resource. We need kL with just 8 frames.

Send-PowerDbgCommand "~* kL 8"

$hasCommandSucceeded = Has-PowerDbgCommandSucceeded

# Check if the command was executed with success.

# It's unlikely to have a failure with the "k" command, but I'm being proactive.

if($false -eq $hasCommandSucceeded)

{

throw "Couldn't get call stacks!"

}

# Let's parse the output of the "k" command and variations, however, we want the symbols

# not the complete call stack.

Parse-PowerDbgSymbolsFromK

# Now let's get the symbols for each thread. We don't need the stack details here.

$symbolsForEachThread = Convert-PowerDbgCSVtoHashTable

# IMPORTANT!!!

# The symbols need to be written in uppercase, because the comparison is doing using uppercase letters to avoid

# mismatches.

$symbols = @{

"KERNEL32!COPYFILE" = $global:g_doingIO;

"KERNEL32!CREATEDIRECTORY" = $global:g_doingIO;

"KERNEL32!CREATEFILE" = $global:g_doingIO;

"KERNEL32!DELETEFILE" = $global:g_doingIO;

"KERNEL32!FILEIOCOMPLETIONROUTINE" = $global:g_doingIO;

"KERNEL32!FINDCLOSE" = $global:g_doingIO;

"KERNEL32!FINDCLOSECHANGENOTIFICATION" = $global:g_doingIO;

"KERNEL32!FINDFIRSTCHANGENOTIFICATION" = $global:g_doingIO;

"KERNEL32!FINDFIRSTFILE" = $global:g_doingIO;

"KERNEL32!FINDFIRSTFILEEX" = $global:g_doingIO;

"KERNEL32!FINDNEXTCHANGENOTIFICATION" = $global:g_doingIO;

"KERNEL32!FINDNEXTFILE" = $global:g_doingIO;

"KERNEL32!FLUSHFILEBUFFERS" = $global:g_doingIO;

"KERNEL32!GETBINARYTYPE" = $global:g_doingIO;

"KERNEL32!GETCURRENTDIRECTORY" = $global:g_doingIO;

"KERNEL32!GETDRIVETYPE" = $global:g_doingIO;

"KERNEL32!GETFILEATTRIBUTES" = $global:g_doingIO;

"KERNEL32!GETFILEATTRIBUTESEX" = $global:g_doingIO;

"KERNEL32!GETFILEINFORMATIONBYHANDLE" = $global:g_doingIO;

"KERNEL32!GETFILESIZE" = $global:g_doingIO;

"KERNEL32!GETFILESIZEEX" = $global:g_doingIO;

"KERNEL32!GETFULLPATHNAME" = $global:g_doingIO;

"KERNEL32!GETTEMPFILENAME" = $global:g_doingIO;

"KERNEL32!GETTEMPPATH" = $global:g_doingIO;

"KERNEL32!LOCKFILE" = $global:g_doingIO;

"KERNEL32!LOCKFILEEX" = $global:g_doingIO;

"KERNEL32!MOVEFILE" = $global:g_doingIO;

"KERNEL32!READDIRECTORYCHANGESW" = $global:g_doingIO;

"KERNEL32!READFILE" = $global:g_doingIO;

"KERNEL32!READFILEEX" = $global:g_doingIO;

"KERNEL32!REMOVEDIRECTORY" = $global:g_doingIO;

"KERNEL32!SEARCHPATH" = $global:g_doingIO;

"KERNEL32!SETCURRENTDIRECTORY" = $global:g_doingIO;

"KERNEL32!SETENDOFFILE" = $global:g_doingIO;

"KERNEL32!SETFILEATTRIBUTES" = $global:g_doingIO;

"KERNEL32!SETFILEPOINTER" = $global:g_doingIO;

"KERNEL32!SETFILEPOINTEREX" = $global:g_doingIO;

"KERNEL32!UNLOCKFILE" = $global:g_doingIO;

"KERNEL32!UNLOCKFILEEX" = $global:g_doingIO;

"KERNEL32!WRITEFILE" = $global:g_doingIO;

"KERNEL32!WRITEFILEEX" = $global:g_doingIO;

"NTDLL!ZWREMOVEIOCOMPLETION" = $global:g_doingIO;

"NTDLL!RTLPWAITFORCRITICALSECTION" = $global:g_waitingForCriticalSection;

"NTDLL!RTLENTERCRITICALSECTION" = $global:g_waitingForCriticalSection;

"KERNEL32!ENTERCRITICALSECTION" = $global:g_waitingForCriticalSection;

"KERNEL32!MSGWAITFORMULTIPLEOBJECTS" = $global:g_threadWaiting;

"KERNEL32!MSGWAITFORMULTIPLEOBJECTSEX" = $global:g_threadWaiting;

"KERNEL32!REGISTERWAITFORSINGLEOBJECT" = $global:g_threadWaiting;

"KERNEL32!SIGNALOBJECTANDWAIT" = $global:g_threadWaiting;

"KERNEL32!UNREGISTERWAIT" = $global:g_threadWaiting;

"KERNEL32!UNREGISTERWAITEX" = $global:g_threadWaiting;

"KERNEL32!WAITFORMULTIPLEOBJECTS" = $global:g_threadWaiting;

"KERNEL32!WAITFORMULTIPLEOBJECTSEX" = $global:g_threadWaiting;

"KERNEL32!WAITFORSINGLEOBJECT" = $global:g_threadWaiting;

"KERNEL32!WAITFORSINGLEOBJECTEX" = $global:g_threadWaiting;

"KERNEL32!WAITORTIMERCALLBACK" = $global:g_threadWaiting;

"USER32!NTUSERGETMESSAGE" = $global:g_threadWaiting;

"USER32!NTUSERMESSAGECALL" = $global:g_threadWaiting;

"USER32!NTUSERWAITMESSAGE" = $global:g_threadWaiting;

"NTDLL!DBGBREAKPOINT" = $global:g_threadWaiting;

"NTDLL!RTLPWAITTHREAD" = $global:g_threadWaiting;

"KERNEL32!SLEEPEX" = $global:g_threadWaiting;

"KERNEL32!SLEEP" = $global:g_threadWaiting;

"NTDLL!NTDELAYEXECUTION" = $global:g_threadWaiting;

"MFC80D!AFXINTERNALPUMPMESSAGE" = $global:g_threadWaiting;

"MFC80!AFXINTERNALPUMPMESSAGE" = $global:g_threadWaiting;

"MSCORWKS!SVR::GC_HEAP::GC_THREAD_STUB" = $global:g_GCthread;

"MSCORSVR!SVR::GC_HEAP::GC_THREAD_STUB" = $global:g_GCthread;

"MSCORSVR!GCHEAP::WAITUNTILGCCOMPLETE" = $global:g_waitUntilGCComplete;

"MSCORWKS!GCHEAP::WAITUNTILGCCOMPLETE" = $global:g_waitUntilGCComplete;

"MSCORWKS!THREAD::SYSSUSPENDFORGC" = $global:g_suspendForGC;

"MSCORSVR!THREAD::SYSSUSPENDFORGC" = $global:g_suspendForGC;

"MSCORWKS!WAITFORFINALIZEREVENT" = $global:g_waitForFinalize;

"MSCORSVR!WAITFORFINALIZEREVENT" = $global:g_waitForFinalize;

"MSCORWKS!SVR::WAITFORFINALIZEREVENT" = $global:g_waitForFinalize;

"MSCORSVR!SVR::WAITFORFINALIZEREVENT" = $global:g_waitForFinalize;

"MSCORWKS!WKS::WAITFORFINALIZEREVENT" = $global:g_waitForFinalize;

"MSCORSVR!WKS::WAITFORFINALIZEREVENT" = $global:g_waitForFinalize;

"MSCORWKS!JITUNTIL_MONCONTENTION" = $global:g_tryingGetLock;

"MSCORSVR!JITUNTIL_MONCONTENTION" = $global:g_tryingGetLock;

"MSWSOCK!WSPRECV" = $global:g_winSockReceivingData;

"WS2_32!RECV" = $global:g_winSockReceivingData;

"MSCORSVR!GCHeap::FINALIZETHREADSTART" = $global:g_finalizerThread;

"MSCORWKS!GCHeap::FINALIZETHREADSTART" = $global:g_finalizerThread;

"MSCORSVR!DEBUGGERRCTHREAD::MAINLOOP" = $global:g_CLRDebuggerThread;

"MSCORWKS!DEBUGGERRCTHREAD::MAINLOOP" = $global:g_CLRDebuggerThread;

"W3DT!WP_CONTEXT::RUNMAINTHREADLOOP" = $global:g_w3wpMainThread;

"W3TP!THREAD_POOL_DATA::THREADPOOLTHREAD" = $global:g_threadPool;

"W3CORE!HTTP_COMPRESSION::COMPRESSIONTHREAD" = $global:g_CompressionThreaad;

"OLE32!CRPCCHANNELBUFFER::SENDRECEIVE2" = $global:g_COMcall;

"MSCORWKS!CLREVENT::WAIT" = $global:g_CLRWorkerThread;

"MSCORSVR!CLREVENT::WAIT" = $global:g_CLRWorkerThread;

"MSCORWKS!THREADPOOLMGR::COMPLETIONPORTTHREADSTART" = $global:g_completionPortIOThread;

"MSCORSVR!THREADPOOLMGR::COMPLETIONPORTTHREADSTART" = $global:g_completionPortIOThread;

"MSCORWKS!THREADPOOLMGR::GATETHREADSTART" = $global:g_gateThread;

"MSCORSVR!THREADPOOLMGR::GATETHREADSTART" = $global:g_gateThread;

"MSCORWKS!THREADPOOLMGR::TIMERTHREADSTART" = $global:g_timerThread;

"MSCORSVR!THREADPOOLMGR::TIMERTHREADSTART" = $global:g_timerThread;

"MSCORWKS!APPDOMAIN::ADUNLOADTHREADSTART" = $global:g_unloadAppDomainThread;

"MSCORSVR!APPDOMAIN::ADUNLOADTHREADSTART" = $global:g_unloadAppDomainThread;

"RPCRT4!THREADSTARTROUTINE" = $global:g_RPCWorkerThread;

"RPCRT4!COMMON_PROCESSCALLS" = $global:g_RPCWorkerThread;

"RPCRT4!LRPC_ADDRESS::RECEIVELOTSACALLS" = $global:g_LPRCWorkerThread;

"OLE32!CDLLHOST::STAWORKERLOOP" = $global:g_hostSTAThread

}

# The array has the right size to fit all threads.

$array = 1..($symbolsForEachThread.count - 1)

# Now we scan all threads and for each threads we scan all frames until a symbols mapped to the hash table is found

# or there are no more frames.

# Below, we need to discount the hast table "key" and "value" strings because they are considered one element.

for([System.Int32] $i = 0; $i -lt ($symbolsForEachThread.count - 1); $i++)

{

# The delimiter is converted to new line. Now it's easy to process each line.

$stack = $symbolsForEachThread[$i.ToString()].Replace($global:g_FrameDelimiter, "`n")

# Sets the default value.

$array[$i] = $global:g_unknownSymbol

# This is to be able to read each line.

$stringReader = [System.IO.StringReader] $stack

# Scan the symbols for each thread, line by line.

while(($frame = $stringReader.ReadLine()) -ne $null)

{

# Now we try to locate the symbol in our hash table.

# Always using uppercase.

if($symbols[$frame.ToUpper()] -ne $null)

{

# If symbol not located we don't assign $null to the array.

if($symbols[$frame.ToUpper()] -ne $null)

{

# If found we assign the constant to the array element.

$array[$i] = $symbols[$frame.ToUpper()]

}

}

}

}

# Force resources to be freed from memory.

$symbols = $null

$symbolsForEachThread = $null

return $array

}

########################################################################################################

# Function: Analyze-PowerDbgThreads

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Analyzes and displays what each thread is doing and the CPU time, sorted by User time.

# This cmdlet is very useful for hangs, high CPU and crashes scenarios.

#

# Attention! If you have a mini-dump with no thread information you may want to create

# a simplified cmdlet that doesn't use the information from !runaway.

# To do that just remove all parts of this script that use User and Kernel time. :)

# This script tries to use the CPU time because it's unlikely to have a dump that not have it.

#

# Changes History: 01/25/08 - Threads with unknown symbol appear in red color.

#

# Mike McIntyre

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Analyze-PowerDbgThreads()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Let's get User time.

Send-PowerDbgCommand "!runaway 1"

$hasCommandSucceeded = Has-PowerDbgCommandSucceeded

if($false -eq $hasCommandSucceeded)

{

throw "This dump has no threads information!"

}

# Gets the array of user mode time.

$arrayOfUserTime = Convert-PowerDbgRUNAWAYtoArray

# Let's get Kernel time.

# It's not necessary to validate the command output because we already did that.

Send-PowerDbgCommand "!runaway 2"

# Parses to get a hash table.

Parse-PowerDbgRUNAWAY

# Gets a hash table from the CSV file created with the cmdlet above.

$kernelTime = Convert-PowerDbgCSVtoHashTable

# Analyze the call stack for each thread to know what they are doing.

$arrayOfClassification = Classify-PowerDbgThreads

# Simple header.

write-Host "Threads sorted by User Time...`n" -foreground Green -background Black

write-Host "Thread Number`tUser Time`tKernel Time`t`tActivity" -foreground Green -background Black

# Displays information to user.

for([System.Int32] $i = 0; $i -lt ($arrayOfUserTime.Count / 2); $i++)

{

# If unknown activity, put it in red.

if($arrayOfClassification[$arrayOfUserTime[$i, 0]] -eq $global:g_unknownSymbol)

{

write-Host " " $arrayOfUserTime[$i, 0] "`t`t" $arrayOfUserTime[$i, 1] "`t" $kernelTime[$arrayOfUserTime[$i, 0]] "`t" $global:g_whatThreadIsDoing[$arrayOfClassification[$arrayOfUserTime[$i, 0]]] -foreground Red -background Black

}

else

{

write-Host " " $arrayOfUserTime[$i, 0] "`t`t" $arrayOfUserTime[$i, 1] "`t" $kernelTime[$arrayOfUserTime[$i, 0]] "`t" $global:g_whatThreadIsDoing[$arrayOfClassification[$arrayOfUserTime[$i, 0]]] -foreground Green -background Black

}

}

Send-PowerDbgComment "Analyze-PowerDbgThreads finished execution."

}

########################################################################################################

# Function: Parse-PowerDbgPRINTEXCEPTION

#

# Parameters: None.

#

# Return: Nothing.

#

# Purpose: Maps the output from the "!PrintException" command using a hash table. The output

# is saved into the file POWERDBG-PARSED.LOG

# All Parse functions should use the same outputfile.

# You can easily map the POWERDBG-PARSED.LOG to a hash table.

# Convert-PowerDbgCSVtoHashTable() does that.

#

# Note: For this version the fields being considered are:

# Exception object:

# Exception type:

# Message:

# InnerException:

# HResult:

#

# Changes History:

#

# Roberto Alexis Farah

# All my functions are provided "AS IS" with no warranties, and confer no rights.

########################################################################################################

function Parse-PowerDbgPRINTEXCEPTION()

{

set-psdebug -strict

$ErrorActionPreference = "stop"

trap {"Error message: $_"}

# Extract output removing commands.

$builder = New-Object System.Text.StringBuilder

# Title for the CSV fields.

$builder = $builder.AppendLine("key,value")

# \s+ --> Scans for one or more spaces.

# (\S+) --> Gets one or more chars/digits/numbers without spaces.

# \s+ --> Scans for one or more spaces.

# (\w+) --> Gets one or more chars.

# .+ --> Scans for one or more chars (any char except for new line).

# \: --> Scans for the ':' char.

# \s+ --> Scans for one or more spaces.

# (.+.) --> Gets the entire remainder string including the spaces.

foreach($line in $(get-content $global:g_fileCommandOutput))

{

if($line -match "((?<key>(^Exception object:)|(^Exception type:)|(^Message:)|(^InnerException:)|(^HResult:))\s+(?<value>.+))")

{

$builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])

}

}

# Send output to our default file.

out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"

}