Generating a Minimal Executable with Visual Studio and MASM

Learn how to write an assembly program with MASM and link it to get a minimal executable. Just install Visual Studio and a Windows SDK, then follow the steps below.

First, we need to set up the prototypes in the assembly code as imports if we are using the Microsoft MASM assembler, rather than an open-source equivalent.

Also, define any constants. A simple “Hello World” program via a message box is a good example.

.586
.model flat, stdcall
option casemap :none

MessageBoxA PROTO STDCALL :DWORD,:DWORD,:DWORD,:DWORD
ExitProcess PROTO STDCALL :DWORD

MB_OK EQU 0
NULL EQU 0

.data
    message db 'Hello, World!', 0

.code
main PROC
    push MB_OK
    push offset message
    push offset message
    push NULL
    call MessageBoxA

    push 0
    call ExitProcess
main ENDP

end main

After the assembly file is written, we need to assemble the assembly before linking.

ml /c /coff /Fo"main.obj" "main.asm"

Next, we just need to link – but with a number of options (command switches).

link /SUBSYSTEM:WINDOWS /RELEASE /ALIGN:16 /OPT:REF /OPT:ICF /INCREMENTAL:NO /FUNCTIONPADMIN /MERGE:.rdata=.text /DEBUG:NONE /EMITPOGOPHASEINFO main.obj Kernel32.Lib User32.Lib

All of this will result in a fairly optimized and minimal executable, created by the Microsoft Visual Studio tools and Windows SDK.

The switch to remove debug information, /DEBUG:NONE /EMITPOGOPHASEINFO, is seemingly not documented.

To minimize the size of sections, for instance, .text and .data, the /ALIGN switch can be used with values set as powers of two.

All of this will result in a fairly optimized and minimal executable, created by the Microsoft Visual Studio tools and Windows SDK.

It also seems possible to generate smaller binaries based on the VS version used, the same file generated with VS 2017 is 850 bytes versus 1.1k bytes in VS 2022. The reasons for which, would need exploring further and any compatibility implications across windows versions.

You can also generate a pure C based executable too.

.386
.MODEL FLAT, STDCALL

;includelib ucrt.lib
;includelib legacy_stdio_definitions.lib

EXTERN printf: PROC

.data
    fmtStr db 'Foo bar baz', 0

.code
main PROC
    push offset fmtStr  ; Push the address of the format string onto the stack
    call printf         ; Call printf
    add esp, 4          ; Clean up the stack (4 bytes for 32-bit address)
    ret
main ENDP

END main

You can uncomment the library includes or pass them at the command line to the linker.

ml /c /coff /Fo"test1.obj" "test1.asm"
link /SUBSYSTEM:CONSOLE /RELEASE test1.obj ucrt.lib legacy_stdio_definitions.lib

If we actually disassemble the final executable in IDA. We will observe multiple subroutine calls leading to a call to __stdio_common_vfprintf; whereas we would naturally expect a call to printf.

This is due to the way modern C runtimes are implemented and the printf function being more complex than it appears. When we call it, the function must handle various format specifiers, width, precision, flags, and different types of arguments (integers, floats, strings, etc.) – which often requires a layered design for efficient and reusable code.

Similar Posts