Skip to content

feat: Support SFrame#1287

Merged
lapla-cogito merged 26 commits into
wild-linker:mainfrom
lapla-cogito:sframe
Dec 18, 2025
Merged

feat: Support SFrame#1287
lapla-cogito merged 26 commits into
wild-linker:mainfrom
lapla-cogito:sframe

Conversation

@lapla-cogito

Copy link
Copy Markdown
Member

SFrame is a relatively new mechanism for storing frame information that occasionally appears in environments such as Arch Linux. I confirmed that all tests are green on Arch.

close #1025

@lapla-cogito

Copy link
Copy Markdown
Member Author

Results from testing with the gcc-static config in trivial-main.c:

(▰╹◡╹)❯ objdump --sframe wild/tests/build/trivial-main.c-gcc-static-host.wild

wild/tests/build/trivial-main.c-gcc-static-host.wild:     file format elf64-x86-64

Contents of the SFrame section .sframe:
  Header :

    Version: SFRAME_VERSION_2
    Flags: SFRAME_F_FDE_SORTED,
           SFRAME_F_FDE_FUNC_START_PCREL
    CFA fixed RA offset: -8
    Num FDEs: 1
    Num FREs: 1

  Function Index :

    func idx [0]: pc = 0x48b1f0, size = 5 bytes
    STARTPC         CFA       FP        RA
    000000000048b1f0  sp+8      u         f
(▰╹◡╹)❯ readelf -s wild/tests/build/trivial-main.c-gcc-static-host.wild | grep 48b1f0
   638: 000000000048b1f0     5 FUNC    GLOBAL HIDDEN    12 _dl_relocate_sta[...]

@lapla-cogito

Copy link
Copy Markdown
Member Author

Since current CI environments can't verify this feature, adding tests using Arch Docker might be worthwhile. Given its relative simplicity, I'd like to consider implementing this.

@davidlattimore

Copy link
Copy Markdown
Member

From looking at maskray's blog posts on the topic - Stack walking: space and time trade-offs and Remarks on SFrame - I'd have expected that we'd need to do a few other things to support SFrames. In particular, it sounds like the entries need to be sorted by the addresses of the functions to which they refer.

It'd also be interesting to verify if --gc-sections works correctly when there are sframes. i.e. that the relocations in the sframe sections aren't preventing otherwise unused sections from being GCed.

Comment thread linker-diff/src/lib.rs
"segment.NOTE.*",
// TODO: RISC-V
"segment.LOAD.RW.alignment",
// GNU ld and lld sometimes don’t generate .sframe sections in cases where we do.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to find out when and why it happens.
Maybe it's related to GC?

@mati865 mati865 Dec 22, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, did some testing and this is indeed related to GC.

GNU ld and LLD seem to always output .sframe, unless --gc-sections is added. Since Wild does that by default, you had to mark sframe sections as must keep.

Sure enough, all tests pass with this patch:

diff --git a/libwild/src/args.rs b/libwild/src/args.rs
index 0ff9701e7c..74b5c6a766 100644
--- a/libwild/src/args.rs
+++ b/libwild/src/args.rs
@@ -379,7 +379,7 @@
             // but do end up writing more, so --no-gc-sections will almost always be as
             // slow or slower than --gc-sections. For that reason, the latter is
             // probably a good default.
-            gc_sections: true,
+            gc_sections: false,
             prepopulate_maps: false,
             sym_info: None,
             merge_sections: true,
diff --git a/libwild/src/layout_rules.rs b/libwild/src/layout_rules.rs
index e37d17d41f..44992fb295 100644
--- a/libwild/src/layout_rules.rs
+++ b/libwild/src/layout_rules.rs
@@ -359,7 +359,7 @@
     SectionRule::exact(secnames::EH_FRAME_SECTION_NAME, SectionRuleOutcome::EhFrame),
     SectionRule::exact(
         secnames::SFRAME_SECTION_NAME,
-        SectionRuleOutcome::Section(SectionOutputInfo::keep(output_section_id::SFRAME)),
+        SectionRuleOutcome::Section(SectionOutputInfo::regular(output_section_id::SFRAME)),
     ),
     SectionRule::exact(
         secnames::NOTE_GNU_PROPERTY_SECTION_NAME,
diff --git a/linker-diff/src/lib.rs b/linker-diff/src/lib.rs
index b08bad44de..40e652d820 100644
--- a/linker-diff/src/lib.rs
+++ b/linker-diff/src/lib.rs
@@ -257,9 +257,6 @@
                 "segment.GNU_PROPERTY.alignment",
                 "segment.GNU_PROPERTY.flags",
                 // GNU ld and lld sometimes don’t generate .sframe sections in cases where we do.
-                // TODO: Figure out why this is happening.
-                "segment.GNU_SFRAME.alignment",
-                "segment.GNU_SFRAME.flags",
             ]
             .into_iter()
             .map(ToOwned::to_owned),

I think, the current behaviour of always emitting sframe is fine. It doesn't slow us down much.

@lapla-cogito lapla-cogito marked this pull request as draft November 11, 2025 08:07
@lapla-cogito lapla-cogito force-pushed the sframe branch 3 times, most recently from 99a1d79 to 6a17029 Compare November 18, 2025 00:52
@lapla-cogito lapla-cogito force-pushed the sframe branch 3 times, most recently from db93e93 to 74580bb Compare November 26, 2025 03:27
@lapla-cogito lapla-cogito marked this pull request as ready for review November 26, 2025 03:37
@lapla-cogito

Copy link
Copy Markdown
Member Author

OK, this should now allow us to add functionality for sorting .sframe sections. The results of running objdump --sframe on the binaries created with each config in trivial-main.c within Arch Docker are as follows:

output
[root@0fcee70c27b6 wild]# objdump --sframe wild/tests/build/trivial-main.c*.wild

wild/tests/build/trivial-main.c-clang-host.wild:     file format elf64-x86-64

No .sframe section present


wild/tests/build/trivial-main.c-clang-static-host.wild:     file format elf64-x86-64

Contents of the SFrame section .sframe:
  Header :

    Version: SFRAME_VERSION_2
    Flags: SFRAME_F_FDE_SORTED,
           SFRAME_F_FDE_FUNC_START_PCREL
    CFA fixed RA offset: -8
    Num FDEs: 1
    Num FREs: 1

  Function Index :

    func idx [0]: pc = 0x48b230, size = 5 bytes
    STARTPC         CFA       FP        RA
    000000000048b230  sp+8      u         f

wild/tests/build/trivial-main.c-clang-static-pie-no-relax-host.wild:     file format elf64-x86-64

Contents of the SFrame section .sframe:
  Header :

    Version: SFRAME_VERSION_2
    Flags: SFRAME_F_FDE_FUNC_START_PCREL
    CFA fixed RA offset: -8
    Num FDEs: 2
    Num FREs: 9

  Function Index :

    func idx [0]: pc = 0x95bd0, size = 176 bytes
    STARTPC         CFA       FP        RA
    0000000000095bd0  sp+8      u         f
    0000000000095bd1  sp+16     c-16      f
    0000000000095bd6  fp+16     c-16      f
    0000000000095bff  sp+8      c-16      f
    0000000000095c00  fp+16     c-16      f

    func idx [1]: pc = 0x95c80, size = 51 bytes
    STARTPC         CFA       FP        RA
    0000000000095c80  sp+8      u         f
    0000000000095c85  sp+16     c-16      f
    0000000000095c8f  fp+16     c-16      f
    0000000000095cae  sp+8      c-16      f

wild/tests/build/trivial-main.c-gcc-host.wild:     file format elf64-x86-64

No .sframe section present


wild/tests/build/trivial-main.c-gcc-indirect-external-host.wild:     file format elf64-x86-64

No .sframe section present


wild/tests/build/trivial-main.c-gcc-static-host.wild:     file format elf64-x86-64

Contents of the SFrame section .sframe:
  Header :

    Version: SFRAME_VERSION_2
    Flags: SFRAME_F_FDE_SORTED,
           SFRAME_F_FDE_FUNC_START_PCREL
    CFA fixed RA offset: -8
    Num FDEs: 1
    Num FREs: 1

  Function Index :

    func idx [0]: pc = 0x48b230, size = 5 bytes
    STARTPC         CFA       FP        RA
    000000000048b230  sp+8      u         f

wild/tests/build/trivial-main.c-gcc-static-pie-no-relax-host.wild:     file format elf64-x86-64

Contents of the SFrame section .sframe:
  Header :

    Version: SFRAME_VERSION_2
    Flags: SFRAME_F_FDE_FUNC_START_PCREL
    CFA fixed RA offset: -8
    Num FDEs: 2
    Num FREs: 9

  Function Index :

    func idx [0]: pc = 0x95bf0, size = 176 bytes
    STARTPC         CFA       FP        RA
    0000000000095bf0  sp+8      u         f
    0000000000095bf1  sp+16     c-16      f
    0000000000095bf6  fp+16     c-16      f
    0000000000095c1f  sp+8      c-16      f
    0000000000095c20  fp+16     c-16      f

    func idx [1]: pc = 0x95ca0, size = 51 bytes
    STARTPC         CFA       FP        RA
    0000000000095ca0  sp+8      u         f
    0000000000095ca5  sp+16     c-16      f
    0000000000095caf  fp+16     c-16      f
    0000000000095cce  sp+8      c-16      

However, there are several points of concern. All these issues have been documented as TODO comments in the code:

  • To ensure proper sorting for all .sframe files, it appears we would need to make redundant modifications to elf_writer::write_file_contents. This would affect linking processes that don't require SFrames, which we wish to avoid. From my understanding, SFrames aren't strictly required to be sorted unless they carry the SFRAME_F_FDE_SORTED flag, so even in the current state, we consider this to meet the minimum requirements for SFrame support.
  • As noted here, we should clarify the specific causes of cases where Wild creates .sframe sections while GNU ld or lld do not. This doesn't seem to be due to section garbage collection, and I don't currently have any leads on the potential cause...

@davidlattimore davidlattimore left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are two (or more) input objects containing sframe sections, do we emit an sframe section with multiple headers? Is it valid to do that, or do the sections need to be merged and have a single header?

Is there a consumer of sframes that we can test this with? I'm not sure what uses SFrames, perhaps gdb or libunwind? It'd be good confirm that it works with some consumer - ideally with eh_frames disabled so that we can be sure it's using the SFrames.

Comment thread libwild/src/elf_writer.rs Outdated
Comment thread libwild/src/sframe.rs Outdated
Comment thread libwild/src/sframe.rs
Comment thread libwild/src/sframe.rs Outdated
@lapla-cogito

lapla-cogito commented Dec 1, 2025

Copy link
Copy Markdown
Member Author

To verify SFrame's behavior beyond what objdump --sframe provides, I conducted the following experiment on native Arch Linux:

  1. Used wild/tests/build/libc-integration.c-gcc-static-pie-host.wild as the target binary (I copied this and named it tmp.wild)
  2. Used objdump --remove-section to remove the .eh_frame and .eh_frame_hdr sections
  3. Ran perf record -e cycles -g ./tmp.wild followed by perf report to verify that stack information was being captured correctly
    (You can see perf's support for SFrame with user events from links like this: https://lists.openwall.net/linux-kernel/2023/11/09/22)

First, here's the result of perf report without removing the eh_frame sections:

  Children      Self  Command          Shared Object     Symbol
+  100.00%   100.00%  tmp_before.wild  tmp_before.wild   [.] 0x00000000000a6af7
+  100.00%     0.00%  tmp_before.wild  tmp_before.wild   [.] _start
+  100.00%     0.00%  tmp_before.wild  tmp_before.wild   [.] __libc_start_main_impl
+  100.00%     0.00%  tmp_before.wild  tmp_before.wild   [.] __libc_start_call_main
+  100.00%     0.00%  tmp_before.wild  tmp_before.wild   [.] 0x00007f0990f10e0e
+  100.00%     0.00%  tmp_before.wild  tmp_before.wild   [.] __run_exit_handlers
+  100.00%     0.00%  tmp_before.wild  tmp_before.wild   [.] call_fini
+  100.00%     0.00%  tmp_before.wild  tmp_before.wild   [.] 0x00007f0990f0eaf7

And here's the result after removal:

  Children      Self  Command          Shared Object     Symbol
+  100.00%   100.00%  tmp.wild  tmp.wild          [.] 0x00000000000a6b18
+  100.00%     0.00%  tmp.wild  tmp.wild          [.] _start
+  100.00%     0.00%  tmp.wild  tmp.wild          [.] __libc_start_main_impl
+  100.00%     0.00%  tmp.wild  tmp.wild          [.] __libc_start_call_main
+  100.00%     0.00%  tmp.wild  tmp.wild          [.] 0x00007f3e19290e0e
+  100.00%     0.00%  tmp.wild  tmp.wild          [.] __run_exit_handlers
+  100.00%     0.00%  tmp.wild  tmp.wild          [.] call_fini
+  100.00%     0.00%  tmp.wild  tmp.wild          [.] 0x00007f3e1928eb18

@thesamesam

thesamesam commented Dec 1, 2025

Copy link
Copy Markdown

Note that the perf support for SFrames is still not yet upstream (but has been making recent progress). The only support is in glibc's backtrace() right now.

@davidlattimore

Copy link
Copy Markdown
Member

Thanks, that's good to know that glibc's backtrace supports it. I wrote a test - #1353 - the commented out part of the test removes eh_frames and relies on sframes instead. It fails at head and passes with this PR.

@davidlattimore

Copy link
Copy Markdown
Member

I just realised that my test wasn't testing multiple object files. Unfortunately, once I extended the test to have a second object file, it failed, presumably because the sframe sections need to be merged.

@lapla-cogito

lapla-cogito commented Dec 4, 2025

Copy link
Copy Markdown
Member Author

While still in debugging, I pushed because I wanted to observe CI results across various distributions. The main changes include:

  • Enhanced functionality to merge SFrame sections when multiple object files contain them. Since FRE address sizes and offset sizes may differ between input files, this implementation uses the maximum size among them to enable merging.
  • Support for obtaining backtraces using SFrame has been available since glibc 2.42. To ensure compatibility, I added a RequiresGlibcVersion directive that checks the installed glibc version and skips the operation if it's lower than the specified version.

These changes allows the backtrace.c tests to pass on my Arch Linux , but further investigation is needed with several distributions... edit: Since failing tests other than those involving Clippy appear to be from GNU ld, it seems appropriate to set SkipLinker:ld for SFrame-related tests.

@lapla-cogito

Copy link
Copy Markdown
Member Author

To get CI green, I needed to take the following points into account, so I implemented the corresponding changes:

There may be a better approach, but at least this one is reliable I think.

@lapla-cogito lapla-cogito marked this pull request as ready for review December 17, 2025 19:07

@davidlattimore davidlattimore left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! This looks like it's really close to being ready to merge :)

Comment thread wild/tests/integration_tests.rs
Comment thread libwild/src/elf_writer.rs Outdated
Comment thread libwild/src/sframe.rs
Comment thread libwild/src/sframe.rs Outdated
Comment thread libwild/src/elf_writer.rs Outdated
@lapla-cogito lapla-cogito merged commit 5331d1b into wild-linker:main Dec 18, 2025
20 checks passed
@lapla-cogito lapla-cogito deleted the sframe branch December 18, 2025 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support SFrame

4 participants