9

I have the following C program:

#include <windows.h>
void __cdecl mainCRTStartup() {
  DWORD bw;
  HANDLE hfile = GetStdHandle(STD_OUTPUT_HANDLE);
  WriteFile(hfile, "Hello, World!\r\n", 15, &bw, 0);
  ExitProcess(0);  /* Needed for successful (0) exit. */
}

I compile it with GCC 4.8.2, using the following command line:

i686-w64-mingw32-gcc -s -Os -fno-ident -fno-stack-protector -fomit-frame-pointer \
-fno-unwind-tables -fno-asynchronous-unwind-tables -falign-functions=1  \
-mpreferred-stack-boundary=2 -falign-jumps=1 -falign-loops=1 -mconsole \
-nostdlib -nodefaultlibs -nostartfiles -o h.exe h.c -lkernel32

The generated .exe file is 2048 bytes long. How can I make it smaller using MinGW, preferably at most 1024 bytes, or (even better) at most 512 bytes?

I'd prefer a solution without writing assembly code, but I'm also interested in assembly solutions.

I've tried -Wl,-N to decrease the number of sections (segments), but that caused a segfault when running the .exe in Wine.

This article suggests that 480 bytes is possible. It uses the following settings:

#pragma comment(linker, "/FILEALIGN:16")
#pragma comment(linker, "/ALIGN:16")// Merge sections
#pragma comment(linker, "/MERGE:.rdata=.data")
#pragma comment(linker, "/MERGE:.text=.data")
#pragma comment(linker, "/MERGE:.reloc=.data")
#pragma optimize("gsy", on)

Unfortunately these #pragmas don't work with MinGW GCC. Are there equivalents?

In here I was able to find the GCC flags -Wl,--section-alignment,16,--file-alignment,16 which bring down the .exe size to 752 bytes. The .exe seems to work in Wine.

By modifying the linker script I was able to merge .data and .rdata, and go down to 736 bytes. I'm using these GCC flags in addition to those above: -Wl,--section-alignment,16,--file-alignment,16,-T,tinygccpe.scr.

I'm still looking for the MinGW equivalent of /MERGE.

This question is similar, but it doesn't attempt to go below 9000 bytes.

I'm also looking for a strip tool (the strip command in MinGW doesn't reduce the .exe size any further) which can remove the DOS stub (which is between offsets 0x40 and 0x80, it contains This program cannot be run in DOS mode., we could save 64 bytes). This code can remove it, but it also breaks all the absolute offsets in the .exe. Unfortunately the linker ld in MinGW isn't able to remove the DOS stub, it's hardcoded in the file bfd/peXXigen.c, just above the NT_SIGNATURE.

Is it possible to strip more headers from the .exe, i.e. headers which the loader doesn't use?

Arnie97
  • 1,020
  • 7
  • 19
pts
  • 80,836
  • 20
  • 110
  • 183
  • 2
    The linked article is very old. It is about VC++ 6, which was released in 1998. What is the motivation for trying to make a do-nothing binary extremely small? In order to do anything productive, you're going to want to link in a runtime library, which will make your binary large again. I'm certainly not advocating writing *bloated* code (very against that, too), but you are tilting at windmills here, in my opinion. There are good reasons why linkers insert padding, and it just doesn't matter once you have a non-trivial amount of code. (Upvoted anyway, though—it's a decent question.) – Cody Gray - on strike Feb 03 '17 at 10:36
  • @Cody Gray: My motivation of saving 1536 PE header and boilerplate bytes is mostly academic. These bytes probably wouldn't bring a noticeable benefit for the user with a modern, multi-gigabyte-RAM system. I'm also developing some programs with a small total amount of code (smaller than 50 000 bytes when compiled with the above settings). – pts Feb 03 '17 at 12:08
  • 1
    Keep in mind there is a minimum cluster size on the storage medium. eg. on Windows, this is typically 4k. Even if you do manage to get the linker to generate a smaller executable it'll still take up that much space on disk. – greatwolf Feb 03 '17 at 12:39
  • @greatwolf: Indeed. Also it will probably use 4 kB of RAM (plus overhead) when run, because the page size is 4kB. – pts Feb 03 '17 at 12:48
  • 1
    Possibly of interest: http://stackoverflow.com/a/16270204/886887 – Harry Johnston Feb 03 '17 at 23:21
  • 3
    If you add `-Wl,--section-alignment,16,--file-alignment,16`, the resulting executable of 752 bytes does not run on Win7 (I just tested). – ssbssa Feb 05 '17 at 01:02
  • @ssbssa: Could you please also test it with 256 and 512 on Windows 7? – pts Feb 05 '17 at 08:22
  • 2
    @pts: Minimum values are `-Wl,--file-alignment,512,--section-alignment,4096`, but those are the default values anyway. – ssbssa Feb 05 '17 at 12:37
  • Indeed, multiple sources confirm that `-Wl,--file-alignment,512,--section-alignment,4096` is the minimum. I can still hope for a 1024-byte executble by merging all sections except for `.reloc` into 1 section starting at file offset 0. – pts Feb 08 '17 at 12:53

1 Answers1

5

This question has extensive online literature, starting from about 1995.

Each version of 32-bit and 64-bit Windows has a different set of rules on what header values they accept in PE .exe executables. For example, Windows 7 accepts .exe files with 0 sections, section alignment 4, file alignment 4, and other versions of Windows (e.g. Windows XP and the most recent Windows 10 in 2020) reject these files.

It's possible to create working .exe files smaller than 2048 bytes though. Example:

  • hh6d.golden.exe (584 bytes) is portable: it works on all Win32 implementations released by Microsoft and also on Wine. (Tested on Windows NT 3.1, Windows 95, Windows XP and Windows 10, Wine 1.6.2, Wine 5.0.)
  • hh2d.golden.exe (408 bytes) works on Windows 95 and up. (Tested on Windows 95, Windows XP, Windows 7 and Windows 10).
  • hh1.golden.exe (268 bytes) doesn't work on Windows XP, it works on Windows 7, it doesn't work on Windows 10.
  • Assembly source code included in the pts-tinype repo.

Here is why it is unlikely that a portable Win32 PE .exe hello-world shorter than 584 bytes will be released:

  • Program code must be put to an executable section, and the earliest file byte offset for that is 512 because SectionAlignment must be at least 512, and the section can't start at the beginning of the file.
  • Minimum hello-world printer i386 Win32 machine code size is 36 bytes. (This excludes the message size, because the message can be stored in the header, within the first 512 bytes.)
  • Minimum size of IMAGE_IMPORT_DESCRIPTORS is 20 bytes, for KERNEL32.DLL. Windows 95 doesn't allow this inside the header, so it must be put after file byte offset 512.
  • Minimum size of IAT (import address table) is 16 bytes: 4 bytes for GetStdHandle, 4 bytes for WriteFile, 4 bytes for ExitProcess, 4 bytes for end-of-list. Since the loader modifies these addresses, they can't be put to the header (which is read-only), so they must be put after file byte offset 512.
  • If we add these up, we get 512 + 36 + 20 + 16 = 584 bytes.

The .exe files smaller than 268 bytes work only on Windows versions earlier than Windows XP, and they don't work on 64-bit Windows systems.

Related literature:

pts
  • 80,836
  • 20
  • 110
  • 183