4
\$\begingroup\$

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.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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
```
\$\endgroup\$
2
  • 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\$ Commented 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 being movlps [rsp+40h], xmm0 if you need to store an odd multiple of 8.) \$\endgroup\$ Commented Nov 19, 2020 at 20:00

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.