blob: 9eca7f8ec8e7d8c84e86a5530782e2352aac9490 [file] [log] [blame]
// Copyright 2020 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "link_map.h"
#include <algorithm>
#include <functional>
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include <tuple>
#include <unordered_set>
#include "bloaty.h"
ABSL_ATTRIBUTE_NORETURN
static void Throw(const char* str, int line) {
std::cerr << __FILE__ << ":" << line << ", " << str << std::endl;
throw bloaty::Error(str, __FILE__, line);
}
#define THROW(msg) Throw(msg, __LINE__)
#define likely(x) __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)
#define GUARD(x) if (unlikely(!(x)))
namespace bloaty_link_map {
namespace {
// VMA LMA Size Align Out In Symbol
re2::RE2 header_regex(R"(^\s*VMA\s*LMA\s*Size\s*Align\s*Out\s*In\s*Symbol)");
// 194 194 13 1 .interp
re2::RE2 line_regex(R"(\s*[0-9a-f]+\s+([0-9a-f]+)\s+([0-9a-f]+)\s+(\d+) ( *)(.*))");
// obj/zircon/system/uapp/blobfs/blobfs.main.cc.o:(.rodata.main)
re2::RE2 level2_regex(R"(^(.*):\((.*)\))");
enum class Level { k1 = 1, k2 = 2, k3 = 3 };
struct Token {
std::string_view line;
uint64_t address;
uint64_t size;
Level level;
std::optional<uint64_t> span;
std::string_view tok;
};
std::string_view into_string_view(const re2::StringPiece& s) {
return std::string_view(s.data(), s.size());
}
re2::RE2 PROMOTED_GLOBAL_NAME_DEMANGLED_PATTERN(R"( \((\.\d+)?\.llvm\.\d+\)$)");
re2::RE2 PROMOTED_GLOBAL_NAME_RAW_PATTERN(R"((\.\d+)?\.llvm\.\d+$)");
std::string StripLlvmPromotedGlobalNames(std::string_view name) {
auto llvm_pos = name.find(".llvm.");
if (llvm_pos == std::string_view::npos) {
return std::string(name);
}
if (absl::EndsWith(name, ")")) {
std::string name_rep(name);
RE2::Replace(&name_rep, PROMOTED_GLOBAL_NAME_DEMANGLED_PATTERN, "");
return name_rep;
}
std::string name_rep(name);
RE2::Replace(&name_rep, PROMOTED_GLOBAL_NAME_RAW_PATTERN, "");
return name_rep;
}
const std::string STRING_LITERAL_NAME = "string literal";
std::string_view NormalizeName(std::string_view name) {
if (absl::StartsWith(name, ".L.str")) {
return STRING_LITERAL_NAME;
}
if (absl::EndsWith(name, " (.cfi)")) {
return name.substr(0, name.size() - 7);
}
return name;
}
// Decides whether a Level 3 token is an annotation.
//
// Returns:
// A 2-tuple (is_annotation, next_thumb2_mode):
// is_annotation: Whether |tok| is an annotation.
// next_thumb2_mode: New |thumb2_mode| value, or None if keep old value.
std::tuple<bool, std::optional<bool>> ParseArmAnnotations(std::string_view tok) {
// Annotations for ARM match '$t', '$d.1', but not '$_21::invoke'.
if (absl::StartsWith(tok, "$") && (tok.size() == 2 || (tok.size() >= 3 && tok[2] == '.'))) {
if (absl::StartsWith(tok, "$t")) {
// Is annotation, enter Thumb2 mode.
return std::tuple<bool, std::optional<bool>>{true, std::optional<bool>{true}};
}
if (absl::StartsWith(tok, "$a")) {
// Is annotation, enter ARM32 mode.
return std::tuple<bool, std::optional<bool>>{true, std::optional<bool>{false}};
}
// Is annotation, keep old |thumb2_mode| value.
return std::tuple<bool, std::optional<bool>>{true, std::nullopt};
}
// Not annotation, keep old |thumb2_mode| value.
return std::tuple<bool, std::optional<bool>>{false, std::nullopt};
}
void Tokenize(std::istringstream& lines, std::function<void(const Token&)> cb) {
std::string one_line;
// A Level 3 symbol can have |size == 0| in some situations (e.g., assembly
// code symbols). To provided better size estimates in this case, the "span"
// of a Level 3 symbol is computed as:
// (A) The |address| difference compared to the next Level 3 symbol.
// (B) If the Level 3 symbol is the last among Level 3 lines nested under a
// Level 2 line: The difference between the Level 3 symbol's |address|
// and the containing Level 2 line's end address.
// To handle (A), |lines| is visited using a one-step lookahead, using
// |sentinel| to handle the last line. To handle (B), |level2_end_address| is
// computed for each Level 2 line.
const std::string sentinel = "0 0 0 0 THE_END";
GUARD(RE2::FullMatch(sentinel, line_regex)) { THROW("sentinel must match regex"); }
uint64_t level2_end_address = 0;
bool thumb2_mode = false;
std::string line;
std::optional<uint64_t> address = std::nullopt;
uint64_t size = 0;
Level level = Level::k1;
std::string tok;
auto process_line = [&](const std::string& next_line) {
re2::StringPiece str_next_address;
re2::StringPiece str_next_size;
re2::StringPiece str_next_level;
re2::StringPiece next_tok;
GUARD(RE2::FullMatch(next_line, line_regex, &str_next_address, &str_next_size, (void*)NULL,
&str_next_level, &next_tok)) {
return;
}
uint64_t next_address = std::stoul(str_next_address.as_string(), nullptr, 16);
uint64_t next_size = std::stoul(str_next_size.as_string(), nullptr, 16);
int int_level = (str_next_level.size() / 8) + 1;
GUARD(int_level >= 1 && int_level <= 3) { THROW("invalid level"); }
Level next_level = static_cast<Level>(int_level);
if (next_level == Level::k3) {
GUARD(level == Level::k2 || level == Level::k3) {
THROW("Cannot jump from Level 1 to Level l3");
}
// Detect annotations. If found, maybe update |thumb2_mode|, then skip.
auto [is_annotation, next_thumb2_mode] = ParseArmAnnotations(into_string_view(next_tok));
if (is_annotation) {
if (next_thumb2_mode.has_value()) {
thumb2_mode = next_thumb2_mode.value();
}
// Skip annotations.
return;
}
if (thumb2_mode) {
// Adjust odd address to even. Alignment is not guanteed for all
// symbols (e.g., data, or x86), so this is judiciously applied.
next_address &= ~(1ULL);
}
} else {
// Resets on leaving Level 3.
thumb2_mode = false;
}
if (address.has_value()) {
std::optional<uint64_t> span = std::nullopt;
if (level == Level::k3) {
span = next_level == Level::k3 ? next_address : level2_end_address;
*span -= *address;
} else if (level == Level::k2) {
level2_end_address = *address + size;
}
cb(Token{
.line = line,
.address = *address,
.size = size,
.level = level,
.span = span,
.tok = tok,
});
}
line = next_line;
address = next_address;
size = next_size;
level = next_level;
tok = next_tok.as_string();
};
while (getline(lines, one_line)) {
process_line(one_line);
}
process_line(sentinel);
}
const std::string SECTION_BSS = ".bss";
const std::string SECTION_BSS_REL_RO = ".bss.rel.ro";
const std::string SECTION_DATA = ".data";
const std::string SECTION_DATA_REL_RO = ".data.rel.ro";
const std::string SECTION_DATA_REL_RO_LOCAL = ".data.rel.ro.local";
const std::string SECTION_DEX = ".dex";
const std::string SECTION_DEX_METHOD = ".dex.method";
const std::string SECTION_OTHER = ".other";
const std::string SECTION_PAK_NONTRANSLATED = ".pak.nontranslated";
const std::string SECTION_PAK_TRANSLATIONS = ".pak.translations";
const std::string SECTION_PART_END = ".part.end";
const std::string SECTION_RODATA = ".rodata";
const std::string SECTION_TEXT = ".text";
// Used by SymbolGroup when they contain a mix of sections.
const std::string SECTION_MULTIPLE = ".*";
const std::unordered_set<std::string> BSS_SECTIONS = {
SECTION_BSS,
SECTION_BSS_REL_RO,
SECTION_PART_END,
};
} // namespace
std::vector<Symbol> ParseLldLinkMap(const std::string& content) {
std::vector<Symbol> syms;
std::istringstream lines(content);
std::string one_line;
GUARD(getline(lines, one_line)) { THROW("must have at least the header line"); }
GUARD(header_regex.ok()) { THROW("can't compile header regex"); }
GUARD(RE2::FullMatch(one_line, header_regex)) { THROW("link map is not in lld v1 format"); }
// Example Format:
// VMA LMA Size Align Out In Symbol
// 194 194 13 1 .interp
// 194 194 13 1 <internal>:(.interp)
// 1a8 1a8 22d8 4 .ARM.exidx
// 1b0 1b0 8 4 obj/sandbox/syscall.o:(.ARM.exidx)
// 400 400 123400 64 .text
// 600 600 14 4 ...:(.text.OUTLINED_FUNCTION_0)
// 600 600 0 1 $x.3
// 600 600 14 1 OUTLINED_FUNCTION_0
// 123800 123800 20000 256 .rodata
// 123800 123800 4 4 ...:o:(.rodata._ZN3fooE.llvm.1234)
// 123800 123800 4 1 foo (.llvm.1234)
// 123804 123804 4 4 ...:o:(.rodata.bar.llvm.1234)
// 123804 123804 4 1 bar.llvm.1234
// ^ ^ ^
// Level 1 ^ ^
// Level 2 ^
// Level 3
//
// Level 1 (Out) specify sections or PROVIDE_HIDDEN lines.
// Level 2 (In) specify object paths and section names within objects, or '<internal>:...'.
// Level 3 (Symbol) specify symbol names or special names such as
// '.L_MergeGlobals'. Annotations such as '$d', $t.42' also appear at Level 3,
// but they are consumed by |tokenizer|, so don't appear hear.
GUARD(line_regex.ok()) { THROW("can't compile line regex"); }
GUARD(level2_regex.ok()) { THROW("can't compile level2 regex"); }
std::optional<std::string> cur_section;
bool cur_section_is_useful = false;
uint64_t promoted_name_count = 0;
// |is_partial| indicates that an eligible Level 3 line should be used to
// update |syms[-1].full_name| instead of creating a new symbol.
bool is_partial = false;
// Assembly code can create consecutive Level 3 lines with |size == 0|. These
// lines can represent
// (1) assembly functions (should form symbol), or
// (2) assembly labels (should NOT form symbol).
// It seems (2) correlates with the presence of a leading Level 3 line with
// |size > 0|. This gives rise to the following strategy: Each symbol S from
// a Level 3 line suppresses Level 3 lines with |address| less than
// |next_usable_address := S.address + S.size|.
uint64_t next_usable_address = 0;
bool in_partitions = false;
bool in_jump_table = false;
uint64_t jump_tables_count = 0;
uint64_t jump_entries_count = 0;
uint64_t mangled_start_idx = 0;
std::string cur_obj;
// Assembly code can create consecutive Level 3 lines with |size == 0|. These
// lines can represent
// (1) assembly functions (should form symbol), or
// (2) assembly labels (should NOT form symbol).
// It seems (2) correlates with the presence of a leading Level 3 line with
// |size > 0|. This gives rise to the following strategy: Each symbol S from
// a Level 3 line suppresses Level 3 lines with |address| less than
// |next_usable_address := S.address + S.size|.
Tokenize(lines, [&](const Token& token) {
// Level 1 data match the "Out" column. They specify sections or
// PROVIDE_HIDDEN lines.
if (token.level == Level::k1) {
// Ignore sections that belong to feature library partitions. Seeing a
// partition name is an indicator that we've entered a list of feature
// partitions. After these, a single .part.end section will follow to
// reserve memory at runtime. Seeing the .part.end section also marks the
// end of partition sections in the map file.
if (absl::EndsWith(token.tok, "_partition")) {
in_partitions = true;
} else if (token.tok == ".part.end") {
// Note that we want to retain .part.end section, so it's fine to
// restart processing on this section, rather than the next one.
in_partitions = false;
}
if (in_partitions) {
// For now, completely ignore feature partitions.
cur_section = std::nullopt;
cur_section_is_useful = false;
} else {
cur_section = token.tok;
// E.g., Want to convert "(.text._name)" -> "_name" later.
mangled_start_idx = cur_section->size() + 1;
cur_section_is_useful = ((BSS_SECTIONS.find(*cur_section) != BSS_SECTIONS.end()) ||
SECTION_RODATA == *cur_section || SECTION_TEXT == *cur_section ||
absl::StartsWith(*cur_section, SECTION_DATA));
}
} else if (cur_section_is_useful) {
// Level 2 data match the "In" column. They specify object paths and
// section names within objects, or '<internal>:...'.
if (token.level == Level::k2) {
std::string mangled_name;
re2::StringPiece paren_value;
GUARD(RE2::FullMatch(token.tok, level2_regex, &cur_obj, &paren_value)) {
THROW("Level 2 regex did not match");
}
bool in_jump_table = absl::StrContains(into_string_view(paren_value), ".L.cfi.jumptable");
if (in_jump_table) {
// Store each CFI jump table as a Level 2 symbol, whose Level 3
// details are discarded.
jump_tables_count++;
// Replaces 'lto.tmp' to prevent problem later.
cur_obj = "";
mangled_name = "** CFI jump table";
} else {
// E.g., '(.text.unlikely._name)' -> '_name'.
mangled_name = into_string_view(paren_value.substr(mangled_start_idx));
is_partial = true;
// As of 2017/11 LLD does not distinguish merged strings from other
// merged data. Feature request is filed under:
// https://bugs.llvm.org/show_bug.cgi?id=35248
if (cur_obj == "<internal>") {
if (cur_section.has_value() && *cur_section == ".rodata" && mangled_name == "") {
// Treat all <internal> sections within .rodata as as string
// literals. Some may hold numeric constants or other data, but
// there is currently no way to distinguish them.
mangled_name = "** lld merge strings";
} else {
// e.g. <internal>:(.text.thunk)
mangled_name = "** " + mangled_name;
}
is_partial = false;
cur_obj = "";
} else if (cur_obj == "lto.tmp" ||
absl::StrContains(into_string_view(cur_obj), "thinlto-cache")) {
cur_obj = "";
}
}
// Create a symbol here since there may be no ensuing Level 3 lines.
// But if there are, then the symbol can be modified later as sym[-1].
syms.push_back(Symbol{
.name = std::move(mangled_name),
.compile_unit = cur_obj,
.section = *cur_section,
.addr = token.address,
.size = token.size,
});
// Level 3 |address| is nested under Level 2, don't add |size|.
next_usable_address = token.address;
} else if (token.level == Level::k3) {
// Level 3 data match the "Symbol" column. They specify symbol names or
// special names such as '.L_MergeGlobals'. Annotations such as '$d',
// '$t.42' also appear at Level 3, but they are consumed by |tokenizer|,
// so don't appear hear.
// Handle .L.cfi.jumptable.
if (in_jump_table) {
// Level 3 entries in CFI jump tables are thunks with mangled names.
// Extracting them as symbols is not worthwhile; we only store the
// Level 2 symbol, and print the count for verbose output. For
// counting, '__typeid_' entries are excluded since they're likely
// just annotations.
if (!absl::StartsWith(token.tok, "__typeid_")) {
jump_entries_count++;
}
return;
}
// Ignore anything with '.L_MergedGlobals' prefix. This seems to only
// happen for ARM (32-bit) builds.
if (absl::StartsWith(token.tok, ".L_MergedGlobals")) {
return;
}
// Use |span| to decide whether to use a Level 3 line for Symbols. This
// is useful for two purposes:
// * This is a better indicator than |size|, which can be 0 for
// assembly functions.
// * If multiple Level 3 lines have the same starting address, this
// cause all but the last line to have |span > 0|. This dedups lines
// with identical symbol names (why do they exist?). Note that this
// also skips legitimate aliases, but that's desired because nm.py
// (downstream) assumes no aliases already exist.
if (*token.span > 0) {
std::string stripped_tok = StripLlvmPromotedGlobalNames(token.tok);
std::string tok;
if (token.tok.size() != stripped_tok.size()) {
promoted_name_count++;
tok = stripped_tok;
} else {
tok = token.tok;
}
tok = NormalizeName(tok);
// Handle special case where a partial symbol consumes bytes before
// the first Level 3 symbol.
if (is_partial && syms.back().addr < token.address) {
// Truncate the partial symbol and leave it without |full_name|.
// The data from the current line will form a new symbol.
syms.back().size = token.address - syms.back().addr;
next_usable_address = token.address;
is_partial = false;
}
if (is_partial) {
syms.back().name = tok;
syms.back().size =
token.size > 0 ? token.size : std::min(syms.back().size, *token.span);
next_usable_address = token.address + syms.back().size;
is_partial = false;
} else if (token.address >= next_usable_address) {
uint64_t size_to_use;
if (absl::StartsWith(tok, "__typeid_")) {
GUARD(token.size == 1) { THROW("Token size"); }
if (absl::EndsWith(tok, "_byte_array")) {
// CFI byte array table: |size| is inaccurate, so use |span|.
size_to_use = *token.span;
} else {
// Likely '_global_addr' or '_unique_member'. These should be:
// * Skipped since they're in CFI tables.
// * Suppressed (via |next_usable_address|) by another Level 3
// symbol.
// Anything that makes it here would be an anomaly worthy of
// investigation, so print warnings.
std::cerr << "Unrecognized __typeid_ symbol at " << token.address << std::endl;
return;
}
} else {
size_to_use = token.size > 0 ? token.size : *token.span;
}
syms.push_back(Symbol{
.name = std::move(tok),
.compile_unit = cur_obj,
.section = *cur_section,
.addr = token.address,
.size = size_to_use,
});
// Suppress symbols with overlapping |address|. This eliminates
// labels from assembly sources.
next_usable_address = token.address + size_to_use;
if (!cur_obj.empty()) {
syms.back().compile_unit = cur_obj;
}
}
}
}
}
});
if (promoted_name_count > 0) {
std::cerr << "Found " << promoted_name_count << " promoted global names" << std::endl;
}
if (jump_tables_count > 0) {
std::cerr << "Found " << jump_tables_count << " CFI jump tables with " << jump_entries_count
<< "total entries" << std::endl;
}
return syms;
}
std::vector<Section> ParseLldLinkMapSections(const std::string& content) {
std::vector<Section> sections;
std::istringstream lines(content);
std::string one_line;
Tokenize(lines, [&](const Token& token) {
// Level 1 data match the "Out" column. They specify sections or
// PROVIDE_HIDDEN lines.
if (token.level == Level::k1) {
sections.push_back(Section{
.name = std::string(token.tok),
.addr = token.address,
.size = token.size,
});
}
});
return sections;
}
namespace {
// ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o
re2::RE2 library_crate_regex(
R"(\/[a-zA-Z0-9_]+\.[a-zA-Z0-9_-]+\.([a-zA-Z0-9_]+)\.[a-zA-Z0-9-]+.*\.rcgu\.o$)");
// ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o
re2::RE2 bin_crate_regex(R"(\/[a-zA-Z0-9_-]+\.([a-zA-Z0-9_]+)\.[a-zA-Z0-9-]+.*\.rcgu\.o$)");
// foobar.rlib(libregex_syntax-579ced0738b0164d-579ced0738b0164d.regex_syntax.c02sfxfu-cgu.13.rcgu.o)
re2::RE2 rlib_crate_regex(R"(rlib\([a-zA-Z_\-0-9]+\.([a-zA-Z0-9_]+)\.[a-zA-Z0-9-]+.*\.rcgu\.o\)$)");
// /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o
re2::RE2 zircon_lib_regex(R"(\/out\/[a-zA-Z0-9_-]+\.zircon\/.*\/obj\/system\/ulib\/(.*)\.o$)");
// obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c.o
re2::RE2 fidling_regex(R"(^obj\/out\/.*\/fidling\/gen\/(.*)\.o$)");
// obj/zircon/public/lib/fidl_base/libfidl_base.a(libfidl_base.decoding.cc.o)
re2::RE2 zircon_fidl_lib_regex(
R"(^obj\/zircon\/public\/lib\/fidl_base\/libfidl_base\.a\(libfidl_base\.(.*)\.cc\.o\)$)");
// obj/zircon/system/uapp/blobfs/blobfs.main.cc.o
re2::RE2 zircon_lib_regex2(R"(^obj\/zircon\/system\/(.*)\.o$)");
} // namespace
std::optional<std::tuple<std::string, std::optional<std::string>>> TransformCompileUnitForFuchsia(
const std::string& compile_unit) {
GUARD(library_crate_regex.ok()) { THROW("can't compile library crate regex"); }
{
std::string crate_name;
if (RE2::PartialMatch(compile_unit, library_crate_regex, &crate_name)) {
return std::tuple{"[crate: " + crate_name + "]", crate_name};
}
}
GUARD(bin_crate_regex.ok()) { THROW("can't compile bin crate regex"); }
{
std::string crate_name;
if (RE2::PartialMatch(compile_unit, bin_crate_regex, &crate_name)) {
return std::tuple{"[crate: " + crate_name + "]", crate_name};
}
}
GUARD(rlib_crate_regex.ok()) { THROW("can't compile rlib crate regex"); }
{
std::string crate_name;
if (RE2::PartialMatch(compile_unit, rlib_crate_regex, &crate_name)) {
return std::tuple{"[crate: " + crate_name + "]", crate_name};
}
}
GUARD(zircon_lib_regex.ok()) { THROW("can't compile zircon lib regex"); }
{
std::string cc_path;
if (RE2::PartialMatch(compile_unit, zircon_lib_regex, &cc_path)) {
// Remove the prefix in last path component
// c/crt1.Scrt1.cc -> c/Scrt1.cc
static re2::RE2 prefix_regex(R"(\/[a-zA-Z0-9\-_]+\.([a-zA-Z0-9\-_]+\.(cc|c))$)");
if (RE2::Replace(&cc_path, prefix_regex, R"(/\1)")) {
return std::tuple{"../../zircon/system/ulib/" + cc_path, std::nullopt};
}
}
}
GUARD(fidling_regex.ok()) { THROW("can't compile fidling regex"); }
{
std::string cc_path;
if (RE2::PartialMatch(compile_unit, fidling_regex, &cc_path)) {
// A: sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c
// B: sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block.fidl.tables.c
// A: sdk/fidl/fuchsia.hardware.block/fuchsia/hardware/block/c/fuchsia.hardware.block_c_client.fidl.client.c
// B: sdk/fidl/fuchsia.hardware.block/fuchsia/hardware/block/c/fidl.client.c
// A: sdk/fidl/fuchsia.io/fuchsia/io/llcpp/fuchsia.io_llcpp.fidl.cc
// B: sdk/fidl/fuchsia.io/fuchsia/io/llcpp/fidl.cc
static re2::RE2 prefix_regex(
R"([a-zA-Z0-9\-\.]+_[a-zA-Z0-9\-_]+\.([a-zA-Z0-9\-\.]+\.(c|cc))$)");
if (RE2::Replace(&cc_path, prefix_regex, R"(\1)")) {
return std::tuple{"fidling/gen/" + cc_path, std::nullopt};
}
}
}
GUARD(zircon_fidl_lib_regex.ok()) { THROW("can't compile zircon fidl lib regex"); }
{
std::string cc_name;
if (RE2::PartialMatch(compile_unit, zircon_fidl_lib_regex, &cc_name)) {
return std::tuple{"../../zircon/system/ulib/fidl/" + cc_name + ".cc", std::nullopt};
}
}
// Rust-specific special casing...
if (absl::StartsWith(compile_unit, "obj/third_party/rust_crates/compat/ring/libring-core.a")) {
return std::tuple<std::string, std::optional<std::string>>{
"[crate: ring]", std::optional<std::string>{"ring"}};
}
return std::nullopt;
}
} // namespace bloaty_link_map