[Note: According to Lee Holmes (one of the PowerShell creators) recommendation I changed the name convention. The images were not updated.]
Sometime ago a colleague of mine, Vandy Rodrigues, from the Messaging team, was enthusiastic to tell me about PowerShell and why I should learn it.
I must admit to my readers that my reaction was skeptical: Why do I need to learn yet another script language?
However, after the first demonstration, I fell in love with PowerShell.
After reading some PowerShell books, I decided to create my first PowerShell code. I thought about the subject and had some ideas. The idea I decided to implement is a PowerShell script that interacts with WinDbg as a replacement for the WinDbg programming language.
Thus, in this article, I introduce you to the PowerDbg library!
The initial idea was to interact with DBGENG.DLL that is part of WinDbg. I discarded this possibility for several reasons, among them limited documentation of DBGENG.DLL.
Then I considered MDBENG.DLL, which is a wrapper for DBGENG.DLL. This is the approach I wanted to use; unfortunately this dll is for internal use only.
In the future, when MDBENG.DLL becomes public, I’ll use it.
Therefore, I chose a third approach, one using the same tools that my readers and customers can use: WSH – Windows Script Host.
PowerDbg is composed of functions that basically do this:
- Send commands to WinDbg.
- Extract the output from commands sent to WinDbg.
OVERVIEW
The PowerDbg functions use 3 files:
POWERDBG.LOG ß Created where WinDbg is running.
POWERDBG-OUTPUT.LOG ß Created where the functions/scripts are called.
POWERDBG-PARSED.LOG ß Created where the functions/scripts are called.
POWERDBG.LOG is the output from commands sent to WinDbg. The parser functions extract information from this file.
POWERDBG-OUTPUT.LOG is the summarized command output. It has just the relevant data. The parser family of functions uses this file.
POWERDBG-PARSED.LOG is created by the parser functions that read the content from POWERDBG-OUTPUT.LOG and create a CSV file, which is the POWERDBG-PARSED.LOG.
The CSV file can be mapped to a hash table, and there’s a specific PowerDbg function that converts this CSV file into a hash table.
The commands are sent to WinDbg using Send-PowerDbgCommand.
This function sends commands to the first WinDbg instance that has the title PowerDbg.
If the WinDbg is not running you can use Start-PowerDbgWinDbg. This function automatically changes the WinDbg title to PowerDbg.
Note: PowerDbg requires a WinDbg instance using the title PowerDbg. If you have an opened WinDbg instance you can change the title by using:
.wtitle PowerDbg
Parse-PowerDbgCOMMANDNAME – These functions extract data from a specific command. There are several parser functions, and I continue to create more.
Some of them are:
Parse-PowerDbgLMI
Parse-PowerDbgDUMPMODULE
Parse-PowerDbgDUMPMD
Parse-PowerDbgDT
To send commands to WinDbg:
Send-PowerDbgDML - Sends commands using DML, in other words, hyperlinks. It uses the function below.
Send-PowerDbgCommand - Uses WMI to communicate with WinDbg. Each command and its output are saved into a log file that is constantly rewritten.
To convert a CSV file into a hash table:
Convert-PowerDbgCSVtoHashTable – Converts a CSV file into a hash table.
To start WinDbg, if there’s no instance running, you can use:
Start-PowerDbgWinDbg
With PowerDbg we can create PowerShell scripts that send commands to WinDbg and identify the output from these commands.
If you want to create your own parser functions, you just need to use mine as templates.
Example #1 - Starting Windbg from PowerShell:
Start-PowerDbgWinDbg "c:\debuggers32bit\windbg.exe" "c:\dumps\dumptest.dmp" "SRV*c:\PUBLICsymbols*http://msdl.microsoft.com/download/symbols"
Example #2 - Using current Windbg instance:
Changes Windbg title:
Send-PowerDbgDML "Display all Threads and TEB " "~* e !teb;kL 1000"
Clicking over the hyperlink above:
This is the source code for PowerDbg (save it into your $profile):
########################################################################################################
# Global variables.
$global:g_instance = $null
$global:g_fileParsedOutput = "POWERDBG-PARSED.LOG"
$global:g_fileCommandOutput = "POWERDBG-OUTPUT.LOG"
# 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
$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.
function Send-PowerDbgCommand([string] $command = $(throw "Error! You must provide a Windbg command."))
# 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)
# 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")
# 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
$aux = $aux.MainModule.Filename
$aux = [System.IO.Path]::GetDirectoryName($aux)
# Create log.
$output = $global:g_instance.SendKeys(".logopen $aux\POWERDBG.LOG")
$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)
# Close log, saving last command and its output.
$output = $global:g_instance.SendKeys(".logclose")
# 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.
# 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.
function Parse-PowerDbgDT([switch] $useFieldNames)
# 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.
# (\w+) --> Gets one or more chars.
# .+ --> Scans for one or more chars (any char except for new line).
# \: --> Scans for the ':' char.
# (.+.) --> 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"] + "," + $matches["value"])
if($line -match "(?<key>0x\S+).+\:\s+(?<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.
function Convert-PowerDbgCSVtoHashTable()
$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.
# 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.
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.")
Send-PowerDbgCommand ".printf /D `"<link cmd=\`"$commandDML\`"><b>$hyperlinkDML</b></link>\n\`"`""
# Function: Parse-PowerDbgNAME2EE
# Purpose: Maps the output from the "!name2ee" command using a hash table. The output
function Parse-PowerDbgNAME2EE()
# 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+)")
# Function: Parse-PowerDbgDUMPMD
# Purpose: Maps the output from the "!dumpmd" command using a hash table. The output
function Parse-PowerDbgDUMPMD()
if($line -match "(?<key>((^Method Name :)|(^MethodTable)|(^Module:)|(^mdToken:)|(^Flags :)|(^Method VA :)))\s+(?<value>\S+)")
# Function: Parse-PowerDbgDUMPMODULE
# Purpose: Maps the output from the "!dumpmodule" command using a hash table. The output
function Parse-PowerDbgDUMPMODULE()
[int] $countFields = 0
# 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+)")
$countFields++
# If nothing was found, let's try to use the .NET Framework 1.1 fields.
if($countFields -lt 3)
if($line -match "(?<key>((^dwFlags)|(^Assembly\*)|(^LoaderHeap\*)|(^TypeDefToMethodTableMap\*)|(^TypeRefToMethodTableMap\*)|(^MethodDefToDescMap\*)|(^FieldDefToDescMap\*)|(^MemberRefToDescMap\*)|(^FileReferencesMap\*)|(^AssemblyReferencesMap\*)|(^MetaData starts at)))\s+(?<value>\S+)")
$hasFound = $true
# Function: Parse-PowerDbgLMI
# Purpose: Maps the output from the "!lmi" command using a hash table. The output
function Parse-PowerDbgLMI()
if($line -match "(?<key>((^.+\:)))\s+(?<value>\S+)")
$strNoLeftSpaces = $matches["key"]
$strNoLeftSpaces = $strNoLeftSpaces.TrimStart()
$builder = $builder.AppendLine($strNoLeftSpaces + "," + $matches["value"])
# Function: Has-PowerDbgCommandSucceeded
# Return: Return $true if the last command succeeded or $false if not.
# Purpose: Return $true if the last command succeeded or $false if not.
function Has-PowerDbgCommandSucceeded
if($line -imatch "(Fail) | (Failed) | (Error) | (Invalid)")
return $false
return $true
# Function: Send-PowerDbgComment
# Parameters: [string] $comment
# Comment to be sent to the debugger.
# Purpose: Sends a bold comment to the debugger. Uses DML.
function Send-PowerDbgComment(
[string] $comment = $(throw "Error! You must provide a comment.")
Send-PowerDbgCommand ".printf /D `"\n<b>$comment</b>\n\n\`"`""