classes/Assembler.ps1
|
class Assembler { [UInt16]$loadAddress [SymbolManager]$symbolManager [System.Collections.ArrayList]$assembly [string]$psSource [byte[]]$binary [string]$binaryHash [int]$MaxPasses = 25 [int]$CurrentPass = 0 [bool]$NoHostOutput [SemanticParser]$parser [Scope[]]$scopes [hashtable]$Macros = [ordered] @{0 = [ordered] @{ BRA = {param($addr)jmp $addr}}} # [ScopeID][Name] = [ScriptBlock] [InputFileStack]$FileStack [HashTable]$SourceLines [SegmentManager]$Segments Assembler() { $this.Init() } hidden [void]Init() { $this.loadAddress = 0x0000 $this.assembly = [System.Collections.ArrayList]@() $this.FileStack = [InputFileStack]::new() $this.SourceLines = @{} $this.Segments = [SegmentManager]::new() } [void] BuildSourceLines() { foreach ($ctx in $this.FileStack.AllContexts) { $key = $ctx.FilePath # already normalized full path if (-not $this.SourceLines.ContainsKey($key)) { # Store the lines only once, ignore duplicates $this.SourceLines[$key] = $ctx.Content ` -replace "`r`n", "`n" ` -replace "`r", "`n" ` -replace ([char]0x2028), "`n" ` -replace ([char]0x2029), "`n" ` -split("`n") } } } [void] LoadFile([string]$filePath) { $this.FileStack.PushFile($filePath) } [void] LoadVirtualFile([string]$virtualName, [string]$sourceCode) { $this.FileStack.PushVirtualFile($virtualName, $sourceCode) } [void]AddLine([byte[]]$bytes, [string]$invocationFile, [int]$invocationLine) { # Write-Host "`$invocation.Line: $($invocationLine)" # Write-Host "File: $($invocationFile)" # Write-Host "Line: $($invocationLine)" # Write-Host "Source: $(($map = $this.parser.LineMap[$invocation.Line]) ? $this.SourceLines[$map.File][$map.Line - 1] : "<nullllll>")" # Write-Host "Source: $($this.SourceLines[$invocationFile]?[$invocationLine - 1] ?? "<nullllll>")" $this.assembly.Add([AssemblyLine]::new( $this.Segments.Current.Name, $this.Segments.Current.PC, $bytes, $invocationLine, 0, $this.SourceLines[$invocationFile]?[$invocationLine - 1] ?? "<null>", "<no psSourceLine>", $invocationFile )) $this.Segments.Emit($bytes) } [void]OpAdd([byte]$OpCode, [string]$invocationFile, [int]$invocationLine) { $this.AddLine(@($OpCode), $invocationFile, $invocationLine) } [void]OpAdd([byte]$OpCode, [byte]$Operand, [string]$invocationFile, [int]$invocationLine) { $this.AddLine(@($OpCode,$Operand), $invocationFile, $invocationLine) } [void]OpAdd([byte]$OpCode, [UInt16]$Operand, [string]$invocationFile, [int]$invocationLine) { $this.AddLine(@($OpCode,(_loByte $Operand),(_hiByte $Operand)), $invocationFile, $invocationLine) } [void]DataAdd([byte[]]$data, [string]$invocationFile, [int]$invocationLine) { $this.AddLine($data, $invocationFile, $invocationLine) } [void]DataAdd([UInt16[]]$data, [string]$invocationFile, [int]$invocationLine) { $this.AddLine([byte[]]($data | ForEach-Object{_loByte $_;_hiByte $_}), $invocationFile, $invocationLine) } [void]Parse() { $this.parser = [SemanticParser]::new($this.FileStack) $this.BuildSourceLines() $this.FileStack.Dispose() $this.psSource = $this.parser.outTokens.value -join '' $this.scopes = $this.parser.scopeManager.scopes $this.symbolManager = $this.parser.symbolManager $this.symbolManager.CurrentPass = $this.CurrentPass $this.symbolManager.scopes = $this.scopes } [void]Assemble() { $success=$false $bin=[System.Collections.Generic.List[byte]]::new() for ($i=1; $i -le $this.MaxPasses -and $success -ne $true; $i++) { $this.CurrentPass = $i $this.symbolManager.CurrentPass = $i if(-not $this.NoHostOutput) { Write-Host "Pass $($i)..." -NoNewline } $this.assembly.Clear() $this.Segments.Reset() $error.Clear() $psError=$null try { $sb = [ScriptBlock]::Create(($this.psSource | out-string)) } catch { if(-not $this.NoHostOutput) { Write-Host " FAILED!" # Write-Host "Error in parsing generated PowerShell source code!" } throw } try { Invoke-Command -ScriptBlock $sb -ErrorAction Stop -ErrorVariable psError #-NoNewScope } catch { if ($psError) { if(-not $this.NoHostOutput) { Write-Host " FAILED!" # Write-Host "Error in executing generated PowerShell source code!" } } throw } ### Solve segment layout $this.Segments.SolveLayout() ### Build binary - BuildBinary() must be run to populate Segments.LowestAddress $binaryData = $this.Segments.BuildBinary() $this.loadAddress = $this.Segments.LowestAddress $bin.Clear() $bin.Add(([byte]($this.loadAddress -band 255))) $bin.Add(([byte](($this.loadAddress -shr 8) -band 255))) $bin.AddRange($binaryData) $oldHash = $this.binaryHash $this.binaryHash = [System.BitConverter]::ToString([System.Security.Cryptography.SHA256CryptoServiceProvider]::new().ComputeHash($bin)) -replace '-','' if(-not $this.NoHostOutput) { # Write-Host (" OK! - Hash: {0}" -f $this.binaryHash) Write-Host " OK!" } if ($this.binaryHash -eq $oldHash) { # if ($this.symbolManager.Symbols.Values.Values.Resolved -contains $false) { # $s = "'" + (($this.symbolManager.Symbols.Values.Values.Where({!$_.Resolved})).Name -join "', '") + "'" # throw [System.Exception]::new("Unable to resolve symbols: $s") # Write-Warning -Message "Unresolved symbols defined: $s" # break # } $this.binary = $bin.ToArray() $success = $true } } if (!$success) { throw [System.Exception]::new("Maximum number of passes exceeded: $($this.MaxPasses)") } } [AssemblyResult]ToResult() { $info = [AssemblyResult]::new() $info.LoadAddress = $this.loadAddress $info.Scopes = $this.scopes $info.Symbols = $this.symbolManager?.GetSymbolTable() $info.SymbolsFull = $this.symbolManager?.GetFullSymbolTable() $info.PSSource = $this.PSSource $info.Segments = $this.Segments?.Segments $info.SegmentInfo = $this.Segments?.DumpSegments() $info.Assembly = $this.Assembly $info.AssemblyList = $this.ListAssembly() $info.Binary = $this.Binary $info.BinaryList = $this.HexDump() $info.BinaryHash = $this.BinaryHash $info.Tokens = $this.parser?.inTokens return $info } ### ### Assembly Lister ### [string]ListAssembly() { $sb = [System.Text.StringBuilder]::new(1024) $sbl = [System.Text.StringBuilder]::new(64) $currentDir = $this.FileStack.AllContexts[0].FilePath if (Test-Path $currentDir) { $currentDir = Split-Path $currentDir -Parent } else { $currentDir = (Get-Location).ProviderPath } $currentFile = "" $displayFile = $currentFile for ($lin=0;$lin -lt $this.assembly.count; $lin++) { if ($this.assembly[$lin].fileName -ne $currentFile) { $currentFile = $this.assembly[$lin].fileName $displayFile = if (Test-Path $currentFile) { Resolve-Path $currentFile -Relative -RelativeBasePath $currentDir } else { $currentFile } # $sb.AppendFormat("File: {0}", $displayFile) # $sb.AppendLine() } $a = ("{0:x4}" -f $this.assembly[$lin].addr) $sbl.Clear() for ($i=1; $i -le $this.assembly[$lin].bytes.Count; $i++) { $sbl.AppendFormat("{0:x2} ", $this.assembly[$lin].bytes[$i-1]) if ($i % 16 -eq 0 -and $i+1 -le $this.assembly[$lin].bytes.Count) { $sbl.Append("`n ") } } $ln = $this.assembly[$lin].lineNumber $col = $this.assembly[$lin].charPosition $c = ("{0}" -f $this.assembly[$lin].psLineText.Trim()) $d = ("{0}" -f $this.assembly[$lin].asmLineText.Trim()) # $sb.AppendFormat("`${0,-4}: {1,-9}- Ln: {2,-3} Col: {3,-3} - {4,-25} - {5}", $a, $sbl.ToString(), $ln, $col, $d, $c) # $sb.AppendFormat("`${0,-4}: {1,-9}- Ln: {2,-3} Col: {3,-3} - {4}", $a, $sbl.ToString(), $ln, $col, $d) $sb.AppendFormat("`${0,-4}: {1,-9}- File:{4} Ln: {2,-4} - {3}", $a, $sbl.ToString(), $ln, $d, $displayFile) $sb.AppendLine() } return $sb.ToString() } ### ### Binary Hex Dumper - Yes, I forgot PS has a Format-Hex command, but mine is neater ;-) ### [string]HexDump() { $sb = [System.Text.StringBuilder]::new(1024) for ($i=0;$i -lt $this.binary.count; $i+=16) { $sb.AppendFormat("`${0:x4}: ", $i) $j=0 for (; $i+$j -lt $this.binary.count -and $j -lt 16; $j++) { $sb.AppendFormat("{0:x2} ", $this.binary[$i+$j]) } while ($j++ -lt 16) { $sb.Append(" ") } $sb.Append(" '") for ($j=0; $i+$j -lt $this.binary.count -and $j -lt 16; $j++) { $sb.AppendFormat("{0}", ((($this.binary[$i+$j] -ge 32 -and $this.binary[$i+$j] -lt 127)) ? ([char]$this.binary[$i+$j]) : ('.')) ) } $sb.AppendLine("'") } return $sb.ToString() } } |