Skip to content

Relocatable addresses #117

Description

@philpax

Problem

Every #[address(...)] lowers to an absolute literal, so the generated code assumes the target module loaded at its preferred image base. That's fine for a 32-bit binary with a fixed base. It's wrong for any x64 target built with /DYNAMICBASE, which is the default — the loader relocates the module and every generated call site, singleton accessor, and extern-value read points somewhere else.

The language can't express what an address is relative to. pyxis.toml carries a name and a pointer size, and nothing else, so the compiler can't tell an absolute VA from an RVA against a module that happened to load where it wanted to.

Context

Function bodies transmute the literal directly:

let function_body = match &function.body {
FunctionBody::Address { address } => {
let address_lit = hex_literal(*address);
let transmute_target = if options.public_addresses {
let const_ident = quote::format_ident!("{}_ADDRESS", function.name);
address_const = quote! {
pub const #const_ident: usize = #address_lit;
};
if in_impl {
quote! { Self::#const_ident }
} else {
quote! { #const_ident }
}
} else {
quote! { #address_lit as usize }
};
quote! {
let f:
unsafe extern #calling_convention

which produces:

impl A {
unsafe fn test() {
unsafe {
let f: unsafe extern "system" fn() = ::std::mem::transmute(0x123 as usize);
f()
}
}

The project manifest has nowhere to declare a base:

pyxis/src/config.rs

Lines 47 to 51 in f95ea87

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
pub name: String,
pub pointer_size: usize,
}

The Just Cause 3 definitions in pyxis-defs are x64, and their addresses are preferred-base VAs — 0x140_091_C10 is the default 0x140000000 image base plus the RVA. Those bindings only work against an unrelocated module.

Related:

  • Option for generating address variables for addressed objects #50 (public_addresses) established that addresses are something consumers reach for, and put the option on the build script rather than the project. The same split applies here.
  • Module/symbol externs #36 (module/symbol externs) is the other half. Once an address is relative to something named, resolving by module and exported symbol becomes another strategy for filling the same slot rather than a separate feature.

Expected vs actual

Expected: generated bindings work against the module as loaded.
Actual: they work only if the loader left the module at its preferred base.

Proposed approach

Speculative, and recorded to open the discussion rather than close it.

Add image_base to pyxis.toml. Keep addresses written exactly as the disassembler shows them. Have the backends emit base() + (addr - image_base), where base() is a hook the consumer supplies. Default that hook to the declared image_base and current output stays byte-identical, so this can land without breaking anyone.

Open: where the hook lives. It could be an extern the generated code declares and the consumer defines, a BuildOptions flag next to public_addresses, or something else. Whether the manifest should name the module itself is also open, and that's where this starts to overlap #36.

Two alternatives worth weighing. Leave the manifest alone and make relocation entirely a consumer problem — document that public_addresses constants are preferred-base VAs and let consumers rebase them. Or write RVAs in the source directly and treat today's absolute form as a legacy spelling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions