Hello MessageBox on Windows ARM64
Description of a simple application written in ARM64 assembly on Windows
This is a short description of a “Hello world” style program for Windows ARM64 written in ARM64 assembly.
To read the article, you should have some familiarity with assembly programming in general, although not necessarily with ARM64. If you are new to assembly programming, I suggest starting with a simple 8-bit instruction set, like 6502 or Z80. A good way to start is Easy 6502 interactive ebook by Nick Morgan.
General set up for ARM64 assembly programming on Windows is described in my earlier article: Using ARM64 Assembly on Windows. The code described here is located in a GitHub repository nemtrif/win32armmsgbox.
Linker Directives
Our assembly source file starts with the following code:
; Linker directives
AREA |.drectve|, DRECTVE
IMPORT __imp_MessageBoxA
IMPORT __imp_ExitProcess
EXPORT WinMain
All commands in this section are assembler directives: instructions to the assembler itself that do not get translated into machine code.
The first directive is AREA which starts a new code or data section. In this case, we create the .drectve section of the Common Object File Format. This special COFF section contains linker options that are used by the linker and removed from the resulting image file. Note that .drectve
name is enclosed in single bars. The reason is that armasm64 supports only uppercase letters, lowercase letters, numeric characters, or the underscore character in symbol names; if we want to use a character outside of that range, like the dot character, we need to enclose the name in single bars.
The first linker directive in the section is IMPORT. The directive unconditionally imports a symbol that is defined in a separate object file. We import two WinAPI calls that we are going to use in our demo: MessageBox and ExitProcess. A few things to note:
Both calls are prefixed with __imp_. That means we are making direct calls, rather than going through the stub; for details, take a look at The case of a curious __imp_. by Dennis A Babkin.
MessageBox
call has a suffix “A”. Win32 API calls that take strings as parameters come in two flavors: “A” (ANSI) and “W” (wide). The “A” versions take strings aschar
arrays and the encoding is dependent on the current system locale. The “W” versions take strings aswchar_t
characters and are encoded as UTF-16.We do not specify the lib files where the symbols are defined (kernel32.lib and user32.lib) - we’ll do that when we invoke the linker.
EXPORT directive exposes the WinMain
to the linker so it can use it as the entry point. We are not using the C runtime library here, and the name of the entry function is not set in advance.
Read-only Initialized Data
The next section is .rdata
; Read-only initialized data - i.e. constants
AREA |.rdata|, DATA, READONLY
hello_world_string DCB "Hello, World!", 0x0
hi_string DCB "Hi!", 0x0
We again use the AREA assembler directive to introduce a data section into the object file. This time, we are creating a .rdata section which contains read-only initialized data such as string literals and other constants.
Here, we define two string literals we are going to use with our message box. One of them will represent the text displayed in the box and the other will be the caption.
In both cases, we use the DCB directive to allocate the memory and initialize the contents: each line starts with a label (i.e. hello_world_string), then comes the DCB directive itself, and finally a number of comma-separated expressions that can be either quoted strings or 8-bit integers.
As we are going to use both our string literals as parameters to a C function, we are adding a 0x0 (i.e. NULL) at their ends. Unlike with C, a quoted string in armasm64 does not implicitly end with a NULL and we have to manually add it.
Executable Code
Finally, we see the actual ARM64 instructions in the .text section:
; Executable code (free format)
AREA |.text|, CODE, ARM64
WinMain PROC
; Function prologue
; See https://devblogs.microsoft.com/oldnewthing/20220824-00/?p=107043
stp fp,lr,[sp,#-0x10]!
mov fp,sp
; Set up parameters for MessageBoxA function:
; int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
; hWnd (NULL) to x0
mov x0,#0
; lpText to x1
adrp x8,hello_world_string
add x1,x8,hello_world_string
; lpCaption to x2
adrp x8,hi_string
add x2,x8,hi_string
; uType = MB_OK (i.e. 0) to x3
mov w3,#0
; __imp_MessageBoxA address to x8
adrp x8,__imp_MessageBoxA
ldr x8,[x8,__imp_MessageBoxA]
; Call the function
blr x8
; Call ExitProcess(0)
; Raymond Chen explains why:
; https://devblogs.microsoft.com/oldnewthing/20100827-00/?p=13023
adrp x8,__imp_ExitProcess
ldr x8,[x8,__imp_ExitProcess]
mov w0,#0
blr x8
ENDP ; WinMain
END
As with other sections, we start with AREA
directive which instructs the assembler to create the .text code section of the object file. The CODE
attribute means that the section contains machine instructions, and ARM64
denotes the instruction set we are going to use.
Entire code for our little application is placed inside of a single function - WinMain. The function starts with the PROC directive (or its synonym FUNCTION
) and ends with the ENDP (or ENDFUNC
) directive.
In general, when programming in assembly we can organize the code in any way we like; however, when we run on top of an operating system such as Windows we want to follow the Aarch64 ABI (application binary interface). In particular, functions should be written and called in accordance with Procedure Call Standard for Aarch64.
WinMain
function starts with a function prologue. The STP instruction is used to push the fp, lr register pair on the stack and then we point fp
to the previous frame pointer value on the stack by using the MOV command.
Now we finally get to the interesting part. MessageBoxA
function takes 4 parameters which are passed in registers x0
-x3
:
A handle (64-bit unsigned integer) to the owner window of the message box we create. There is no owner, so we set register
x0
to0
with aMOV
command.A pointer to const null-terminated string that will be displayed as the message box text. As you can recall, we have allocated it in .rdata segment with
DCB
directive and labeled ithello_world_string
. Loading the string address intox1
register is a two-step process: first we call ADRP instruction to get the relative address to the 4kb page where the label is located and store it inx8
; then we add the offset to the page to calculate the address and store it inx1
.A pointer to the message box caption is loaded into
x2
. The mechanism is the same as for the previous parameter: adrp to get the address of the page, then add the offset into the page.The last parameter is an unsigned integer that specifies the type of the message box. We want it to be
0
(MSG_OK) so we call mov to load zero intow3
(which is just the lower 32 bits ofx3
).
With parameters loaded into appropriate registers, all we need to do is load the function address into a register and invoke BLR to jump to the address stored in the register. The mechanics are very similar to the pattern we used to get an address of a string literal: first we call adrp to get the page address; then we call LDR to load the function address to x8
.
Here we are not interested in the return value. If we were, we would have checked x0
after the function call.
If WinMain
was an ordinary function and not an entry to the program, we would now write a function epilogue to restore the stack and return to the caller. However, in this case we need to call ExitProcess
Windows API call to terminate the process. The procedure is equivalent (albeit simpler) to the call to MessageBoxA: we load the exit code parameter into x0
( or w0
), use the adrp + ldr combination to load the address of the function to x8
register and then call it via blr.
All that is needed to wrap up this little sample program is a couple of directives: first ENDP to end the function and then END to mark the end of the source file.