WinDbg.psm1

################################################################################
##
## WinDbg.psm1
## Automate WinDbg with PowerShell scripting
##
## To use:
## In WinDbg, set up a server: .server tcp:Port=10456
##
## In PowerShell
##
## Import-Module windbg
## Connect-DbgSession -remote "tcp:Port=10456,Server=<server>"
## Other remote connection strings / protocols work, too.
##
## For local kernel debugging:
##
## Import-Module Windbg -ArgumentList 'C:\<path>\kd.exe'
## Connect-DbgSession -ArgumentList "-kl"
## Invoke-DbgCommand "!process"
##
## Cleaning up:
## Disconnect-DbgSession
##
################################################################################

param($CdbPath =
    @("C:\Program Files\Debugging Tools for Windows (x86)\cdb.exe","C:\Debuggers\cdb.exe","C:\Program Files (x86)\Windows Kits\*\Debuggers\x64\cdb.exe")
)

$SCRIPT:windbgProcess = $null
$SCRIPT:currentConnection = $null
$SCRIPT:debugee = 0

$env:PATH += ";$psScriptRoot"

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    Disconnect-DbgSession
    $env:PATH = $env:PATH.Replace(";$psScriptRoot", "")
}

## Debug a process
function New-DbgSession
{
    param(
        [Parameter(Mandatory)]
        $Id
    )

    $SCRIPT:debugee = $Id
    Connect-DbgSession -ArgumentList "-p $Id"
}

## Connect to a windbg remote session
function Connect-DbgSession
{
    [CmdletBinding(DefaultParameterSetName = "Remote")]
    param(
        [Parameter(ParameterSetName = "Remote")]
        $Remote = "tcp:Port=10456,Server=$($ENV:Computername)",

        [Parameter(ParameterSetName = "ArgumentList", Mandatory = $true)]
        $ArgumentList
    )
        
    if($SCRIPT:currentConnection -and ($SCRIPT:currentConnection -ne $remote))
    {
        throw "Already connected to $remote. Use Disconnect-DbgSession, " + 
            "then connect to another instance."
    }
    
    ## Launch cdb.exe, the command-line version of WinDbg.
    ## Take control of its input and output streams, which we'll use
    ## to capture commands and their output.
    if(-not $SCRIPT:currentConnection)
    {
        foreach($realPath in @($cdbPath))
        {
            if($resolved = Get-Command $realPath -ErrorAction Ignore)
            {
                $cdbPath = $resolved.Source
                break
            }
        }
        
        $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo
        $processStartInfo.FileName = $cdbPath
        $processStartInfo.WorkingDirectory = (Get-Location).Path

        if($PSBoundParameters.ContainsKey("Remote"))
        {
                $processStartInfo.Arguments = "-remote $remote"
        }
        else
        {
                $processStartInfo.Arguments = $argumentList
        }
        
        $processStartInfo.UseShellExecute = $false
        $processStartInfo.RedirectStandardInput = $true
        $processStartInfo.RedirectStandardOutput = $true 

        $SCRIPT:windbgProcess = 
            [System.Diagnostics.Process]::Start($processStartInfo)
        $SCRIPT:currentConnection = $remote

        if(-not $SCRIPT:windbgProcess)
        {
            return
        }
        
        ## Ignore the stuff that was in the session before we
        ## connected
       $null = Invoke-DbgCommand "!loadby sos clr"
       $null = Receive-DbgOutput
    }
}

## Detatch / stop debugging
function Disconnect-DbgSession
{
    if($SCRIPT:windbgProcess -and (-not $SCRIPT:windbgProcess.HasExited))
    {
        $SCRIPT:windbgProcess.StandardOutput.Close()
        $SCRIPT:windbgProcess.StandardInput.Close()
        $SCRIPT:windbgProcess.Kill()
    }
    
    $SCRIPT:currentConnection = $null
    $SCRIPT:windbgProcess = $null
}

## Stop debugging and kill the process
function Stop-DbgSession
{
    if($SCRIPT:windbgProcess -and (-not $SCRIPT:windbgProcess.HasExited))
    {
        $SCRIPT:windbgProcess.StandardOutput.Close()
        $SCRIPT:windbgProcess.StandardInput.Close()
        $SCRIPT:windbgProcess.Kill()
    }
    
    $SCRIPT:currentConnection = $null
    $SCRIPT:windbgProcess = $null

    if($SCRIPT:debugee)
    {
        Stop-Process -Id $SCRIPT:debugee -ErrorAction Ignore
    }
}

## Invoke a command in the connected WinDbg session, and return
## its output
function Invoke-DbgCommand
{
    if(-not $SCRIPT:windbgProcess)
    {
        throw "Not connected. Use Connect-DbgSession to connect to an " +
            "instance of WinDbg."
    }
    
    ## Hack - '?' causes a request for keyboard input. Intercept that.
    if($args -eq "?")
    {
        Get-DbgHelp
        return
    }

    $SCRIPT:windbgProcess.StandardInput.WriteLine("$args")
    $SCRIPT:windbgProcess.StandardInput.Flush()
    
    ## The 'g' command restarts the program, so we don't want to wait for our
    ## output.
    if(($args -ne "g") -and ($args -ne "♠"))
    {
        $output = Receive-DbgOutput

        ## Handle SOS errors
        if($output -match 'c0000005 Exception in')
        {
            Invoke-DbgCommand ($args -join " ")
        }
        else
        {
            $output
        }
    }
}

## Retrieve pending output from the connected WinDbg session
function Receive-DbgOutput
{
    ## Add a special tag so that we know the end of the command
    ## response
    $sent = "PSWINDBG_COMPLETE_{0:o}" -f [DateTime]::Now        
    $SCRIPT:windbgProcess.StandardInput.WriteLine(".echo $sent")
    $SCRIPT:windbgProcess.StandardInput.Flush()
    
    $received = New-Object System.Text.StringBuilder

    ## Wait for the response to end
    while($received.ToString().IndexOf($sent) -lt 0)
    {
        $null = $received.AppendLine($SCRIPT:windbgProcess.StandardOutput.ReadLine())
    }
    $output = $received.ToString()

    ## remove leading prompt if exists
    if($output -match "^\w+:\w+>")
    {
        ## remove "X:YYY> "
        $output = $output.Replace($matches[0], "")
    }

    ## remove trailing output
    $sentIndex = $output.IndexOf($sent)
    $lastNewline = $output.LastIndexOf("`r`n", $sentIndex)
    
    ## if there are no newlines before the end token, then there was no output
    if($lastNewline -gt 0)
    {
        $results = $output.SubString(0, $lastNewline) -split "`r`n" | Foreach-Object Trim
        $results
    }
}

function Suspend-DbgSession
{
    <#
     
    .SYNOPSIS
     
    Breaks into the currently running application.
     
    #>


    $member = @'
        [DllImport("kernel32.dll")]
        public static extern bool SetConsoleCtrlHandler(
            IntPtr handlerRoutive,
            bool add);
'@


    $type = Add-Type -MemberDefinition $member -Name ConsoleControl -Passthru
    $null = $type::SetConsoleCtrlHandler([IntPtr]::Zero, $true)
}

function Resume-DbgSession
{
    <#
     
    .SYNOPSIS
     
    Resumes an application being debugged
     
    #>


    Invoke-DbgCommand g
}

function Get-DbgHelp
{
    @'
B[C|D|E][<bps>] - clear/disable/enable breakpoint(s)
BL - list breakpoints
BA <access> <size> <addr> - set processor breakpoint
BP <address> - set soft breakpoint
D[type][<range>] - dump memory
DT [-n|y] [[mod!]name] [[-n|y]fields]
   [address] [-l list] [-a[]|c|i|o|r[#]|v] - dump using type information
DV [<name>] - dump local variables
E[type] <address> [<values>] - enter memory values
G[H|N] [=<address> [<address>...]] - go
K <count> - stacktrace
KP <count> - stacktrace with source arguments
LM[k|l|u|v] - list modules
LN <expr> - list nearest symbols
P [=<addr>] [<value>] - step over
Q - quit
R [[<reg> [= <expr>]]] - view or set registers
S[<opts>] <range> <values> - search memory
SX [{e|d|i|n} [-c "Cmd1"] [-c2 "Cmd2"] [-h] {Exception|Event|*}] - event filter
T [=<address>] [<expr>] - trace into
U [<range>] - unassemble
version - show debuggee and debugger version
X [<*|module>!]<*|symbol> - view symbols
? <expr> - display expression
?? <expr> - display C++ expression
$< <filename> - take input from a command file
 
<expr> unary ops: + - not by wo dwo qwo poi hi low
       binary ops: + - * / mod(%) and(&) xor(^) or(|)
       comparisons: == (=) < > !=
       operands: number in current radix, public symbol, <reg>
<type> : b (byte), w (word), d[s] (doubleword [with symbols]),
         a (ascii), c (dword and Char), u (unicode), l (list)
         f (float), D (double), s|S (ascii/unicode string)
         q (quadword)
<pattern> : [(nt | <dll-name>)!]<var-name> (<var-name> can include ? and *)
<range> : <address> <address>
        : <address> L <count>
 
User-mode options:
~ - list threads status
~#s - set default thread
| - list processes status
|#s - set default process
 
x86 options:
DG <selector> - dump selector
<reg> : [e]ax, [e]bx, [e]cx, [e]dx, [e]si, [e]di, [e]bp, [e]sp, [e]ip, [e]fl,
        al, ah, bl, bh, cl, ch, dl, dh, cs, ds, es, fs, gs, ss
        dr0, dr1, dr2, dr3, dr6, dr7
        fpcw, fpsw, fptw, st0-st7, mm0-mm7
         xmm0-xmm7
<flag> : iopl, of, df, if, tf, sf, zf, af, pf, cf
<addr> : #<16-bit protect-mode [seg:]address>,
         &<V86-mode [seg:]address>
     
'@

}

Set-Alias dbg Invoke-DbgCommand
Export-ModuleMember -Alias *
Export-ModuleMember -Function *