BMOW title
Floppy Emu banner

A Handmade Executable File

Make a Windows program by stuffing bytes into a buffer and writing it to disk: no compiler, no assembler, no linker, no nothing! It was the obvious conclusion of my recent efforts to gain more control over what goes into my executables, and this time I could set every bit exactly as I wanted it. Yes, I am still a control freak.

I began with a simple C program called ExeBuilder to construct the buffer and write it to disk in a file named handmade.exe. ExeBuilder looks like this:

#include "stdafx.h"
#include <Windows.h>

int main(int argc, char* argv[])
{
    HANDLE hFile = CreateFile("handmade.exe", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 
                              FILE_ATTRIBUTE_NORMAL, NULL);
    BYTE* buf = (BYTE*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024);

    DWORD exeSize = BuildExe(buf);

    DWORD numberOfBytesWritten;
    WriteFile(hFile, buf, exeSize, &numberOfBytesWritten, NULL);

    HeapFree(GetProcessHeap(), 0, buf);
    CloseHandle(hFile);
    printf("wrote handmade.exe\n");
    return 0;
}

All of the interesting work happens in BuildExe(). This function manually constructs a valid Windows PE header, filling the required header fields and leaving the optional ones zeroed, then creates a single .text section and fills it with a few bytes of program code. The program in this case doesn’t do much – it just returns the number 44.

Sorting out the PE header details and determining which fields were actually required was a chore. All my testing was performed under Windows 7 64-bit edition. If you try these examples on your PC, it appears that earlier versions of Windows were more permissive with PE headers, while Windows 8 and 10 may be more strict about empty PE fields.

Here’s my first implementation of BuildExe(), which makes a nice standard executable with a single .text section containing 4 bytes of code.

inline void setbyte(BYTE* pBuf, DWORD off, BYTE val) { pBuf[off] = val; }
inline void setword(BYTE* pBuf, DWORD off, WORD val) { *(WORD*)(&pBuf[off]) = val; }
inline void setdword(BYTE* pBuf, DWORD off, DWORD val) { *(DWORD*)(&pBuf[off]) = val; }
inline void setstring(BYTE* pBuf, DWORD off, char* val) { lstrcpy((char*)&pBuf[off], val); }

DWORD BuildExe(BYTE* exe)
{
    // 1. DOS HEADER, 64 bytes
    setstring(exe, 0, "MZ"); // DOS header signature is 'MZ'
    setdword(exe, 60, 64); // DOS e_lfanew field gives the file offset to the PE header

    // 2. PE HEADER, at offset DOS.e_lfanew, 24 bytes
    setstring(exe, 64, "PE"); // PE header signature is 'PE\0\0'
    setword(exe, 68, 0x14C); // PE.Machine = IMAGE_FILE_MACHINE_I386
    setword(exe, 70, 1); // PE.NumberOfSections = 1
    setword(exe, 84, 208); // PE.SizeOfOptionalHeader = offset between the optional header and the section table
    setword(exe, 86, 0x103); // PE.Characteristics = IMAGE_FILE_32BIT_MACHINE | IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_RELOCS_STRIPPED

    // 3. OPTIONAL HEADER, follows PE header, 96 bytes
    setword(exe, 88, 0x10B); // Optional header signature is 10B
    setdword(exe, 104, 4096); // Opt.AddressOfEntryPoint = RVA where code execution should begin
    setdword(exe, 116, 0x400000); // Opt.ImageBase = base address at which to load the program, 0x400000 is standard
    setdword(exe, 120, 4096); // Opt.SectionAlignment = alignment of section in memory at run-time, 4096 is standard
    setdword(exe, 124, 512); // Opt.FileAlignment = alignment of sections in file, 512 is standard
    setword(exe, 136, 4); // Opt.MajorSubsystemVersion = minimum OS version required to run this program
    setdword(exe, 144, 4096*2); // Opt.SizeOfImage = total run-time memory size of all sections and headers
    setdword(exe, 148, 512); // Opt.SizeOfHeaders = total file size of header info before the first section
    setword(exe, 156, 3); // Opt.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI, command-line program
    setdword(exe, 180, 14); // Opt.NumberOfRvaAndSizes = number of data directories following
    
    // 4. DATA DIRECTORIES, follows optional header, 8 bytes per directory 
    // offset and size for each directory is zero

    // 5. SECTION TABLE, follows data directories, 40 bytes
    setstring(exe, 296, ".text"); // name of 1st section
    setdword(exe, 304, 4); // sectHdr.VirtualSize = size of the section in memory at run-time
    setdword(exe, 308, 4096); // sectHdr.VirtualAddress = RVA for the section
    setdword(exe, 312, 4); // sectHdr.SizeOfRawData = size of the section data in the file
    setdword(exe, 316, 512); // sectHdr.PointerToRawData = file offset of this section's data
    setdword(exe, 332, 0x60000020); // sectHdr.Characteristics = IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE

    // 6. .TEXT SECTION, at sectHdr.PointerToRawData (aligned to Opt.FileAlignment)
    setbyte(exe, 512, 0x6A); // PUSH
    setbyte(exe, 513, 0x2C); // value to push
    setbyte(exe, 514, 0x58); // POP EAX
    setbyte(exe, 515, 0xC3); // RETN

    return 516; // size of exe
}

The resulting file is 516 bytes. Check to make sure it works:

exebuilder-test

The executable is built from six data structures, which are numbered in the code’s comments. The cross-references in these structures are sometimes specified as offsets within the file, and sometimes as relative virtual addresses or RVAs. File offsets reflect the executable as it exists on disk, while RVAs reflect how it’s loaded in memory at run-time. An RVA is a run-time offset from the executable’s base address in memory. Getting these two confused will lead to problems!

DOS Header – The only fields that must be filled are the ‘MZ’ signature at the beginning and the e_lfanew parameter at the end (unless you’re actually writing a DOS program). e_lfanew gives the offset to the PE header, which in this case follows immediately after.

PE Header – The true PE header doesn’t contain much, because all the good stuff is in the optional header. The PE header specifies 1 section (the single .text section with the code to return 44), and 208 bytes combined size for the next two sections.

Optional Header – The optional header is only optional if you don’t care whether the program works. Some noteworthy values:

  • SectionAlignment – Each section of the executable (.text, .data, etc) must be alignment to this boundary in memory at run-time. The standard is 4096 or 4K, the size of a single page of virtual memory.
  • AddressOfEntryPoint – Program execution will begin at this memory offset from the base address. Because the section alignment is 4096, the program’s single .text section will be loaded at offset 4096, and execution should begin at the first byte of that section.
  • FileAlignment – Similar to section alignment, but for the file on disk instead of the program in memory. The standard is 512 bytes, the size of a single disk sector.
  • SizeOfHeaders – This isn’t really the combined size of all the headers, but rather the file offset to the first section’s data. Normally that’s the same as the combined size of all headers plus any necessary padding.

Data Directories – A typical executable would store offsets and sizes for its data directories here, the number of which is given in the optional header. Data directories are used to specify the program’s imports and exports, references to debug symbols, and other useful things. Manually constructing an import data directory is a bit complicated, so I didn’t do it. That’s why the program just returns 44 instead of doing something more interesting that would have required Win32 DLL imports. Handmade.exe does not have any data directories at all.

If you’re wondering why there are 14 data directories each with zero offset and size, instead of just specifying zero data directories, that’s a small mystery. According to tutorials I read, some parts of the OS will attempt to find info in data directories even if the number of data directories is zero. So the only safe way to have an empty data directory is to have a full table of offsets and sizes, all set to zero. However, I found other examples that did specify zero data directories and that reportedly worked fine. I didn’t look into the question any further, since it turned out not to matter anyway.

Section Table – For each section, there’s an entry here in the section table. Handmade.exe only has a single .text section, so there’s just one table entry. It gives the section size as 4 bytes, which is all that’s needed for the “return 44” code. The section will be loaded in memory at RVA 4096, which is also the program’s entry point.

Section Data – Finally comes the actual data of the .text section, which is x86 machine code. This is the meat of the program. The section data must be aligned to 512 bytes, so there’s some padding between the section table and start of the section data.

Here’s what dumpbin says about this handmade executable. Many of the fields are zero or have bogus values, but it doesn’t seem to matter:

Microsoft (R) COFF/PE Dumper Version 11.00.50727.1
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file handmade.exe

PE signature found

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
             14C machine (x86)
               1 number of sections
               0 time date stamp Wed Dec 31 16:00:00 1969
               0 file pointer to symbol table
               0 number of symbols
              D0 size of optional header
             103 characteristics
                   Relocations stripped
                   Executable
                   32 bit word machine

OPTIONAL HEADER VALUES
             10B magic # (PE32)
            0.00 linker version
               0 size of code
               0 size of initialized data
               0 size of uninitialized data
            1000 entry point (00401000)
               0 base of code
               0 base of data
          400000 image base (00400000 to 00401FFF)
            1000 section alignment
             200 file alignment
            0.00 operating system version
            0.00 image version
            4.00 subsystem version
               0 Win32 version
            2000 size of image
             200 size of headers
               0 checksum
               3 subsystem (Windows CUI)
               0 DLL characteristics
               0 size of stack reserve
               0 size of stack commit
               0 size of heap reserve
               0 size of heap commit
               0 loader flags
               E number of directories
               0 [       0] RVA [size] of Export Directory
               0 [       0] RVA [size] of Import Directory
               0 [       0] RVA [size] of Resource Directory
               0 [       0] RVA [size] of Exception Directory
               0 [       0] RVA [size] of Certificates Directory
               0 [       0] RVA [size] of Base Relocation Directory
               0 [       0] RVA [size] of Debug Directory
               0 [       0] RVA [size] of Architecture Directory
               0 [       0] RVA [size] of Global Pointer Directory
               0 [       0] RVA [size] of Thread Storage Directory
               0 [       0] RVA [size] of Load Configuration Directory
               0 [       0] RVA [size] of Bound Import Directory
               0 [       0] RVA [size] of Import Address Table Directory
               0 [       0] RVA [size] of Delay Import Directory


SECTION HEADER #1
   .text name
       4 virtual size
    1000 virtual address (00401000 to 00401003)
       4 size of raw data
     200 file pointer to raw data (00000200 to 00000203)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         Execute Read

  Summary

        1000 .text

Sometimes a picture is worth 1000 words, so I also made a color-coded hex dump of the executable file:

handmade-layout1

 
Shrinking It

After doing all this, of course my first thought was to try making it smaller. There’s a lot of empty padding between the section table and the section data, due to the 512 byte alignment of sections in the file. There must be some way to shrink or eliminate that padding, right? I tried reducing Opt.FileAlignment to 4, moving the .TEXT section data down to 336, and adjusting sectHdr.PointerToRawData accordingly. All I got for my effort was an error complaining “handmade.exe is not a valid Win32 application.” I’m unsure why it didn’t work. Maybe the OS doesn’t like sections that aren’t 512 byte aligned in the file, no matter what the PE header says.

Then I thought maybe I could reuse the header as the section data. By changing sectHdr.PointerToRawData to 0, I could make the Windows loader use a copy of the executable header as the .TEXT section data. 0 is 512 byte aligned, so there wouldn’t be any alignment problems. It seemed strange, since an executable header is not x86 code, but by stuffing the 4 bytes of code into an unused area of the header and adjusting Opt.AddressOfEntryPoint, I could theoretically patch everything up. Lo and behold, it worked! The new executable was only 340 bytes.

With the 4 bytes of code now stored inside the header, I wondered if I really needed a section at all. The Windows loader will load the header into memory along with all the sections, so maybe I could just eliminate the .TEXT section completely, and rely on the entry point address to point the way to the code stored in the header?

This worked too, but not without a lot of futzing around. After setting PE.NumberOfSections to 0, PE.SizeOfOptionalHeader and Opt.SizeOfHeaders both had to be set to zero. They’re both essentially offsets to section structures, and with no sections, apparently a 0 offset is required. Opt.SectionAlignment also had to be reduced to 2048, and I honestly have no idea why. With those changes, the modified program worked.

With the elimination of the section table, this should have been enough to shrink the executable to 300 bytes, but I found that anything smaller than 328 bytes wouldn’t work. It appeared that the OS assumes a minimum size for the optional header or the data directories, regardless of the sizes specified in the header. So 28 bytes of padding are required at the end of handmade.exe. The 328 byte version of BuildExe() is shown here, with the changes from the previous version highlighted:

DWORD BuildExe(BYTE* exe)
{
    // 1. DOS HEADER, 64 bytes
    setstring(exe, 0, "MZ"); // DOS header signature is 'MZ'
    setdword(exe, 60, 64); // DOS e_lfanew field gives the file offset to the PE header

    // 2. PE HEADER, at offset DOS.e_lfanew, 24 bytes
    setstring(exe, 64, "PE"); // PE header signature is 'PE\0\0'
    setword(exe, 68, 0x14C); // PE.Machine = IMAGE_FILE_MACHINE_I386
    setword(exe, 70, 0); // PE.NumberOfSections = 1
    setword(exe, 84, 0); // PE.SizeOfOptionalHeader = offset between the optional header and the section table
    setword(exe, 86, 0x103); // PE.Characteristics = IMAGE_FILE_32BIT_MACHINE | IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_RELOCS_STRIPPED

    // 3. OPTIONAL HEADER, follows PE header, 96 bytes
    setword(exe, 88, 0x10B); // Optional header signature is 10B
    setdword(exe, 104, 296); // Opt.AddressOfEntryPoint = RVA where code execution should begin
    setdword(exe, 116, 0x400000); // Opt.ImageBase = base address at which to load the program, 0x400000 is standard
    setdword(exe, 120, 2048); // Opt.SectionAlignment = alignment of section in memory at run-time, 4096 is standard
    setdword(exe, 124, 512); // Opt.FileAlignment = alignment of sections in file, 512 is standard
    setword(exe, 136, 4); // Opt.MajorSubsystemVersion = minimum OS version required to run this program
    setdword(exe, 144, 4096*2); // Opt.SizeOfImage = total run-time memory size of all sections and headers
    setdword(exe, 148, 0); // Opt.SizeOfHeaders = total file size of header info before the first section
    setword(exe, 156, 3); // Opt.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI, command-line program
    setdword(exe, 180, 14); // Opt.NumberOfRvaAndSizes = number of data directories following
    
    // 4. DATA DIRECTORIES, follows optional header, 8 bytes per directory 
    // offset and size for each directory is zero

    // 5. SECTION TABLE, follows data directories, 40 bytes
    // no section table

    // 6. .TEXT SECTION, at sectHdr.PointerToRawData (aligned to Opt.FileAlignment)
    setbyte(exe, 296, 0x6A); // PUSH
    setbyte(exe, 297, 0x2C); // value to push
    setbyte(exe, 298, 0x58); // POP EAX
    setbyte(exe, 299, 0xC3); // RETN

    return 328; // size of exe
}

Here’s another pretty picture, showing the 328 byte executable file:

handmade-layout2

 
Maximum Shrinking

328 bytes was pretty good, but of course I wanted to do better. A popular technique seen in other “small PE” examples is to move down the PE header and everything that follows it, so that it overlaps the DOS header. This is possible because most of the DOS header is just wasted space, as far as a Windows executable is concerned.

The PE header can be moved down as low as offset 4 within the file. It must be 4-byte aligned, and it can’t be at offset 0 because then it would overwrite the required ‘MZ’ signature at the start of the file. Doing this is simple: just move everything but the DOS header down by 60 bytes.

The only complication with overlapping the DOS and PE headers this way is with the DWORD at file offset 60. This value is the e_lfanew parameter that gives the file offset to the PE header, so it now must be 4. But due to the overlapping, it’s also the Opt.SectionAlignment parameter that specifies the alignment between sections in memory at run-time. Hopefully Windows is OK with a 4-byte section alignment! It turns out that it’s fine, but only if Opt.FileAlignment is also 4. I’m not sure why.

These changes should have been enough to shrink the file to 240 bytes, but once again the OS seems to require 28 bytes of padding at the end of the file. Here’s the updated 268 byte version of BuildExe():

DWORD BuildExe(BYTE* exe)
{
    // 1. DOS HEADER, 64 bytes
    setstring(exe, 0, "MZ"); // DOS header signature is 'MZ'
    // don't set DOS.e_lfanew, it's part of the overlapped PE header

    // 2. PE HEADER, at offset DOS.e_lfanew, 24 bytes
    setstring(exe, 64-60, "PE"); // PE header signature is 'PE\0\0'
    setword(exe, 68-60, 0x14C); // PE.Machine = IMAGE_FILE_MACHINE_I386
    setword(exe, 70-60, 0); // PE.NumberOfSections = 1
    setword(exe, 84-60, 0); // PE.SizeOfOptionalHeader = offset between the optional header and the section table
    setword(exe, 86-60, 0x103); // PE.Characteristics = IMAGE_FILE_32BIT_MACHINE | IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_RELOCS_STRIPPED

    // 3. OPTIONAL HEADER, follows PE header, 96 bytes
    setword(exe, 88-60, 0x10B); // Optional header signature is 10B
    setdword(exe, 104-60, 296-60); // Opt.AddressOfEntryPoint = RVA where code execution should begin
    setdword(exe, 116-60, 0x400000); // Opt.ImageBase = base address at which to load the program, 0x400000 is standard
    setdword(exe, 120-60, 4); // Opt.SectionAlignment = alignment of section in memory at run-time, 4096 is standard
    setdword(exe, 124-60, 4); // Opt.FileAlignment = alignment of sections in file, 512 is standard
    setword(exe, 136-60, 4); // Opt.MajorSubsystemVersion = minimum OS version required to run this program
    setdword(exe, 144-60, 4096*2); // Opt.SizeOfImage = total run-time memory size of all sections and headers
    setdword(exe, 148-60, 0); // Opt.SizeOfHeaders = total file size of header info before the first section
    setword(exe, 156-60, 3); // Opt.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI, command-line program
    setdword(exe, 180-60, 14); // Opt.NumberOfRvaAndSizes = number of data directories following
    
    // 4. DATA DIRECTORIES, follows optional header, 8 bytes per directory 
    // offset and size for each directory is zero

    // 5. SECTION TABLE, follows data directories, 40 bytes
    // no section table

    // 6. .TEXT SECTION, at sectHdr.PointerToRawData (aligned to Opt.FileAlignment)
    setbyte(exe, 296-60, 0x6A); // PUSH
    setbyte(exe, 297-60, 0x2C); // value to push
    setbyte(exe, 298-60, 0x58); // POP EAX
    setbyte(exe, 299-60, 0xC3); // RETN

    return 268; // size of exe
}

And another pretty picture, with some color blending going on where data structures overlap:

handmade-layout3

According to several sources, 268 bytes is the absolute minimum size for a working executable under Windows 7 64-bit edition. There are other tricks that would shrink the header even more, but then I’d just have to add more padding. I can go no further!

Read 15 comments and join the conversation 

15 Comments so far

  1. data_miner - October 12th, 2015 5:59 am

    Not in 64bit, but in DOS up to XP 32bit my smallest “Hello World” need 8 bytes.

    C:\Temp>debug
    -n hello.com
    -a
    160C:0100 mov ah,9
    160C:0102 mov dx,82
    160C:0105 int 21
    160C:0107 ret
    160C:0108
    -rcx
    CX 0000
    :8
    -w
    00008 Bytes werden geschrieben.
    -q

    C:\Temp>hello Hello World$
    Hello World
    C:\Temp>

    Hello World Contest:
    http://www.heise.de/forum/heise-online/News-Kommentare/30-Jahre-IBM-PC-Ein-Tramp-erobert-die-Welt/Final-8-bytes/posting-19263271/show/

  2. arvenius - October 12th, 2015 6:17 am

    you should also take a look at Crinkler, which does not only strip the PE header but also compresses the exe during linktime. its a demoscene tool used by many groups for making tiny intros usually starting at 1k sizelimit.
    http://crinkler.net/

  3. Steve Chamberlin - October 12th, 2015 8:30 am

    @data_miner, I thought DOS executables still required a 64 bytes DOS header with an “MZ” signature? I’ve never experimented with DOS development.

    @arvenius, good idea, and actually I did read up on Crinkler and some related tools just yesterday. It’s got me motivated to try making a 4K intro of my own. It looks like there are some nice frameworks for getting started at http://iquilezles.org/www/material/isystem1k4k/isystem1k4k.htm

  4. Nils - October 12th, 2015 9:55 am

    In case anyone’s interested in a tiny tool that really does something – I once built a config patch for the Amiga to stop a boot logo from jumping around: http://aminet.net/package/util/wb/FixPrefs

    The executable is 304 bytes but I wasted 46 bytes on a version string, so essentially it’s 258 bytes.

  5. Jochen - October 12th, 2015 9:56 am

    Even with “normal” C compilers, you can create EXE files with about 688 bytes… no need to write all the data “by hand”…
    http://blog.kalmbach-software.de/2008/02/02/smallest-application-size-for-win32-console-application/

  6. Steve Chamberlin - October 12th, 2015 12:55 pm

    @Jochen, do you know if that 688 byte EXE works under W7-64? The instructions say to set /ALIGN to 16, but when I tried setting it to anything smaller than 512, the EXE failed to load. The smallest working W7-64 EXE I was able to make with the standard compiler and linker was 1024 bytes: that’s with a single section, and the default 512 byte section alignment. You can manually truncate the resulting EXE to 516 bytes and it still works, but the Microsoft linker seems to pad the last section out to a 512 byte boundary, no matter how small it actually is.

  7. Jochen - October 12th, 2015 9:46 pm

    @Steve: Yes, for x64, you need to remove the /ALIGN:32 flag; then it works… but the size increaes to 1024 bytes…

  8. data_miner - October 13th, 2015 1:29 am

    @Steve Chamberlin

    HELLO.COM has 8 bytes, no header.

    C:\temp>dir
    Volume in Laufwerk C: hat keine Bezeichnung.
    Volumeseriennummer: 1CA6-4682

    Verzeichnis von C:\temp

    13.10.2015 11:24 .
    13.10.2015 11:24 ..
    13.10.2015 11:24 8 HELLO.COM
    1 Datei(en) 8 Bytes
    2 Verzeichnis(se), 130.459.312.128 Bytes frei

    C:\temp>

  9. Anonymous - October 13th, 2015 11:31 am
  10. EvilJay - October 14th, 2015 3:50 pm

    Forgot to mention: the excerpt has been slightly modified to represent a standalone DOS “MiniMZ” executable. To be part of a PE header, the MZ header must of course have the NewPEMarker field set to 0x40.

  11. EvilJay - October 14th, 2015 4:15 pm

    [Hm, well, my first post isn’t showing yet, but the addition immediately did. In case the first message has been lost, here it is again (broken up into 2 parts), please remove if it turns out to be a duplicate after all.]

    Hi, everybody!

    The smallest DOS .exe I’ve managed to create is 28 bytes in length. The only code it uses is the function call to terminate itself (mov ah,4ch ; int 21h). I’ve moved it into the stack initialization fields and set the header size field to zero so that the header is loaded at the start of the code segment by the DOS loader. The only thing then left to do was to set CS:IP to point exactly at the Stack Segment initialization field.

    To illustrate, here’s an excerpt from a project of mine that actually uses this (it’s a “PE shrinker without compression” that also uses the “align 4” trick. Works on relatively simple assembly language programs compiled with TASM or GoAsm so far, they only work on 32bit systems after shrinking, though. Lots of weird stuff had to be done there. *g*):

  12. EvilJay - October 14th, 2015 4:16 pm

    // A structure representing a minimal MZ header.
    struct MZHeader
    {
    WORD MZMagic;
    WORD NumberOfBytes;
    WORD NumberOfPages;
    WORD NumberOfRelocs;
    WORD SizeOfHeader;
    WORD MinMem;
    WORD MaxMem;
    WORD InitSS;
    WORD InitSP;
    WORD Checksum;
    WORD IP;
    WORD CS;
    WORD NewPEMarker;
    WORD OverlayNumber;
    } StdMZHdr;

    StdMZHdr.MZMagic = ‘ZM’; // Will be saved as “MZ”.
    StdMZHdr.NumberOfBytes = 0x1c;
    StdMZHdr.NumberOfPages = 1;
    StdMZHdr.NumberOfRelocs = 0;
    StdMZHdr.SizeOfHeader = 0;
    StdMZHdr.MinMem = 0;
    StdMZHdr.MaxMem = 0xffff;
    StdMZHdr.InitSS = 0x4cb4;
    StdMZHdr.InitSP = 0x21cd;
    StdMZHdr.Checksum = 0;
    StdMZHdr.IP = 0xe;
    StdMZHdr.CS = 0;
    StdMZHdr.NewPEMarker = 0;
    StdMZHdr.OverlayNumber = 0;

    Here’s a hexdump of the “MiniMZ”:
    4D 5A 1C 00 01 00 00 00 00 00 00 00 FF FF B4 4C CD 21 00 00 0E 00 00 00 00 00 00 00

  13. Anonymous - January 21st, 2018 5:24 pm

    You can minimise the code to exit a DOS program even further by using ‘CD 20’ (int 20h) rather than ‘B4 4C CD 21’ (mov ah, 4Ch : int 21h).

  14. example - August 14th, 2019 8:24 pm

    IMAGE_NT_HEADERS.OptionalHeader.NumberOfRvaAndSizes = 2;

    only needs to be 2 not 14 according to
    https://reverseengineering.stackexchange.com/questions/4210/pe-file-data-directory

    thus, you could cut another 96 bytes.

  15. bttr - July 16th, 2021 9:30 pm

    @data_miner: The smallest DOS .COM file is exactly 1 byte. You just need the `ret` instruction.

Leave a reply. For customer support issues, please use the Customer Support link instead of writing comments.