Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to implement JIT for bytecode interpreter in C without resorting to assembly?

I've written a bytecode interpreter for a simple stack-based VM in (GNU) C. All VM instructions are implemented as code between labels. Most instructions do not consume inline arguments as they just pop them off the VM stack (e.g. add pops two numbers a, b off the stack and pushes a+b back on).

I thought JIT would be as simple as identifying basic blocks in the bytecode, and generating "dynamic superinstructions" by just stitching together instruction implementations by memcpying between labels into a buffer marked as executable using mmap/mprotect. This works for all instructions except the push b instruction that pushes the next byte of the bytecode stream onto the VM stack (e.g. push 17).

Is there any, at least somewhat portable, way I can implement instructions that consume arguments in a similar fashion, either by "injecting" arguments into each instance of the push instruction that gets memcpyied (thus faking closures at runtime (?)) or at least by somehow accessing the PC (though any mention of PC is probably inherently not portable) of the running instance using some variant of setjmp/longjmp that lets me read the PC value?

EDIT: PC could potentially be useful by doing something like the following in pseudo-C (???)

char arg;
char args[] = {1, 2, 3};

push:
  uintptr_t pc = get_pc();
  arg = args[some_clever_hash(pc)];
  stack.push(arg);

Maybe something like this would be more realistic?:

char arg;
char args[] = {1, 2, 3};

push:
  setarg();
  stack.push(arg);

void setarg(void)
{
  uintptr_t pc = get_pc_of_caller();
  arg = args[some_clever_hash(pc)];
}

like image 768
Gepapado Avatar asked Oct 16 '25 13:10

Gepapado


2 Answers

A good trick might be to have two 'templates' for your push code -- one that pushes the constant 0x01020304 and a second that pushes the constant 0x10203040 (for 32-bit constants). Then you xor the two templates -- if all goes well, all but 4 bytes of the result will be 0, and those 4 bytes will be 0x11, 0x22, 0x33, and 0x44, and those identify which 4 bytes of the template to fill in with your 4 bytes to generate a push for any specific constant. If the templates aren't the same size or differ in ways other than those 4 values, you know you need to do something else.

like image 102
Chris Dodd Avatar answered Oct 18 '25 09:10

Chris Dodd


I'm not sure if I'm understanding the question, but you don't really need to do anything PC-relative, do you? It's only that way in the bytecode. All you really need is an implementation of pushing an immediate (of the appropriate size) onto the VM stack, and a fixup function per-architecture. The implementation can push any arbitrary value (although for the sake of generality you should probably avoid values like 0 that might have more efficient codings, and indeed your best bet might be something like 0x01020304), and the fixup function handles replacing some bytes of the implementation with the appropriate constant from the bytecode. Obviously which bytes go where, and the details of things like endian-swapping, are going to vary per CPU architecture and implementation. Obviously portability is hard to get here, and the details will have to be gleaned the hard way, but once you do that for a given system you can add an assert that the bytes of 0x01020304 are found in the expected place in the implementation template. (Maybe even generate two versions, with two distinct values, for added safety). That way, if the implementation changes out from under you, at least you will be able to bomb out early in the compilation phase, instead of executing erroneous code at runtime.

like image 32
hobbs Avatar answered Oct 18 '25 09:10

hobbs



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!