Skip to content

Commit

Permalink
Added: More efficient memory use for Assembly Hook Properties
Browse files Browse the repository at this point in the history
  • Loading branch information
Sewer56 committed Dec 22, 2023
1 parent 11e72c5 commit 1e2aa75
Show file tree
Hide file tree
Showing 8 changed files with 591 additions and 120 deletions.
26 changes: 18 additions & 8 deletions docs/dev/design/assembly-hooks/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,13 @@ The following table below shows common hook lengths, for:
- [Targeted Memory Allocation (TMA)](../../platform/overview.md#recommended-targeted-memory-allocation) (expected best case) when above `Relative Jump` range.
- Worst case scenario.

| Architecture | Relative | TMA | Worst Case |
|----------------|---------------------|--------------|-----------------|
| x86^[1]^ | 5 bytes (+- 2GiB) | 5 bytes | 5 bytes |
| x86_64 | 5 bytes (+- 2GiB) | 6 bytes^[2]^ | 13 bytes^[3]^ |
| x86_64 (macOS) | 5 bytes (+- 2GiB) | 13 bytes^[4]^| 13 bytes^[3]^ |
| ARM64 | 4 bytes (+- 128MiB) | 12 bytes^[6]^| 20 bytes^[5]^ |
| ARM64 (macOS) | 4 bytes (+- 128MiB) | 12 bytes^[6]^| 20 bytes^[5]^ |
| Architecture | Relative | TMA | Worst Case |
| -------------- | ------------------- | ------------- | ------------- |
| x86^[1]^ | 5 bytes (+- 2GiB) | 5 bytes | 5 bytes |
| x86_64 | 5 bytes (+- 2GiB) | 6 bytes^[2]^ | 13 bytes^[3]^ |
| x86_64 (macOS) | 5 bytes (+- 2GiB) | 13 bytes^[4]^ | 13 bytes^[3]^ |
| ARM64 | 4 bytes (+- 128MiB) | 12 bytes^[6]^ | 20 bytes^[5]^ |
| ARM64 (macOS) | 4 bytes (+- 128MiB) | 12 bytes^[6]^ | 20 bytes^[5]^ |

^[1]^: x86 can reach any address from any address with relative branch due to integer overflow/wraparound.
^[2]^: [`jmp [<Address>]`, with &lt;Address&gt; at &lt; 2GiB](../../arch/operations.md#jumpabsoluteindirect).
Expand Down Expand Up @@ -206,4 +206,14 @@ This means a few functionalities must be supported here:

- Supporting Assembly via FASM.
- As this is only possible in Windows (FASM can't be recompiled on other OSes as library), this feature will be getting dropped.
- The `Reloaded.Hooks` wrapper will continue to ship FASM for backwards compatibility, however mods are expected to migrate to the new library in the future.
- The `Reloaded.Hooks` wrapper will continue to ship FASM for backwards compatibility, however mods are expected to migrate to the new library in the future.

## Limits

Assembly hook info is packed by default to save on memory space. By default, the following limits apply:

| Property | 4 Byte Instruction (e.g. ARM) | x86 | Unknown |
| -------------------- | ----------------------------- | ------ | ------- |
| Max Branch Length | 4 | 5 | 8 |
| Max Orig Code Length | 16KiB | 4KiB | 128MiB |
| Max Hook Code Length | 2MiB | 128KiB | 1GiB |
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,28 @@ extern crate alloc;
use crate::{
api::{
buffers::buffer_abstractions::{Buffer, BufferFactory},
errors::assembly_hook_error::{ArrayTooShortKind, AssemblyHookError},
errors::assembly_hook_error::AssemblyHookError,
jit::compiler::Jit,
length_disassembler::LengthDisassembler,
rewriter::code_rewriter::CodeRewriter,
settings::assembly_hook_settings::AssemblyHookSettings,
traits::register_info::RegisterInfo,
},
helpers::{
atomic_write_masked::atomic_write_masked,
make_inline_rel_branch::{make_inline_branch, INLINE_BRANCH_LEN},
},
helpers::atomic_write_masked::atomic_write_masked,
internal::assembly_hook::create_assembly_hook,
};
use alloc::boxed::Box;
use bitfield::bitfield;
use core::marker::PhantomData;

bitfield! {
/// `AddImmediate` represents the bitfields of the ADD (immediate) instruction
/// in AArch64 architecture.
pub struct AssemblyHookPackedProps(u8);
impl Debug;
use core::marker::PhantomData;
use core::ptr::NonNull;

/// Length of the 'branch to orig' inline array.
branch_to_orig_len, set_branch_to_orig_len: 6, 4;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use super::assembly_hook_props_x86::*;

/// Length of the 'branch to hook' inline array.
branch_to_hook_len, set_branch_to_hook_len: 3, 1;
#[cfg(target_arch = "aarch64")]
use super::assembly_hook_props_4byteins::*;

/// True if the hook is enabled, else false.
is_enabled, set_is_enabled: 0;
}

impl AssemblyHookPackedProps {
/// Creates a new `AssemblyHookPackedProps` with specified properties.
pub fn new(is_enabled: bool, branch_to_orig_len: u8, branch_to_hook_len: u8) -> Self {
let mut props = AssemblyHookPackedProps(0);
props.set_is_enabled(is_enabled);
props.set_branch_to_orig_len(branch_to_orig_len);
props.set_branch_to_hook_len(branch_to_hook_len);
props
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64")))]
use super::assembly_hook_props_unknown::*;

/// Represents an assembly hook.
#[repr(C)] // Not 'packed' because this is not in array and malloc in practice will align this.
Expand All @@ -58,26 +37,13 @@ where
TBufferFactory: BufferFactory<TBuffer>,
{
// docs/dev/design/assembly-hooks/overview.md
/// The code placed at the hook function when the hook is enabled.
enabled_code: Box<[u8]>, // 0

/// The code placed at the hook function when the hook is disabled.
disabled_code: Box<[u8]>, // 4/8

/// The address of the stub containing custom code.
stub_address: usize, // 8/16

/// Stores sizes of 'branch_to_orig_opcode' and 'branch_to_hook_opcode'.
props: AssemblyHookPackedProps, // 12/24

/// The code to branch to 'orig' segment in the buffer, when disabling the hook.
branch_to_orig_opcode: [u8; INLINE_BRANCH_LEN], // 13/25
stub_address: usize, // 0

/// The code to branch to 'hook' segment in the buffer, when enabling the hook.
branch_to_hook_opcode: [u8; INLINE_BRANCH_LEN], // 18/30 (-1 on AArch64)
/// Address of 'props' structure
props: NonNull<AssemblyHookPackedProps>, // 4/8

// End: 40 (AArch64) [no pad: 33]
// End: 24/40 (x86) [no pad: 23/35]
// Struct size: 8/16 bytes.

// Dummy type parameters for Rust compiler to comply.
_unused_buf: PhantomData<TBuffer>,
Expand All @@ -99,29 +65,11 @@ where
TBufferFactory: BufferFactory<TBuffer>,
{
pub fn new(
is_enabled: bool,
branch_to_orig: Box<[u8]>,
branch_to_hook: Box<[u8]>,
enabled_code: Box<[u8]>,
disabled_code: Box<[u8]>,
props: NonNull<AssemblyHookPackedProps>,
stub_address: usize,
) -> Result<Self, AssemblyHookError<TRegister>> {
let branch_to_orig_opcode =
Self::inline_branch(&branch_to_orig, ArrayTooShortKind::ToOrig)?;
let branch_to_hook_opcode =
Self::inline_branch(&branch_to_hook, ArrayTooShortKind::ToHook)?;
let props = AssemblyHookPackedProps::new(
is_enabled,
branch_to_orig_opcode.len() as u8,
branch_to_hook_opcode.len() as u8,
);

Ok(Self {
props,
branch_to_orig_opcode,
branch_to_hook_opcode,
enabled_code,
disabled_code,
stub_address,
_unused_buf: PhantomData,
_unused_tj: PhantomData,
Expand Down Expand Up @@ -215,28 +163,52 @@ where
/// If the hook is already enabled, this function does nothing.
/// If the hook is disabled, this function will write the hook to memory.
pub fn enable(&self) {
let num_bytes = self.props.branch_to_hook_len() as usize;
self.write_hook(&self.branch_to_hook_opcode, &self.enabled_code, num_bytes);
unsafe {
let props = self.props.as_ref();
let num_bytes = props.get_branch_to_hook_len();
self.write_hook(
props.get_branch_to_hook_slice(),
props.get_enabled_code(),
num_bytes,
);
}
}

/// Disables the hook.
/// This will cause the hook to be no-opped.
/// If the hook is already disabled, this function does nothing.
/// If the hook is enabled, this function will no-op the hook.
pub fn disable(&self) {
let num_bytes = self.props.branch_to_orig_len() as usize;
self.write_hook(&self.branch_to_orig_opcode, &self.disabled_code, num_bytes);
unsafe {
let props = self.props.as_ref();
let num_bytes = props.get_branch_to_orig_len();
self.write_hook(
props.get_branch_to_orig_slice(),
props.get_disabled_code(),
num_bytes,
);
}
}

/// Returns true if the hook is enabled, else false.
pub fn get_is_enabled(&self) -> bool {
self.props.is_enabled()
unsafe { self.props.as_ref().is_enabled() }
}
}

fn inline_branch(
rc: &[u8],
kind: ArrayTooShortKind,
) -> Result<[u8; INLINE_BRANCH_LEN], AssemblyHookError<TRegister>> {
make_inline_branch(rc).map_err(|e| AssemblyHookError::InlineBranchError(e, kind))
impl<TBuffer, TJit, TRegister, TDisassembler, TRewriter, TBufferFactory> Drop
for AssemblyHook<TBuffer, TJit, TRegister, TDisassembler, TRewriter, TBufferFactory>
where
TBuffer: Buffer,
TJit: Jit<TRegister>,
TRegister: RegisterInfo + Clone + Default,
TDisassembler: LengthDisassembler,
TRewriter: CodeRewriter<TRegister>,
TBufferFactory: BufferFactory<TBuffer>,
{
fn drop(&mut self) {
unsafe {
self.props.as_mut().free();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use bitfield::bitfield;

bitfield! {
/// Defines the data layout of the Assembly Hook data.
/// For architectures which use 4 byte instructions.
pub struct AssemblyHookPackedProps(u32);
impl Debug;

/// True if the hook is enabled, else false.
pub is_enabled, set_is_enabled: 0;

/// Length of the 'disabled code' array.
u16, disabled_code_len, set_disabled_code_len_impl: 12, 1; // Max 16KiB.

/// Length of the 'enabled code' array.
u32, enabled_code_len, set_enabled_code_len_impl: 31, 13; // Max 2MiB.
}

impl AssemblyHookPackedProps {
/// Gets the length of the 'enabled code' array.
pub fn get_enabled_code_len(&self) -> usize {
self.enabled_code_len() as usize * 4
}

/// Gets the length of the 'disabled code' array.
pub fn get_disabled_code_len(&self) -> usize {
self.disabled_code_len() as usize * 4
}

/// Sets the length of the 'enabled code' array with a minimum value of 4.
pub fn set_enabled_code_len(&mut self, len: usize) {
debug_assert!(
len >= 4 && len % 4 == 0,
"Length must be a multiple of 4 and at least 4"
);
self.set_enabled_code_len_impl((len / 4) as u32);
}

/// Sets the length of the 'disabled code' array with a minimum value of 4.
pub fn set_disabled_code_len(&mut self, len: usize) {
debug_assert!(
len >= 4 && len % 4 == 0,
"Length must be a multiple of 4 and at least 4"
);
self.set_disabled_code_len_impl((len / 4) as u16);
}

/// Sets the 'branch to orig' length field based on the provided length.
pub fn set_branch_to_hook_len(&mut self, _len: usize) {
// no-op, for API compatibility
}

/// Gets the length of the 'branch to hook' array. Always 4 for AArch64.
pub fn get_branch_to_hook_len(&self) -> usize {
4
}

/// Sets the 'branch to orig' length field based on the provided length.
pub fn set_branch_to_orig_len(&mut self, _len: usize) {
// no-op, for API compatibility
}

/// Gets the length of the 'branch to orig' array. Always 4 for AArch64.
pub fn get_branch_to_orig_len(&self) -> usize {
4
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_enabled_and_disabled_code_lengths() {
let mut props = AssemblyHookPackedProps(0);

// Test setting and getting enabled code length
props.set_enabled_code_len(123 * 4); // Multiples of 4
assert_eq!(props.get_enabled_code_len(), 123 * 4);

// Test setting and getting disabled code length
props.set_disabled_code_len(456 * 4); // Multiples of 4
assert_eq!(props.get_disabled_code_len(), 456 * 4);
}

#[test]
#[should_panic(expected = "Length must be a multiple of 4 and at least 4")]
fn test_invalid_code_length() {
let mut props = AssemblyHookPackedProps(0);
props.set_enabled_code_len(5); // Should panic, not a multiple of 4
}

#[test]
fn test_branch_lengths() {
let mut props = AssemblyHookPackedProps(0);

// Setting and getting branch lengths, always 4 for AArch64
props.set_branch_to_hook_len(4);
assert_eq!(props.get_branch_to_hook_len(), 4);

props.set_branch_to_orig_len(4);
assert_eq!(props.get_branch_to_orig_len(), 4);
}
}
Loading

0 comments on commit 1e2aa75

Please sign in to comment.