Search code examples
windowsassemblyx86-64calling-convention

Why "mov rcx, rax" is required when calling printf in x64 assembler?


I am trying to learn x64 assembler. I wrote "hello world" and tried to call printf using the following code:

EXTERN printf: PROC

PUBLIC hello_world_asm

.data
hello_msg db "Hello world", 0


.code
hello_world_asm PROC
push rbp ; save frame pointer
mov rbp, rsp ; fix stack pointer
sub rsp, 8 * (4 + 2) ; shadow space (32bytes)

lea rax, offset hello_msg
mov rcx, rax ; <---- QUESTION ABOUT THIS LINE
call printf

; epilog. restore stack pointer
mov rsp, rbp
pop rbp
ret
hello_world_asm ENDP


END

At the beginning I called printf without "mov rcx, rax", which ended up with access violation. Getting all frustrated I just wrote in C++ a call to printf and looked in the disassembler. There I saw the line "mov rcx, rax" which fixed everything, but WHY do I need to move RAX to RCX ??? Clearly I am missing something fundamental.

Thanks for your help!

p.s. a reference to good x64 assembler tutorial is more than welcome :-) couldn't find one.


Solution

  • It isn't required, this code just wastes an instruction by doing an lea into RAX and then copying to RCX, when it could do

    lea   rcx, hello_msg
    call  printf              ; printf(rcx, rdx, r8, r9, stack...)
    

    printf on 64-bit Windows ignores RAX as an input; RAX is the return-value register in the Windows x64 calling convention (and can also be clobbered by void functions). The first 4 args go in RCX, RDX, R8, and R9 (if they're integer/pointer like here).

    Also note that FP args in xmm0..3 have to be mirrored to the corresponding integer register for variadic functions like printf (MS's docs), but for integer args it's not required to movq xmm0, rcx.


    In the x86-64 System V calling convention, variadic functions want al = the number of FP args passed in registers. (So you'd xor eax,eax to zero it). But the x64 Windows convention doesn't need that; it's optimized to make variadic functions easy to implement (instead of for higher performance / more register args for normal functions).

    A few 32-bit calling conventions pass an arg in EAX, for example Irvine32, or gcc -m32 -mregparm=1. But no standard x86-64 calling conventions do. You can do whatever you like with private asm functions you write, but you have to follow the standard calling conventions when calling library functions.


    Also note that lea rax, offset hello_msg was weird; LEA uses memory-operand syntax and machine encoding (and gives you the address instead of the data). offset hello_msg is an immediate, not a memory operand. But MASM accepts it as a memory operand anyway in this context.

    You could use mov ecx, offset hello_msg in position-dependent code, otherwise you want a RIP-relative LEA. I'm not sure of the MASM syntax for that.