Sunday, October 09, 2011

What you see vs what you get : matching source and IL with Mono.Cecil and PowerShell

Following on from yesterday, looking at how Mono.Cecil pairs up debug information in the .pdb file with just the sequence points the IL -- which follows the approach taken by coverage tools such as dot-net-coverage. The code is much the same as before:

<#
.SYNOPSIS
This script inspects a method by introspection.
.DESCRIPTION
This script uses Mono.Cecil introspection to tie instructions with source.
It loads the referenced assemblies into the current AppDomain, so should be run in a
separate process to your main script
.NOTES
File Name : Inspect-MethodWithMonoCecil.ps1
Requires : PowerShell Version 2.0 (3.0 or alternative launcher for .net 4/FxCop 10.0)
.EXAMPLE
.\Inspect-MethodWithMonoCecil.ps1 -CecilPath ...\Mono.Cecil -AssemblyPath ...\Tinesware.Rules.dll
-ClassName Tinesware.Rules.SpellCheckRule
-MethodName VisitField | ? { $_.Context -ne "<None>" } | Out-GridView
.PARAMETER CecilPath
The path to the directory where Mono.Cecil.*.dll have been installed
.PARAMETER AssemblyPath
The path to the assembly to inspect
.PARAMETER ClassName
The name of the class containg the Method. Nested types are separated with + e.g.
Project.Namespace.Type+NestedType
.PARAMETER MethodName
The name of the method to inspect
#>
param (
[Parameter(Mandatory = $true)] [string] $CecilPath,
[Parameter(Mandatory = $true)] [string] $AssemblyPath,
[Parameter(Mandatory = $true)] [string] $ClassName,
[Parameter(Mandatory = $true)] [string] $MethodName,
[switch] $Help)
if ($help -or (-not (Test-Path $CecilPath -PathType Container)) -or
(-not (Test-Path $AssemblyPath -PathType Leaf)) -or
(-not (Test-Path $(Join-Path -Path $CecilPath -ChildPath 'Mono.Cecil.dll') -PathType Leaf)))
{
Get-Help $MyInvocation.MyCommand.Definition
return
}
Add-Type -Path $(Join-Path -Path $CecilPath -ChildPath 'Mono.Cecil.dll')
Add-Type -Path $(Join-Path -Path $CecilPath -ChildPath 'Mono.Cecil.Pdb.dll')
# load assembly with debug symbols
# We could reflect into $assembly to get the built-in symbol path, which is messy
# In most case it's safe to assume the files are co-located
$assembly = [Mono.Cecil.AssemblyDefinition]::ReadAssembly($AssemblyPath)
$symbolPath = [System.IO.Path]::ChangeExtension($assembly.MainModule.FullyQualifiedName, ".pdb")
$provider = New-Object Mono.Cecil.Pdb.PdbReaderProvider
$reader = $provider.GetSymbolReader($assembly.MainModule, $symbolPath)
$assembly.MainModule.ReadSymbols($reader)
# load the type
$splitnames = @($ClassName.Split('+'))
$types = @($assembly.Modules | % { $_.Types })
$type = $types | ? { $_.FullName -eq $splitnames[0] } | Select-Object -First 1
# handle nested types TODO
if (($splitnames.Length -gt 1) -and $type) {
$splitnames[1..$($splitnames.Length-1)] | % {
$name = $_
if ($type) { $type = $type.NestedTypes | ? { $_.Name -eq $name } | Select-Object -First 1 }
}
}
if (-not $type) {
Write-Error "Class named '$ClassName' not found" -Category ObjectNotFound -TargetObject $ClassName
return
}
# load the method, being ready to resolve overloads
$methods = @($type.Methods | ? { $_.Name -eq $MethodName })
if (-not $methods) {
Write-Error "Method named '$MethodName' not found" -Category ObjectNotFound -TargetObject "$ClassName.$MethodName"
return
}
# User choice adapted from http://blogs.technet.com/b/jamesone/archive/2009/06/24/how-to-get-user-input-more-nicely-in-powershell.aspx
Function Select-Item
{
Param( [String]$Caption="Please make a selection",
[String]$Message="Choices are presented below",
[String[]]$choiceList,
[int]$default=0)
$choicedesc = New-Object System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]
$choiceList | foreach { $choicedesc.Add((New-Object "System.Management.Automation.Host.ChoiceDescription" -ArgumentList $_))}
$Host.ui.PromptForChoice($caption, $message, $choicedesc, $default)
}
$index = 0
if ($methods.Length -gt 1) {
$choices = @($methods | % {
$parameters = @($_.Parameters | % { "$($_.ParameterType) $($_.Name)" } )
$parameters = $parameters -join ", "
[string] "&$($index)$([char]8)$($_.Name)($Parameters)"
$index += 1
})
$index = Select-Item "Choose which overload to use" "The various overloads are:" $choices 0
}
# Overloads resolved, get the source
$method = $methods[$index]
$source = $method.Body.Instructions | % { $_.SequencePoint } | ? { $_ } | % { $_.Document } | ? { $_ } | % { $_.Url } | ? { $_ } | Select-Object -First 1
$sourceListing = Get-Content $source
# Source context to code snippet
Function Extract-Snippet ([Mono.Cecil.Cil.SequencePoint] $context)
{
if ( (-not $context.Document) -or (-not $context.Document.Url) -or (-not $context.EndLine) -or ($context.StartLine -ge $sourceListing.Length)) {
"<None>"
} else {
$snippet = @($sourceListing[$($context.StartLine-1)..$($context.EndLine-1)])
$snippet[0] = (" " * ($context.StartColumn-1)) + $snippet[0].SubString($context.StartColumn-1)
$snippet[-1] = $snippet[-1].SubString(0, $context.EndColumn-1)
}
}
## Report Instructions
$method.Body.Instructions | % {
$sequencePoint = @($_.SequencePoint.StartLine, $_.SequencePoint.StartColumn, $_.SequencePoint.EndLine, $_.SequencePoint.EndColumn)
if (-not $_.SequencePoint.EndLine) { $sequencePoint = "null" }
if ($_.SequencePoint.StartLine -ge $sourceListing.Length) { $sequencePoint = "0xfeefee" }
New-Object PSObject -Property @{
OpCode = $_.OpCode;
Context = [System.String]::Join("`r`n", (Extract-Snippet $_.SequencePoint));
Details = $_;
SequencePoint = $sequencePoint
}
}
view raw gistfile1.ps1 hosted with ❤ by GitHub

with the addition of the line/column data in SequencePoint ("null" if the values are not given, "0xfeefee" for the well-known compiler generated fake line number).


No comments :