Skip to content

Instantly share code, notes, and snippets.

@pommicket
Last active May 5, 2020 17:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pommicket/2fad79640658aefe790394acd65bd16f to your computer and use it in GitHub Desktop.
Save pommicket/2fad79640658aefe790394acd65bd16f to your computer and use it in GitHub Desktop.
Call C functions with a dynamic number of arguments x64 MSVC
;;; Call C functions with a dynamic number of arguments x64 MSVC ;;;
;;; Written in NASM ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; In jurisdictions that recognize copyright laws, the author or authors
;; of this software dedicate any and all copyright interest in the
;; software to the public domain. We make this dedication for the benefit
;; of the public at large and to the detriment of our heirs and
;; successors. We intend this dedication to be an overt act of
;; relinquishment in perpetuity of all present and future rights to this
;; software under copyright law.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
;; IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
;; OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
;; ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
;; OTHER DEALINGS IN THE SOFTWARE.
;;
;; For more information, please refer to <http://unlicense.org/>
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;To compile this:
;; first get nasm: https://nasm.us/
;; add it to your path, then do:
;; nasm -f win64 win64call.asm
;; You will get win64call.obj
;;To use this in a C/C++ program:
;; typedef void (*FnPtr)();
;; extern unsigned long long win64_call(FnPtr fn, void *args, long long nargs);
;; extern float win64_callf(FnPtr fn, void *args, long long nargs);
;; extern double win64_calld(FnPtr fn, void *args, long long nargs);
;; extern SomeType win64_call_other(FnPtr fn, void *args, long long nargs);
;; (all of these refer to the same actual function)
;; With MSVC's calling convention, all arguments are treated as if they were 64-bit.
;; This means you need to convert integer arguments to unsigned long long/uint64_t before using them.
;; So if you have integer arguments, you probably want to pass an unsigned long long * for args.
;; Floating point arguments can either be "reinterpreted" as unsigned long longs (see 2nd example), or
;; you can pass a double *for args instead.
;; &((unsigned long long *)args)[i] should be a pointer to the ith argument.
;; If you have a 1, 2, 4, or 8 byte struct argument, convert it to an integer, then pass it (keep in mind that
;; if your struct is 8 bytes but not aligned to 8 bytes, the *(uint64_t *)&x trick will cause an unaligned read).
;; Otherwise, pass it by pointer.
;; For returning structs: if your type is 1, 2, 4, or 8 bytes and is POD, it will be returned as an integer.
;; Otherwise, you need to pass a pointer to the struct as the first argument.
;; for more info see https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention
;;So, if you want to call it:
;; simple example (integer arguments):
;; int foo(int a, int b, int c) {
;; return a+b+c;
;; }
;; int main() {
;; FnPtr fn = (FnPtr)foo;
;; unsigned long long args[3] = {
;; -1000, -3, 65
;; };
;; int ret = (int)win64_call(fn, args, 3);
;; printf("%d\n", ret);
;; }
;; more involved example (with floating-point numbers):
;; float bar(float a, double b, int c, double d, long e) {
;; return a-(float)b + sinf((float)c) - (float)cos(d) + (float)e;
;; }
;; int main() {
;; FnPtr fn = (FnPtr)bar;
;; float a = -1.6f;
;; double b = 3.0, d = 33.7;
;; unsigned long long args[5] = {
;; *(uint32_t *)&a, *(uint64_t *)&b, -12, *(uint64_t *)&d, 4
;; };
;; float ret = win64_callf(fn, args, 5);
;; printf("%f\n", ret);
;; }
;;Why might you need this?
;; Sometimes you don't know how many arguments you're gonna call a function with at compile time.
;; For example, maybe you're making an interpreted programming language which calls out to C.
;; (python uses libffi but that's not easy to compile...)
;; Usually it will be with a function pointer you got from GetProcAddress.
;;If you find a bug...
;; Please let me know by emailing pommicket@pommicket.com.
global win64_call
global win64_callf
global win64_calld
global win64_call_other
section .text
; takes:
; rcx - fn - pointer to function
; rdx - args - pointer to arguments
; r8 - nargs - number of arguments
win64_call:
win64_callf:
win64_calld:
win64_call_other:
; use "shadow store" to save rsi
mov [rsp+24], rsi
mov rax, rcx ; function pointer (rcx may be overwritten)
mov r11, rdx ; args (rdx may be overwritten)
mov r10, r8 ; index_of_argument
mov rsi, rsp ; save original stack pointer
; we need to make sure the stack pointer is aligned to 16 bytes when the function is called.
; for some reason, even though we have to align it or stuff breaks, sometimes when
; our function is called, it's not 16-byte aligned :/
; find number of stack arguments:
cmp r8, 4
jg .align_stack
mov r8, 0 ; if there are <=4 arguments, set the number of stack arguments to 0
.align_stack:
and r8, 1 ; is the number of stack arguments even or odd?
lea r8, [rsp+8*r8]
; r8 is now equivalent to where the stack pointer will be (mod 16) when we call the function
and r8, 0xf ; take r8 mod 16
sub rsp, r8 ; align the stack pointer so when we call the function it's 16-byte aligned
lea r11, [r11+8*r10] ; go to end of arguments--we go from right to left
; because that's the order things are pushed onto the stack
cmp r10, 0
je .loop_end ; no arguments
.loop:
dec r10 ; --index_of_argument
sub r11, 8 ; --arg
cmp r10, 0
jg .after_1st
; NOTE: we have to set both the integer and floating-point register for every argument because
; a. we don't know if it's integer or floating point
; b. varargs expects to have the value in both registers
; 1st argument
mov rcx, qword [r11]
movsd xmm0, qword [r11]
jmp .continue
.after_1st:
cmp r10, 1
jg .after_2nd
; 2nd argument
mov rdx, qword [r11]
movsd xmm1, qword [r11]
jmp .continue
.after_2nd:
cmp r10, 2
jg .after_3rd
; 3rd argument
mov r8, qword [r11]
movsd xmm2, qword [r11]
jmp .continue
.after_3rd:
cmp r10, 3
jg .after_4th
; 4th argument
mov r9, qword [r11]
movsd xmm3, qword [r11]
jmp .continue
.after_4th:
; additional argument
push qword [r11]
.continue:
cmp r10, 0 ; if index_of_argument > 0
jg .loop
.loop_end:
sub rsp, 32 ; "shadow store"
call rax ; function pointer stored here before
mov rsp, rsi ; restore original stack pointer
; restore rsi
mov rsi, [rsp+24]
ret
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment