Calling external functions with x64 DynASM
One non-obvious corner of using DynASM to generate x64 machine code is how to make calls to existing C functions, such as printf
. x64 makes this difficult because the address space is 64-bits, but most instructions can only have 32-bit immediate values or 32-bit offsets.
One of the few instructions which accept a 64-bit immediate is for moving a constant into a register - in the Intel and AMD manuals, this is the mov r64, imm64
instruction. DynASM can emit this instruction, but using the mnemonic mov64
rather than mov
. This leads to the following pattern for calling functions:
| mov64 rax, (uint64_t)printf
| call rax
The above pattern works, but it costs you a register, which is less than ideal.
An alternative pattern is to use the same approach as shared libraries: import tables. The basic idea is that the import table contains a list of function pointers, and calls are done using the call [rip + imm32]
instructions, where imm32
is the distance between the instruction pointer and the function pointer. DynASM will emit this instruction in x64 mode when given assembler of the form call qword [label]
, so the only tricky bit left is to stick the appropriate function pointer at the appropriate label. One pattern for doing this is the following:
|.section code, imports
|.macro call_extern, target
| .imports
| ->__imp__..target:
| .dword (uint32_t)target
| .dword ((uint64_t)target >> 32)
| .code
| call qword [->__imp__..target]
|.endmacro
| call_extern printf
The .section code, imports
directive tells DynASM that we want to append instructions to two buffers rather than one, and that said buffers should be called .code
and .imports
. For each buffer, it registers a directive of the same name which sets the current output buffer to that one.
The .macro call_extern, target
directive registers a one-argument macro called call_extern
. Internally it uses token-pasting (the syntax for which is ..
, and has the same effect as the C preprocessor's ##
) to create a label, uses .dword
directives to write the function pointer, and uses .imports
and .code
to switch between output buffers.
Once the macro is registered, calls are done using call_extern
.