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.
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.
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.
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.
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.
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.
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:
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).
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:
CreateFileA
A function (despite its name) that you use to open existing files or create new filesWriteFile
A function that writes data to a fileReadFile
A function that reads data from a fileCloseHandle
A function that closes a file and flushes any cached data to the storage deviceGetStdHandle
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)GetLastError
A function you can use to retrieve a Windows error code if an error occurs in the execution of any of these functionsListing 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:
open
or openNew
functions (a little later in the program) or the getStdOutHandle
and getStdErrHandle
functions.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:
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):
The read
function, a wrapper around the Win32 API __imp_ReadFile
function, has the following arguments:
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:
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:
GENERIC_READ
and GENERIC_WRITE
).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).open
function doesn’t specify any special security; it simply passes NULL (0) as this argument.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).FILE_ATTRIBUTE_NORMAL
attribute (for example, not read-only).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:
GENERIC_WRITE
.CREATE_ALWAYS
creation disposition flag to CreateFileA
rather than OPEN_EXISTING
.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:
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:
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
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.
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.