Offscreen Colonies
Did your virus scanner flag a download from our site? Click here to let us explain why!

Articles

Creating a tiny Windows executable

by BoyC / Conspiracy

So you decided to create a 64k intro. You fire up Visual Studio 2022, create a new C++ project, set it to 32 bit release mode, type int main(){}, hit compile... and the executable is 9 kilobytes. This used to be a lot worse, but this is surely not a great start.

Let's do some project setting magic to make things smaller:

C/C++ options

You'll need to add some C++ Additional options: /d2noftol3 to avoid some of what's coming and /Zc:threadSafeInit- to remove some code that will likely go unused around static data. The calling Convention should be set to __fastcall as that is the smallest option. No need for C++ exceptions (turn them off) and also no need for floating point exceptions (/fp:except-) Also enable intrinsic functions (/Oi) and disable run-time type information (/GR-) Favor size or speed should be set to - you guessed it - Favor Small Code (/Os), along with Optimization to Maximum Optimization (Favor Size) (/O1) Disable SDL Checks (/sdl-) and Security Checks (/GS-)

The default runtime library setting of Multithreaded DLL is the main reason of the original file size of only 9 kilobytes. This setting used to default to Multithreaded, which links the relevant parts of the Component Runtime (CRT) into the executable itself. Turning this to Multithreaded can be a good sanity check to make sure nothing slips through later (binary size for our empty executable now jumps to 83.5k). We'll be removing the whole CRT in the next section, so this is fine.

Linker options

The manifest should obviously go (disable the /MANIFEST linker option) Turn off incremental linking as this increases file size (/INCREMENTAL:NO) Turn off randomizing the base address (/DYNAMICBASE:NO), this will help with consistent execution should you run into exe packer issues. Also set the Base Address to 0x600000, this is required for kkrunchy, should you choose that compressor. Also set the SubSystem to Windows (/SUBSYSTEM:WINDOWS) - which also requires the int main() to be replaced to a standard Windows application entry point. For now.

At this point all these settings result in an executable of 76.5 kilobytes that does nothing but can be developed normally with code that is generated to be fairly small.

Ignore All Default Libraries

Enabling Ignore All Default Libraries (/NODEFAULTLIB) removes ALL of the operating system ballast added to the executable by the compiler by default, meaning you'll need to supply your own. There are various libraries to do this, but we've come to depend on a simpler solution in our codebase. Once this option is enabled, the linker will complain of a missing _WinMainCRTStartup function, the default entry point for Windows applications. So now the main function turns into

extern "C"
{
void WinMainCRTStartup() {}
}

From this point on, all bets are off: there's no new, no delete, even basic math functions such as sin and pow will be missing, not to mention global variables won't be initialized. However the executable suddenly shrinks to 1.5 kilobytes, the smallest it will ever get. There's still some ballast in there (the debug information reference), but that will be removed by the exe packer.

Adding back basic functionality

We have decided to implement new and delete functions for ourselves that guarantee to allocate memory that's zeroed out. This is important because while developing the engine the assumption that any allocated memory is all 0 can save a lot of code.

__declspec( noinline ) void* __cdecl operator new( unsigned int s )
{
  return HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, s );
}
__declspec( noinline ) void* __cdecl operator new[]( unsigned int s )
{
  return HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, s );
}

__declspec( noinline ) void __cdecl operator delete( void* p )
{
  HeapFree( GetProcessHeap(), 0, p );
}

__declspec( noinline ) void __cdecl operator delete( void* p, unsigned int )
{
  HeapFree( GetProcessHeap(), 0, p );
}

__declspec( noinline ) void __cdecl operator delete[]( void* p )
{
  HeapFree( GetProcessHeap(), 0, p );
}

Next we'll add back global variable initialization. This is a bit of technical magic but will help keep some semblance of normalcy when developing your app. We'll also add back the WinMain function call as well for the same reason, but this can be omitted.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#pragma warning( disable : 4725 )

extern "C" typedef void(__cdecl* _PVFV)();

#pragma section(".CRT$XCA", read, write)
#pragma data_seg(".CRT$XCA")        // start of ctor section
_PVFV __xc_a[] = { 0 };

#pragma section(".CRT$XCZ", read, write)
#pragma data_seg(".CRT$XCZ")        // end of ctor section
_PVFV __xc_z[] = { 0 };

#pragma data_seg()
#pragma comment(linker, "/merge:.CRT=.rdata")

extern "C"
{
  __forceinline void _initterm(_PVFV* pfbegin, _PVFV* pfend)
  {
    while (pfbegin < pfend)
    {
      if (*pfbegin != 0)
        (**pfbegin)();
      ++pfbegin;
    }
  }

  int _fltused = 0;

  void WinMainCRTStartup()
  {
    _initterm( __xc_a, __xc_z );
    WinMain( GetModuleHandle( 0 ), 0, 0, 0 );
    ExitProcess( 0 );
  }
}

The executable will now be 2.5 kilobytes, but actually do some initialization. For 4k releases this would already mostly be a luxury.

Adding back more complex functionality

At this point a lot of libraries will start implementing various mathematical functions in inline assembler, creating a whole mess of linker issues. We decided to strip out what we need from the original C runtime, create a new library from it and link to that. The lib command supplied with Visual Studio is of help to do this. We can supply a list of the functions we require in a definitions file and it will create the library we need.

Create a text file containing the following (let's call it msvcrt.def):

EXPORTS
_purecall
memset
memcpy
qsort
qsort_s

The command lib.exe /def:msvcrt.def /out:msvcrt_mini.lib /machine:x86 will create a library containing the listed functions. Lib.exe can be found under VC\Tools\MSVC\14.36.32532\bin\Hostx86\x86\ or equivalent in the Visual Studio install folder. When the linker complains about a missing CRT function all you need to do is add it to the list and recreate the library.

Note: #pragma comment() linking of libraries won't work due to Ignore All Default Libraries. In order to link the new library (and any other library) you'll need to add it to Additional Dependencies under the Linker Input settings.

Our list of used functions as of writing this is the following: _purecall memset memcpy qsort qsort_s sprintf sin abs fabs fmod tan acos atan atan2 cos sqrt pow floor _ftol2 log memcmp exp realloc ldexp free malloc exit

In order to access standard Windows functionality like window creation and message handling you'll need to add some other additional libraries to the linker inputs. The following is a good starting set (assuming Directx11 as your graphics API of choice): d3d11.lib dsound.lib uuid.lib user32.lib gdi32.lib oleaut32.lib kernel32.lib Shlwapi.lib msvcrt_mini.lib