[PowerShell Script] PowerDbg v2.3 - Using PowerShell to Control WinDbg
It has been a while since my previous blog post. I’ve been busy with other tasks, including the new PowerDbg version 2.3.
There’s a lot of new stuff on this new version and my estimate is that about 80% of the most used Windbg commands have been covered so far. J
Let me present the new cmdlets:
Parse-PowerDbgVERTARGET
Parses the vertarget command, extracting User mode time and Kernel mode time.
Parse-PowerDbgRUNAWAY
Parses the !runaway 1 or !runaway 2.
Convert-PowerDbgRUNAWAYtoArray
Converts the output from !runaway 1 or !runaway 2 to a two dimension array with the elements in the same order as the command output. This means that item[0,0] shows the thread consuming more CPU time and item[0,1] shows the CPU time.
Parse-PowerDbgK
Parses the output from the k command, including the k command variations. The exception is kPn because kPn breaks the arguments into different lines.
The hash table for this cmdlet contains the thread number and the complete call stack for each thread.
Parse-PowerDbgSymbolsFromK
Parses the output from the k command, including the k command variations. The exception is kPn because kPn breaks the arguments into different lines.
The hash table for this parser contains the thread number and the symbols for each stack.
Parse-PowerDbgLM1M
Parses the output of lm1m command. It’s useful for finding the modules that are part of the process.
Classify-PowerDbgThreads
This cmdlet analyzes all threads and returns an array in which each item has a constant value. The value indicates what the thread is doing according to these constants:
$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
. . .
. . .
. . .
Great news! It’s very easy to add more symbols and improve the power of this cmdlet!
It works for native and managed codes.
Analyze-PowerDbgThreads
This may be the PowerDbg cmdlet that you will use most of the time. This cmdlet uses some of the cmdlets above to analyze the kind of work each thread is doing, then it displays the results sorted by User time.
This cmdlet is very useful for hangs, crashes, and high cpu scenarios! At any point, during your live or post-mortem debugging, you can run this command and understand what each thread is doing and the corresponding User and Kernel time.
It works for native and managed code.
With all cmdlets the PowerDbg library has so far, it will be easier for me and for you to build the next script.
Look, for example, at how Analyze-PowerDbgThreads works:
Attention! It’s better to use .reload /f instead of .reload to avoid delays when executing the k command and its variations. This delay may cause errors because the script may continue the execution while the WinDbg command is still running.
This is the source code for PowerDbg v2.0 (save it into your $profile).
It doesn’t look formatted, but it is. After copying and pasting it you will see. J
########################################################################################################
# PowerDbg v 2.3
#
# 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.
#
# 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.
#
# 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 {"Error message: $_"}
# 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
}
# 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}")
# Close log, saving last command and its output.
$output = $global:g_instance.SendKeys(".logclose")
$output = $global:g_instance.SendKeys("{ENTER}")
# A delay is required here.
start-sleep 3
# Extract output removing commands.
$builder = New-Object System.Text.StringBuilder
get-content "$aux\POWERDBG.LOG" | foreach-object {if(($_ -notmatch "^.:...>") -and ($_ -notmatch "^Opened log file") -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"
# Forces a delay because it may take time to get the stacks.
start-sleep 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."
}
If you find a bug hidden in this new PowerDbg version, let me know.
I hope you enjoy using it to create your own scripts!
Comments
Anonymous
December 21, 2007
I just fixed two bugs. This new version is 2.1Anonymous
December 31, 2007
Another bug fixed. This new version is 2.2Anonymous
January 17, 2008
For the latest powershell-script, 1)Can I download from somewhere ? Or do I have to copy and paste ? 2)Can you tell me the environment requirements for using this library ? Keep up the good work. ThanksAnonymous
January 17, 2008
Hi nativecpp, 1- For now you cannot download it. You have to copy and paste it. If you use MS Word and save the file as text file, it should not add extra spaces between lines. This approach works fine. 2- The environment is: PowerShell v 1.0 .NET Framework 2.0 Debugging Tools For Windows Attention to that: a) The PowerDbg library should be in your $profile file. From PS if you type notepad $profile you'll be able to now the location of this file and the name. You'll need to create it if it doesn't exist. In my case the code for the PowerDbg library is here: C:UsersrafarahDocumentsWindowsPowerShellMicrosoft.PowerShell_profile.ps1 So, my Microsoft.PowerShell_profile.ps1 has the code posted in this blog article. Because the PowerDbg is into the $profile, you can call the commandlets from any place. b) After updating the $profile you need to close and reopen PS again. Doing that, the PowerShell will recognize the updates. c) The Windbg window must have its title changed to PowerDbg. Rememember: because PowerDbg simulates a user using WinDbg, you cannot change the windows focus during the time it is interacting with WinDbg. If you do that the PowerDbg and PowerDbg scripts won't be able to "talk" with WinDbg. d) If you get errors, most of the time they are related to timing issues. To solve that you just need to run the script again after making sure the WinDbg stopped to process the command or increase the start-sleep delay. It usually happens when you send a "k" command from Send-PowerDbgCommand and it takes too much time to solve the symbols. Anyway, it not happens that often. If you have any other questions, doubts or problems, let me know! I'll be happy to help you because I want my readers to have a good experience using my little toys. :) Any suggestions for future cmdlets or scrips are welcome.Anonymous
January 24, 2008
Version 2.3 - My co-worker Cristhian Uribe asked me to put in red the threads doing unknown activity when running Analyze-PowerDbgThreads and I added two more API calls based on his feedback.Anonymous
January 31, 2008
Sorry for being dumb! But i dont see the source code? Can you re-post it please.Anonymous
January 31, 2008
Hi solidstore! You are not dumb, I am! :) For some reason I changed the code and didn't paste it. I'm going to do that between today and tomorrow. Thanks for letting me know!Anonymous
February 22, 2008
I added a function to return the results of ~*e !clrstack -p The output is something like this: 0x1184 82 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x1360 175 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x228 33 {} 0x928 201 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x12bc 91 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x11a8 105 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x894 202 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x11bc 49 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x10a4 68 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0xf20 75 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0xde8 57 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x1468 185 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x1474 130 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x17c4 120 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... 0x7c8 34 {SomeNameSpace.SomeManagedFunction, SomeNameSpace... Which is useful for me, because I can then use it like this: $res = Analyze-PowerDbgClrThreads $res.keys | %{$res[$_]."SomeNameSpace.SomeManagedFunction".this} Which gives me a list of object addresses like this: 0x1dbcb630 0x1e2f2c78 0x0927793c 0x0a302de4 0x0a280308 ########################################################################################################
Function: Analyze-PowerDbgClrThreads
Parameters: None.
Return: hash by thread id by method name of hashtables
Purpose:
Changes History: 01/25/08 - Threads with unknown symbol appear in red color.
Ed Fancher
All my functions are provided "AS IS" with no warranties, and confer no rights.
######################################################################################################## function Analyze-PowerDbgClrThreads() { $ThreadRegEx = "OSs+Threads+Id:s+(0x[0-9a-fA-F]+)s((d+))" $ParameterLineRegEx = "s+(w+)s+=s+(.*)" #a real regex, since $matches was behaving funny. $FunctionRegEx = [regex] "([0-9a-fA-F]+)s+([0-9a-fA-F]+)s+(?<fname>[^(]+)" $FrameRegex = "([0-9a-fA-F]+)s+([0-9a-fA-F]+)s+[[A-Fa-f0-9: ]+]" Send-PowerDbgCommand "~*e !clrstack -p" sleep 120 #get the log written out by powerdbg. write-host $global:g_fileCommandOutput $pdl = get-content $global:g_fileCommandOutput $currentThread = ""; $currentFunction = ""; $threads = @{}; return; #loop through each line for($i=0; $i -lt $pdl.length;$i++) { #if this matches, we've seen a thread. if ($pdl[$i] -match $ThreadRegEx) { $currentThread = $matches[1] + " " + $matches[2]; $threads[$currentThread] = @{}; } if ($currentThread -eq "") { continue; } #ignore frames, since I'm not usually interested in them. if ($pdl[$i] -match $FrameRegex) { continue; } #if this matches, we have a function so add it onto the last found thread. $fmatch = $FunctionRegEx.Match($pdl[$i]); if ($fmatch.success) { $currentFunction = $fmatch.Groups["fname"].Value; $threads[$currentThread][$currentFunction] = @{}; } if ($currentFunction -eq "") { continue; } #if there are any parameters add them on the the last found method. if ($pdl[$i] -match $ParameterLineRegEx) { $threads[$currentThread][$currentFunction][$matches[1]] = $matches[2]; } } $threads; }
- Anonymous
February 22, 2008
Hey Ed, congratulations! This is an awesome cmdlet! :) Thanks for sharing with us!