1
\$\begingroup\$

I`m currently trying to delve into x64 assembly under windows using NASM, and created a minimalistic "Hello World" application.

It is mainly meant as an educational resource for me and possibly others, hence the heavy documentation style.

A full repo with build instructions and code is located at hello_kernel32, this is the relevant source file:

;; Resources:
;; https://sonictk.github.io/asm_tutorial/
;; https://gist.github.com/mcandre/b3664ffbeb4f5764b36a397fafb04f1c
;; https://retroscience.net/x64-assembly.html


;; Make clear this file contains 64bit assembly
bits 64

;; Use rip-relative addressing
default rel

;; Export entry symbol (this is specified in the call to link.exe)
global _start

;; Import external symbols
;; (all of them exist in kernel32.lib, which gets passed to link.exe in addition to the programs object file hello.obj)
;;
;; Why import symbols/functions from kernel32.lib?
;;
;; In windows, the "low level API stack" is: Kernel < Syscalls < ntdll.dll < kernel32.dll (and others like user32.dll)
;;
;; * The kernel itself cannot be accessed by user programs for obvious reasons (CPU ring protection modes)
;; * Syscalls could be performed, but are undocumented and evidently unstable between different versions of windows
;; * ntdll.dll is only partially documented and not intended for external use
;; * kernel32.dll (and friends) are the "official" low-level entry points to the windows API
extern GetStdHandle
extern WriteFile
extern ExitProcess


;; This section contains read-only data
section .rodata

;; Store the output string followed by CRLF as a sequence of bytes, at address 'msg'
msg db "Hello World!", 0x0d, 0x0a

;; The length will be needed by the output function, and can be statically calculated at assembly time by using 'equ'
;; It is actually a nifty trick that calculates the offset between the current address '$', and the address of 'msg'
;; See https://nasm.us/doc/nasmdoc3.html#section-3.2.4
msg_len equ $ - msg


;; This section contains the code
section .text

_start:
    ;; For being able to print text, we first need to acquire a HANDLE to STDOUT
    ;; This HANDLE is a required parameter for the call to WriteFile

    ;; HANDLE = GetStdHandle(-11)
    ;;
    ;; See https://docs.microsoft.com/en-us/windows/console/getstdhandle
    ;;
    ;; Parameter 1 (rcx): requests the type of HANDLE, -11 is the constant for STDOUT
    ;; Return value (rax): HANDLE (an address with some type of meaning) is stored in rax, as per calling conventions
    mov rcx, -11
    sub rsp, 40 ;; Allocate 32 bytes of shadow space and 8 bytes to keep the stack 16-byte aligned
    call GetStdHandle
    add rsp, 40 ;; Undo shadow space + alignment padding

    ;; code = WriteFile(HANDLE, msg, msg_len, NULL, NULL)
    ;;
    ;; See https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
    ;;
    ;; Parameter 1 (rcx): HANDLE to write to
    ;; Parameter 2 (rdx): Address of message to print
    ;; Parameter 3 (r8): Length of message
    ;; Parameter 4 (r9): Write amount of written bytes to this address, null pointer
    ;;                   (Required according to docs when parameter 5 is null, but passing null seems to work just fine)
    ;; Parameter 5 (on stack): Unused optional parameter, null pointer
    ;; Return value (rax): Nonzero on success
    mov rcx, rax
    mov rdx, msg
    mov r8, msg_len
    mov r9, 0
    push qword 0
    sub rsp, 32 ;; The previous push aligned us to 16 bytes, only allocate shadow space
    call WriteFile
    add rsp, 40 ;; Undo shadow space + push

    ;; ExitProcess(code)
    ;;
    ;; See https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess
    ;;
    ;; Parameter 1 (rcx): Exit code
    mov rcx, rax
    sub rsp, 40 ;; Shadow space + 8 byte alignment
    call ExitProcess
    ;; add rsp, 40 ;; Not necessary because the previous function call will end the program anyway

My main question is about:

  • Shadow space + stack alignment

Most resources I found seem to completely neglect this when doing simple "hello world" programs. My program also seems to run just fine when removing all the sub rsp/add rsp statements.

So I'm wondering what the implications of not following those conventions really means for code correctness.

My current understanding is that GetStdHandle/WriteFile/ExitProcess are simply not using shadow space and also do not perform any operation that requires an aligned stack in their current implementation as present on my machine.

However, any update to kernel32.dll is free to change those implementations in a way that relies on shadow space being present and/or an aligned stack.

Therefore code neglecting shadow space/stack alignment for external calls is incorrect in the general sense of incorrectly interfacing with an external API, even though many APIs may be built in a way they can tolerate that slightly incorrect access (but this tolerance is an implementation detail that may change at any time).

-> Is this definition/understanding correct? / Anything to add/clarify?

  • General question

Are there any other apparent mistakes / misunderstandings in the code or comments?

\$\endgroup\$
1
  • \$\begingroup\$ About your specific questions, I would search Stack Overflow for answers, if I didn't find any answers I would ask a question. Code review is really only about improving code through general reviews. \$\endgroup\$
    – pacmaninbw
    Aug 18, 2022 at 13:08

1 Answer 1

2
\$\begingroup\$
  • Shadow space + stack alignment

Most resources I found seem to completely neglect this when doing simple "hello world" programs.

Is this true? Keep in mind that the most common way to take it into account is to do something like the following:

  • in the prologue, allocate however much space this function ever needs (including the parameter and shadow space of any callee), plus either 8 to compensate for the call to that function or using push rbp to both save rbp and restore alignment.
  • in the body of the function, don't change rsp (eg don't push qword 0 but mov qword [rsp + something], 0).

I looked at a few tutorials and guides, and they used that technique. That technique is semi-required on win64 anyway, at least if you want SEH unwinding, since the stack unwinding data is quite restrictive in what it can express. Code that employs that technique looks like it's neglecting alignment and shadow space, but they are taken into account in the amount of stack space allocated by the prologue.

Anyway I recommend that you employ that technique also, even if you aren't emitting unwinding information yet, you may as well get used to it.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.