Skip to content

Add support for suppressing name mangling with \01 prefix #1689

Open

Description

@madsmtm

Normal Mach-O symbols are prefixed with _, non-prefixed symbols are reserved for special use. EDIT: Other platforms have similar name mangling rules.

LLVM allows you to opt out of symbol mangling by prefixing your symbol with the \01 byte marker, see the language reference. Cranelift should probably implement something similar.

For example, the linkme crate's distributed_slice uses this to access the special section$start$SEGMENT$SECTION and section$end$SEGMENT$SECTION symbols that the linker will replace with the address to respectively the start and end of the specified symbol.

The basic pattern they use is something like this:

use std::{mem, slice};

unsafe extern "Rust" {
    #[link_name = "\x01section$start$__DATA$__MY_SECTION"]
    static MY_SECTION_START: [fn(); 0];
    #[link_name = "\x01section$end$__DATA$__MY_SECTION"]
    static MY_SECTION_STOP: [fn(); 0];
}

#[used]
#[unsafe(link_section = "__DATA,__MY_SECTION,regular,no_dead_strip")]
static ITEM1: fn() = || println!("1");

#[used]
#[unsafe(link_section = "__DATA,__MY_SECTION,regular,no_dead_strip")]
static ITEM2: fn() = || println!("2");

fn main() {
    let start = (&raw const MY_SECTION_START).cast::<fn()>();
    let stop = (&raw const MY_SECTION_STOP).cast::<fn()>();

    let byte_offset = stop as usize - start as usize;
    let len = byte_offset / mem::size_of::<fn()>();
    let items = unsafe { slice::from_raw_parts(start, len) };

    for fnptr in items {
        fnptr();
    }
}

That is, add various items to a custom __DATA section, and then use the special directives to get the bounds of that section.

You can observe the difference with:

$ rustc foo.rs --emit obj -o llvm.o
$ objdump -t llvm.o | rg section
0000000000000000         *UND* section$end$__DATA$__MY_SECTION
0000000000000000         *UND* section$start$__DATA$__MY_SECTION

$ $cg_clif_dir/dist/rustc-clif foo.rs --emit obj -o clif.o
$ objdump -t clif.o | rg section
0000000000000000         *UND* _section$end$__DATA$__MY_SECTION
0000000000000000         *UND* _section$start$__DATA$__MY_SECTION

The Cranelift-compiled one will have symbol references with \x01_ (the 0x01 byte is invisible), while the LLVM-compiled one will have symbol references with no prefix.

EDIT: Related to #1520.

Activity

  1. madsmtm commented on Aug 10, 2026

    @madsmtm
    MemberAuthor

    This is now also important for the ctor crate, as it uses a similar pattern internally since mmastrac/linktime#368 (released in ctor v0.9.0), CC @mmastrac. See also #1588 (which will be fixed soon by #1648).

  2. bjorn3 commented on Aug 10, 2026

    @bjorn3
    Member

    \x01 is an LLVM invention and doesn't have anything to do with Mach-O. It is a no-op on ELF and has effect on Windows too. We never officially blessed this as supported by Rust the language. The LLVM backend just accidentally supported it due to not supporting it needing an extra check. I would prefer not to support this in cg_clif unless it is officially blessed as being part of Rust and required to be supported by all backends. I don't know if cg_gcc can implement it at all for example.

  3. changed the title [-]Add support for Mach-O unprefixed symbol `\x01` convention[/-] [+]Add support for suppressing name mangling with `\01` prefix[/+] on Aug 10, 2026
  4. changed the title [-]Add support for suppressing name mangling with `\01` prefix[/-] [+]Add support for suppressing Mach-O name mangling with `\01` prefix[/+] on Aug 10, 2026
  5. madsmtm commented on Aug 10, 2026

    @madsmtm
    MemberAuthor

    Do you know of a good alternative that linkme etc. could use instead? Inline assembly?

  6. changed the title [-]Add support for suppressing Mach-O name mangling with `\01` prefix[/-] [+]Add support for suppressing name mangling with `\01` prefix[/+] on Aug 10, 2026
  7. madsmtm commented on Aug 10, 2026

    @madsmtm
    MemberAuthor

    I guess we could formally add something like this to the language:

    #[unsafe(link_section(name = "section$start$__DATA$__MY_SECTION", mangling = "none"))]

    But that has MSRV concerns so would probably take a while to get adopted :/

  8. madsmtm commented on Aug 10, 2026

    @madsmtm
    MemberAuthor

    For the record, something similar is possible in both Clang and GCC with:

    #include <stdio.h>
    
    typedef void (*FnPtr)(void);
    
    extern const FnPtr MY_SECTION_START[] __asm__("section$start$__DATA$__MY_SECTION");
    extern const FnPtr MY_SECTION_STOP[] __asm__("section$end$__DATA$__MY_SECTION");
    
    void func1(void) {
        printf("1\n");
    }
    
    __attribute__((section("__DATA,__MY_SECTION"), used))
    const FnPtr ITEM1 = func1;
    
    void func2(void) {
        printf("2\n");
    }
    
    __attribute__((section("__DATA,__MY_SECTION"), used))
    const FnPtr ITEM2 = func2;
    
    int main() {
        for (const FnPtr *p = MY_SECTION_START; p < MY_SECTION_STOP; p++) {
            (*p)();
        }
        return 0;
    }

    Though this uses __asm__, which is definitely lower-level (__attribute__((section(...))) disallows \01 like rustc perhaps should've done too).

  9. mmastrac commented on Aug 10, 2026

    @mmastrac

    mangling = "none" would be great, and ideally for both sections and symbols. FWIW we also need that escape hatch for WASM where some section names were baked into WASM custom sections (and for supporting other section types like .fini_array).

    The ctor MSRV is 1.60 which might allow me to move over to a global_asm approach (I can test that out), but this is an escape hatch the language badly needs in any case.

  10. madsmtm commented on Aug 10, 2026

    @madsmtm
    MemberAuthor

    Getting the symbols with inline assembly on Aarch64 macOS PIC is definitely possible:

    // Replace start and end in above example with:
    let mut start: *const fn();
    unsafe { asm!("adrp {}, section$start$__DATA$__MY_SECTION@GOTPAGE", out(reg) start) };
    unsafe { asm!("ldr {}, [{}, section$start$__DATA$__MY_SECTION@GOTPAGEOFF]", out(reg) start, in(reg) start) };
    
    let mut stop: *const fn();
    unsafe { asm!("adrp {}, section$end$__DATA$__MY_SECTION@GOTPAGE", out(reg) stop) };
    unsafe { asm!("ldr {}, [{}, section$end$__DATA$__MY_SECTION@GOTPAGEOFF]", out(reg) stop, in(reg) stop) };

    But the instructions to emit here depends on the relocation model and the architecture, which would be kinda undesirable for linkme/ctor.

  11. mmastrac commented on Aug 10, 2026

    @mmastrac

    This might work as well, but I need to make sure it doesn't interact badly with other functionality:

    global_asm!(
        ".globl _ls_start_foo",
        ".set _ls_start_foo, section$start$__DATA$foo",
        ".globl _ls_end_foo",
        ".set _ls_end_foo, section$end$__DATA$foo",
    );
  12. mmastrac commented on Aug 10, 2026

    @mmastrac

    Confirming this works:

    mmastrac/linktime#518

    I need to think a bit more about this to make sure it doesn't break in strange ways, but scattered-collect's tests pass fully which exercise all the demons in link-time manipulation, including those that test if a section is empty.

  13. bjorn3 commented on Aug 10, 2026

    @bjorn3
    Member

    But the instructions to emit here depends on rust-lang/rust#114929 and the architecture, which would be kinda undesirable for linkme/ctor.

    Does macOS have multiple relocation models in practice at all? I would expect outside of perhaps the dynamic linker itself and some core system utils, everything would be have to be dynamically linked. And -Crelocation-model=static and -Crelocation-model=pic seem to result in the exact same assembly on aarch64-apple-darwin, using a GOT relocation for statics in both cases.

  14. madsmtm commented on Aug 10, 2026

    @madsmtm
    MemberAuthor

    Does macOS have multiple relocation models in practice at all?

    I couldn't find a way to get the compiler to generate something else either, but I'm not that experienced in how relocation models work, so I put it as a sort of disclaimer (and because making this work with inline asm for non-Mach-O targets would possibly need it?). Might also be a different story on older x86 macOS / ARM iOS?

    Regardless, I like the global_asm! alias solution that mmastrac proposes better than inline asm.

  15. mmastrac commented on Aug 10, 2026

    @mmastrac

    Slightly better approach which avoids exporting the renamed symbol but still allows Rust to link it:

    global_asm!(
        ".private_extern _ls_start_foo",
        ".set _ls_start_foo, section$start$__DATA$foo",
        ".private_extern _ls_end_foo",
        ".set _ls_end_foo, section$end$__DATA$foo",
    );

    If a caller does want explicit duplicates like the \x01 prefix previously allowed, they would need to disambiguate using a name built from the cargo crate-name env var, module path, file, etc. link-section doesn't need that.

    The alternative is a naked function and just always emit the GOT-related load. I'm not super-familiar with relocation types on macOS, but it appears that the linker folds unnecessary GOT-relative instructions as needed so this is probably safe as an alternative.

  16. mmastrac commented on Aug 10, 2026

    @mmastrac

    Ok, so results of a research spike over in linktime:

    • .private_extern + .set works for ld64
    • in rust-lld, .set does not support section$start$__DATA$foo because those are unknown symbols at the time of linking and the lld driver doesn't know how to make them work as aliases (arguably an lld bug).
      • caveat to above: a local alias does work because it collapses into a standard extern reference before lld touches it
    • a .quad symbol works and a naked function works, but neither of those support const context which may be a breaking change for some crates
    • \x01 is the portable solution today and unfortunately I have not found a replacement that is cross-linker compatible and const-friendly.
    • global_asm! and asm! blocks get promoted to their own files in cranelift which may be a big surprise downstream

    What would help:

    • #[unsafe(link_section = "...", mangled = "...")], but only if we get a cfg(cranelift) so we can use it there, because that would be a pretty rough MSRV hit
    • \x01 support in cranelift (sucks to enshrine it, but maybe it can be deprecated in a few years)
    • Fixing lld (a less painful MSRV hit)
    • Special-casing some basic forms of asm-blocks to codegen in the same file (ick)
  17. bjorn3 commented on Aug 11, 2026

    @bjorn3
    Member

    Maybe we could support \x01 but only for \x01section$start$ and \x01section$end$ on Mach-O platforms and on nothing else? And with a comment that the exception might get removed if we get something like #[unsafe(link_name(symbol = "...", modifiers = "+verbatim"))] (or whatever syntax ends up getting implemented).

  18. madsmtm commented on Aug 11, 2026

    @madsmtm
    MemberAuthor

    I'd be fine with that

  19. mmastrac commented on Aug 11, 2026

    @mmastrac

    That would be perfect. I don't believe any of the linker shenanigan crates rely on any other use of it. I'd be happy to spitball a proposal with someone to start the process for a revision on link_name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    O-macosOperating system: MacOS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions