7

Assuming that a VM runs a JIT compiler on otherwise "interpreted" code, such as a line by line interpreter or some form of bytecode/IL code and determines that it can create optimised native code for some part of what is running, what mechanism is actually used to "emit" and then call the emitted code?

If I was writing a normal compiler, I couldsimply write the desired binary to a file, to be run later by the system in whatever manner it usually would be, but how would a JIT compiler do that during runtime, running the new code within the same process that created it? Would it simply write the raw bytes into an array of contiguous memory, cast that memory/arrays pointer as a function pointer, and call it? Or are there some other functions available in low level languages to facilitate this?

AIWalker
  • 1,355

3 Answers3

9

If I was writing a normal compiler, I couldsimply write the desired binary to a file, to be run later by the system in whatever manner it usually would be, but how would a JIT compiler do that during runtime, running the new code within the same process that created it?

There is no reason a JIT compiler couldn't do the same thing as well. In fact, that is exactly what the MJIT compiler in the YARV Ruby VM does: it generates C code, writes it to a file, calls GCC to compile that file into a library, then links the library into the current process.

And it turns out this is actually a lot more efficient than it sounds!

Or are there some other functions available in low level languages to facilitate this?

This is not really a feature of the language, more of the host platform. Most modern platforms have a concept which allows dynamically linking code into a process, in Windows, those are called "DLL", in macOS "dyld", in Java "JAR" or "class file", and so on.

Would it simply write the raw bytes into an array of contiguous memory, cast that memory/arrays pointer as a function pointer, and call it?

That is another possibility.

A third possibility is that most modern languages have a way of calling "external" code (often called a "Foreign Function Interface" or "FFI"). So, you could just use the standard FFI API to call that generated code.

A fourth possibility is that many language implementations use native code somewhere, so they need to have a way of interacting with that code anyway. For example, in the Oracle HotSpot JVM, the low-level functionality is implemented in C++ and called as native code. (For example, the code that implements the iadd bytecode is written in C++, not in Java, and is executed natively.) So, the language implementation must already have a way of calling native code anyway.

Jörg W Mittag
  • 104,619
2

The reality on a modern operating system is that you can't just run any compiled code, you need the operating system to give you permission for that. For example, on MacOS getting that permission will be difficult, on iOS it can be impossible.

You will carefully check what functionality for this is available on your operating system. It may be that you can just generate a file just like a linker would, mark it to be "executable" and load it just like a dynamic library. If you are lucky, you can do that with data in memory: You tell the operating system "I want this data to become executable code" and the OS does it for you.

On some OS you may need permission from the manufacturer to do this. For example, an iPhone is supposed to be secure. It wouldn't be very secure if anyone could run code that has never been reviewed, so you would need permission from Apple to be able to do this. At the extreme, in iOS 16 users will be able to turn on a security setting that makes JIT compilation impossible. If you have some nation state seriously after you, that's the setting that you would use, because it prevents many possible attacks. Also if you are just paranoid and think they are after you.

gnasher729
  • 49,096
0

You simply allocate some memory, put the code in the memory, and call the code. This is all that the OS's program loader really does, anyway. It puts the code into memory, and then it calls a certain memory address, which is the starting point of the program.

There are a few unique complications, but not many:

  1. You may need to flush (reset) the instruction cache so the CPU reads the instructions you just wrote, instead of whatever was there previously. Self-modifying code is rare, so many CPUs don't detect this automatically.
  2. You may need to mark the allocated memory as executable. Modern CPUs will not execute code unless the memory region is specifically marked as executable. This is an extra layer of security to try and catch bugs where you accidentally call data (like a web page) which could lead to evil people installing viruses. If you're calling some memory on purpose, and it's not a bug, you need to tell the CPU that.

Would it simply write the raw bytes into an array of contiguous memory, cast that memory/arrays pointer as a function pointer, and call it?

In C or C++, this is a typical way to do it. It's undefined behaviour according to C and C++, but it's defined behaviour for the CPU, as long as you are sure that the compiler does what you expect, since compilers are allowed to wrongly compile undefined behaviour. In practice, compilers tend to compile this the way you expect, even with optimization turned on. If you want to guarantee it, you might use inline assembly code to do the call.

If you are using a language more like Java, Python, or Ruby, you might have considerably more difficulty trying to convince the interpreter to call some random byte buffer.

If you wrote your compiler in assembly language (!) then of course you would just write a call instruction.