Last active
November 3, 2024 16:00
-
-
Save Washi1337/367eede6e00b31e29355626d5e2f3078 to your computer and use it in GitHub Desktop.
A program to emit a tiny .NET binary program printing Hello World to the standard output. Blog post: https://blog.washi.dev/posts/tinysharp/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text; | |
using AsmResolver; | |
using AsmResolver.DotNet; | |
using AsmResolver.DotNet.Builder.Metadata.Blob; | |
using AsmResolver.DotNet.Builder.Metadata.Strings; | |
using AsmResolver.DotNet.Code.Cil; | |
using AsmResolver.DotNet.Signatures; | |
using AsmResolver.IO; | |
using AsmResolver.PE; | |
using AsmResolver.PE.DotNet.Builder; | |
using AsmResolver.PE.Code; | |
using AsmResolver.PE.DotNet; | |
using AsmResolver.PE.DotNet.Cil; | |
using AsmResolver.PE.DotNet.Metadata; | |
using AsmResolver.PE.DotNet.Metadata.Tables; | |
using AsmResolver.PE.DotNet.Metadata.Tables.Rows; | |
using AsmResolver.PE.File; | |
using AsmResolver.PE.File.Headers; | |
using static AsmResolver.PE.DotNet.Cil.CilOpCodes; | |
namespace TinySharp; | |
internal static class Program | |
{ | |
public static void Main() | |
{ | |
var module = new ModuleDefinition("Dummy"); | |
// Segment containing our string to print. | |
var segment = new DataSegment(Encoding.ASCII.GetBytes("Hello, World!\0")); | |
// Initialize a new PE image and set up some default values. | |
var image = new PEImage | |
{ | |
ImageBase = 0x00000000004e0000, // Pick some random image base we will have our module be loaded at. | |
PEKind = OptionalHeaderMagic.PE64, // Force PE64 to avoid import directory. | |
MachineType = MachineType.Amd64 // PE64 needs 64-bit arch. | |
}; | |
// Ensure PE is loaded at the provided image base. | |
image.DllCharacteristics &= ~DllCharacteristics.DynamicBase; | |
// Create new metadata streams. | |
var tablesStream = new TablesStream(); | |
var blobStreamBuffer = new BlobStreamBuffer(); | |
var stringsStreamBuffer = new StringsStreamBuffer(); | |
// Add empty module row. | |
tablesStream.GetTable<ModuleDefinitionRow>().Add(default); | |
// Add container type def for our main function (<Module>). | |
tablesStream.GetTable<TypeDefinitionRow>().Add(new TypeDefinitionRow( | |
0, 0, 0, 0, 1, 1 | |
)); | |
var methodTable = tablesStream.GetTable<MethodDefinitionRow>(); | |
// Add puts method. | |
methodTable.Add(new MethodDefinitionRow( | |
SegmentReference.Null, | |
MethodImplAttributes.PreserveSig, | |
MethodAttributes.Static | MethodAttributes.PInvokeImpl, | |
stringsStreamBuffer.GetStringIndex("puts"), | |
blobStreamBuffer.GetBlobIndex(new DummyProvider(), | |
MethodSignature.CreateStatic(module.CorLibTypeFactory.Void, module.CorLibTypeFactory.IntPtr), ThrowErrorListener.Instance), | |
1 | |
)); | |
// Add main method calling puts. | |
using var codeStream = new MemoryStream(); | |
var assembler = new CilAssembler(new BinaryStreamWriter(codeStream), new CilOperandBuilder(new OriginalMetadataTokenProvider(null), ThrowErrorListener.Instance)); | |
assembler.WriteInstruction(new CilInstruction(Ldc_I4, 0x12345678)); // To be replaced with the address to the string to print (applied with a patch below). | |
assembler.WriteInstruction(new CilInstruction(Call, new MetadataToken(TableIndex.Method, 1))); | |
assembler.WriteInstruction(new CilInstruction(Ret)); | |
var body = new CilRawTinyMethodBody(codeStream.ToArray()) | |
.AsPatchedSegment() | |
.Patch(2, AddressFixupType.Absolute32BitAddress, new Symbol(segment.ToReference(0x1b0))); // +0x1B0 is necessary due to a bug in AsmResolver 5.3.0. This won't be necessary in 5.4.0. | |
methodTable.Add(new MethodDefinitionRow( | |
body.ToReference(), | |
0, | |
MethodAttributes.Static, | |
0, | |
blobStreamBuffer.GetBlobIndex(new DummyProvider(), | |
MethodSignature.CreateStatic(module.CorLibTypeFactory.Void), ThrowErrorListener.Instance), | |
1 | |
)); | |
// Add urctbase module reference | |
tablesStream.GetTable<ModuleReferenceRow>().Add(new ModuleReferenceRow(stringsStreamBuffer.GetStringIndex("ucrtbase"))); | |
// Add P/Invoke metadata to the puts method. | |
tablesStream.GetTable<ImplementationMapRow>().Add(new ImplementationMapRow( | |
ImplementationMapAttributes.CallConvCdecl, | |
tablesStream.GetIndexEncoder(CodedIndex.MemberForwarded).EncodeToken(new MetadataToken(TableIndex.Method, 1)), | |
stringsStreamBuffer.GetStringIndex("puts"), | |
1 | |
)); | |
// Define assembly manifest. | |
tablesStream.GetTable<AssemblyDefinitionRow>().Add(new AssemblyDefinitionRow( | |
0, | |
1, 0, 0, 0, | |
0, | |
0, | |
stringsStreamBuffer.GetStringIndex("puts"), // The CLR does not allow for assemblies with a null name. Reuse the name "puts" to safe space. | |
0 | |
)); | |
// Add all .NET metadata to the PE image. | |
image.DotNetDirectory = new DotNetDirectory | |
{ | |
EntryPoint = new MetadataToken(TableIndex.Method, 2), | |
Metadata = new Metadata | |
{ | |
VersionString = "v4.0.", // Needs the "." at the end. (original: v4.0.30319) | |
Streams = | |
{ | |
tablesStream, | |
blobStreamBuffer.CreateStream(), | |
stringsStreamBuffer.CreateStream() | |
} | |
} | |
}; | |
// Assemble PE file. | |
var file = new MyBuilder().CreateFile(image); | |
// Put string to print in the padding data. | |
file.ExtraSectionData = segment; | |
// Write to disk. | |
file.Write("output.exe"); | |
} | |
} | |
internal class DummyProvider : ITypeCodedIndexProvider | |
{ | |
public uint GetTypeDefOrRefIndex(ITypeDefOrRef type) => throw new NotImplementedException(); | |
} | |
public class MyBuilder : ManagedPEFileBuilder | |
{ | |
protected override PESection CreateTextSection(IPEImage image, ManagedPEBuilderContext context) | |
{ | |
// We override this method to only have it emit the bare minimum .text section. | |
var methodTable = context.DotNetSegment.DotNetDirectory.Metadata? | |
.GetStream<TablesStream>()! | |
.GetTable<MethodDefinitionRow>()!; | |
for (uint rid = 1; rid <= methodTable.Count; rid++) | |
{ | |
ref var methodRow = ref methodTable.GetRowRef(rid); | |
var bodySegment = methodRow.Body.IsBounded | |
? methodRow.Body.GetSegment() | |
: null; | |
if (bodySegment is not null) | |
{ | |
context.DotNetSegment.MethodBodyTable.AddNativeBody(bodySegment, 4); | |
methodRow.Body = bodySegment.ToReference(); | |
} | |
} | |
return new PESection(".text", | |
SectionFlags.ContentCode | SectionFlags.MemoryExecute | SectionFlags.MemoryRead, | |
context.DotNetSegment); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for sharing :) Additionally, if you want to know about Hamlet's parent-child relationships you might visit https://freekidsbooks.org/hamlet-parent-child-relationships/ this awesome article. This interesting study illuminates the themes of love, deceit, and tragedy while examining the complex connections between parents and children in Hamlet. An engaging investigation for literature enthusiasts.