I've been fascinated by (and interested in) assembly language for a number of years. I fondly remember my "Peter Norton Assembly" book and wish I hadn't sent it to a better home several years ago. But, hey, it was a major moving of house and I had to have as few items as possible. The sacrifices we make…
Anyway, I had some spare time this past weekend and decided to try and see if I could get a simple 64-bit assembly language program working under Win64. My goal was to write some text to a console window and exit without destroying my entire system. I managed to succeed in that endeavour and had a bit of fun along the way. My wife thinks I have a bizarre definition of fun. If you continue to read the rest of this post keep in mind I said I was "interested" in assembly. By no means is this intended to be a tutorial or guide in writing efficient and professional assembly! As the comments in the code indicate, there was no true attempt at optimisation, etc!
A couple of links that I ran across and found helpful (Links accurate as of today. Who knows what the future may bring?):
- Raymond Chen's (excellent) blog, The Old New Thing, posting entitled "The History of Calling Conventions, Part 5: AMD64" found here.
In order to make the code just a tiny bit more interesting I decided to call the "cpuid" function to get the Vendor ID from the processor and write that out to the console window. Information on the "cpuid" function is available in pdf format on both the Intel and AMD websites. One thing that I found interesting is that the 4 byte chunks (of which there are 3) that make up the output of the Vendor ID request from the "cpuid" function are stored in the registers in an "out of order" fashion. Look at the code below that gets the chunks from the registers and you'll see what I mean (I hope).
NOTE: Multiple edits made per comment(s) etc. over time and things may change (perhaps dramatically) in the future
; a (very) simple x64 masm (ml64.exe) demo that calls the cpuid function
; and writes the results to a console window.
; this is a simple experiment and may not be exemplary assembly code
; for one thing there is no true attempt at optimisation (such as removing
; possibly duplicate sections of code) or error handling.
; args are passed in registers as follows:
; RCX: 1st integer argument
; RDX: 2nd integer argument
; R8: 3rd integer argument
; R9: 4th integer argument
; the stack is used for args beyond 4 (after space reserved for first 4)
; this reserved space on the stack for first 4 arguments is
; the "register home space" or "register parameter stack area"
; unlike the traditional 32-bit __stdcall convention, the Windows x64
; calling convention uses caller-managed stack space. This is known as
; the __fastcall calling convention.
; win api functions that are called below.
; contrary to earlier comment, this is a
; small optimisation attempt in that if call
; by "api name" such as GetStdHandle there
; will be a stub generated that then jumps
; to the proper location. this avoids that
; extra jump from the stub location.
externdef __imp_GetStdHandle : qword
externdef __imp_ExitProcess : qword
externdef __imp_WriteFile : qword
externdef __imp_lstrlen : qword
externdef __imp_wsprintfA : qword
; the "variables" used in the main code below such as the text to write
; possibly carriage return (13 / 0Dh) and line feed (10 / 0Ah) CRLF would
; not ideally be embedded in the string values but this is a simple test
; the message to output is split into two parts:
; the first part is static
; later we replace the 'x' characters with the actual value
; labelling the memory region where the replacement characters
; are copied makes this easier and does not rely on offset from
; the beginning of the message text (only the offset from 'x' buffer)
; because there is no null terminator (0) at the end of vndrid msglen
; below will get the length of the full string as null follows vndrbuf
; the 'x' buffer will be exactly filled with the results.
.data
vndrid byte 0Dh, 0Ah, "The processor Vendor ID is '"
vndrbuf byte "xxxxxxxxxxxx", "'.", 0Dh, 0Ah, 0
writtenfmt byte 0Dh, 0Ah, "Number of bytes written: %u", 0Dh, 0Ah, 0
; uninitialised variables (aka bss segment)
; these will be zero-initialised at runtime
.data?
hFile qword ?
msglen dword ?
BytesWritten dword ?
writtenbuf byte 32 dup(?)
; and we're off...
.code
main proc frame
; for easy reference:
; registers that should be preserved (non-volatile)
; rbp, rbx, rdi, rsi, rsp, r12, r13, r14, r15
; registers that do not need to be preserved (volatile / scratch)
; rax, rcx, rdx, r8, r9, r10, r11
; prologue (there are other options)
; a frame pointer is not used in this code (fpo - frame pointer omission).
; refer to "x64 exception handling" in "x64 ABI conventions".
push rbx ; rbx is a non-volatile register used below by cpuid
.pushreg rbx ; unwind info
sub rsp, 30h ; adjust stack for register home space, parameter(s),
; and alignment.
; WriteFile takes 5 parameters and thus uses stack
; for the 5th parameter.
.allocstack 30h ; unwind info stack allocation
.endprolog ; unwind info mark end of prologue
; get the handle for the console window to use when writing text
; this is used by WriteFile win api function below
mov ecx, -11 ; put -11 (STD_OUTPUT_HANDLE) into ecx (dword)
call __imp_GetStdHandle
mov hFile, rax ; move returned handle to hFile
; The Windows 64-bit paradigm includes allocating the largest amount
; of stack space needed and then reusing that space. As such the
; stack pointer (rsp) does not change typically due to the reduced
; usage of pushes (and matched pops). There are no operations from
; this point that change rsp so it is used as the base for offsets.
; If rsp did change then using rbp (or other register as a frame pointer)
; can make addressing and unwinding easier.
; And in such a case the register used as a frame pointer may point
; into the frame rather than at the base (if dynamic allocation used).
; In other words, if we did modify rsp outside of the prologue, we would
; need to ensure that x64 ABI requirements are satisfied.
lea rcx, vndrid ; load address of vndrid text into rcx
call __imp_lstrlen ; call win api function to get length of text
mov msglen, eax ; move length of string from eax to msglen
xor eax, eax ; 0 indicates we want the vendor id string
cpuid ; invoke the cpuid instruction to get the string
; the result is now in ebx, edx, and ecx
lea r9, vndrbuf ; Point directly to the 'x' placeholder block
mov [r9], ebx ; the first 4 bytes of the vendor id
mov [r9 + 04h], edx ; the next 4 bytes of the vendor id
mov [r9 + 08h], ecx ; the final 4 bytes of the vendor id
mov qword ptr [rsp + 20h], 0 ; LPOVERLAPPED lpOverlapped
lea r9, BytesWritten ; LPDWORD lpNumberOfBytesWritten
mov r8d, msglen ; DWORD nNumberOfBytesToWrite
lea rdx, vndrid ; LPCVOID lpBuffer
mov rcx, hFile ; HANDLE hFile
call __imp_WriteFile ; call win api to write text to console
; to give the sample a little more interest, we'll write the number of bytes
; written by WriteFile just above (in BytesWritten) using a short text statement.
; this will require formatting the dword as character data.
; to do this we use wsprintfA which provides printf-style formatting.
; note: wsprintfA does not receive the size of the output buffer.
; if the buffer is not large enough for the formatted output,
; a buffer overflow will occur.
; given the known strings in this sample we will not overflow as written.
mov r8d, BytesWritten ; value to format, BytesWritten from WriteFile above
lea rdx, writtenfmt ; the format string
lea rcx, writtenbuf ; output buffer
call __imp_wsprintfA ; format the number of bytes written as decimal ascii
; write the formatted byte count to the console.
; wsprintfA returns the number of characters written to
; writtenbuf, so its return value in eax can be used directly
; as the nNumberOfBytesToWrite argument for WriteFile.
mov qword ptr [rsp + 20h], 0 ; LPOVERLAPPED lpOverlapped
lea r9, BytesWritten ; LPDWORD lpNumberOfBytesWritten
mov r8d, eax ; DWORD nNumberOfBytesToWrite
lea rdx, writtenbuf ; LPCVOID lpBuffer
mov rcx, hFile ; HANDLE hFile
call __imp_WriteFile ; write the formatted byte count string
exit:
xor ecx, ecx ; pass 0 as the process exit code
call __imp_ExitProcess ; terminate the process rather than returning
; as main is the process entry point,
; there is no caller to return to from the entry point
; ExitProcess terminates the calling process and does not return,
; so the following epilogue is unreachable during normal execution.
;
; The .pushreg/.allocstack directives above provide the unwind
; metadata describing how the prologue changed the stack and rbx register.
; An explicit epilogue is therefore not required for the .pdata/.xdata
; unwind information to be correct.
;
; A conventional epilogue, if this function did return instead of
; calling ExitProcess, would look like this:
;
;add rsp, 30h
;pop rbx
;ret
main endp
end
The command I used to compile the above is (apologies if wrapping make it difficult to read):
Hurray! Unless you sell AMD CPUs then you might think 'AuthenticAMD' would look better…
If you see something profoundly wrong in the above, please let me know. It's not as if I am going to be churning out assembly code all of a sudden, but I would still be interested in knowing… that is, after all, the purpose of this exploration – to understand better.