Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Initial disassembler implementation #23

Merged
merged 20 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Reemit.Common.UnitTests/RangeMappedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ public void Cast_Called_ConstructsWithNewValue()
Assert.IsType<RangeMapped<uint>>(actualRangeMapped);
}

[Fact]
public void At_Called_ConstructsWithNewPosition()
{
// Arrange
var rangeMapped = new RangeMapped<int>(0x52, 0x30, 0x1592);

// Act
var actualRangeMapped = rangeMapped.At(0x4);

// Assert
Assert.Equal(0x1592, actualRangeMapped.Value);
Assert.IsType<int>(actualRangeMapped.Value);
Assert.IsType<RangeMapped<int>>(actualRangeMapped);
Assert.Equal(0x56, actualRangeMapped.Position);
}

private void AssertLengthAndPosition(IRangeMapped expected, IRangeMapped actual)
{
Assert.Equal(expected.Length, actual.Length);
Expand Down
8 changes: 3 additions & 5 deletions Reemit.Common/RangeMapped.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

namespace Reemit.Common;

public readonly struct RangeMapped<TValue>(int position, int length, TValue value) : IRangeMapped
public readonly record struct RangeMapped<TValue>(int Position, int Length, TValue Value) : IRangeMapped
{
public int Position { get; } = position;
public int Length { get; } = length;
public TValue Value { get; } = value;

public int End => Position + Length;

public static implicit operator TValue(RangeMapped<TValue> rangeMapped) => rangeMapped.Value;

public RangeMapped<TResult> With<TResult>(TResult otherValue) => new(Position, Length, otherValue);

public RangeMapped<TValue> At(int offset) => this with { Position = Position + offset };

public RangeMapped<TResult> Cast<TResult>()
{
var v = Value;
Expand Down
52 changes: 52 additions & 0 deletions Reemit.Decompiler.Clr.UnitTests/Disassembler/OpcodeDecoderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Reemit.Decompiler.Clr.Disassembler;

namespace Reemit.Decompiler.Clr.UnitTests.Disassembler;

public class OpcodeDecoderTests
{
[Theory]
[InlineData(Opcode.nop, 0x00)]
[InlineData(Opcode.add, 0x58)]
[InlineData(Opcode.conv_u, 0xe0)]
public void DecodeOpcode_Standard_OpcodeDecoded(Opcode expectedOpcode, byte opcodeByte)
{
// Arrange

// Act
var actualOpcode = DecodeBuffer([ opcodeByte ]);

// Assert
Assert.Equal(expectedOpcode, actualOpcode.Opcode);
Assert.False(actualOpcode.IsExtended);
Assert.Equal(ExtendedOpcode.None, actualOpcode.ExtendedOpcode);
}

[Theory]
[InlineData(ExtendedOpcode.arglist, false, 0x00)]
[InlineData(ExtendedOpcode.ldarga, false, 0x0a)]
[InlineData(ExtendedOpcode.@readonly, true, 0x1e)]
public void DecodeOpcode_Extended_OpcodeDecoded(
ExtendedOpcode expectedExtendedOpcode,
bool expectedPrefix,
byte opcodeByte)
{
// Arrange

// Act
var actualOpcode = DecodeBuffer([ 0xfe, opcodeByte ]);

// Assert
Assert.Equal(Opcode.Extended, actualOpcode.Opcode);
Assert.True(actualOpcode.IsExtended);
Assert.Equal(expectedExtendedOpcode, actualOpcode.ExtendedOpcode);
Assert.Equal(expectedPrefix, actualOpcode.IsPrefix);
}

private OpcodeInfo DecodeBuffer(byte[] buffer)
{
using var stream = new MemoryStream(buffer);
var decoder = new OpcodeDecoder(stream);

return decoder.Decode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Reemit.Decompiler.Clr.Disassembler;

namespace Reemit.Decompiler.Clr.UnitTests.Disassembler;

public class OpcodeOperandTableTests
{
[Fact]
public void ResolveOperandType_Standard_OperandTypeResolved()
{
// Arrange
var opcode = Opcode.call;
OpcodeInfo opcodeInfo = opcode;

// Act
var actual1 = OpcodeOperandTable.GetOperandType(opcode);
var actual2 = OpcodeOperandTable.GetOperandType(opcodeInfo);

// Assert
Assert.Equal(OperandType.MetadataToken, actual1);
Assert.Equal(OperandType.MetadataToken, actual2);
}

[Fact]
public void ResolveOperandType_Extended_OperandTypeResolved()
{
// Arrange
var opcode = ExtendedOpcode.constrained;
OpcodeInfo opcodeInfo = opcode;

// Act
var actual1 = OpcodeOperandTable.GetOperandType(opcode);
var actual2 = OpcodeOperandTable.GetOperandType(opcodeInfo);

// Assert
Assert.Equal(OperandType.MetadataToken, actual1);
Assert.Equal(OperandType.MetadataToken, actual2);
}

[Fact]
public void ResolveOperandType_Standard_OperandTypeNotResolved()
{
// Arrange
var opcode = Opcode.add;
OpcodeInfo opcodeInfo = opcode;

// Act
var actual1 = OpcodeOperandTable.GetOperandType(opcode);
var actual2 = OpcodeOperandTable.GetOperandType(opcodeInfo);

// Assert
Assert.Equal(OperandType.None, actual1);
Assert.Equal(OperandType.None, actual2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Reemit.Decompiler.Clr.Disassembler;

namespace Reemit.Decompiler.Clr.UnitTests.Disassembler;

public class StreamDisassemblerTests
{
[Theory]
[MemberData(nameof(GetInstructionTestCases))]
public void Disassemble_Buffer_InstructionsDisassembled(
IEnumerable<Instruction> expectedInstructions,
byte[] bytecode)
{
// Arrange
using var stream = new MemoryStream(bytecode);
var disassembler = new StreamDisassembler(stream);

// Act
var actualInstructions = disassembler.Disassemble().Select(x => x.Value);

// Assert
Assert.Equal(expectedInstructions, actualInstructions, InstructionComparer.Instance);
}

public static IEnumerable<object[]> GetInstructionTestCases() =>
new[]
{
CreateInstructionTestCase(
[
new Instruction(Opcode.nop, Operand.None),
new Instruction(Opcode.nop, Operand.None),
new Instruction(Opcode.nop, Operand.None),
],
[
0x00,
0x00,
0x00,
]),
CreateInstructionTestCase(
[
new Instruction(
Opcode.call,
new Operand(
OperandType.MetadataToken,
[ 0x50, 0x20, 0x30, 0x00 ])),
],
[
0x28,
0x50,
0x20,
0x30,
0x00,
]),
CreateInstructionTestCase(
[
new Instruction(
Opcode.@switch,
new Operand(
OperandType.JumpTable,
[
0x02, 0x00, 0x00, 0x00, // Jump count
0x50, 0x20, 0x30, 0x00, // Jump 1
0x10, 0x50, 0x90, 0x20, // Jump 2
])),
],
[
0x45, // switch opcode
0x02, 0x00, 0x00, 0x00, // Jump count
0x50, 0x20, 0x30, 0x00, // Jump 1
0x10, 0x50, 0x90, 0x20, // Jump 2
])
};

private static object[] CreateInstructionTestCase(
IEnumerable<Instruction> expectedInstructions,
byte[] bytecode) =>
new object[] { expectedInstructions, bytecode };
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
Binary file not shown.
42 changes: 42 additions & 0 deletions Reemit.Decompiler.Clr/Disassembler/Generate-Opcode-Enums.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
$OpcodeFile = "Opcodes.txt"
$Namespace = "Reemit.Decompiler.Clr.Disassembler"
$OpcodeTable = @()
$ExtendedOpcodeTable = @()

Get-Content $OpcodeFile | %{
$P = $_.Split(' ')

if ($P.Length -eq 2) {
$OpcodeTable += @{ Opcode = $P[0]; Mnemonic = $P[1] }
} else {
$ExtendedOpcodeTable += @{ Opcode = $P[1]; Mnemonic = $P[2] }
}
}

function Create-Enum {
param($EnumName, $OpcodeTable, $IsExtended)

$CS = ""
$CS += "namespace $Namespace;`r`n"
$CS += "`r`n"
$CS += "public enum " + $EnumName + " : byte`r`n"
$CS += "{`r`n"

$OpcodeTable | %{
$CS += " [Mnemonic(`"$($_.Mnemonic)`", $($IsExtended.ToString().ToLower()))]`r`n"
$CS += " @$($_.Mnemonic.TrimEnd(".").Replace(".", "_")) = $($_.Opcode),`r`n`r`n"
}

if ($IsExtended -eq $False) {
$CS += " Extended = 0xFE,`r`n"
} else {
$CS += " None = 0xFF,`r`n"
}

$CS += "}`r`n"

return $CS
}

Create-Enum "Opcode" $OpcodeTable $False | Out-File "Opcode.g.cs"
Create-Enum "ExtendedOpcode" $ExtendedOpcodeTable $True | Out-File "ExtendedOpcode.g.cs"
6 changes: 6 additions & 0 deletions Reemit.Decompiler.Clr/Disassembler/IDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Reemit.Decompiler.Clr.Disassembler;

public interface IDecoder<T>
{
T Decode();
}
19 changes: 19 additions & 0 deletions Reemit.Decompiler.Clr/Disassembler/Instruction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Reemit.Decompiler.Clr.Disassembler;

public readonly struct Instruction
{
public OpcodeInfo OpcodeInfo { get; }

public Operand Operand { get; }

public Instruction(OpcodeInfo opcodeInfo)
: this(opcodeInfo, Operand.None)
{
}

public Instruction(OpcodeInfo opcodeInfo, Operand operand)
{
OpcodeInfo = opcodeInfo;
Operand = operand;
}
}
21 changes: 21 additions & 0 deletions Reemit.Decompiler.Clr/Disassembler/InstructionComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;

namespace Reemit.Decompiler.Clr.Disassembler;

public class InstructionComparer : IEqualityComparer<Instruction>
{
public static readonly InstructionComparer Instance = new InstructionComparer();

public bool Equals(Instruction x, Instruction y) =>
OpcodeInfoComparer.Instance.Equals(x.OpcodeInfo, y.OpcodeInfo) &&
OperandComparer.Instance.Equals(x.Operand, y.Operand);

public int GetHashCode(Instruction obj)
{
var hc = new HashCode();
hc.Add(obj.Operand);
hc.Add(obj.OpcodeInfo);

return hc.ToHashCode();
}
}
47 changes: 47 additions & 0 deletions Reemit.Decompiler.Clr/Disassembler/InstructionDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Immutable;

namespace Reemit.Decompiler.Clr.Disassembler;

public class InstructionDecoder(Stream stream) : IDecoder<Instruction>
{
private readonly BinaryReader _binaryReader = new(stream);

private readonly OpcodeDecoder _opcodeDecoder = new(stream);

public Instruction Decode()
{
var opcode = _opcodeDecoder.Decode();
var operand = DecodeOperand(opcode);

return new Instruction(opcode, operand);
}

private Operand DecodeOperand(OpcodeInfo opcodeInfo)
{
var operandType = OpcodeOperandTable.GetOperandType(opcodeInfo);
byte[] operandValue;

if (operandType != OperandType.JumpTable)
{
var operandSize = OperandSizeTable.SizeTable[operandType];
operandValue = _binaryReader.ReadBytes(operandSize);
}
else
{
operandValue = DecodeJumpTable();
}

return new(operandType, operandValue);
}

private byte[] DecodeJumpTable()
{
var jumpCount = _binaryReader.ReadUInt32();

// Technically incorrect given jumpCount is an unsigned int32.
var jumps = _binaryReader.ReadBytes((int)(jumpCount * 4));

// Somewhat inefficient, but doing it this way for now.
return BitConverter.GetBytes(jumpCount).Concat(jumps).ToArray();
}
}
Loading
Loading