Virtual Memory

Virtual memory management.

Overview

AsmJit's virtual memory management is divided into three main categories:

  • Low level interface that provides cross-platform abstractions for virtual memory allocation. Implemented in VirtMem namespace. This API is a thin wrapper around operating system specific calls such as 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.
  • Middle level API that is provided by JitAllocator, which uses VirtMem internally and offers nicer API that can be used by users to allocate executable memory conveniently. JitAllocator tries to be smart, for example automatically using dual mapping or MAP_JIT on hardened environments.
  • High level API that is provided by JitRuntime, which implements Target interface and uses JitAllocator under the hood. Since JitRuntime inherits from Target it makes it easy to use with CodeHolder. Many AsmJit examples use JitRuntime for its simplicity and easy integration.

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.

Hardened Environments

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.

Namespaces

Classes

Enumerations

Enumeration Type Documentation

class JitAllocatorOptions : uint32_tenumstrong◆ 

Options used by JitAllocator.

ConstantDescription
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.

Remarks
Dual mapping would be automatically turned on by JitAllocator in case of hardened runtime that enforces W^X policy, so specifying this flag is essentially forcing to use dual mapped pages even when RWX pages can be allocated and dual mapping is not necessary.
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 JitAllocator itself by having blocks of different allocation granularities. Using this feature only for few allocations won't pay off as the allocator may need to create more blocks initially before it can take the advantage of variable block granularity.

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 release() or reset().

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 alloc() and release() when the allocator has either no blocks or have all blocks fully occupied.

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 [fnPtr - 8] to decode whether the function has his metadata present. However, reading 8 bytes below a function means that if a function is placed at the very beginning of a memory mapped region, it could try to read bytes that are inaccessible. And since AsmJit can be compiled as a shared library and used by applications instrumented by UBSAN, it's not possible to conditionally compile the support only when necessary.

Remarks
This flag controls a workaround to make it possible to use LLVM UBSAN with AsmJit's JitAllocator. There is no undefined behavior even when kDisableInitialPadding is used, however, that doesn't really matter as LLVM's UBSAN introduces one, and according to LLVM developers it's a "trade-off". This flag is safe to use when the code is not instrumented with LLVM's UBSAN.
kUseLargePages 

Enables the use of large pages, if they are supported and the process can actually allocate them.

Remarks
This flag is a hint - if large pages can be allocated, JitAllocator would try to allocate them. However, if the allocation fails, it will still try to fallback to use regular pages as JitAllocator is designed to minimize allocation failures, so a regular page is better than no page at all. Also, if a block JitAllocator wants to allocate is too small to consume a whole large page, regular page(s) will be allocated as well.
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.

Remarks
If kUseLargePages option is used, the allocator would prefer large pages only when allocating a block that has a sufficient size. Usually the allocator first allocates smaller block and when more requests come it will start increasing the block size of next allocations. This option makes it sure that even the first allocation would be the same as a minimum large page when large pages are enabled and can be allocated.
kCustomFillPattern 

Use a custom fill pattern, must be combined with kFlagFillUnusedMemory.