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.
A program for Windows on ARM64: printf of the C runtime writes a line, exit ends it. File: examples\windows_arm\hello_arm64.asm.
.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
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.
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.
.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
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).
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 | ARM32 (Thumb-2) | |
|---|---|---|
| Directive | .arch arm64 | .arch thumb |
| File kind | PE32+, machine ARM64 | PE32, machine ARMNT |
| Arguments | x0 to x7 | r0 to r3, then the stack |
| Address of a label | adrp + add :lo12: (4 KB pages, any distance) | adr / ldr from the pc (4 KB reach) |
| Call an import | adrp + ldr + blr | ldr r3, name + blx r3 |
| Instruction length | always 4 bytes | 2 or 4 bytes |
| Debug server | asmodbg64 | asmodbg |
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.
| Machine | Kind | Runs on | Asmonite | Dismonite |
|---|---|---|---|---|
0xAA64 | ARM64 (AArch64), PE32+ | Windows 10 and 11 on ARM64 | assemble, debug (.arch arm64) | disassemble, debug |
0x01C4 | ARM32 with Thumb-2 (ARMNT), PE32 | Windows RT (8, 8.1) and Windows 10 on ARM; Windows 11 on ARM64 no longer runs 32-bit ARM programs | assemble, debug (.arch thumb) | disassemble, debug |
0x01C0, 0x01C2 | the old ARM (ARM mode and Thumb of before Thumb-2) | Windows CE, Pocket PC, Windows Mobile, Embedded Compact; on no Windows PC | no: there is no assembler for it | only 0x01C0: disassembled (the export writes the code as bytes with the instruction as comment), no debugging |
0xA641, 0xA64E | ARM64EC and ARM64X (Windows 11) | Windows 11 on ARM64 | no | no |
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.
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.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..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.