recorded decision · public · no signup

accepted2026-06-22rust-lang/rfcs

Asm const ptr

What was chosen

  • Extending the existing `const` operand is preferred over adding a new operand or extending the `sym` operand.adr
  • The `const` operand for `asm!` and `global_asm!` will be extended to accept pointer values.adr
  • The `const` operand corresponds to the C `'i'` operand, operating on the pointer value itself, not the memory behind it.adr

What was ruled out· 2

  • Wide pointers (e.g., `&str`) cannot be passed directly to the `const` operand to avoid confusion with hypothetical interpolation.adr
  • Extending the `sym` operand was rejected due to potential ambiguity with argument types.adr

Constraints

  • Pointer values passed to the `const` operand must be computed during const evaluation.adr
  • Pointers in const evaluation are symbolic, not integer values, as global addresses are unknown at compile-time.adr
  • The existing `sym` operand is limited to hard-coded paths and cannot easily access fields of statics.adr

Consequences

  • Using `const` with pointers in position-independent code requires rip-relative instructions or relocations to avoid linker errors.adr
  • The `const` operand expands to the symbol name referenced by the pointer, plus an integer offset if applicable.adr

The recorded why

§ Summary

The const operand to asm! and global_asm! currently only accepts integers. Change it to also accept pointer values. The value must be computed during const evaluation. The operand expands to the name of the symbol that the pointer references, plus an integer offset when necessary.

§ Motivation

Right now, the only way to reference a global symbol from inline asm is to use the sym operand type.

use std::arch::asm;

static MY_GLOBAL: i32 = 10;

fn main() {
    let mut addr: *const i32;
    unsafe {
        asm!(
            "lea {1}(%rip), {0}",
            out(reg) addr,
            sym MY_GLOBAL,
            options(att_syntax)
        );
    }
    assert_eq!(addr, &MY_GLOBAL as *const i32);
}

However, the sym operand has several limitations:

  • It can only be used with a hard-coded path to one specific global.
  • It can only reference the global as a whole, not a field of the global.

Generics and const-evaluation

The sym operand lets you use generic parameters:

#[unsafe(naked)]
extern "C" fn asm_trampoline<T>() {
    naked_asm!(
        "
            tail {}
        ",
        sym trampoline::<T>
    )
}

extern "C" fn trampoline<T>() { ... }

And you can compute integers in const evaluation:

use std::arch::asm;

const fn math() -> i32 {
    1 + 2 + 3
}

fn main() {
    let mut six: i32;
    unsafe {
        asm!(
            "mov ${1}, {0:e}",
            out(reg) six,
            const math(),
            options(att_syntax)
        );
    }
    println!("{}", six);
}

However, asm is otherwise incompatible with const eval. Const evaluation is only usable to compute integer constants; it cannot access symbols. For example:

#[unsafe(naked)]
extern "C" fn asm_trampoline<const FAST: bool>() {
    naked_asm!(
        "tail {}",
        sym if FAST { fast_impl } else { slow_impl },
    )
}

extern "C" fn slow_impl() { ... }
extern "C" fn fast_impl() { ... }
error: expected a path for argument to `sym`
 --> src/lib.rs:8:13
  |
8 |         sym if FAST { fast_impl } else { slow_impl },
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

And pointers also do not work:

use std::arch::asm;

trait HasGlobal {
    const PTR: *const Self;
}

static MY_I32: i32 = 42;
impl HasGlobal for i32 {
    const PTR: *const i32 = &MY_I32;
}

fn get_addr<T: HasGlobal>() -> *const T {
    let mut addr: *const T;
    unsafe {
        asm!(
            "lea {1}(%rip), {0}",
            out(reg) addr,
            sym T::PTR,
            options(att_syntax)
        );
    }
    addr
}
error: invalid `sym` operand
  --> src/lib.rs:18:13
   |
18 |             sym T::PTR,
   |             ^^^^^^^^^^ is a `*const T`
   |
   = help: `sym` operands must refer to either a function or a static

Casting the pointer to usize does not help:

error: pointers cannot be cast to integers during const eval
  --> src/lib.rs:18:19
   |
18 |             const T::PTR as usize,
   |                   ^^^^^^^^^^^^^^^
   |
   = note: at compile-time, pointers do not have an integer value

The Linux kernel currently works around this limitation by using a macro:

macro_rules! get_addr {
    ($out:ident, $global:path) => {
        core::arch::asm!(
            "lea {1}(%rip), {0}",
            out(reg) $out,
            sym $global,
            options(att_syntax)
        )
    };
}

static MY_I32: i32 = 42;

fn main() {
    let x: *const i32;
    unsafe { get_addr!(x, MY_I32) };
    println!("{}", unsafe { *x });
}

With the macro it is possible to use the sym operand to access a global specified by the caller. However, this has the disadvantage of being a macro rather than a function call, and you also cannot get around the fact that you must specify the name of the global directly in the macro invocation.

Accessing fields

Let's say you want to access the field of a static.

use std::arch::asm;

#[repr(C)]
struct MyStruct {
    a: i32,
    b: i32,
}

static MY_GLOBAL: MyStruct = MyStruct {
    a: 10,
    b: 42,
};

fn main() {
    let mut addr: *const i32;
    unsafe {
        asm!(
            "lea {1}(%rip), {0}",
            out(reg) addr,
            sym MY_GLOBAL.b,
            options(att_syntax)
        );
    }
    assert_eq!(addr, &MY_GLOBAL.b as *const i32);
}
error: expected a path for argument to `sym`
  --> src/main.rs:20:17
   |
20 |             sym MY_GLOBAL.b,
   |                 ^^^^^^^^^^^

The only way to fix this is to use offset_of!.

use std::arch::asm;
use std::mem::offset_of;

#[repr(C)]
struct MyStruct {
    a: i32,
    b: i32,
}

static MY_GLOBAL: MyStruct = MyStruct { a: 10, b: 42 };

fn main() {
    let mut addr: *const i32;
    unsafe {
        asm!(
            "lea ({1} + {2})(%rip), {0}",
            out(reg) addr,
            sym MY_GLOBAL,
            const offset_of!(MyStruct, b),
            options(att_syntax)
        );
    }
    assert_eq!(addr, &MY_GLOBAL.b as *const i32);
}

Having to use offset_of! to access a field is inconvenient. If we could pass a pointer instead of being limited to a symbol name, then this would be no issue as we could pass &MY_GLOBAL.b.

§ Guide-level explanation

When writing assembly, you may use the const operand to insert a value that was evaluated in const context. The following types are supported:

  • Integers.
  • Pointers. (To sized types.)
  • Function pointers.

The const operand inserts the value directly into the inline assembly verbatim. The value will be evaluated using const evaluation, which ensures that the inserted value is known at compile time.

Note that when working with pointers in const evaluation, the pointers are evaluated "symbolically". That is to say, in const eval, a pointer is a symbolic value represented as an allocation and an offset. It's impossible to turn a symbolic pointer into an integer during const eval. It's done this way because when const evaluation runs, we don't yet know the address of globals.

The same caveat actually applies to assembly. We might not yet know the address of a symbol or function when running the assembler or linker. For this reason, linkers use similar symbolic math when working with pointers. This has consequences for how you are allowed to use symbols in assembly.

The rest of the guide-level explanation will discuss what happens in practice when you use the const operand in different scenarios. Note that all of these examples also apply to the sym operand.

Use in the .text section

Most commonly, instructions written in an inline assembly block will be stored in the .text section. This is where your executable machine code is stored.

You can use the const operand to write a compile-time integer into the machine code. For example:

use std::arch::asm;

fn main() {
    let a: i32;
    unsafe {
        asm!(
            "mov ${}, {:e}",
            const 42,
            out(reg) a,
            options(att_syntax),
        );
    }
    println!("{}", a);
}

This will expand to a program where a mov instruction is used to write the value 42 into a register, and the value of that register is then printed. The value 42 is hard-coded into the mov instruction.

Position-independent code

When you use const with pointers rather than integers, you must think about position-independent code.

Position-independent code is a special way of compiling machine code so that it doesn't rely on the absolute address in memory it is stored at, and it is the default on most Rust targets. This has various advantages:

  • When loading shared libraries, you can store them at any unused address. There is no risk that two shared libraries need to be stored at the same location.
  • It allows for address space layout randomization (ASLR), which is a mitigation that exploitation harder. The idea is that every time you run an executable, you store everything at a new address so that exploits cannot hardcode the address something is stored at.

However this means that the actual address of global variables is not yet known at link-time. Since some instructions require the value to be known at link-time, this can lead to linker errors when the const operand is used incorrectly.

As an example of this going wrong, consider this code:

use std::arch::asm;

static FORTY_TWO: i32 = 42;

fn main() {
    let a: *const i32;
    unsafe {
        asm!(
            "mov ${}, {}",
            const &FORTY_TWO,
            out(reg) a,
            options(att_syntax),
        );
    }
    println!("{:p}", a);
}

This will fail a linker error on most targets.

This error is because a mov instruction requires you to hard-code the actual integer value into the instruction, but the address that FORTY_TWO will have when you execute the code is not yet known when the assembly code is turned into machine code.

Note that if you compiled this for a target such as x86_64-unknown-none which does not use position independent code by default, then you will not get an error because the absolute address of FORTY_TWO is known at compile-time, so hard-coding it in mov is not an issue.

Relative values

Note that whether it fails doesn't just depend on the instruction, but also the kind of expression the constant is used in. For example, consider this code:

use std::arch::asm;

static FORTY_TWO: i32 = 42;

fn main() {
    let a: *const i32;
    unsafe {
        asm!(
            "mov $({} - .), {}",
            const &FORTY_TWO,
            out(reg) a,
            options(att_syntax),
        );
    }
    println!("{:p}", a);
}
0x3cfb8

Here, the argument to mov is going to be $(FORTY_TWO - .) where the period means "the address of this instruction". In this case, since FORTY_TWO and the mov instruction are stored in the same object file, the linker is able to compute the offset between the two addresses, even though it doesn't know the absolute value of either address.

Rip-relative instructions

This comes up more often with rip-relative instructions, which are instructions where the hard-coded value is relative to the instruction pointer (rip register). For example, using the load-effective-address (lea) instruction:

use std::arch::asm;

static FORTY_TWO: i32 = 42;

fn main() {
    let a: *const i32;
    unsafe {
        asm!(
            "lea {}(%rip), {}",
            const &FORTY_TWO,
            out(reg) a,
            options(att_syntax),
        );
    }
    println!("{:p}", a);
}
0x562b445610ac

The above code creates a lea instruction that computes the value of %rip plus some hard-coded offset. This allows the instruction to store the real address of FORTY_TWO into a by hard-coding the offset between FORTY_TWO and the lea instruction.

This kind of rip-relative instruction exists on basically every architecture.

Symbols from dynamically loaded libraries

When you pass a pointer value to a symbol from a dynamically loaded library, then it's not possible to use either absolute or relative addresses to access it. The address is truly not known until runtime. This is for several reasons:

  • The location at which the library is loaded is not known until runtime.
  • Even if you knew the location of the library, the library could have been recompiled, so you don't even know the offset of the symbol in the library until runtime.

When you use the const operand with a pointer to a symbol from a dynamically loaded library, you must use the symbol in one of the few contexts where this is permitted. The simplest example of this is the call instruction:

use std::arch::asm;

fn main() {
    let exit_code: i32 = 42;

    unsafe {
        asm!(
            "call {}",
            const libc::exit,
            in("rdi") exit_code,
            options(att_syntax,noreturn),
        );
    }
}

In this scenario, the linker will expand call to different things depending on where the symbol comes from and the platform. For example, on Linux, if you call a symbol from another library, it uses a mechanism called the procedure linkage table (PLT). Usually, the way this works is that instead of calling libc::exit directly, it will call a dummy function in the PLT (which has a constant offset from the call instruction). The dummy function will jump to the real libc::exit function with the help of the dl loader.

Another scenario is global variables that are not functions. At least on Linux, a global offset table (GOT) is used. Basically, the idea is that you are going to store a big array of pointers called the GOT, and your executable or library will include instructions to the linker (called relocations) that tell the linker to replace each pointer with the address of a given symbol. Since the GOT has a known fixed offset from your machine code, you can look up the address of any symbol through the GOT.

use libc::FILE;
use std::arch::asm;

unsafe extern "C" {
    static stdin: *const FILE;
}

fn main() {
    // The GOT has a pointer of type `*const *const FILE` that points
    // to the real stdin global. This asm code will load the address
    // of that GOT entry into `a`.
    let a: *const *const *const FILE;
    unsafe {
        asm!(
            "leaq {}@GOTPCREL(%rip), {}",
            const &stdin,
            out(reg) a,
            options(att_syntax),
        );
    }
    // Check that dereferencing the GOT entry gives the address of
    // stdin.
    println!("offset: {}", unsafe { (&raw const stdin).byte_offset_from(*a)});
}
offset: 0

Here, the @GOTPCREL directive tells the linker to create an entry in the GOT containing the value before the @ sign, and the expression then evaluates to the address of the GOT entry.

That said, you would usually not use the @GOTPCREL directive with the const operand in machine code. The @GOTPCREL directive is mainly useful for loading the address of the global into a register, and there is a significantly simpler alternative for that: use the in(reg) operand instead of const.

use libc::FILE;
use std::arch::asm;

unsafe extern "C" {
    static stdin: *const FILE;
}

fn main() {
    let a: *const *const FILE;
    unsafe {
        asm!(
            "mov {}, {}",
            in(reg) &stdin,
            out(reg) a,
            options(att_syntax),
        );
    }
    println!("offset: {}", unsafe { (&raw const stdin).byte_offset_from(a)});
}
0

In this scenario, the compiler will compute the address of stdin before the assembly block using whichever mechanism is most efficient for the given symbol. In this case, that is a lookup using the GOT, but for a locally-defined symbol it would not need a GOT lookup.

Use in other sections

The .text section of the binary contains the executable machine code, and this section is normally immutable. This ensures that if many programs load the same shared library, the parts that constitute the .text section will be identical across each copy, meaning that the same physical memory can be reused for each copy of the library.

However, sections other than the .text section may not be immutable. For example, the section that contains static mut variables is mutable. In this case, we can make use of something called a relocation. This is a directive to the dl loader, which tells it to replace a given location with the address of a given symbol.

When you use the const operand to place a value in a custom section, relocations are automatically used when necessary. This means that even though the address of FORTY_TWO and stdin are not known in the below example, it's still possible to store the addresses in static data:

use libc::FILE;
use std::arch::asm;

static FORTY_TWO: i32 = 42;

unsafe extern "C" {
    static stdin: *const FILE;
    static my_section_start: usize;
}

fn main() {
    // This asm block no longer computes a value at runtime. Instead,
    // it injects directives that instruct the assembler to create a
    // new section in the compiled binary and write data to it.
    #[allow(named_asm_labels)]
    unsafe {
        asm!(
            ".pushsection .my_data_section, \"aw\"",
            ".globl my_section_start",
            ".balign 8",
            "my_section_start:",
            ".quad {} - .", // period = address of this .quad
            ".quad {}",
            ".quad {}",
            ".popsection",
            const &FORTY_TWO,
            const &FORTY_TWO,
            const &stdin,
            options(att_syntax),
        );
    }

    let section: *const usize = unsafe { &my_section_start };

    let value1 = unsafe { *section.add(0).cast::<isize>() };
    let value2 = unsafe { *section.add(1).cast::<*const i32>() };
    let value3 = unsafe { *section.add(2).cast::<*const *const FILE>() };

    println!("{},{}", value1, unsafe { (&raw const FORTY_TWO).byte_offset_from(section) });
    println!("{:p},{:p}", value2, &raw const FORTY_TWO);
    println!("{:p},{:p}", value3, &raw const stdin);
}
-75980,-75980
0x5a1f461700ac,0x5a1f461700ac
0x7da04bf026b0,0x7da04bf026b0

In this case, the asm block ends up creating a section containing three integers:

  • The offset from the section to the FORTY_TWO global.
  • The address of the FORTY_TWO global.
  • The address of the stdin global.

Only the first of these three values is actually a constant value, and if you inspect the binary, the actual values in the section are going to be -75980, 0, 0. The two zeros are filled in when loading the program into memory based on relocations emitted by the linker.

Note that if you try to use stdin with {} - . to make it relative, then this will fail to compile because there is no relocation to insert a relative address when the symbol is from a dynamically loaded library.

§ Reference-level explanation

The const operand has different behavior depending on the provided argument. It accepts the following types:

  • Any integer type.
  • Raw pointers and references to sized types.
  • Function pointers.

The argument is evaluated using const evaluation.

Integer values

If the argument type is any integer type, then the value is inserted into the asm block as plain text. This behavior exists on stable Rust today.

If the argument type is a raw pointer, but the value of the raw pointer is an integer, then the behavior is the same as when passing an integer type. This includes cases such as:

  • core::ptr::null()
  • 0xA000_000 as *mut u8
  • core::ptr::null().wrappind_add(1000)
  • core::ptr::without_provenance(1000)

Pointer values to a named symbol

When the argument type is a raw pointer, reference, or function pointer that points at a named symbol, then the compiler will insert symbol_name into the asm block as plain text. In this scenario, it is equivalent to using the sym operand.

When the pointer was created from a named symbol, but is offset from the symbol itself (e.g. it points at a field of the symbol), then the compiler will insert symbol_name+offset (or symbol_name-offset) into the asm block as plain text. In this scenario, using {} with a const operand is equivalent to writing {}+offset (or {}-offset) with the sym operand.

The compiler may choose to emit the symbol name by inserting it into the asm verbatim, or by using certain backend-specific operands (e.g. 'i' or 's'), depending on what the backend supports.

Pointer values to an unnamed global

Not all globals are named. For exam

Excerpt — the full document is at the cited source: text/3848-asm-const-ptr.md