AsmJit
Low-Latency Machine Code Generation
Virtual memory management.
AsmJit's virtual memory management is divided into three main categories:
VirtualAlloc()
and mmap()
and it's intended to be used by AsmJit's higher level API. Low-level virtual memory functions can be used to allocate virtual memory, change its permissions, and to release it. Additionally, an API that allows to create dual mapping (to support hardened environments) is provided.MAP_JIT
on hardened environments.The main difference between VirtMem and JitAllocator is that VirtMem can only be used to allocate whole pages, whereas JitAllocator has malloc()
like API that allows to allocate smaller quantities that usually represent the size of an assembled function or a chunk of functions that can represent a module, for example. JitAllocator then tracks used space of each page it maintains. Internally, JitAllocator uses two bit arrays to track occupied regions in each allocated block of pages.
In the past, allocating virtual memory with Read+Write+Execute (RWX) access permissions was easy. However, modern operating systems and runtime environments often use hardening, which typically prohibits mapping pages with both Write and Execute permissions (known as the W^X policy). This presents a challenge for JIT compilers because generated code for a single function is unlikely to fit in exactly N pages without leaving some space empty. To accommodate this, the execution environment may need to temporarily change the permissions of existing pages to read+write (RW) to insert new code into them, however, sometimes it's not possible to ensure that no thread is executing code in such affected pages in a multithreaded environment, in which multiple threads may be executing generated code.
Such restrictions leave a lot of complexity on the application, so AsmJit implements a dual mapping technique to make the life of AsmJit users easier. In this technique, a region of memory is mapped to two different virtual addresses with different access permissions. One virtual address is mapped with read and write (RW) access, which is used by the JIT compiler to write generated code. The other virtual address is mapped with read and execute (RX) access, which is used by the application to execute the generated code.
However, implementing dual mapping can be challenging because it typically requires obtaining an anonymous file descriptor on most Unix-like operating systems. This file descriptor is then passed to mmap() twice to create the two mappings. AsmJit handles this challenge by using system-specific techniques such as memfd_create()
on Linux, shm_open(SHM_ANON)
on BSD, and MAP_REMAPDUP
with mremap()
on NetBSD. The latter approach does not require a file descriptor. If none of these options are available, AsmJit uses a plain open()
call followed by unlink()
.
The most challenging part is actually obtaining a file descriptor that can be passed to mmap()
with PROT_EXEC
. This is still something that may fail, for example the environment could be hardened in a way that this would not be possible at all, and thus dual mapping would not work.
Dual mapping is provided by both VirtMem and JitAllocator.
Options used by JitAllocator.
Constant | Description |
---|---|
kNone | No options. |
kUseDualMapping | Enables the use of an anonymous memory-mapped memory that is mapped into two buffers having a different pointer. The first buffer has read and execute permissions and the second buffer has read+write permissions. See VirtMem::allocDualMapping() for more details about this feature.
|
kUseMultiplePools | Enables the use of multiple pools with increasing granularity instead of a single pool. This flag would enable 3 internal pools in total having 64, 128, and 256 bytes granularity. This feature is only recommended for users that generate a lot of code and would like to minimize the overhead of |
kFillUnusedMemory | Always fill reserved memory by a fill-pattern. Causes a new block to be cleared by the fill pattern and freshly released memory to be cleared before making it ready for another use. |
kImmediateRelease | When this flag is set the allocator would immediately release unused blocks during When this flag is not set the allocator would keep one empty block in each pool to prevent excessive virtual memory allocations and deallocations in border cases, which involve constantly allocating and deallocating a single block caused by repetitive calling |
kDisableInitialPadding | This flag enables placing functions (or allocating memory) at the very beginning of each memory mapped region. Initially, this was the default behavior. However, LLVM developers working on undefined behavior sanitizer (UBSAN) decided that they want to store metadata before each function and to access such metadata before an indirect function call. This means that the instrumented code always reads from
|
kUseLargePages | Enables the use of large pages, if they are supported and the process can actually allocate them.
|
kAlignBlockSizeToLargePage | Forces JitAllocator to always align block size to be at least as big as a large page, if large pages are enabled. This option does nothing if large pages are disabled.
|
kCustomFillPattern | Use a custom fill pattern, must be combined with |