Notepad is a bare-metal implementation of a text editor for the Microsoft Windows operating system, written entirely in Macro Assembler (MASM).
github.com

Notepad is a bare-metal implementation of a text editor for the Microsoft Windows operating system, written entirely in Macro Assembler (MASM). Unlike standard software development involving high-level abstractions (C#, C++, Python), this project interacts directly with the Win32 API and the CPU registers, bypassing the C Runtime (CRT) entirely.
This repository serves as a reference implementation for systems programmers, malware analysts, and computer science students studying the PE (Portable Executable) format, Windows message loops, and low-level memory management. It demonstrates the dichotomy between legacy x86 (Flat Memory Model) and modern x64 (Microsoft x64 ABI) calling conventions within a single codebase.
Pre-built binaries: notepad.zip
Contains the x86, x64 and native ARM64 executables, ready to run on Windows.
The source archive above carries the full tree for all three targets.
| Component | Specification | Notes |
|---|---|---|
| Assembler | ml.exe (x86) / ml64.exe (x64) / armasm64.exe (ARM64) | Microsoft Macro Assembler, and the ARM64 assembler for the third target |
| Linker | link.exe | Microsoft Incremental Linker |
| Subsystem | WINDOWS | Graphical User Interface (GUI) |
| Entry Point | start | Custom entry, no main() wrapper |
| Resource Compiler | rc.exe | Compiles menus, icons, and manifests |
The application relies strictly on standard dynamic link libraries found in all Windows versions since XP:
dwmapi.dll is deliberately absent from that list. The title bar colour and the
Mica backdrop are set through it, but it is resolved with LoadLibrary at run
time and released again, so it never enters the import table. On a system that
has no Desktop Window Manager every one of those calls degrades to nothing and
the editor still runs.
The application implements a standard Windows Event-Driven Architecture. It does not poll for input; rather, it yields CPU time until the Operating System pushes a message to the thread's message queue.
The entry point initializes the WNDCLASSEX structure and spawns the main window. It then enters an infinite loop, consuming approximately 0% CPU when idle.
; Pseudo-assembly representation of the core loop (x64)
MessageLoop:
mov rcx, OFFSET msg
xor rdx, rdx
xor r8, r8
xor r9, r9
call GetMessage ; Blocking call, waits for OS event
test eax, eax
jz ExitProgram ; WM_QUIT received
; Modeless Dialog Handling (Find/Replace)
mov rcx, hFindReplaceDlg
mov rdx, OFFSET msg
call IsDialogMessage ; Checks if msg belongs to Find/Replace dialog
test eax, eax
jnz MessageLoop ; If handled, skip Dispatch
call TranslateMessage ; Virtual-Key -> character
call DispatchMessage ; Route to WndProc
jmp MessageLoop
The dialog filter comes first on purpose. A modeless Find/Replace window
receives its keystrokes through the same queue as the main window, and without
IsDialogMessage the Tab key would not move between its fields.
The codebase highlights critical differences in assembly programming between the
three targets. The first two share an assembler and differ in calling
convention; the third differs in both.
ret n).invoke macro for simplified API calls.armasm64.exe, and this is the part that surprises peopleAREA instead of segment directives, DCB/DCD/DCQ instead ofdb/dd/dq, EXPORT/IMPORT instead of PUBLIC/EXTERN, and no STRUCTEQU offset. It is also notas: .section, .global and .quad belong to a different toolchainSTP x29, x30, [sp, #-16]!.ADRP plus ADD forms a 4 KB-page-relative address and itsOne manifest and one version resource serve all three images. The ARM64 build
reuses x64/notepad.rc, because a resource script carries no architecture of
its own.
Since malloc and free (C-Runtime) are unavailable, the application interfaces directly with the Windows Heap Manager via kernel32:
HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size)HeapFree(hHeap, 0, pMemory)This is critically used for:
The application uses Unicode (UTF-16 LE) throughout:
CreateWindowExW, SendMessageW, etc.MultiByteToWideCharInstead of using a basic EDIT control, the application uses RichEdit 2.0 (riched20.dll) which provides:
EM_FINDTEXTEXStyles: WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_NOHIDESEL
For word wrap toggle, horizontal scrolling is added/removed: WS_HSCROLL | ES_AUTOHSCROLL
File operations adhere to strict transactional steps to ensure data integrity:
GENERIC_READ or GENERIC_WRITEHeapAllocMultiByteToWideChar if neededThe search feature uses the Common Dialog Box Library (FindText / ReplaceText) for the UI, with search logic implemented via RichEdit messages:
EM_FINDTEXTEX with FINDTEXTEX structureEM_EXSETSEL to highlight matching textEM_REPLACESEL for text substitutionReal-time display of:
Ln X, Col YA Win32 window gets none of the modern Windows appearance for free, and the
surfaces that make it up answer to mechanisms that have nothing to do with each
other. theme.asm keeps them together, because that is the only way they stay
in step.
Dark mode. View -> Theme offers Use System Setting, Light and Dark.
The system option reads AppsUseLightTheme under
HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize and
follows WM_SETTINGCHANGE, so switching Windows over switches the editor with
it; an explicit Light or Dark choice deliberately ignores that. The registry is
read and never written - the program still creates no files and stores no
settings of its own, so the choice lives for the session only.
Three surfaces have to be told separately. The title bar is the Desktop Window
Manager's, set through DwmSetWindowAttribute with attribute 20, and with 19 as
well because Windows 10 builds before 19041 spelled it that way. The editor is a
RichEdit control: EM_SETBKGNDCOLOR paints its whole client area, including the
margin past the last line, and the text colour travels separately as a
CHARFORMAT2 with CFM_COLOR. That message also raises EN_CHANGE, whose
handler reads the modify flag and rewrites the title, so notifications are
silenced for the duration - otherwise the window opens claiming an untouched
document has been edited.
The status bar is painted by hand. comctl32 fills its own background from a
system colour that no message overrides and SetWindowTheme does not reach, so
in dark mode the part is marked SBT_OWNERDRAW and drawn in WM_DRAWITEM. In
light mode the control is left to draw itself, which it already does correctly.
Owner-draw keeps the pointer rather than the string, so the text is copied into
the module's own buffer first: a caller's stack buffer would be dangling by the
time the control repainted.
PerMonitorV2 DPI is implemented, not merely declared. The manifest asks for
it, which tells Windows to stop scaling the window on the application's behalf
and hand the job over as WM_DPICHANGED. Declaring it without handling the
message is worse than not declaring it at all - the window then keeps its 96 DPI
pixel sizes on a 192 DPI monitor and comes out half size. The handler rebuilds
the editor font through MulDiv and moves the window to the rectangle Windows
suggests, taken verbatim: computing one instead is how windows drift across
monitors on every change. The font is also rechecked once at startup, because a
window can open on a monitor whose DPI differs from the system value the first
font was built for, and on a stationary window no WM_DPICHANGED ever arrives
to correct it.
Shift+Minimise hides to the notification area. Plain Minimise keeps its
ordinary meaning; the shortcut is an addition, never a replacement, so nobody
loses a window by reflex. Double-clicking the icon brings it back, right-clicking
offers Restore and Exit, and Exit restores first so the save prompt appears on a
window that can be seen. The icon is IDI_APPLICATION from the system - the
resource script carries no icon of its own, and inventing one would cost bytes
for nothing.
| Shortcut | Action |
|---|---|
| Ctrl+N | New document |
| Ctrl+O | Open file |
| Ctrl+S | Save file |
| Ctrl+Shift+S | Save As |
| Ctrl+P | |
| Ctrl+Z | Undo |
| Ctrl+X | Cut |
| Ctrl+C | Copy |
| Ctrl+V | Paste |
| Ctrl+A | Select All |
| Ctrl+F | Find |
| Ctrl+H | Replace |
| F3 | Find Next |
| Shift+F3 | Find Previous |
| Del | Delete selection |
| Shift+Minimise | Hide to the notification area |
| Metric | Notepad ASM (x64) | Notepad ASM (x86) | MS Notepad (Win11) | VS Code |
|---|---|---|---|---|
| Disk Usage | ~20 KB | ~18 KB | ~200 KB + Deps | ~300 MB |
| RAM Usage (Idle) | ~1.5 MB | ~1.2 MB | ~12 MB | ~400 MB |
| Startup Time | < 10ms | < 10ms | ~200ms | ~2500ms |
| Dependencies | System DLLs only | System DLLs only | UWP / CRT | Electron / Node.js |
Note: The tiny memory footprint is due to the lack of garbage collection, JIT compilation, or interpreted runtime environments. The application maps directly to OS pages.
The project includes a PowerShell build script (build.ps1) that automates the assembly and linking process.
Clone the repository:
git clone https://github.com/wesmar/notepad.git
cd notepad
Run the Build Script:
.\build.ps1
The script will:
bin/ folderManual Compilation (x64 Example):
cd x64
rc /c65001 notepad.rc
ml64 /c /Cp /Cx /Zd /Zf /Zi main.asm
ml64 /c /Cp /Cx /Zd /Zf /Zi file.asm
ml64 /c /Cp /Cx /Zd /Zf /Zi edit.asm
link main.obj file.obj edit.obj notepad.res /subsystem:windows /entry:start /out:Notepad_x64.exe /MANIFEST:EMBED /MANIFESTINPUT:notepad.manifest kernel32.lib user32.lib gdi32.lib comdlg32.lib shell32.lib shlwapi.lib comctl32.lib
This project is not merely a tool, but a pedagogical instrument for:
Reverse Engineering Training:
Malware Analysis Research:
Operating Systems Study:
notepad/
├── bin/ # Compiled executables
│ ├── Notepad_x86.exe # 32-bit executable (~21 KB)
│ ├── Notepad_x64.exe # 64-bit executable (~24 KB)
│ └── Notepad_arm64.exe # native ARM64 executable (~19 KB)
├── x86/ # 32-bit source files
│ ├── main.asm # Entry point, WinMain, WndProc
│ ├── file.asm # File operations (New, Open, Save, Print)
│ ├── edit.asm # Edit functions (Find, Replace, Status Bar)
│ ├── theme.asm # Dark mode, Mica, DPI, owner-drawn status bar
│ ├── tray.asm # Shift+Minimise to the notification area
│ ├── data.inc # Data structures, constants, variables
│ ├── proto.inc # Function prototypes, API declarations
│ ├── notepad.rc # Resource script (manifest reference)
│ └── notepad.manifest # Application manifest (DPI awareness, etc.)
├── x64/ # 64-bit source files
│ ├── main.asm # Entry point, WinMain, WndProc (x64 ABI)
│ ├── file.asm # File operations (x64 calling convention)
│ ├── edit.asm # Edit functions (x64 calling convention)
│ ├── theme.asm # Dark mode, Mica, DPI, owner-drawn status bar
│ ├── tray.asm # Shift+Minimise to the notification area
│ ├── data.inc # Data structures (64-bit handles, alignment)
│ ├── proto.inc # Function prototypes (EXTERN declarations)
│ ├── notepad.rc # Resource script
│ └── notepad.manifest # Application manifest
├── arm64/ # native ARM64 source files (armasm64 syntax)
│ ├── main.asm # Entry point, WinMain, WndProc (AAPCS64)
│ ├── file.asm # File operations (AAPCS64)
│ ├── edit.asm # Edit functions (AAPCS64)
│ ├── theme.asm # Dark mode, Mica, DPI, owner-drawn status bar
│ ├── tray.asm # Shift+Minimise to the notification area
│ ├── constants.inc # EQU constants and hand-written struct offsets
│ └── globals.inc # IMPORT declarations for the shared globals
├── build.ps1 # Automated build pipeline
├── LICENSE.md # MIT License
└── README.md # Documentation
View -> Theme lasts for the session. The program writes nothing to the registry and creates no files of its own, and that is worth more than persistence.MIT License. Free for academic, personal, and commercial use. Attribution to the original author is appreciated but not mandatory.
Marek Wesolowski
Project Repository: https://github.com/wesmar/notepad
Add a comment