I've been learning MASM64 over the last few days and written a simple demo, so I can get feedback on my understanding of x64 assembly programming.
It's really basic: it asks the user for their name, greets them, and then reverses the name string.
The point of this question is to get a confirmation I understand the basic stuff like calling convention, stack alignment, etc. correctly. It's surely not about performance.
The complete code:
extrn GetStdHandle : proc
extrn WriteConsoleA : proc
extrn ReadConsoleA : proc
.const
STDIN_HANDLE_ID equ -10
STDOUT_HANDLE_ID equ -11
INVALID_HANDLE_VALUE equ -1
NULL equ 0
.data
promptText db "Enter your name: ", 0
greetingText db "Hello, ", 0
backwardsText db "Your name backwards is: ", 0
.code
; Counts characters in a null-terminated string.
; Parameters:
; 1. Pointer to string.
; Return value:
; -> string length (without the terminator).
;
stringLength proc
; No need for shadow space.
mov rdx, rcx
mov rcx, -1
_nextChar:
inc rcx
mov al, byte ptr [rdx + rcx]
test al, al
jnz _nextChar
mov rax, rcx
ret
stringLength endp
; Displays a null-terminated string.
; Parameters:
; 1. Pointer to string to be displayed.
; 2. Number of characters to be displayed.
; Return value:
; -> On success: 0.
; -> On failure: -1.
;
stringPrint proc
; Reserve shadow space.
sub rsp, 48h
; Save string address for later use.
mov [rsp + 38h], rcx
call stringLength
; If (length == 0), just end the function.
test rax, rax
jz _stringPrintEnd
; Save string length for later use.
mov [rsp + 40h], rax
mov rcx, STDOUT_HANDLE_ID
call GetStdHandle
cmp rax, INVALID_HANDLE_VALUE
je _stringPrintEnd
mov qword ptr [rsp + 28h], NULL
mov r9, NULL
mov r8d, [rsp + 40h]
mov rdx, [rsp + 38h]
mov rcx, rax ; console handle
call WriteConsoleA
test rax, rax
jz _stringPrintError
xor rax, rax
_stringPrintEnd:
; Free shadow space.
add rsp, 48h
ret
_stringPrintError:
mov rax, -1
jmp _stringPrintEnd
stringPrint endp
; Reads count-1 characters from the standard input.
; Parameters:
; 1. Pointer to buffer to store the characters.
; 2. Character count.
; Return value:
; -> On success: 0.
; -> On error: -1.
;
stringRead proc
; Reserve shadow space.
sub rsp, 48h
; Save volatile registers for later use.
mov [rsp + 38h], rcx
mov [rsp + 40h], rdx
mov rcx, STDIN_HANDLE_ID
call GetStdHandle
cmp rax, INVALID_HANDLE_VALUE
je _stringReadError
mov qword ptr [rsp + 20h], NULL
lea r9, [rsp + 28h]
mov r8, [rsp + 40h]
mov rdx, [rsp + 38h]
mov rcx, rax
call ReadConsoleA
test rax, rax
jz _stringReadError
xor rax, rax
_stringReadError:
; Free shadow space.
add rsp, 48h
ret
stringRead endp
; Reverses a null-terminated string.
; Parameters:
; 1. Pointer to string to be reversed.
; 2. Pointer to buffer to store the output.
;
stringReverse proc
; Reserve shadow space.
sub rsp, 38h
; Save volatile registers.
mov [rsp + 28h], rcx ; src buffer
mov [rsp + 30h], rdx ; dst buffer
; RCX is already set.
call stringLength
test rax, rax
jz _stringReverseEnd
mov rcx, [rsp + 28h]
dec rcx
mov rdx, [rsp + 30h]
add rdx, rax
; Terminate the dst buffer.
mov byte ptr [rdx], 0
_reverseNextChar:
inc rcx
dec rdx
mov al, byte ptr [rcx]
; Check for terminator in src buffer.
test al, al
jz _stringReverseEnd
mov byte ptr [rdx], al
jmp _reverseNextChar
_stringReverseEnd:
xor rax, rax
; Free shadow space.
add rsp, 38h
ret
stringReverse endp
main proc
; Reserve shadow space: 20h for calls + 28h for buffers.
sub rsp, 48h
; Zero the buffers.
mov qword ptr [rsp + 20h], 0
mov qword ptr [rsp + 28h], 0
mov qword ptr [rsp + 30h], 0
mov qword ptr [rsp + 38h], 0
mov qword ptr [rsp + 40h], 0
lea rcx, promptText
call stringPrint
mov rdx, 14h
lea rcx, [rsp + 20h] ; reading buffer
call stringRead
lea rdx, [rsp + 34h] ; destination buffer
lea rcx, [rsp + 20h] ; source buffer
call stringReverse
lea rcx, greetingText
call stringPrint
lea rcx, [rsp + 20h]
call stringPrint
lea rcx, backwardsText
call stringPrint
lea rcx, [rsp + 34h]
call stringPrint
xor rax, rax
; Free shadow space.
add rsp, 48h
ret
main endp
end
Thanks for the help.
1 Answer 1
The stringLength leaf function (no prolog/epilog) could still be a bit simpler:
stringLength proc
mov rax, -1
_nextChar:
inc rax
cmp byte ptr [rcx + rax], 0
jne _nextChar
ret
stringLength endp
The comments on the stringPrint frame function suggest that a second parameter exists.
; Displays a null-terminated string.
; Parameters:
; 1. Pointer to string to be displayed.
; 2. Number of characters to be displayed.
This is not the case.
When the call to GetStdHandle fails, you jump to _stringPrintEnd where I think you could jump to _stringPrintError.
The 5th parameter for WriteConsoleA, the one that goes onto the stack, must go to [rsp + 20h]
right above the register home area. You wrote [rsp + 28h]
.
A simpler exit is:
test rax, rax
mov rax, -1
jz _stringPrintEnd
xor rax, rax
_stringPrintEnd:
add rsp, 48h
ret
The exit from the stringRead frame function is not very useful. It will always return RAX=0
. In other words: the test is redundant.
test rax, rax
jz _stringReadError
xor rax, rax
_stringReadError:
In the stringReverse proc, it is easy to write the loop using a single conditional jump, omitting the second jump. I know that you're not seeking performance but then again this is assembly...
mov al, 0 ; To terminate the dst buffer.
_reverseNextChar:
inc rcx
mov [rdx], al
dec rdx
mov al, [rcx]
test al, al
jnz _reverseNextChar
Some ideas
Instead of determining the string length yourself, why don't you use the lpNumberOfCharsRead that you receive from invoking ReadConsoleA?
Why do you bother to zero your source and destination buffers. That's not a useful operation (unless you doubt that the input from ReadConsoleA will be zero-terminated by default).
And if you insist on clearing these buffers then please try not to use that many bytes. Next is much smaller:xor eax, eax mov [rsp + 20h], rax mov [rsp + 28h], rax mov [rsp + 30h], rax mov [rsp + 38h], rax mov [rsp + 40h], rax
It will be much nicer if you introduced a couple of newlines in the output:
greetingText db 13, 10, "Hello, ", 0 backwardsText db 13, 10, "Your name backwards is: ", 0
The point of this question is to get a confirmation I understand the basic stuff like calling convention, stack alignment, etc. correctly.
Except for that 5th parameter on WriteConsoleA, this seems to be fine. Well done.
For maximum adherence to the conventions you could store the register parameters RCX
and RDX
in the shadow memory that the caller had to set aside for that purpose.
Below is how it changes stringRead (similar for stringPrint and stringReverse):
stringRead proc
; Save volatile registers in register home area for later use.
mov [rsp + 8], rcx
mov [rsp + 16], rdx
; Reserve shadow space, local storage and alignment
sub rsp, 38h
mov rcx, STDIN_HANDLE_ID
call GetStdHandle
...
mov qword ptr [rsp + 20h], NULL
lea r9, [rsp + 28h]
mov r8, [rsp + 38h + 16]
mov rdx, [rsp + 38h + 8]
mov rcx, rax
call ReadConsoleA
...
add rsp, 38h
ret
stringRead endp
```
-
1\$\begingroup\$ Thanks for taking the time to read it and for your commentary. I can't believe I didn't spot at least some of those errors, haha. Also, the suggestion to use the caller-allocated shadow space was an eye-opener, made me realize I didn't fully understand the convention. Once again - thanks! \$\endgroup\$mdx– mdx2020年10月20日 09:16:17 +00:00Commented Oct 20, 2020 at 9:16
-
\$\begingroup\$ Since this is x86-64, the standard way to store a bunch of zeros is with SSE or SSE2.
xorps xmm0,xmm0
/movaps [rsp + 20h], xmm0
etc. (With the last store beingmovlps [rsp+40h], xmm0
if you need to store an odd multiple of 8.) \$\endgroup\$Peter Cordes– Peter Cordes2020年11月19日 20:00:07 +00:00Commented Nov 19, 2020 at 20:00