├── README.md └── tinydotnet_1536 ├── tinydotnet_1536.deps.json ├── tinydotnet_1536.dll ├── tinydotnet_1536.runtimeconfig.dev.json └── tinydotnet_1536.runtimeconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # TinyDotNet 2 | This project documents my research into creating tiny .NET executables. At the moment I can successfully make small programs (e.g. Hello World) with a file size of 1,536 bytes (actually using 1104 bytes of this, with some slack in the PE headers too), and am experimenting with pushing this down to 1,024 bytes. 3 | 4 | ## Motivation and Goals 5 | 6 | My motivation for this project is the idea of creating a 4K [demo](https://en.wikipedia.org/wiki/Demoscene) in .NET, i.e. a program that displays realtime graphics and plays music, using no more than 4096 bytes. I'm not so great at the music and graphics side of things but I like the idea of pushing the limits of the .NET platform, especially since no suitable compression tools exist for 4K size targets. 7 | 8 | I'm primarily targeting .NET Core, since the Core CLR is open-source and cross-platform. This makes the challenge and result more interesting - a fully cross-platform demo would be quite fun to make - but also more achievable than targeting standard .NET, as I can debug the CLR's assembly loader. 9 | 10 | The ultimate goal is to build a generic compressor tool for .NET Core applications, targeting the 4K file size. 11 | 12 | ## Shrinking the Assembly 13 | 14 | Assemblies for .NET Core are DLL files which are hosted with the `dotnet` tool (and some others). The structure is just like a regular .NET executable - it's a PE file with a .NET metadata directory. We can use many of the usual PE shrinking tricks, alongside some .NET specific ones. 15 | 16 | ### Metadata Names 17 | 18 | By default the metadata has names for classes, variables, etc. which we can shorten down to a single character each. This saves us a whole load of string data we don't want. 19 | 20 | ### Assembly Attributes 21 | 22 | The compiler puts a lot of informational attributes onto the assembly object. These have some pretty long string names and require a `CustomAttribute` table entry each. These can be trivially removed manually with Reflexil and on average saves around 600 bytes. 23 | 24 | ### DOS Stub 25 | 26 | The DOS stub obviously isn't needed. We can get rid of that entirely and have the NT/PE header directly following the DOS header (`e_lfanew = sizeof(DOS_Header)+1`). This saves a bunch of bytes from the start of the file, although it doesn't matter a whole lot since the initial section must align to 0x200. 27 | 28 | ### Entry Point 29 | 30 | The entry point begins with an x86 indirect jump to the address at `0x00402000`, which contains `0x000027E8`, which is the IAT address for `mscoree.dll!_CoreExeMain`. This EP is never executed if you load the DLL with `dotnet`. I have verified this by replacing the EP with garbage opcodes and `int3` breakpoints. This means we can point the EP to any valid RVA, and remove the entry point instructions. 31 | 32 | ### Data Directories 33 | 34 | There are Import, Resource, Relocation, Debug, IAT, and .NET MetaData directories by default. The `NumberOfRVAsAndSizes` field of the PE header must be set to 16 because otherwise the loader can't find the .NET MetaData directory. This means no directory folding tricks. The debug table can be completely removed, as can the import table (it isn't actually used by the loader!), which means the IAT and relocation table can go too. 35 | 36 | ### Sections 37 | 38 | By default there are three sections: .text, .rsrc, and .reloc for the metadata and code, resources, and relocation table respectively. We can completely delete the resource section as it only contains the manifest and version data, which isn't required. We can also delete the relocation section as it isn't needed once we get rid of the import table. Having only one section is critical because each additional section must, due to the alignment, add at least 512 bytes. By rebuilding the PE with 512-byte alignment, this cut down executable gets us to the 1,536 byte mark for a small Hello World. 39 | 40 | ## Future Improvement 41 | 42 | - Rename the assembly from "tinype" to "t". Saves 5 bytes. 43 | - Can stuff main code into .ctor and run it directly from there. Means we get rid of the IL from .ctor, one method entry, and the method name. Saves 21 bytes. 44 | - `#GUID` table entry was "removed" by shifting it to the end of the stream table and decrementing the stream count, but the table entry is still there. Shifting things up saves about 16 bytes but so far I've not had much luck with getting this working. I think it messes up an offset somewhere, or maybe a length check, but I can't really tell where. 45 | 46 | ## Negative Results 47 | 48 | Here's where all my failed attempts go. 49 | 50 | ### Native import shenanigans 51 | 52 | Adding a native DLL (e.g. user32.dll) to the import table doesn't work. The main module loads fine, but the dependency loader tries to load the native DLL as a .NET Core assembly. 53 | 54 | ### Section at offset zero 55 | 56 | Creating a PE with a single section at offset 0, with no import directory, IAT, relocations, etc. got me a different error - the PE load sort of works, but CoreCLR's sanity checks fail: 57 | 58 | ``` 59 | Assert failure(PID 21220 [0x000052e4], Thread: 12436 [0x3094]): Precondition failure: FAILED: addressStart >= previousAddressEnd && (offsetSize == 0 || offsetStart >= previousOffsetEnd) 60 | FAILED: CheckSection(currentAddress, section->VirtualAddress, section->Misc.VirtualSize, currentOffset, section->PointerToRawData, section->SizeOfRawData) 61 | d:\code\coreclr\src\utilcode\pedecoder.cpp, line: 363 62 | FAILED: CheckNTHeaders() 63 | d:\code\coreclr\src\inc\pedecoder.inl, line: 713 64 | 65 | CORECLR! CHECK::Trigger + 0x275 (0x00007ffb`6089d4d5) 66 | CORECLR! PEDecoder::HasCorHeader + 0x302 (0x00007ffb`6091a922) 67 | CORECLR! PEDecoder::IsNativeMachineFormat + 0x68 (0x00007ffb`60e97d48) 68 | CORECLR! MappedImageLayout::MappedImageLayout + 0x59C (0x00007ffb`610e8bfc) 69 | CORECLR! PEImageLayout::Map + 0x45D (0x00007ffb`610ebb1d) 70 | CORECLR! PEImage::CreateLayoutMapped + 0x2E9 (0x00007ffb`60b56c99) 71 | CORECLR! PEImage::GetLayoutInternal + 0x306 (0x00007ffb`60b58616) 72 | CORECLR! PEImage::GetLayout + 0xD9 (0x00007ffb`60b582b9) 73 | CORECLR! BinderAcquireImport + 0x105 (0x00007ffb`60e2d045) 74 | CORECLR! BINDER_SPACE::AssemblyBinder::GetAssembly + 0x2B5 (0x00007ffb`614fc185) 75 | File: d:\code\coreclr\src\utilcode\pedecoder.cpp Line: 427 76 | Image: D:\Code\coreclr\bin\Product\Windows_NT.x64.Debug\CoreRun.exe 77 | ``` 78 | 79 | We can see here that the CLR is checking to see if the section's raw base address is greater than the current read pointer for the headers. This prevents us from crushing the PE headers and .NET metadata together. A shame really because that'd get us to the 1,024 byte mark easily. 80 | -------------------------------------------------------------------------------- /tinydotnet_1536/tinydotnet_1536.deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtimeTarget": { 3 | "name": ".NETCoreApp,Version=v2.0", 4 | "signature": "da39a3ee5e6b4b0d3255bfef95601890afd80709" 5 | }, 6 | "compilationOptions": {}, 7 | "targets": { 8 | ".NETCoreApp,Version=v2.0": { 9 | "tinydotnet_1536/1.0.0": { 10 | "runtime": { 11 | "tinydotnet_1536.dll": {} 12 | } 13 | } 14 | } 15 | }, 16 | "libraries": { 17 | "tinydotnet_1536/1.0.0": { 18 | "type": "project", 19 | "serviceable": false, 20 | "sha512": "" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tinydotnet_1536/tinydotnet_1536.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gsuberland/TinyDotNet/6850a20b6f74e5e1758e8f97c6ce072b96cca73f/tinydotnet_1536/tinydotnet_1536.dll -------------------------------------------------------------------------------- /tinydotnet_1536/tinydotnet_1536.runtimeconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtimeOptions": { 3 | "additionalProbingPaths": [ 4 | "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /tinydotnet_1536/tinydotnet_1536.runtimeconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtimeOptions": { 3 | "tfm": "netcoreapp2.0", 4 | "framework": { 5 | "name": "Microsoft.NETCore.App", 6 | "version": "2.0.0" 7 | } 8 | } 9 | } --------------------------------------------------------------------------------