mirror of
https://github.com/pound-emu/ballistic.git
synced 2026-10-24 23:45:05 -07:00
338 lines
11 KiB
C
338 lines
11 KiB
C
//! This module provides JIT crash debugging and Guest PC tracking for Ballistic.
|
|
//!
|
|
//! When the host CPU encounters a SIGSEGV or SIGILL inside the JIT buffer, this module
|
|
//! intercepts the OS signal, maps the faulting host instruction pointer (RIP) back to the original
|
|
//! Guest Program Counter, and invokes a user-defined callback or logs the error before aborting.
|
|
//!
|
|
//! # Examples
|
|
//!
|
|
//! Initialization and tracking a JIT block:
|
|
//!
|
|
//! ```c
|
|
//! #include "bal_jit_debug.h"
|
|
//! #include "bal_memory.h"
|
|
//! #include <assert.h>
|
|
//!
|
|
//! bal_allocator_t allocator = {0};
|
|
//! bal_allocator_default_init(&allocator);
|
|
//!
|
|
//! bal_logger_init_default();
|
|
//!
|
|
//! bal_jit_debug_context_t debug_context = {0};
|
|
//! bal_error_t err = bal_jit_debug_init(&allocator, &debug_context);
|
|
//! assert(err == BAL_SUCCESS);
|
|
//!
|
|
//! uint8_t jit_code[64] = {0};
|
|
//! bal_jit_instruction_map_t mappings[] = {
|
|
//! { .x86_offset = 0, .guest_pc_offset = 0 },
|
|
//! { .x86_offset = 16, .guest_pc_offset = 4 }
|
|
//! };
|
|
//!
|
|
//! err = bal_jit_debug_add_block(&debug_context, jit_code, sizeof(jit_code), 0x1000, mappings, 2);
|
|
//! assert(err == BAL_SUCCESS);
|
|
//!
|
|
//! err = bal_jit_debug_register_signal_handler(&debug_context, jit_code, sizeof(jit_code));
|
|
//! assert(err == BAL_SUCCESS);
|
|
//!
|
|
//! bal_jit_debug_unregister_signal_handler(&debug_context);
|
|
//! bal_jit_debug_destroy(&allocator, &debug_context);
|
|
//! ```
|
|
|
|
#ifndef BALLISTIC_BAL_JIT_DEBUG_H
|
|
#define BALLISTIC_BAL_JIT_DEBUG_H
|
|
|
|
#include "bal_attributes.h"
|
|
#include "bal_errors.h"
|
|
#include "bal_log.h"
|
|
#include "bal_memory.h"
|
|
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#define BAL_JIT_DEBUG_ENTRY_CAPACITY 8192
|
|
#define BAL_JIT_DEBUG_ARENA_CAPACITY_BYTES (4 * 1024 * 1024) // 4 MiB
|
|
|
|
#ifdef __cplusplus
|
|
extern "C"
|
|
{
|
|
#endif // __cplusplus
|
|
|
|
/// Callback when a crash occurs inside a JIT block.
|
|
typedef void (*bal_jit_crash_callback_t)(void *user_data,
|
|
uint64_t guest_pc,
|
|
uint64_t host_rip,
|
|
uint32_t jit_offset,
|
|
const void *jit_block_start,
|
|
uint32_t jit_block_size);
|
|
|
|
typedef struct
|
|
{
|
|
/// Byte offset from the start of the JIT block.
|
|
uint32_t x86_offset;
|
|
|
|
/// Offset from the JIT block's base Guest PC.
|
|
uint32_t guest_pc_offset;
|
|
} bal_jit_instruction_map_t;
|
|
|
|
static_assert(8 == sizeof(bal_jit_instruction_map_t), "Struct size mismatch");
|
|
|
|
/// Metadata for a single compiled JIT block.
|
|
typedef struct
|
|
{
|
|
uint64_t base_guest_pc;
|
|
uint32_t instruction_count;
|
|
uint32_t pad0;
|
|
bal_jit_instruction_map_t *mappings;
|
|
uint64_t pad1;
|
|
} bal_jit_block_metadata_t;
|
|
|
|
static_assert(32 == sizeof(bal_jit_block_metadata_t), "Struct size mismatch");
|
|
|
|
typedef struct
|
|
{
|
|
void *rx_start;
|
|
uint32_t rx_size;
|
|
uint32_t pad0;
|
|
bal_jit_block_metadata_t *metadata;
|
|
uint64_t pad1;
|
|
} bal_jit_block_entry_t;
|
|
|
|
static_assert(32 == sizeof(bal_jit_block_entry_t), "Struct size mismatch");
|
|
|
|
BAL_ALIGNED(64) typedef struct
|
|
{
|
|
bal_jit_block_entry_t *entries;
|
|
|
|
/// Memory Layout of the arena:
|
|
///
|
|
/// Offset 0x0000: bal_jit_block_metadata_t (Block 0).
|
|
/// Offset 0x0020: Array of bal_jit_instruction_map_t (N * 8 bytes).
|
|
/// Offset 0x0020 + (N*8): bal_jit_block_metadata_t (Block 1).
|
|
/// Offset ....: Array of bal_jit_instruction_map_t (M * 8 bytes).
|
|
/// etc...
|
|
uint8_t *metadata_arena;
|
|
size_t entry_count;
|
|
|
|
// The entry list capacity. This is NOT in bytes.
|
|
size_t entry_capacity;
|
|
size_t arena_offset;
|
|
|
|
// Cold data.
|
|
|
|
// The metadata arena capacity in bytes.
|
|
size_t arena_capacity;
|
|
|
|
void *jit_buffer_start;
|
|
void *jit_buffer_end;
|
|
bal_jit_crash_callback_t crash_callback;
|
|
void *crash_callback_user_data;
|
|
bal_error_t status;
|
|
|
|
/// Integrity check.
|
|
uint32_t magic;
|
|
|
|
uint8_t pad[40];
|
|
} bal_jit_debug_context_t;
|
|
|
|
static_assert(128 == sizeof(bal_jit_debug_context_t), "Struct size mismatch");
|
|
|
|
/// Initializes the JIT debug context.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * [`BAL_SUCCESS`] on success.
|
|
/// * [`BAL_ERROR_INVALID_ARGUMENT`] if `allocator` or `context` is `NULL`.
|
|
/// * [`BAL_ERROR_ALLOCATION_FAILED`] if the allocator fails to allocate the entries or arena.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```c
|
|
//! #include "bal_jit_debug.h"
|
|
//! #include <assert.h>
|
|
//!
|
|
//! bal_allocator_t allocator = {0};
|
|
//! bal_allocator_default_init(&allocator);
|
|
//!
|
|
//! bal_logger_init_default();
|
|
//!
|
|
//! bal_jit_debug_context_t debug_context = {0};
|
|
//! bal_error_t err = bal_jit_debug_init(&allocator, &debug_context);
|
|
//! assert(err == BAL_SUCCESS);
|
|
//!
|
|
//! bal_jit_debug_unregister_signal_handler(&debug_context);
|
|
//! bal_jit_debug_destroy(&allocator, &debug_context);
|
|
/// ```
|
|
BAL_COLD bal_error_t bal_jit_debug_init(const bal_allocator_t *allocator,
|
|
bal_jit_debug_context_t *context);
|
|
|
|
/// Destroys the JIT debug context and frees all internal allocations.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// * `allocator` must be a valid pointer to the allocator used during initialization.
|
|
/// * `context` must be a valid pointer to an initialized context.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```c
|
|
/// #include "bal_jit_debug.h"
|
|
/// #include <assert.h>
|
|
///
|
|
/// bal_allocator_t allocator = {0};
|
|
/// bal_allocator_default_init(&allocator);
|
|
///
|
|
/// bal_logger_init_default();
|
|
///
|
|
/// bal_jit_debug_context_t context = {0};
|
|
/// bal_jit_debug_init(&allocator, &context);
|
|
///
|
|
/// bal_jit_debug_destroy(&allocator, &context);
|
|
/// assert(context.entries == NULL);
|
|
/// assert(context.metadata_arena == NULL);
|
|
/// ```
|
|
BAL_COLD void bal_jit_debug_destroy(const bal_allocator_t *allocator,
|
|
bal_jit_debug_context_t *context);
|
|
|
|
/// Records a newly compiled JIT block and its instruction mappings into the debug context.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// * `context` must be initialized.
|
|
/// * `mapping` must point to an array of at least `instruction_count` valid mappings.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * [`BAL_SUCCESS`] on success.
|
|
/// * [`BAL_ERROR_INVALID_ARGUMENT`] if the function arguments are NULL or if
|
|
/// `instruction_count` is `0`.
|
|
/// * [`BAL_ERROR_BUFFER_OVERFLOW`] if the entry list or metadata arena is full.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```c
|
|
/// #include "bal_jit_debug.h"
|
|
/// #include <assert.h>
|
|
///
|
|
/// bal_allocator_t allocator = {0};
|
|
/// bal_allocator_default_init(&allocator);
|
|
///
|
|
/// bal_logger_init_default();
|
|
///
|
|
/// bal_jit_debug_context_t context = {0};
|
|
/// bal_jit_debug_init(&allocator, &context);
|
|
///
|
|
/// uint8_t jit_code[64] = {0};
|
|
/// bal_jit_instruction_map_t mappings[] = {
|
|
/// { .x86_offset = 0, .guest_pc_offset = 0 },
|
|
/// { .x86_offset = 16, .guest_pc_offset = 4 }
|
|
/// };
|
|
///
|
|
/// bal_error_t err = bal_jit_debug_add_block(&context, jit_code, sizeof(jit_code), 0x1000,
|
|
/// mappings, 2);
|
|
/// assert(err == BAL_SUCCESS);
|
|
/// assert(context.entry_count == 1);
|
|
///
|
|
/// bal_jit_debug_destroy(&allocator, &context);
|
|
/// ```
|
|
BAL_HOT bal_error_t bal_jit_debug_add_block(bal_jit_debug_context_t *context,
|
|
void *rx_start,
|
|
uint32_t rx_size,
|
|
uint64_t base_guest_pc,
|
|
const bal_jit_instruction_map_t *mapping,
|
|
uint32_t instruction_count);
|
|
|
|
/// Registers the OS-level signal/exception handler to intercept faults inside the JIT buffer.
|
|
///
|
|
/// If `context.crash_callback` is not NULL, Ballistic will call it when the compiled JIT code
|
|
/// creates a segmentation fault, else Ballistic logs the fault, crash the program, and
|
|
/// generate a core dump.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// * `jit_buffer_start` must point to a valid executable memory region of at least
|
|
/// `jit_buffer_size` bytes.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * [`BAL_SUCCESS`] on success.
|
|
/// * [`BAL_ERROR_INVALID_ARGUMENT`] if the function arguments are NULL or if
|
|
/// `jit_buffer_size` is `0`.
|
|
/// * [`BAL_ERROR_ALLOCATION_FAILED`] if the OS fails to register the handler.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```c
|
|
/// #include "bal_jit_debug.h"
|
|
/// #include <assert.h>
|
|
///
|
|
/// bal_allocator_t allocator = {0};
|
|
/// bal_allocator_default_init(&allocator);
|
|
///
|
|
/// bal_logger_init_default();
|
|
///
|
|
/// bal_jit_debug_context_t context = {0};
|
|
/// bal_jit_debug_init(&allocator, &context);
|
|
///
|
|
/// uint8_t jit_buffer[4096] = {0};
|
|
/// bal_error_t err = bal_jit_debug_register_signal_handler(&context, jit_buffer,
|
|
/// sizeof(jit_buffer));
|
|
/// assert(err == BAL_SUCCESS);
|
|
/// assert(context.jit_buffer_start == jit_buffer);
|
|
///
|
|
/// bal_jit_debug_unregister_signal_handler(&context);
|
|
/// bal_jit_debug_destroy(&allocator, &context);
|
|
/// ```
|
|
BAL_COLD bal_error_t bal_jit_debug_register_signal_handler(bal_jit_debug_context_t *context,
|
|
void *jit_buffer_start,
|
|
size_t jit_buffer_size);
|
|
|
|
/// Unregisters the OS-level signal/exception handler.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// * `context` must be a valid pointer.
|
|
///
|
|
/// # Warning
|
|
///
|
|
/// Calling this function sets `context.jit_buffer_start` and `context.jit_buffer_end` to NULL.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```c
|
|
/// #include "bal_jit_debug.h"
|
|
/// #include <assert.h>
|
|
///
|
|
/// bal_allocator_t allocator = {0};
|
|
/// bal_allocator_default_init(&allocator);
|
|
///
|
|
/// bal_logger_init_default();
|
|
///
|
|
/// bal_jit_debug_context_t context = {0};
|
|
/// bal_jit_debug_init(&allocator, &context);
|
|
///
|
|
/// uint8_t jit_buffer[4096] = {0};
|
|
/// bal_jit_debug_register_signal_handler(&context, jit_buffer, sizeof(jit_buffer));
|
|
///
|
|
/// bal_jit_debug_unregister_signal_handler(&context);
|
|
/// assert(context.jit_buffer_start == NULL);
|
|
/// assert(context.jit_buffer_end == NULL);
|
|
///
|
|
/// bal_jit_debug_destroy(&allocator, &context);
|
|
/// ```
|
|
BAL_COLD void bal_jit_debug_unregister_signal_handler(bal_jit_debug_context_t *context);
|
|
|
|
#ifdef BALLISTIC_BUILD_TESTS
|
|
|
|
/// This functions is exposed for testing purposes only.
|
|
bool handle_jit_fault(uint64_t rip, uint64_t rbp);
|
|
|
|
#endif // BALLISTIC_BUILD_TESTS
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif // __cplusplus
|
|
|
|
#endif // BALLISTIC_BAL_JIT_DEBUG_H
|