<< PreviousNext >>

Dynamic function calls

This problem is not too much about standard C++, but it may be interesting anyway. Sadly, the code given in this problem will only work on 32-bit X86 machines. On Visual Studio, it is quite easy to compile for 32-bit (it is the default on VS2008 at least), either change it in the IDE, or open the appropriate Visual Studio Command Prompt. On linux (using gcc at least), you can simply add the switch -m32 and it will work if you have gcc-multilib or similar package installed on your system. Anyway, the problem is solvable without running any code!

This is something I tried to do when implementing dynamic function calls in the Storm compiler. I needed something similar to the reflections in Java, where it is possible to pass an array of parameters to a function handle. So I started coding and came up with the code shown below, and it seemed to work fine. It worked fine until I called functions with 4 parameters, then it crashes. The question is why?

Background

Function calls in C/C++ uses the cdecl calling convention by default. This means that parameters are passed on the stack, pushed from right to left. A call to add1(2, 3, 4) would look like this:

push 4;
push 3;
push 2;
call add1;
pop ?;
pop ?;
pop ?;

It is the callers responsibility to clear the stack, that is why we do the three pop instructions afterwards. Since we do not care about what was popped, we can simply do add esp, 12 instead.

The stack on x86 grows towards lower addresses (ie push eax is sub esp, 4; mov eax, [esp]), we can see that the contents of the stack just before the function call is the same order as the elements would be stored in an array. That means we can call a function with parameters from an array like this: 1: Copy the stack pointer somewhere (first asm block) 2: Copy the array byte-for-byte to the top of the stack (memcpy) 3: Adjust the stack pointer and call the function (second asm block).

main.cpp

#include <iostream>
#include <cstring>

using namespace std;

typedef size_t nat;

// Call the function 'fn', with a dynamic list of parameters.
nat callFn(void *fn, nat count, nat *params) {
  nat pSize = count * sizeof(void *);
  nat *stack;
  nat result;

#if defined(_M_IX86)
  __asm mov stack, esp;
#elif defined(__i386__)
  __asm__ ("movl %%esp, %0;\n" : "=r"(stack)::);
#else
#error "NOT SUPPORTED, only works on x86!"
#endif

  // Copy parameters to the stack.
  nat *to = stack - count;
  memcpy(to, params, pSize);

#if defined(_M_IX86)
  __asm {
    sub esp, pSize;
    call fn;
    add esp, pSize;
    mov result, eax;
  };
#elif defined(__i386__)
  __asm__ ("subl %1, %%esp;\n"
	   "call *%2;\n"
	   "add %1, %%esp;\n"
	   "mov %%eax, %0;\n"
	   : "=r"(result)
	   : "r"(pSize), "r"(fn) : "memory");
#endif

  return result;
}


// Simple functions that add some parameters.
nat add1(nat a) { return a; }
nat add2(nat a, nat b) { return a + b; }
nat add3(nat a, nat b, nat c) { return a + b + c; }
nat add4(nat a, nat b, nat c, nat d) { return a + b + c + d; }
nat add5(nat a, nat b, nat c, nat d, nat e) { return a + b + c + d + e; }


// Test it!
int main() {
  nat numbers[] = { 1, 2, 3, 4, 5, 6 };
  cout << "1: " << callFn((void *)add1, 1, numbers) << endl;
  cout << "2: " << callFn((void *)add2, 2, numbers) << endl;
  cout << "3: " << callFn((void *)add3, 3, numbers) << endl;
  cout << "4: " << callFn((void *)add4, 4, numbers) << endl;
  cout << "5: " << callFn((void *)add5, 5, numbers) << endl;

  return 0;
}

Download files

Answer and comments