Mods: Embed symdb in linked executable (#2216)

* Mods: Embed symdb in linked executable

* Update to symgen v1.2.3
This commit is contained in:
Luke Street
2026-07-15 00:26:34 -06:00
committed by GitHub
parent adfb830b4d
commit 0f2a00cd1f
5 changed files with 101 additions and 76 deletions
-1
View File
@@ -290,7 +290,6 @@ jobs:
path: |
build/install/*.exe
build/install/*.dll
build/install/*.symdb
build/install/res/
build/install/mods/
build/install/debug.7z
+2
View File
@@ -519,6 +519,8 @@ endif ()
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
target_link_libraries(dusklight PRIVATE dl)
elseif (ANDROID)
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
endif ()
if (NOT APPLE)
-3
View File
@@ -19,9 +19,6 @@ for install_path in build/install/*; do
[[ "$(basename "$install_path")" == *.* ]] && continue
cp -r "$install_path" build/appdir/usr/bin
done
if [[ -f build/install/dusklight.symdb ]]; then
cp build/install/dusklight.symdb build/appdir/usr/bin
fi
cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,1024x1024} build/appdir/usr/share/icons/hicolor
cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications
+13 -13
View File
@@ -2,7 +2,7 @@ include_guard(GLOBAL)
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
set(_SYMGEN_VERSION "1.2.0")
set(_SYMGEN_VERSION "1.2.3")
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
mark_as_advanced(SYMGEN_PATH)
@@ -108,21 +108,21 @@ function(setup_symbol_manifest target)
if (WIN32)
set(_input --pdb "$<TARGET_PDB_FILE:${target}>")
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
else ()
set(_input --binary "$<TARGET_FILE:${target}>")
if (APPLE)
set(_out "$<TARGET_BUNDLE_CONTENT_DIR:${target}>/Resources/dusklight.symdb")
else ()
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
endif ()
endif ()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND "${SYMGEN_EXE}" manifest ${_input} --out "${_out}"
COMMENT "Generating symbol manifest"
VERBATIM)
if (NOT APPLE)
install(FILES "${_out}" DESTINATION .)
if (APPLE)
# Room for the load command `symgen manifest --embed` inserts post-link.
target_link_options(${target} PRIVATE "LINKER:-headerpad,0x200")
endif ()
# The manifest is embedded into the image as a new section, located at runtime through
# the descriptor manifest.cpp reserves. On Apple platforms this command must stay
# attached before the ad-hoc codesign POST_BUILD command: the patch invalidates any
# existing signature.
add_custom_command(TARGET ${target} POST_BUILD
COMMAND "${SYMGEN_EXE}" manifest ${_input} --embed "$<TARGET_FILE:${target}>"
COMMENT "Embedding symbol manifest"
VERBATIM)
endfunction()
+86 -59
View File
@@ -6,18 +6,14 @@
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <limits>
#include <utility>
#include <vector>
#include <SDL3/SDL_filesystem.h>
#include <zstd.h>
#include "aurora/lib/logging.hpp"
#include "dusk/io.hpp"
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
@@ -63,6 +59,31 @@ struct Entry {
};
static_assert(sizeof(Entry) == 24);
/*
* `symgen manifest --embed` appends the symbol manifest to the linked executable as an added
* section and patches this descriptor with its location.
*/
struct SymdbDescriptor {
volatile uint64_t magic;
volatile uint64_t rva;
volatile uint64_t size;
};
constexpr uint64_t kDescriptorMagic = 0x52444842444d5953ull; // "SYMDBHDR"
#if defined(_WIN32)
#pragma section(".symdbh", read)
__declspec(allocate(".symdbh"))
#if defined(__clang__)
__attribute__((used))
#endif
constinit const SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
#elif defined(__APPLE__)
__attribute__((section("__DATA,__symdbh"), used)) constinit const SymdbDescriptor
s_symdbDescriptor{kDescriptorMagic, 0, 0};
#else
__attribute__((section("symdbh"), used)) constinit const SymdbDescriptor s_symdbDescriptor{
kDescriptorMagic, 0, 0};
#endif
struct State {
std::vector<uint8_t> data;
const Entry* entries = nullptr;
@@ -149,13 +170,28 @@ bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
#elif defined(__linux__)
struct Ctx {
std::vector<uint8_t>* id;
uintptr_t probe;
uintptr_t base = 0;
bool found = false;
} ctx{&outId};
} ctx{&outId, reinterpret_cast<uintptr_t>(&s_symdbDescriptor)};
dl_iterate_phdr(
[](dl_phdr_info* info, size_t, void* data) -> int {
auto* ctx = static_cast<Ctx*>(data);
// The first callback is the main executable.
// Select the image containing our descriptor: the game is not always the
// first entry (on Android it is libmain.so, behind the app process).
bool contains = false;
for (int i = 0; i < info->dlpi_phnum; ++i) {
const auto& phdr = info->dlpi_phdr[i];
if (phdr.p_type == PT_LOAD && ctx->probe >= info->dlpi_addr + phdr.p_vaddr &&
ctx->probe - (info->dlpi_addr + phdr.p_vaddr) < phdr.p_memsz)
{
contains = true;
break;
}
}
if (!contains) {
return 0;
}
ctx->base = info->dlpi_addr;
for (int i = 0; i < info->dlpi_phnum; ++i) {
const auto& phdr = info->dlpi_phdr[i];
@@ -178,7 +214,7 @@ bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
p = desc + ((note->n_descsz + 3) & ~3u);
}
}
return 1; // only inspect the main executable
return 1; // matched our image; stop either way
},
&ctx);
outBase = ctx.base;
@@ -190,13 +226,6 @@ bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
#endif
}
std::filesystem::path manifest_path() {
const char* basePath = SDL_GetBasePath();
std::filesystem::path dir =
basePath != nullptr ? std::filesystem::path{basePath} : std::filesystem::current_path();
return dir / "dusklight.symdb";
}
std::string hex_string(const uint8_t* data, size_t len) {
std::string out;
out.reserve(len * 2);
@@ -216,40 +245,12 @@ void initialize() {
}
s_state.initialized = true;
const auto path = manifest_path();
std::error_code ec;
if (!std::filesystem::exists(path, ec)) {
Log.info("no symbol manifest at {}; by-name resolution unavailable",
io::fs_path_to_string(path));
if (s_symdbDescriptor.magic != kDescriptorMagic) {
Log.error("symbol manifest descriptor is corrupt");
return;
}
std::vector<uint8_t> data;
try {
data = io::FileStream::ReadAllBytes(path);
} catch (const std::exception& e) {
Log.error("failed to read symbol manifest {}: {}", io::fs_path_to_string(path), e.what());
return;
}
if (data.size() < sizeof(Header)) {
Log.error(
"symbol manifest {} is truncated ({} bytes)", io::fs_path_to_string(path), data.size());
return;
}
Header header{};
std::memcpy(&header, data.data(), sizeof(header));
if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) {
Log.error("symbol manifest {} has wrong magic/version", io::fs_path_to_string(path));
return;
}
const auto compression = static_cast<Compression>(header.compression);
if ((compression != Compression::None && compression != Compression::Zstd) ||
header.buildIdLen > sizeof(header.buildId) ||
header.compressedLen > data.size() - sizeof(Header) ||
header.uncompressedLen > std::numeric_limits<size_t>::max() ||
(compression == Compression::None && header.compressedLen != header.uncompressedLen))
{
Log.error("symbol manifest {} is malformed", io::fs_path_to_string(path));
if (s_symdbDescriptor.rva == 0) {
Log.info("no symbol manifest embedded; by-name resolution unavailable");
return;
}
@@ -259,41 +260,67 @@ void initialize() {
Log.error("cannot determine the running image's build id; ignoring symbol manifest");
return;
}
const auto* blob = reinterpret_cast<const uint8_t*>(imageBase + s_symdbDescriptor.rva);
const auto blobLen = static_cast<size_t>(s_symdbDescriptor.size);
if (blobLen < sizeof(Header)) {
Log.error("embedded symbol manifest is truncated ({} bytes)", blobLen);
return;
}
Header header{};
std::memcpy(&header, blob, sizeof(header));
if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) {
Log.error("embedded symbol manifest has wrong magic/version");
return;
}
const auto compression = static_cast<Compression>(header.compression);
if ((compression != Compression::None && compression != Compression::Zstd) ||
header.buildIdLen > sizeof(header.buildId) ||
header.compressedLen > blobLen - sizeof(Header) ||
header.uncompressedLen > std::numeric_limits<size_t>::max() ||
(compression == Compression::None && header.compressedLen != header.uncompressedLen))
{
Log.error("embedded symbol manifest is malformed");
return;
}
// The manifest travels inside the image it describes, so a mismatch here means broken
// build tooling rather than a stale file — but it still guards resolved addresses.
if (imageId.size() != header.buildIdLen ||
std::memcmp(imageId.data(), header.buildId, imageId.size()) != 0)
{
Log.error("symbol manifest {} is stale: built for {}, running image is {}",
io::fs_path_to_string(path), hex_string(header.buildId, header.buildIdLen),
Log.error("embedded symbol manifest is stale: built for {}, running image is {}",
hex_string(header.buildId, header.buildIdLen),
hex_string(imageId.data(), imageId.size()));
return;
}
const auto compressedLen = static_cast<size_t>(header.compressedLen);
const auto uncompressedLen = static_cast<size_t>(header.uncompressedLen);
std::vector<uint8_t> payload;
const auto* storedPayload = data.data() + sizeof(Header);
std::vector<uint8_t> data;
const auto* storedPayload = blob + sizeof(Header);
if (compression == Compression::None) {
payload.assign(storedPayload, storedPayload + compressedLen);
data.assign(storedPayload, storedPayload + compressedLen);
} else {
payload.resize(uncompressedLen);
data.resize(uncompressedLen);
const size_t decompressedLen =
ZSTD_decompress(payload.data(), payload.size(), storedPayload, compressedLen);
ZSTD_decompress(data.data(), data.size(), storedPayload, compressedLen);
if (ZSTD_isError(decompressedLen)) {
Log.error("failed to decompress symbol manifest {}: {}", io::fs_path_to_string(path),
Log.error("failed to decompress embedded symbol manifest: {}",
ZSTD_getErrorName(decompressedLen));
return;
}
if (decompressedLen != payload.size()) {
Log.error("symbol manifest {} decompressed to {} bytes, expected {}",
io::fs_path_to_string(path), decompressedLen, payload.size());
if (decompressedLen != data.size()) {
Log.error("embedded symbol manifest decompressed to {} bytes, expected {}",
decompressedLen, data.size());
return;
}
}
data = std::move(payload);
const uint64_t entriesEnd = uint64_t{header.entryCount} * sizeof(Entry);
if (entriesEnd > data.size()) {
Log.error("decompressed symbol manifest {} is malformed", io::fs_path_to_string(path));
Log.error("decompressed embedded symbol manifest is malformed");
return;
}