Assembler for Windows on ARM

Write, build and debug ARM64 (AArch64) and ARM32 (Thumb-2) – without Visual Studio.

With .arch arm64 Asmonite makes a Windows file for ARM64 (AArch64, PE32+), with .arch thumb one for 32-bit ARM (ARM32, Thumb-2, PE32). The assembler writes the DOS header, the PE header, the sections and the import table itself; a program is a single text file. To check it, Dismonite reads the result back, and the debugger runs on a real Windows on ARM computer.

The examples are in the folder examples\windows_arm of the ZIP. Asmonite runs on an ordinary Windows computer to build; the result runs on a Windows on ARM computer.

ARM64: hello world

A program for Windows on ARM64: printf of the C runtime writes a line, exit ends it. File: examples\windows_arm\hello_arm64.asm.

You write
.format pe console
.arch arm64
.import msvcrt.dll printf, exit

.code
start:
  stp x29, x30, [sp, #-16]!
  adrp x0, msg
  add x0, x0, :lo12:msg   ; first argument in x0
  adrp x16, printf
  ldr x16, [x16, :lo12:printf]
  blr x16
  mov x0, #0
  adrp x16, exit
  ldr x16, [x16, :lo12:exit]
  blr x16

.data
msg db 'Hello, World!', 10, 0
Dismonite shows (from the finished .exe)
start:
  stp x29, x30, [sp, #-0x10]!
  adrp x0, section_data
  add x0, x0, #0
  adrp x16, import_table
  ldr x16, [x16, #0x40]
  blr x16
  mov x0, #0
  adrp x16, import_table
  ldr x16, [x16, #0x48]
  blr x16

The program is not tied to a fixed address: Windows puts an ARM64 program anywhere. So there are no absolute addresses. adrp loads the page (4 KB) of a label relative to the pc, :lo12: is the rest inside the page. The same pair fetches the address of an imported function from the import table (ldr x16, [x16, :lo12:printf]) and blr x16 calls it. The first argument is in x0, the others in x1 to x7; the result comes back in x0. On the right you see how Dismonite reads the same file again: the names of the imports have become positions in import_table, and Dismonite can turn it back into an .asm that gives the same bytes.

ARM32: Thumb-2

Windows on ARM runs 32-bit programs as Thumb-2 (PE32, machine ARMNT). The same program in 32 bit. File: examples\windows_arm\hello_arm32.asm.

You write
.format pe console
.arch thumb
.import msvcrt.dll printf, exit

.code
start:
  push {r4, lr}
  adr r0, msg             ; first argument in r0
  ldr r3, printf          ; the address of printf
  blx r3
  movs r0, #0
  ldr r3, exit
  blx r3

.data
msg db 'Hello, World!', 10, 0
Dismonite shows (from the finished .exe)
start:
  push {r4, lr}
  adr r0, msg
  ldr.w r3, [pc, #0x50]
  blx r3
  movs r0, #0
  ldr.w r3, [pc, #0x4c]
  blx r3

There are no page pairs here: adr and ldr with a label work from the pc and reach only 4 KB. So code, data and imports are in one section, and the assembler puts the import address within reach. The arguments are in r0 to r3, the rest on the stack. The suffix .w in the listing of Dismonite marks the long Thumb-2 form of an instruction (4 bytes).

A window: MessageBox

With .format pe gui you get a window program. This one opens a message box with MessageBoxA (examples\windows_arm\messagebox_arm64.asm).

.format pe gui
.arch arm64
.import user32.dll MessageBoxA
.import kernel32.dll ExitProcess

.code
start:
  stp x29, x30, [sp, #-16]!
  mov x0, #0              ; no owner window
  adrp x1, text
  add x1, x1, :lo12:text
  adrp x2, caption
  add x2, x2, :lo12:caption
  mov w3, #0              ; type: MB_OK
  adrp x16, MessageBoxA
  ldr x16, [x16, :lo12:MessageBoxA]
  blr x16
  mov x0, #0
  adrp x16, ExitProcess
  ldr x16, [x16, :lo12:ExitProcess]
  blr x16

.data
text db 'Hello from a message box!', 0
caption db 'messagebox_arm64', 0

The four arguments of MessageBoxA are in x0 to x3 (owner, text, caption, type). The window opens on the desktop where the program runs: on the same computer under the debugger, on the remote one with a debug server.

ARM64 and Thumb-2 compared

ARM64ARM32 (Thumb-2)
Directive.arch arm64.arch thumb
File kindPE32+, machine ARM64PE32, machine ARMNT
Argumentsx0 to x7r0 to r3, then the stack
Address of a labeladrp + add :lo12: (4 KB pages, any distance)adr / ldr from the pc (4 KB reach)
Call an importadrp + ldr + blrldr r3, name + blx r3
Instruction lengthalways 4 bytes2 or 4 bytes
Debug serverasmodbg64asmodbg

Which ARM files exist

The PE header of every Windows file says which processor it is for. For ARM there are more than two kinds; this is how Asmonite and Dismonite treat them.

MachineKindRuns onAsmoniteDismonite
0xAA64ARM64 (AArch64), PE32+Windows 10 and 11 on ARM64assemble, debug (.arch arm64)disassemble, debug
0x01C4ARM32 with Thumb-2 (ARMNT), PE32Windows RT (8, 8.1) and Windows 10 on ARM; Windows 11 on ARM64 no longer runs 32-bit ARM programsassemble, debug (.arch thumb)disassemble, debug
0x01C0, 0x01C2the old ARM (ARM mode and Thumb of before Thumb-2)Windows CE, Pocket PC, Windows Mobile, Embedded Compact; on no Windows PCno: there is no assembler for itonly 0x01C0: disassembled (the export writes the code as bytes with the instruction as comment), no debugging
0xA641, 0xA64EARM64EC and ARM64X (Windows 11)Windows 11 on ARM64nono

For comparison: on a Windows 10 computer with ARM64 (about 47,000 PE files) there were ARM64, ARM32 with Thumb-2, x86 and x64 files. Part of the x86 files hold hybrid code (an x86 header with ARM64 instructions, which Windows ships for the emulation) and pure language files (.mui). The old ARM was there only in files brought along by the user.

Build and debug

  1. Write and build. Open a file from examples\windows_arm in Asmonite and press F9, or asmoc hello_arm64.asm at the command prompt. The result is an .exe that any Windows on ARM computer runs.
  2. Debug server on the ARM computer. asmodbg (32 bit) and asmodbg64 (ARM64) are in the folder debugserver of the ZIP. They run where the program is to run. The connection is over ssh or directly on the network with a pairing code; on a Windows on ARM computer itself it also works locally.
  3. Debug. Choose the target Windows on ARM under Debug > Debug target. Breakpoints, single steps, registers, stack and memory are shown by Asmonite on the lines of the source. Registers can be changed while the program is stopped.
  4. Examine other programs. Dismonite opens any ARM or ARM64 .exe, shows the code with labels and comments and exports an .asm that Asmonite turns back into the same file.

The instruction info (F1) knows the ARM and ARM64 instructions: syntax, allowed operands, flags and an example.

Next

All examples

The small programs for Windows, DOS and ARM with their result.

The examples page →

Dismonite

Disassembler and debugger for x86, x64, ARM, ARM64 and Thumb-2.

dismonite.com ↗

Download

One ZIP with everything; unpack, start, done.

To the download →