16
Stand-Alone Assembly Language Programs

Until now, this book has relied upon a C/C++ main program to call the example code written in assembly language. Although this is probably the biggest use of assembly language in the real world, it is also possible to write stand-alone code (no C/C++ main program) in assembly language.

In the context of this chapter, stand-alone assembly language programs means that you’re writing an executable program in assembly that does not directly link into a C/C++ program for execution. Without a C/C++ main program calling your assembly code, you’re not dragging along the C/C++ library code and runtime system, so your programs can be smaller and you won’t have external naming conflicts with C/C++ public names. However, you’ll have to do much of the work yourself that C/C++ libraries do by writing comparable assembly code or calling the Win32 API.

The Win32 API is a bare-metal interface to the Windows operating system that provides thousands of functions you can call from a stand-alone assembly language program—far too many to consider in this chapter. This chapter provides a basic introduction to Win32 applications (especially console-based applications). This information will get you started writing stand-alone assembly language programs under Windows.

To use the Win32 API from your assembly programs, you’ll need to download the MASM32 library package from https://www.masm32.com/.1 Most of the examples in this chapter assume the MASM32 64-bit include files are available on your system in the C:\masm32 subdirectory.

16.1 Hello World, by Itself

Before showing you some of the wonders of Windows stand-alone assembly language programming, perhaps the best place to start is at the beginning: with a stand-alone “Hello, world!” program (Listing 16-1).

; Listing 16-1.asm

; A stand-alone assembly language version of 
; the ubiquitous "Hello, world!" program.

; Link in the Windows Win32 API:

            includelib kernel32.lib

; Here are the two Windows functions we will need
; to send "Hello, world!" to the standard console device:

            extrn __imp_GetStdHandle:proc
            extrn __imp_WriteFile:proc

            .code
hwStr       byte    "Hello World!"
hwLen       =       $-hwStr

; This is the honest-to-goodness assembly language
; main program:

main        proc
            
; On entry, stack is aligned at 8 mod 16. Setting aside
; 8 bytes for "bytesWritten" ensures that calls in main have
; their stack aligned to 16 bytes (8 mod 16 inside function),
; as required by the Windows API (which __imp_GetStdHandle and
; __imp_WriteFile use. They are written in C/C++).
            
            lea     rbx, hwStr
            sub     rsp, 8
            mov     rdi, rsp      ; Hold # of bytes written here

; Note: must set aside 32 bytes (20h) for shadow registers for
; parameters (just do this once for all functions). 
; Also, WriteFile has a 5th argument (which is NULL), 
; so we must set aside 8 bytes to hold that pointer (and
; initialize it to zero). Finally, stack must always be 
; 16-byte-aligned, so reserve another 8 bytes of storage
; to ensure this.

            sub     rsp, 030h  ; Shadow storage for args
                    
; Handle = GetStdHandle(-11);
; Single argument passed in ECX.
; Handle returned in RAX.

            mov     rcx, -11                     ; STD_OUTPUT
            call    qword ptr __imp_GetStdHandle ; Returns handle
                                                 ; in RAX
                    
; WriteFile(handle, "Hello World!", 12, &bytesWritten, NULL);
; Zero out (set to NULL) "lpOverlapped" argument:
            
            xor     rcx, rcx
            mov     [rsp + 4 * 8], rcx
            
            mov     r9, rdi    ; Address of "bytesWritten" in R9
            mov     r8d, hwLen ; Length of string to write in R8D
            lea     rdx, hwStr ; Ptr to string data in RDX
            mov     rcx, rax   ; File handle passed in RCX
            call    qword ptr __imp_WriteFile
            
; Clean up stack and return:

            add     rsp, 38h
            ret
main        endp
            end

Listing 16-1: Stand-alone “Hello, world!” program

The __imp_GetStdHandle and __imp_WriteFile procedures are functions inside Windows (they are part of the so-called Win32 API, even though this is 64-bit code that is executing). The __imp_GetStdHandle procedure, when passed the (admittedly magic) number –11 as an argument, returns a handle to the standard output device. With this handle, calls to __imp_WriteFile will send the output to the standard output device (the console). To build and run this program, use the following command:

ml64 listing16-1.asm /link /subsystem:console /entry:main

The MASM /link command line option tells it that the following commands (to the end of the line) are to be passed on to the linker. The /subsystem:console (linker) command line option tells the linker that this program is a console application (that is, it will run in a command line window). The /entry:main linker option passes along the name of the main program to the linker. The linker stores this address in a special location in the executable file so Windows can determine the starting address of the main program after it loads the executable file into memory.

16.2 Header Files and the Windows Interface

Near the beginning of the “Hello, world!” example in Listing 16-1, you’ll notice the following lines:

includelib kernel32.lib

; Here are the two Windows functions we will need
; to send "Hello, world!" to the standard console device:

extrn __imp_GetStdHandle:proc
extrn __imp_WriteFile:proc

The kernel32.lib library file contains the object module definitions for many of the Win32 API functions, including the __imp_GetStdHandle and __imp_WriteFile procedures. Inserting extrn directives for all the Win32 API functions into your assembly language programs is an incredible amount of work. The proper way to deal with these function definitions is to include them in a header (include) file and then include that file in every application you write that uses the Win32 API functions.

The bad news is that creating an appropriate set of header files is a gargantuan task. The good news is that somebody else has already done all that work for you: the MASM32 headers. Listing 16-2 is a rework of Listing 16-1 that uses the MASM32 64-bit include files to obtain the Win32 external declarations. Note that we incorporate MASM32 via an include file, listing16-2.inc, rather than use it directly. This will be explained in a moment.

; Listing 16-2

            include    listing16-2.inc
            includelib kernel32.lib               ; File I/O library

; Include just the files we need from masm64rt.inc:

;           include \masm32\include64\masm64rt.inc
;           OPTION DOTNAME                        ; Required for macro files
;           option casemap:none                   ; Case sensitive
;           include \masm32\include64\win64.inc
;           include \masm32\macros64\macros64.inc
;           include \masm32\include64\kernel32.inc

            .data
bytesWrtn   qword   ?
hwStr       byte    "Listing 16-2", 0ah, "Hello, World!", 0
hwLen       =       sizeof hwStr

            .code
         
**********************************************************

; Here is the "asmMain" function.
        
            public  asmMain
asmMain     proc
            push    rbx
            push    rsi
            push    rdi
            push    r15
            push    rbp
            mov     rbp, rsp
            sub     rsp, 56            ; Shadow storage
            and     rsp, -16
            
            mov     rcx, -11           ; STD_OUTPUT
            call    __imp_GetStdHandle ; Returns handle
             
            xor     rcx, rcx
            mov     bytesWrtn, rcx
            
            lea     r9, bytesWrtn      ; Address of "bytesWritten" in R9
            mov     r8d, hwLen         ; Length of string to write in R8D 
            lea     rdx, hwStr         ; Ptr to string data in RDX
            mov     rcx, rax           ; File handle passed in RCX
            call    __imp_WriteFile
                         
allDone:    leave
            pop     r15
            pop     rdi
            pop     rsi
            pop     rbx
            ret     ; Returns to caller
asmMain     endp
            end

Here’s the listing16-2.inc include file:

; listing16-2.inc

; Header file entries extracted from MASM32 header
; files (placed here rather than including the 
; full MASM32 headers to avoid namespace pollution
; and speed up assemblies).

PPROC           TYPEDEF PTR PROC        ; For include file prototypes

externdef __imp_GetStdHandle:PPROC
externdef __imp_WriteFile:PPROC

Listing 16-2: Using the MASM32 64-bit include files

Here’s the build command and sample output:

C:\>ml64 /nologo listing16-2.asm kernel32.lib /link /nologo /subsystem:console /entry:asmMain
 Assembling: listing16-2.asm

C:\>listing16-2
Listing 16-2
Hello, World!

The MASM32 include file

include \masm32\include64\masm64rt.inc

includes all the other hundreds of include files that are part of the MASM32 64-bit system. Sticking this include directive into your programs provides your application with access to a huge number of Win32 API functions, data declarations, and other goodies (such as MASM32 macros).

However, your computer will pause for a bit when you assemble your source file. That’s because that single include directive winds up including many tens of thousands of lines of code into your program during assembly. If you know which header file(s) contain the actual declarations you want to use, you can speed up your compilations by including just the files you need (as was done in listing16-2.asm using the MASM32 64-bit include files).

Including masm64rt.inc into your programs has one other problem: namespace pollution. The MASM32 include file introduces thousands and thousands of symbols into your program, and there is a chance a symbol you want to use has already been defined in the MASM32 include files (for a different purpose than the one you have in mind). If you have a file grep utility, a program that searches through files in a directory and recursively in subdirectories for a particular string, you can easily locate all occurrences of a particular symbol you want to use in your file and copy that symbol’s definition into your own source file (or, better yet, into a header file you create specifically for this purpose). This is the approach this chapter uses for many of the example programs.

16.3 The Win32 API and the Windows ABI

The Win32 API functions all adhere to the Windows ABI calling convention. This means that calls to these functions can modify all the volatile registers (RAX, RCX, RDX, R8, R9, R10, R11, and XMM0 to XMM5) but must preserve the nonvolatile registers (the others not listed here). Also, API calls pass parameters in RDX, RCX, R8, R9 (and XMM0 to XMM3), and then on the stack; the stack must be 16-byte-aligned prior to the API call. See the discussion of the Windows ABI throughout this book for more details.

16.4 Building a Stand-Alone Console Application

Take a look at the (simplified) build command from the preceding section:2

ml64 listing16-2.asm /link /subsystem:console /entry:asmMain

The /subsystem:console option tells the linker that in addition to possible GUI windows the application might create, the system must also create a special window for the application to display console information. If you run the program from a Windows command line, it uses the already-open console window of the cmd.exe program.

16.5 Building a Stand-Alone GUI Application

To create a pure Windows GUI application that does not also open up a console window, you can specify /subsystem:windows rather than /subsystem:console. The simple dialog box application in Listing 16-3 is an example of an especially simple Windows application. It displays a simple dialog box and then quits when the user clicks the OK button in the dialog box.

; Listing 16-3

; Dialog box demonstration.

            include    listing16-3.inc
            includelib user32.lib

          ; include \masm32\include64\masm64rt.inc

            .data

msg         byte    "Dialog Box Demonstration",0
DBTitle     byte    "Dialog Box Title", 0
            
            .code
         
**********************************************************

; Here is the "asmMain" function.
        
            public  asmMain
asmMain     proc
            push    rbp
            mov     rbp, rsp
            sub     rsp, 56         ; Shadow storage
            and     rsp, -16
            
            xor     rcx, rcx        ; HWin = NULL
            lea     rdx, msg        ; Message to display
            lea     r8, DBTitle     ; Dialog box title
            mov     r9d, MB_OK      ; Has an "OK" button
            call    MessageBox
                         
allDone:    leave
            ret     ; Returns to caller
asmMain     endp
            end

Listing 16-3: A simple dialog box application

Here’s the listing16-3.inc include file:

; listing16-3.inc

; Header file entries extracted from MASM32 header
; files (placed here rather than including the 
; full MASM32 headers to avoid namespace pollution
; and speed up assemblies).

PPROC           TYPEDEF PTR PROC        ; For include file prototypes

MB_OK                                equ 0h

externdef __imp_MessageBoxA:PPROC
MessageBox equ <__imp_MessageBoxA>

Here is the build command for the program in Listing 16-3:

C:\>ml64 listing16-3.asm /link /subsystem:windows /entry:asmMain

Figure 16-1 shows the runtime output from Listing 16-3.

file:///Users/DisPater/Desktop/Hyde501089/Production/IndesignFiles/image_fi/501089c16/f16001.tiff

Figure 16-1: Sample dialog box output

16.6 A Brief Look at the MessageBox Windows API Function

Although creating GUI applications in assembly language is well beyond the scope of this book, the MessageBox function is sufficiently useful (even in console applications) to be worth a special mention.

The MessageBox function has four parameters:

  1. RCX Window handle. This is usually NULL (0), implying that the message box is a stand-alone dialog box that is not associated with any particular window.
  2. RDX Message pointer. RDX contains a pointer to a zero-terminated string that will be displayed in the body of the message box.
  3. R8 Window title. R8 contains a pointer to a zero-terminated string that is displayed in the title bar of the message box window.
  4. R9D Message box type. This is an integer value that specifies the type of buttons and other icons appearing in the message box. Typical values are the following: MB_OK, MB_OKCANCEL, MB_ABORTRETRYIGNORE, MB_YESNOCANCEL, MB_YESNO, and MB_RETRYCANCEL.

The MessageBox function returns an integer value in RAX corresponding to the button that was pressed (if MB_OK was specified, that’s the value that the message box returns when the user clicks the OK button).

16.7 Windows File I/O

One thing missing from most of the example code in this book has been a discussion of file I/O. Although you can easily make C Standard Library calls to open, read, write, and close files, it seemed appropriate to use file I/O as an example in this chapter to cover this missing detail.

The Win32 API provides many useful functions for file I/O: reading and writing file data. This section describes a small number of these functions:

  1. CreateFileA A function (despite its name) that you use to open existing files or create new files
  2. WriteFile A function that writes data to a file
  3. ReadFile A function that reads data from a file
  4. CloseHandle A function that closes a file and flushes any cached data to the storage device
  5. GetStdHandle A function, which you’ve already seen, that returns the handle of one of the standard input or output devices (standard input, standard output, or standard error)
  6. GetLastError A function you can use to retrieve a Windows error code if an error occurs in the execution of any of these functions

Listing 16-4 demonstrates the use of these functions as well as the creation of some useful procedures that call these functions. Note that this code is rather long, so I’ve taken the liberty of breaking it into smaller chunks, with individual explanations in front of each section.

The Win32 file I/O functions are all part of the kernel32.lib library module. Therefore, Listing 16-4 uses the includelib kernel32.lib statement to automatically link in this library during the build phase. To speed up assembly and reduce namespace pollution, this program does not automatically include all of the MASM32 equate files (via an include \masm32\include64\masm64rt.inc statement). Instead, I’ve collected all the necessary equates and other definitions from the MASM32 header files and placed them in the listing16-4.inc header file (which appears a little later in this chapter). Finally, the program also includes the aoalib.inc header file, just to use a few of the constants defined in that file (such as cr and nl):

; Listing 16-4 

; File I/O demonstration.

            include    listing16-4.inc
            include    aoalib.inc   ; To get some constants
            includelib kernel32.lib ; File I/O library

            .const
prompt      byte    "Enter (text) filename:", 0
badOpenMsg  byte    "Could not open file", cr, nl, 0

            .data

inHandle    dword   ?
inputLn     byte    256 dup (0)

fileBuffer  byte    4096 dup (0)

The following code constructs wrapper code around each of the file I/O functions to preserve the volatile register values. These functions use the following macro definitions to save and restore the register values:

            .code

rcxSave     textequ <[rbp - 8]>
rdxSave     textequ <[rbp - 16]>
r8Save      textequ <[rbp - 24]>
r9Save      textequ <[rbp - 32]>
r10Save     textequ <[rbp - 40]>
r11Save     textequ <[rbp - 48]>
xmm0Save    textequ <[rbp - 64]>
xmm1Save    textequ <[rbp - 80]>
xmm2Save    textequ <[rbp - 96]>
xmm3Save    textequ <[rbp - 112]>
xmm4Save    textequ <[rbp - 128]>
xmm5Save    textequ <[rbp - 144]>
var1        textequ <[rbp - 160]>

mkActRec    macro
            push    rbp
            mov     rbp, rsp
            sub     rsp, 256        ; Includes shadow storage
            and     rsp, -16        ; Align to 16 bytes
            mov     rcxSave, rcx
            mov     rdxSave, rdx
            mov     r8Save, r8
            mov     r9Save, r9
            mov     r10Save, r10
            mov     r11Save, r11
            movdqu  xmm0Save, xmm0
            movdqu  xmm1Save, xmm1
            movdqu  xmm2Save, xmm2
            movdqu  xmm3Save, xmm3
            movdqu  xmm4Save, xmm4
            movdqu  xmm5Save, xmm5
            endm
     
rstrActRec  macro
            mov     rcx, rcxSave
            mov     rdx, rdxSave
            mov     r8, r8Save 
            mov     r9, r9Save 
            mov     r10, r10Save
            mov     r11, r11Save
            movdqu  xmm0, xmm0Save
            movdqu  xmm1, xmm1Save
            movdqu  xmm2, xmm2Save
            movdqu  xmm3, xmm3Save
            movdqu  xmm4, xmm4Save
            movdqu  xmm5, xmm5Save
            leave
            endm

The first function appearing in Listing 16-4 is getStdOutHandle. This is a wrapper function around __imp_GetStdHandle that preserves the volatile registers and explicitly requests the standard output device handle. This function returns the standard output device handle in the RAX register. Immediately following getStdOutHandle are comparable functions that retrieve the standard error handle and the standard input handle:

; getStdOutHandle - Returns stdout handle in RAX:

getStdOutHandle proc
                mkActRec
                mov     rcx, STD_OUTPUT_HANDLE
                call    __imp_GetStdHandle  ; Returns handle
                rstrActRec
                ret
getStdOutHandle endp
                
; getStdErrHandle - Returns stderr handle in RAX:

getStdErrHandle proc
                mkActRec
                mov     rcx, STD_ERROR_HANDLE
                call    __imp_GetStdHandle  ; Returns handle
                rstrActRec
                ret
getStdErrHandle endp

; getStdInHandle - Returns stdin handle in RAX:

getStdInHandle proc
               mkActRec
               mov     rcx, STD_INPUT_HANDLE
               call    __imp_GetStdHandle   ; Returns handle
               rstrActRec
               ret
getStdInHandle endp

Now consider the wrapper code for the write function:

; write - Write data to a file handle.
 
; RAX - File handle.
; RSI - Pointer to buffer to write.
; RCX - Length of buffer to write.

; Returns:

; RAX - Number of bytes actually written
;       or -1 if there was an error.
            
write       proc
            mkActRec
            
            mov     rdx, rsi        ; Buffer address
            mov     r8, rcx         ; Buffer length
            lea     r9, var1        ; bytesWritten
            mov     rcx, rax        ; Handle
            xor     r10, r10        ; lpOverlapped is passed
            mov     [rsp+4*8], r10  ; on the stack
            call    __imp_WriteFile
            test    rax, rax        ; See if error
            mov     rax, var1       ; bytesWritten
            jnz     rtnBytsWrtn     ; If RAX was not zero
            mov     rax, -1         ; Return error status

rtnBytsWrtn:
            rstrActRec
            ret
write       endp

The write function writes data from a memory buffer to the output file specified by a file handle (which could also be the standard output or standard error handle, if you want to write data to the console). The write function expects the following parameter data:

  1. RAX File handle specifying the write destination. This is typically a handle obtained by the open or openNew functions (a little later in the program) or the getStdOutHandle and getStdErrHandle functions.
  2. RSI Address of the buffer containing the data to write to the file.
  3. RCX Number of bytes of data to write to the file (from the buffer).

This function does not follow the Windows ABI calling convention. Although there isn’t an official assembly language calling convention, many assembly language programmers tend to use the same registers that the x86-64 string instructions use. For example, the source data (buffer) is passed in RSI (the source index register), and the count (buffer size) parameter appears in the RCX register. The write procedure moves the data to appropriate locations for the call to __imp_WriteFile (as well as sets up additional parameters).

The __imp_WriteFile function is the actual Win32 API write function (technically, __imp_WriteFile is a pointer to the function; the call instruction is an indirect call through this pointer). The __imp_WriteFile has the following arguments:

  1. RCX File handle.
  2. RDX Buffer address.
  3. R8 Buffer size (really, 32 bits in R8D).
  4. R9 Address of a dword variable to receive the number of bytes written to the file; this will equal the buffer size if the write operation is successful.
  5. [rsp + 32] lpOverlapped value; just set this to NULL (0). As per the Windows ABI, callers pass all parameters beyond the fourth parameter on the stack, leaving room (shadow parameters) for the first four.

On return from __imp_WriteFile, RAX contains a nonzero value (true) if the write was successful, and zero (false) if there was an error. If there was an error, you can call the Win32 GetLastError function to retrieve the error code.

Note that the write function returns the number of bytes written to the file in the RAX register. If there was an error, write returns -1 in the RAX register.

Next up are the puts and newLn functions:

; puts - Outputs a zero-terminated string to standard output device.

; RSI - Address of string to print to standard output.

            .data
stdOutHnd   qword   0
hasSOHndl   byte    0

            .code
puts        proc
            push    rax
            push    rcx
            cmp     hasSOHndl, 0
            jne     hasHandle

            call    getStdOutHandle
            mov     stdOutHnd, rax
            mov     hasSOHndl, 1

; Compute the length of the string:
            
hasHandle:  mov     rcx, -1
lenLp:      inc     rcx
            cmp     byte ptr [rsi][rcx * 1], 0
            jne     lenLp
            
            mov     rax, stdOutHnd
            call    write

            pop     rcx
            pop     rax
            ret
puts        endp

; newLn - Outputs a newline sequence to the standard output device:

newlnSeq    byte    cr, nl

newLn       proc
            push    rax
            push    rcx
            push    rsi
            cmp     hasSOHndl, 0
            jne     hasHandle
            
            call    getStdOutHandle
            mov     stdOutHnd, rax
            mov     hasSOHndl, 1

hasHandle:  lea     rsi, newlnSeq
            mov     rcx, 2
            mov     rax, stdOutHnd
            call    write
                                   
            pop     rsi
            pop     rcx
            pop     rax
            ret
newLn       endp

The puts and newLn procedures write strings to the standard output device. The puts function writes a zero-terminated string whose address you pass in the RSI register. The newLn function writes a newline sequence (carriage return and line feed) to the standard output device.

These two functions have a tiny optimization: they call getStdOutHandle only once to obtain the standard output device handle. On the first call to either of these functions, they call getStdOutHandle and cache the result (in the stdOutHnd variable) and set flag (hasSOHndl) that indicates that the cached value is valid. Thereafter, these functions use the cached value rather than continually calling getStdOutHandle to retrieve the standard output device handle.

The write function requires a buffer length; it does not work on zero-terminated strings. Therefore, the puts function must explicitly determine the length of the zero-terminated string before calling write. The newLn function doesn’t have to do this because it knows the length of the carriage return and line feed sequence (two characters).

The next function in Listing 16-4 is the wrapper for the read function:

; read - Read data from a file handle.

; EAX - File handle.
; RDI - Pointer to buffer receive data.
; ECX - Length of data to read.

; Returns:

; RAX - Number of bytes actually read
;       or -1 if there was an error.
            
read        proc
            mkActRec
            
            mov     rdx, rdi        ; Buffer address
            mov     r8, rcx         ; Buffer length
            lea     r9, var1        ; bytesRead
            mov     rcx, rax        ; Handle
            xor     r10, r10        ; lpOverlapped is passed
            mov     [rsp+4*8], r10  ; on the stack
            call    __imp_ReadFile
            test    rax, rax        ; See if error
            mov     rax, var1       ; bytesRead
            jnz     rtnBytsRead     ; If RAX was not zero
            mov     rax, -1         ; Return error status

rtnBytsRead:
            rstrActRec
            ret
read        endp

The read function is the input analog to the write function. The parameters are similar (note, however, that read uses RDI as the destination address for the buffer parameter):

  1. RAX File handle.
  2. RDI Destination buffer to store data read from file.
  3. RCX Number of bytes to read from the file.

The read function, a wrapper around the Win32 API __imp_ReadFile function, has the following arguments:

  1. RCX File handle.
  2. RDX File buffer address.
  3. R8 Number of bytes to read.
  4. R9 Address of dword variable to receive the number of bytes actually read.
  5. [rsp + 32] Overlapped operation; should be NULL (0). As per the Windows ABI, callers pass all parameters beyond the fourth parameter on the stack, leaving room (shadow parameters) for the first four.

The read function returns -1 in RAX if there was an error during the read operation. Otherwise, it returns the actual number of bytes read from the file. This value can be less than the requested read amount if the read operation reaches the end of the file (EOF). A 0 return value generally indicates EOF has been reached.

The open function opens an existing file for reading, writing, or both. It is a wrapper function for the Windows CreateFileA API call:

; open - Open existing file for reading or writing.

; RSI - Pointer to filename string (zero-terminated).
; RAX - File access flags.
;       (GENERIC_READ, GENERIC_WRITE, or
;       "GENERIC_READ + GENERIC_WRITE")

; Returns:

; RAX - Handle of open file (or INVALID_HANDLE_VALUE if there
;       was an error opening the file).

open        proc
            mkActRec
            
            mov     rcx, rsi               ; Filename
            mov     rdx, rax               ; Read and write access
            xor     r8, r8                 ; Exclusive access
            xor     r9, r9                 ; No special security
            mov     r10, OPEN_EXISTING     ; Open an existing file
            mov     [rsp + 4 * 8], r10     
            mov     r10, FILE_ATTRIBUTE_NORMAL
            mov     [rsp + 5 * 8], r10
            mov     [rsp + 6 * 8], r9      ; NULL template file
            call    __imp_CreateFileA
            rstrActRec
            ret
open        endp

The open procedure has two parameters:

  1. RSI A pointer to a zero-terminated string containing the filename of the file to open.
  2. RAX A set of file access flags. These are typically the constants GENERIC_READ (to open a file for reading), GENERIC_WRITE (to open a file for writing), or GENERIC_READ + GENERIC_WRITE (to open a file for reading and writing).

The open function calls the Windows CreateFileA function after setting up the appropriate parameters for the latter. The A suffix on CreateFileA stands for ASCII. This particular function expects the caller to pass an ASCII filename. Another function, CreateFileW, expects Unicode filenames, encoded as UTF-16. Internally, Windows uses Unicode filenames; when you call CreateFileA, it converts the ASCII filename to Unicode and then calls CreateFileW. The open function sticks with ASCII characters.

The CreateFileA function has the following parameters:

  1. RCX Pointer to zero-terminated (ASCII) string holding the name of the file to open.
  2. RDX Read and write access flags (GENERIC_READ and GENERIC_WRITE).
  3. R8 Sharing mode flag (0 means exclusive access). Controls whether another process can access the file while the current process has it open. Possible flag values are FILE_SHARE_READ, FILE_SHARE_WRITE, and FILE_SHARE_DELETE (or a combination of these).
  4. R9 Pointer to a security descriptor. The open function doesn’t specify any special security; it simply passes NULL (0) as this argument.
  5. [rsp + 32] This parameter holds the creation disposition flag. The open function opens an existing file, so it passes OPEN_EXISTING here. Other possible values are CREATE_ALWAYS, CREATE_NEW, OPEN_ALWAYS, OPEN_EXISTING, or TRUNCATE_EXISTING. The OPEN_EXISTING value requires that the file exists, or it will return an open error. Being the fifth parameter, this is passed on the stack (in the fifth 64-bit slot).
  6. [rsp + 40] This parameter contains the file attributes. This function simply uses the FILE_ATTRIBUTE_NORMAL attribute (for example, not read-only).
  7. [rsp + 48] This parameter is a pointer to a file template handle. The open function doesn’t use a file template, so it passes NULL (0) in this argument.

The open function returns a file handle in the RAX register. If there was an error, this function returns INVALID_HANDLE_VALUE in RAX.

The openNew function is also a wrapper around the CreateFileA function:

; openNew - Creates a new file and opens it for writing.

; RSI - Pointer to filename string (zero-terminated).

; Returns:

; RAX - Handle of open file (or INVALID_HANDLE_VALUE if there
;       was an error opening the file).

openNew     proc
            mkActRec
            
            mov     rcx, rsi                         ; Filename
            mov     rdx, GENERIC_WRITE+GENERIC_WRITE ; Access
            xor     r8, r8                           ; Exclusive access
            xor     r9, r9                           ; No security
            mov     r10, CREATE_ALWAYS               ; Open a new file
            mov     [rsp + 4 * 8], r10 
            mov     r10, FILE_ATTRIBUTE_NORMAL
            mov     [rsp + 5 * 8], r10
            mov     [rsp + 6 * 8], r9                ; NULL template
            call    __imp_CreateFileA
            rstrActRec
            ret
openNew     endp

openNew creates a new (empty) file on the disk. If the file previously existed, openNew will delete it before opening the new file. This function is almost identical to the preceding open function, with the following two differences:

The closeHandle function is a simple wrapper around the Windows CloseHandle function. You pass the file handle of the file to close in the RAX register. This function returns 0 in RAX if there was an error, or a nonzero file if the file close operation was successful. The only purpose of this wrapper is to preserve all the volatile registers across the call to the Windows CloseHandle function:

; closeHandle - Closes a file specified by a file handle.

; RAX - Handle of file to close.

closeHandle proc
            mkActRec
            
            call    __imp_CloseHandle

            rstrActRec
            ret
closeHandle endp

Although this program doesn’t explicitly use getLastError, it does provide a wrapper around the getLastError function (just to show how it would be written). Whenever one of the Windows functions in this program returns an error indication, you have to call getLastError to retrieve the actual error code. This function has no input parameters. It returns the last Windows error code generated in RAX.

It is very important to call getLastError immediately after a function returns an error indication. If you call any other Windows functions between the error and retrieval of the error code, those intervening calls will reset the last error code value.

As was the case for the closeHandle function, the getLastError procedure is a very simple wrapper around the Windows GetLastError function that preserves volatile register values across the call:

; getLastError - Returns the error code of the last Windows error.

; Returns:

; RAX - Error code.

getLastError proc
             mkActRec
             call   __imp_GetLastError
             rstrActRec
             ret
getLastError endp

The stdin_read is a simple wrapper function around the read function that reads its data from the standard input device (rather than from a file on another device):

; stdin_read - Reads data from the standard input.

; RDI - Buffer to receive data.
; RCX - Buffer count (note that data input will
;       stop on a newline character if that
;       comes along before RCX characters have
;       been read).

; Returns:

; RAX - -1 if error, bytes read if successful.

stdin_read  proc
            .data
hasStdInHnd byte    0
stdInHnd    qword   0
            .code
            mkActRec
            cmp     hasStdInHnd, 0
            jne     hasHandle
            
            call    getStdInHandle
            mov     stdInHnd, rax
            mov     hasStdInHnd, 1
            
hasHandle:  mov     rax, stdInHnd   ; Handle
            call    read
                                            
            rstrActRec
            ret
stdin_read  endp

stdin_read is similar to the puts (and newLn) procedure insofar as it caches the standard input handle on its first call and uses that cached value on subsequent calls. Note that stdin_read does not (directly) preserve the volatile registers. This function does not directly call any Windows functions, so it doesn’t have to preserve the volatile registers (stdin_read calls the read function, which preserves the volatile registers). The stdin_read function has the following parameters:

  1. RDI Pointer to destination buffer that will receive the characters read from the standard input device.
  2. RCX Buffer size (maximum number of bytes to read).

This function returns the actual number of bytes read in the RAX register. This value may be less than the value passed in RCX. If the user presses enter, this function immediately returns. This function does not zero-terminate the string read from the standard input device. Use the value in the RAX register to determine the string’s length. If this function returns because the user pressed enter on the standard input device, that carriage return will appear in the buffer.

The stdin_getc function reads a single character from the standard input device and returns that character in the AL register:

; stdin_getc - Reads a single character from the standard input.
;              Returns character in AL register.

stdin_getc  proc
            push    rdi
            push    rcx
            sub     rsp, 8

            mov     rdi, rsp
            mov     rcx, 1
            call    stdin_read
            test    eax, eax        ; Error on read?
            jz      getcErr
            movzx   rax, byte ptr [rsp]

getcErr:    add     rsp, 8
            pop     rcx
            pop     rdi 
            ret
stdin_getc  endp

The readLn function reads a string of characters from the standard input device and places them in a caller-specified buffer. The arguments are as follows:

  1. RDI Address of the buffer.
  2. RCX Maximum buffer size. (readLn allows the user to enter a maximum of RCX – 1 characters.)

This function will put a zero-terminating byte at the end of the string input by the user. Furthermore, it will strip out the carriage return (or newline or line feed) character at the end of the line. It returns the character count in RAX (not counting the enter key):

; readLn - Reads a line of text from the user.
;          Automatically processes backspace characters
;          (deleting previous characters, as appropriate).
;          Line returned from function is zero-terminated
;          and does not include the ENTER key code (carriage
;          return) or line feed.

; RDI - Buffer to place line of text read from user.
; RCX - Maximum buffer length.

; Returns:

; RAX - Number of characters read from the user
;       (does not include ENTER key).

readLn      proc
            push    rbx
            
            xor     rbx, rbx           ; Character count
            test    rcx, rcx           ; Allowable buffer is 0?
            je      exitRdLn
            dec     rcx                ; Leave room for 0 byte
readLp:
            call    stdin_getc         ; Read 1 char from stdin
            test    eax, eax           ; Treat error like ENTER
            jz      lineDone
            cmp     al, cr             ; Check for ENTER key
            je      lineDone
            cmp     al, nl             ; Check for newline code
            je      lineDone
            cmp     al, bs             ; Handle backspace character
            jne     addChar
            
; If a backspace character came along, remove the previous
; character from the input buffer (assuming there is a
; previous character).

            test    rbx, rbx           ; Ignore BS character if no
            jz      readLp             ; chars in the buffer
            dec     rbx
            jmp     readLp

; If a normal character (that we return to the caller),
; then add the character to the buffer if there is
; room for it (ignore the character if the buffer is full).
            
addChar:    cmp     ebx, ecx           ; See if we're at the
            jae     readLp             ; end of the buffer
            mov     [rdi][rbx * 1], al ; Save char to buffer
            inc     rbx
            jmp     readLp

; When the user presses ENTER (or the line feed) key
; during input, come down here and zero-terminate the string.

lineDone:   mov     byte ptr [rdi][rbx * 1], 0 
            
exitRdLn:   mov     rax, rbx        ; Return char cnt in RAX
            pop     rbx
            ret
readLn      endp

Here’s the main program for Listing 16-4, which reads a filename from the user, opens that file, reads the file data, and displays the data on the standard output device:

**********************************************************

; Here is the "asmMain" function.

            public  asmMain
asmMain     proc
            push    rbx
            push    rsi
            push    rdi
            push    rbp
            mov     rbp, rsp
            sub     rsp, 64         ; Shadow storage
            and     rsp, -16

; Get a filename from the user:

            lea     rsi, prompt
            call    puts

            lea     rdi, inputLn
            mov     rcx, lengthof inputLn
            call    readLn
            
; Open the file, read its contents, and display
; the contents to the standard output device:

            lea     rsi, inputLn
            mov     rax, GENERIC_READ
            call    open

            cmp     eax, INVALID_HANDLE_VALUE
            je      badOpen
            
            mov     inHandle, eax
            
; Read the file 4096 bytes at a time:

readLoop:   mov     eax, inHandle
            lea     rdi, fileBuffer
            mov     ecx, lengthof fileBuffer
            call    read
            test    eax, eax        ; EOF?
            jz      allDone
            mov     rcx, rax        ; Bytes to write
            
            call    getStdOutHandle
            lea     rsi, fileBuffer
            call    write
            jmp     readLoop
            
badOpen:    lea     rsi, badOpenMsg
            call    puts
            
allDone:    mov     eax, inHandle
            call    closeHandle
            
            leave
            pop     rdi
            pop     rsi
            pop     rbx
            ret     ; Returns to caller
asmMain     endp
            end

Listing 16-4: File I/O demonstration program

Here’s the build command and sample output for Listing 16-4:

C:\>nmake /nologo /f listing16-4.mak
        ml64 /nologo listing16-4.asm  /link /subsystem:console /entry:asmMain
 Assembling: listing16-4.asm
Microsoft (R) Incremental Linker Version 14.15.26730.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:listing16-4.exe
listing16-4.obj
/subsystem:console
/entry:asmMain

C:\>listing16-4
Enter (text) filename:listing16-4.mak
listing16-4.exe: listing16-4.obj listing16-4.asm
        ml64 /nologo listing16-4.asm \
                /link /subsystem:console /entry:asmMain

Here’s the listing16-4.inc include file:

; listing16-4.inc

; Header file entries extracted from MASM32 header
; files (placed here rather than including the 
; entire set of MASM32 headers to avoid namespace 
; pollution and speed up assemblies).

STD_INPUT_HANDLE                     equ -10
STD_OUTPUT_HANDLE                    equ -11
STD_ERROR_HANDLE                     equ -12
CREATE_NEW                           equ 1
CREATE_ALWAYS                        equ 2
OPEN_EXISTING                        equ 3
OPEN_ALWAYS                          equ 4
FILE_ATTRIBUTE_READONLY              equ 1h
FILE_ATTRIBUTE_HIDDEN                equ 2h
FILE_ATTRIBUTE_SYSTEM                equ 4h
FILE_ATTRIBUTE_DIRECTORY             equ 10h
FILE_ATTRIBUTE_ARCHIVE               equ 20h
FILE_ATTRIBUTE_NORMAL                equ 80h
FILE_ATTRIBUTE_TEMPORARY             equ 100h
FILE_ATTRIBUTE_COMPRESSED            equ 800h
FILE_SHARE_READ                      equ 1h
FILE_SHARE_WRITE                     equ 2h
GENERIC_READ                         equ 80000000h
GENERIC_WRITE                        equ 40000000h
GENERIC_EXECUTE                      equ 20000000h
GENERIC_ALL                          equ 10000000h
INVALID_HANDLE_VALUE                 equ -1

PPROC           TYPEDEF PTR PROC        ; For include file prototypes

externdef __imp_GetStdHandle:PPROC
externdef __imp_WriteFile:PPROC
externdef __imp_ReadFile:PPROC
externdef __imp_CreateFileA:PPROC
externdef __imp_CloseHandle:PPROC
externdef __imp_GetLastError:PPROC

Here’s the listing16-4.mak makefile:

listing16-4.exe: listing16-4.obj listing16-4.asm
    ml64 /nologo listing16-4.asm \
        /link /subsystem:console /entry:asmMain

16.8 Windows Applications

This chapter has provided just a glimpse of what is possible when writing pure assembly language applications that run under Windows. The kernel32.lib library provides hundreds of functions you can call, covering such diverse topic areas as manipulating filesystems (for example, deleting files, looking up filenames in a directory, and changing directories), creating threads and synchronizing them, processing environment strings, allocating and deallocating memory, manipulating the Windows registry, sleeping for a certain time period, waiting for events to occur, and much, much more.

The kernel32.lib library is but one of the libraries in the Win32 API. The gdi32.lib library contains most of the functions needed to create GUI applications running under Windows. Creating such applications is well beyond the scope of this book, but if you want to create stand-alone Windows GUI applications, you need to become intimately familiar with this library. The following “For More Information” section provides links to internet resources if you’re interested in creating stand-alone Windows GUI applications in assembly language.

16.9 For More Information

If you want to write stand-alone 64-bit assembly language programs that run under Windows, your first stop should be https://www.masm32.com/. Although this website is primarily dedicated to creating 32-bit assembly language programs that run under Windows, it has a large amount of information for 64-bit programmers as well. More importantly, this site contains the header files you will need to access the Win32 API from your 64-bit assembly language programs.

If you’re serious about writing Win32 API–based Windows applications in assembly language, Charles Petzold’s Programming Windows, Fifth Edition (Microsoft, 1998) is an absolutely essential purchase. This book is old (do not get the newer edition for C# and XAML), and you likely will have to purchase a used copy. It was written for C programmers (not assembly), but if you know the Windows ABI (which you should by now), translating all the C calls into assembly language isn’t that difficult. Though much of this information about the Win32 API is available online (such as at the MASM32 site), having all the information available in a single (very large!) book is essential.

Another good source on the web for Win32 API calls is software analyst Geoff Chappell’s Win32 Programming page (https://www.geoffchappell.com/studies/windows/win32/).

The Iczelion tutorials were the original standard for writing Windows programs in x86 assembly language. Although they were originally written for 32-bit x86 assembly language, there have been several translations of this code to 64-bit assembly language, for example: http://masm32.com/board/index.php?topic=4190.0/.

The HLA Standard Library and examples (which can be found at https://www.randallhyde.com/) contain a ton of Windows code and API function calls. Though this code is all 32-bit, translating it to 64-bit MASM code is easy.

16.10 Test Yourself

  1. What is the linker command line option needed to tell MASM that you’re building a console application?
  2. What website should you visit to get Win32 programming information?
  3. What is the major drawback to including \masm32\include64\masm64rt.inc in all your assembly language source files?
  4. What linker command line option lets you specify the name of your assembly language main program?
  5. What is the name of the Win32 API function that lets you bring up a dialog box?
  6. What is wrapper code?
  7. What is the Win32 API function you would use to open an existing file?
  8. What Win32 API function do you use to retrieve the last Windows error code?