Skip to content

[BinaryFormat] Add "SFrame" structures and constants #147264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions llvm/include/llvm/BinaryFormat/SFrame.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//===-- llvm/BinaryFormat/SFrame.h ---SFrame Data Structures ----*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
/// \file
/// This file contains data-structure definitions and constants to support
/// unwinding based on .sframe sections. This only supports SFRAME_VERSION_2
/// as described at https://sourceware.org/binutils/docs/sframe-spec.html
///
/// Naming conventions follow the spec document. #defines converted to constants
/// and enums for better C++ compatibility.
//===----------------------------------------------------------------------===//

#ifndef LLVM_BINARYFORMAT_SFRAME_H
#define LLVM_BINARYFORMAT_SFRAME_H

#include "llvm/Support/Compiler.h"
#include "llvm/Support/DataTypes.h"

namespace llvm {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can we use namespace llvm::sframe { ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.


namespace sframe {

constexpr uint16_t SFRAME_MAGIC = 0xDEE2;

enum : uint8_t {
SFRAME_VERSION_1 = 1,
SFRAME_VERSION_2 = 2,
};

/// sframe_preable.sfp_flags flags.

Choose a reason for hiding this comment

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

Recently SFRAME_F_FDE_FUNC_START_PCREL was added. This will be released as a SFrame V2 erratum (in the upcoming GNU Binutils 2.45). Adding this flag was the chosen way to fix relocatable links properly without abusing the available relocations in the supported arches. This may help https://sourceware.org/pipermail/binutils/2025-July/142222.html.

We need to aim for LLVM to generate SFrame V2 sections with SFRAME_F_FDE_FUNC_START_PCREL flag. SFRAME_F_FDE_FUNC_START_PCREL flag means that the SFrame FDE function start address is an offset from the field itself to the start PC of the function.

Copy link
Contributor

Choose a reason for hiding this comment

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

Heh. Here is a comment from the linker prototype:

https://github.com/Sterling-Augustine/llvm-project/blob/61c53c66abeb558e581a9f7ecd860a170031acaa/lld/ELF/SyntheticSections.cpp#L819

It would be very nice if there were a way to support binaries with more than 2gb of text too. (I know technically, it is 4gb, but only if the sframe segment is in the middle of the text segment.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's cool. Thanks for letting me know. I've added the new constant to this PR, and I've also update my (WIP) parser/dumper to account for this change.

enum : uint8_t {
SFRAME_F_FDE_SORTED = 0x1,
SFRAME_F_FRAME_POINTER = 0x2,
};

/// Possible values for sframe_header.sfh_abi_arch.
enum : uint8_t {
SFRAME_ABI_AARCH64_ENDIAN_BIG = 1,
SFRAME_ABI_AARCH64_ENDIAN_LITTLE = 2,
SFRAME_ABI_AMD64_ENDIAN_LITTLE = 3
};

/// SFrame FRE Types. Bits 0-3 of sframe_func_desc_entry.sfde_func_info.
enum : uint8_t {
SFRAME_FRE_TYPE_ADDR1 = 0,
SFRAME_FRE_TYPE_ADDR2 = 1,
SFRAME_FRE_TYPE_ADDR4 = 2,
};

/// SFrame FDE Types. Bit 4 of sframe_func_desc_entry.sfde_func_info.
enum : uint8_t {
SFRAME_FDE_TYPE_PCINC = 0,
SFRAME_FDE_TYPE_PCMASK = 1,
};

/// Speficies key used for signing return addresses. Bit 5 of
/// sframe_func_desc_entry.sfde_func_info.
enum : uint8_t {
SFRAME_AARCH64_PAUTH_KEY_A = 0,
SFRAME_AARCH64_PAUTH_KEY_B = 1,
};

/// Size of stack offsets. Bits 5-6 of sframe_fre_info.fre_info.
enum : uint8_t {
SFRAME_FRE_OFFSET_1B = 0,
SFRAME_FRE_OFFSET_2B = 1,
SFRAME_FRE_OFFSET_4B = 2,
};

/// Stack frame base register. Bit 0 of sframe_fre_info.fre_info.
enum : uint8_t { SFRAME_BASE_REG_FP = 0, SFRAME_BASE_REG_SP = 1 };

LLVM_PACKED_START

struct sframe_preamble {
uint16_t sfp_magic;
uint8_t sfp_version;
uint8_t sfp_flags;
};

struct sframe_header {
sframe_preamble sfh_preamble;
uint8_t sfh_abi_arch;
int8_t sfh_cfa_fixed_fp_offset;
int8_t sfh_cfa_fixed_ra_offset;
uint8_t sfh_auxhdr_len;
uint32_t sfh_num_fdes;
uint32_t sfh_num_fres;
uint32_t sfh_fre_len;
uint32_t sfh_fdeoff;
uint32_t sfh_freoff;
};

struct sframe_func_desc_entry {
int32_t sfde_func_start_address;
uint32_t sfde_func_size;
uint32_t sfde_func_start_fre_off;
uint32_t sfde_func_num_fres;
uint8_t sfde_func_info;
uint8_t sfde_func_rep_size;
uint16_t sfde_func_padding2;

uint8_t getPAuthKey() const { return (sfde_func_info >> 5) & 1; }
uint8_t getFDEType() const { return (sfde_func_info >> 4) & 1; }
uint8_t getFREType() const { return sfde_func_info & 0xf; }
void setPAuthKey(uint8_t P) { setFuncInfo(P, getFDEType(), getFREType()); }
void setFDEType(uint8_t D) { setFuncInfo(getPAuthKey(), D, getFREType()); }
void setFREType(uint8_t R) { setFuncInfo(getPAuthKey(), getFDEType(), R); }
void setFuncInfo(uint8_t PAuthKey, uint8_t FDEType, uint8_t FREType) {
sfde_func_info =
((PAuthKey & 1) << 5) | ((FDEType & 1) << 4) | (FREType & 0xf);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This part was inspired by Elf32_Sym::setBindingAndType and friends.

};

struct sframe_fre_info {
uint8_t fre_info;

bool isReturnAddressSigned() const { return fre_info >> 7; }
uint8_t getOffsetSize() const { return (fre_info >> 5) & 3; }
uint8_t getOffsetCount() const { return (fre_info >> 1) & 0xf; }
uint8_t getBaseRegister() const { return fre_info & 1; }
void setReturnAddressSigned(bool RA) {
setFREInfo(RA, getOffsetSize(), getOffsetCount(), getBaseRegister());
}
void setOffsetSize(uint8_t Sz) {
setFREInfo(isReturnAddressSigned(), Sz, getOffsetCount(),
getBaseRegister());
}
void setOffsetCount(uint8_t N) {
setFREInfo(isReturnAddressSigned(), getOffsetSize(), N, getBaseRegister());
}
void setBaseRegister(uint8_t Reg) {
setFREInfo(isReturnAddressSigned(), getOffsetSize(), getOffsetCount(), Reg);
}
void setFREInfo(bool RA, uint8_t Sz, uint8_t N, uint8_t Reg) {
fre_info = ((RA & 1) << 7) | ((Sz & 3) << 5) | ((N & 0xf) << 1) | (Reg & 1);
}
};

struct sframe_frame_row_entry_addr1 {
uint8_t sfre_start_address;
sframe_fre_info sfre_info;
};

struct sframe_frame_row_entry_addr2 {
uint16_t sfre_start_address;
sframe_fre_info sfre_info;
};

struct sframe_frame_row_entry_addr4 {
uint32_t sfre_start_address;
sframe_fre_info sfre_info;
};

LLVM_PACKED_END

} // namespace sframe
} // namespace llvm

#endif // LLVM_BINARYFORMAT_SFRAME_H
1 change: 1 addition & 0 deletions llvm/unittests/BinaryFormat/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_llvm_unittest(BinaryFormatTests
MsgPackDocumentTest.cpp
MsgPackReaderTest.cpp
MsgPackWriterTest.cpp
SFrameTest.cpp
TestFileMagic.cpp
)

98 changes: 98 additions & 0 deletions llvm/unittests/BinaryFormat/SFrameTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//===- SFrameTest.cpp -----------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "llvm/BinaryFormat/SFrame.h"
#include "gtest/gtest.h"

using namespace llvm;
using namespace llvm::sframe;

namespace {
// Test structure sizes and triviality.
static_assert(std::is_trivial_v<sframe_preamble>);
static_assert(sizeof(sframe_preamble) == 4);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This sort of "structure exactly matches the protocol" (with the implication that these might be memory mapped from inputs) always makes me a bit uncomfortable - do other parts of BinaryFormat do this? (and if we/they do, should the members use some of the types we have for that sort of type punning - that handle endianness, etc, if the file being read mismatches the current architecture being executed on)

Copy link
Contributor

@Sterling-Augustine Sterling-Augustine Jul 7, 2025

Choose a reason for hiding this comment

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

The sframe specification expects consumers to memory map the new PT_GNU_SFRAME segment (and to handle any endianness issues), and carefully specifies the sizes and offsets of every field. So although we don't necessarily need to match that for our working copies, we do need to match that for reading and writing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The COFF and ELF parsers both heavily rely on directly accessing memory-mapped files. Accessing the data directly is simpler/faster than a separate in-memory representation. But getting the error-handling right can be a bit tricky. (In particular, you have to make sure you don't read past the end of the file.)

But yes, if we're going to do this, we should use the types that automatically handle the endianness/alignment issues, like packed_endian_specific_integral/ulittle32_t/etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

COFF and ELF parsers indeed use endian-specific integers, but they don't define these types inside the BinaryFormat library. What they do is instead is redefine those types inside the Object library (include/llvm/Object/{COFF,ELFTypes}.h). The types in BinaryFormat don't have asserts on their size, but it does look like they (intentionally or not) follow the on-disk layout. So this PR is sort of consistent with that.

That said, my plan was not to use these structures by reinterpret_casting the mmapped data. I was planning to use the DataExtractor (see dependent PR) to take care of the endianness when reading from the data (which can still be mmapped). I considered templatizing on the endianness, but I wanted to avoid that because of the additional wrapping needed when switching between generic and concrete code. I can do that, if desired, but this seemed like it was easier to do. This also means I technically don't need the structure layout to match the protocol, but I think it's a nice form of documentation at least.

Another reason is that the initial version of this file based on @Sterling-Augustine's prototype (a fact I neglected to mention). Looking at the generation code, I see that it also has no need for the layout or endianness of this structure, but that's not entirely the case in the linker (which needs to produce and consume the format). The code, at least in the current form, does look like it could benefit from endian specific integers, though I think we could find precedents for using DataExtractors as well (e.g., the debug_names linker, whose functionality is actually quite similar to this).

Circling back, I think the main question is what mechanism do we use for parsing. The options I see are:

  1. endian-specific integers plus reinterpret_cast (used e.g. in ELF and COFF)
  2. DataExtractor (used mainly in DWARF, which includes eh_frame)
  3. memcpy + swapByteOrder (used in MachO and some other stuff)

I'd prefer the second option, but I'm happy to go with anything, particularly if it means the parser can be reused in the linker.

Assuming we go with the first option, the second question is whether we do the ELF thing of defining the structures in both BinaryFormat and Object libraries, or we just have a single version (where?).

Copy link
Collaborator

Choose a reason for hiding this comment

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

DataExtractor is fine, I guess, as long as the overhead of copying the data isn't a significant bottleneck.

Please don't use swapByteOrder; basically everyone who tries to write explicit byte-swapping code messes up, and it's hard to follow even if you get it right.

Copy link
Collaborator Author

@labath labath Jul 9, 2025

Choose a reason for hiding this comment

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

@Sterling-Augustine, @dwblaikie, any thoughts on this? I'm particularly interested in whether you have plans to reuse the parsing code in the linker. My plan was to provide an iterator-based api to access the FDEs. In this setup, it would copy all of the FDE data in order to extract it and swap it.

I'm pretty sure that's fine for dumping (which is accessing everything anyway, and is not performance critical), and for LLDB (which only accesses one FDE at a time), but maybe it's not okay for the linker which might only need to access the offset field of the FDE (to sort it) and the rest could be memcpyed into the output buffer?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems as easy/probably better to use the stuff in Endian.h to provide an endian correct view type over the underlying thing? The iterator can return a static_cast pointer to struct with the endian types as members - and readers can read/write the underlying memory without the need to copy/bitfiddle any parts that aren't needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

The linker needs to touch two fields in the sframe_func_desc_entry itself (start_address and fre_off), but it also needs to sort them (and their corresponding sets of fres). And then write them back out. The prototype works by keeping pointers, reading the fields, and then simply memcopying the bytes (which is how almost all of the other sections are handled), and finally writing in fields at specific offsets. It's somewhat inelegant, but probably quite a bit faster than full decode.

The gnu linker does a full decode and memcpy into intermediate structures, if I'm not mistaken.

A good parser would make the code quite a bit cleaner, but would also have to cope with relocations (which go through a couple of different data structures) and discarded function sections, which might make it more complicated.

So my current thought is that I won't.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I'm starting to warm up to the idea of templatizing the parser on the endianness. The FDEs could be nicely exposed as something like ArrayRef<FDE<Endian>>. FREs have a variable length, which makes that trickier, but they're also quite small, so maybe it's better to just copy them -- I'll see how the code looks like.

Which then brings up a followup question: Would you be okay with defining a single set of endian-aware types in the BinaryFormat library? I'd like to avoid the ELF duplication which has one set of types in BinaryFormat and another in Object.

Regarding the linker, I guess we'll see how it goes. I would hope that we can reuse at least some part of the parsing code there (naively, I would hope that we can separate the part about parsing the format from the one which resolves relocations), but I also don't know much about that part of the code base.


static_assert(std::is_trivial_v<sframe_header>);
static_assert(sizeof(sframe_header) == 28);

static_assert(std::is_trivial_v<sframe_func_desc_entry>);
static_assert(sizeof(sframe_func_desc_entry) == 20);

static_assert(std::is_trivial_v<sframe_frame_row_entry_addr1>);
static_assert(sizeof(sframe_frame_row_entry_addr1) == 2);

static_assert(std::is_trivial_v<sframe_frame_row_entry_addr2>);
static_assert(sizeof(sframe_frame_row_entry_addr2) == 3);

static_assert(std::is_trivial_v<sframe_frame_row_entry_addr4>);
static_assert(sizeof(sframe_frame_row_entry_addr4) == 5);

TEST(SFrameTest, FDEFlags) {
sframe_func_desc_entry FDE = {};
EXPECT_EQ(FDE.sfde_func_info, 0u);
EXPECT_EQ(FDE.getPAuthKey(), SFRAME_AARCH64_PAUTH_KEY_A);
EXPECT_EQ(FDE.getFDEType(), SFRAME_FDE_TYPE_PCINC);
EXPECT_EQ(FDE.getFREType(), SFRAME_FRE_TYPE_ADDR1);

FDE.setPAuthKey(SFRAME_AARCH64_PAUTH_KEY_B);
EXPECT_EQ(FDE.sfde_func_info, 0x20u);
EXPECT_EQ(FDE.getPAuthKey(), SFRAME_AARCH64_PAUTH_KEY_B);
EXPECT_EQ(FDE.getFDEType(), SFRAME_FDE_TYPE_PCINC);
EXPECT_EQ(FDE.getFREType(), SFRAME_FRE_TYPE_ADDR1);

FDE.setFDEType(SFRAME_FDE_TYPE_PCMASK);
EXPECT_EQ(FDE.sfde_func_info, 0x30u);
EXPECT_EQ(FDE.getPAuthKey(), SFRAME_AARCH64_PAUTH_KEY_B);
EXPECT_EQ(FDE.getFDEType(), SFRAME_FDE_TYPE_PCMASK);
EXPECT_EQ(FDE.getFREType(), SFRAME_FRE_TYPE_ADDR1);

FDE.setFREType(SFRAME_FRE_TYPE_ADDR4);
EXPECT_EQ(FDE.sfde_func_info, 0x32u);
EXPECT_EQ(FDE.getPAuthKey(), SFRAME_AARCH64_PAUTH_KEY_B);
EXPECT_EQ(FDE.getFDEType(), SFRAME_FDE_TYPE_PCMASK);
EXPECT_EQ(FDE.getFREType(), SFRAME_FRE_TYPE_ADDR4);
}

TEST(SFrameTest, FREFlags) {
sframe_fre_info Info = {};
EXPECT_EQ(Info.fre_info, 0u);
EXPECT_FALSE(Info.isReturnAddressSigned());
EXPECT_EQ(Info.getOffsetSize(), SFRAME_FRE_OFFSET_1B);
EXPECT_EQ(Info.getOffsetCount(), 0u);
EXPECT_EQ(Info.getBaseRegister(), SFRAME_BASE_REG_FP);

Info.setReturnAddressSigned(true);
EXPECT_EQ(Info.fre_info, 0x80u);
EXPECT_TRUE(Info.isReturnAddressSigned());
EXPECT_EQ(Info.getOffsetSize(), SFRAME_FRE_OFFSET_1B);
EXPECT_EQ(Info.getOffsetCount(), 0u);
EXPECT_EQ(Info.getBaseRegister(), SFRAME_BASE_REG_FP);

Info.setOffsetSize(SFRAME_FRE_OFFSET_4B);
EXPECT_EQ(Info.fre_info, 0xc0u);
EXPECT_TRUE(Info.isReturnAddressSigned());
EXPECT_EQ(Info.getOffsetSize(), SFRAME_FRE_OFFSET_4B);
EXPECT_EQ(Info.getOffsetCount(), 0u);
EXPECT_EQ(Info.getBaseRegister(), SFRAME_BASE_REG_FP);

Info.setOffsetCount(3);
EXPECT_EQ(Info.fre_info, 0xc6u);
EXPECT_TRUE(Info.isReturnAddressSigned());
EXPECT_EQ(Info.getOffsetSize(), SFRAME_FRE_OFFSET_4B);
EXPECT_EQ(Info.getOffsetCount(), 3u);
EXPECT_EQ(Info.getBaseRegister(), SFRAME_BASE_REG_FP);

Info.setBaseRegister(SFRAME_BASE_REG_SP);
EXPECT_EQ(Info.fre_info, 0xc7u);
EXPECT_TRUE(Info.isReturnAddressSigned());
EXPECT_EQ(Info.getOffsetSize(), SFRAME_FRE_OFFSET_4B);
EXPECT_EQ(Info.getOffsetCount(), 3u);
EXPECT_EQ(Info.getBaseRegister(), SFRAME_BASE_REG_SP);
}

} // namespace