Merge branch 'upstream/master' into fuchsia

Change-Id: I86bf67113dbb17d7e77e97fa71125f5cb2c78a6d
diff --git a/.gitignore b/.gitignore
index 2075731..67d9094 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,17 @@
 /bloaty
 !tests/testdata/**
 *.dSYM
+.vscode
+.clangd
+.cache
+/build
+/CMakeFiles
+/Testing
+CMakeCache.txt
+CTestTestfile.cmake
+DartConfiguration.tcl
+Makefile
+bloaty_package.bloaty
+capstone.pc
+cmake_install.cmake
+compile_commands.json
diff --git a/.gitmodules b/.gitmodules
index 11b244e..e6b7b11 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,6 +16,9 @@
 [submodule "third_party/demumble"]
 	path = third_party/demumble
 	url = https://github.com/nico/demumble.git
+[submodule "third_party/rustc-demangle"]
+	path = third_party/rustc-demangle
+	url = https://github.com/alexcrichton/rustc-demangle.git
 [submodule "third_party/zlib"]
 	path = third_party/zlib
 	url = https://github.com/madler/zlib
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 868d557..bbebe3f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,8 +1,9 @@
-cmake_minimum_required(VERSION 3.5)
+cmake_minimum_required(VERSION 3.9)
 cmake_policy(SET CMP0048 NEW)
 project (Bloaty VERSION 1.1)
-include(CTest)
 set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_GENERATOR "Ninja")
+include(CTest)
 set_property(GLOBAL PROPERTY USE_FOLDERS ON)  # Group projects in visual studio
 
 # Options we define for users.
@@ -12,6 +13,18 @@
 option(BLOATY_ENABLE_BUILDID "Enable build id." ON)
 option(BLOATY_ENABLE_RE2 "Enable the support for regular expression functions." ON)
 
+if(APPLE)
+# When building bloaty on macOS infra builders,
+# need to add the libc++.a from the CIPD toolchain, otherwise there are linker errors
+# e.g. https://ci.chromium.org/p/fuchsia/builders/try/bloaty-x64-mac/b8874842054253378656?
+#
+# To achieve this, find the libc++.a location from the location of the clang binary.
+# CMAKE_CXX_COMPILER
+get_filename_component(CLANG_CIPD_TOOLCHAIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY CACHE)
+set(CMAKE_EXE_LINKER_FLAGS
+    "${CMAKE_EXE_LINKER_FLAGS} ${CLANG_CIPD_TOOLCHAIN_DIR}/../lib/libc++.a")
+endif(APPLE)
+
 if(UNIX)
 find_package(PkgConfig)
 find_package(ZLIB)
@@ -19,7 +32,9 @@
   pkg_search_module(RE2 re2)
 endif()
 pkg_search_module(CAPSTONE capstone)
-pkg_search_module(PROTOBUF protobuf)
+# Always use bundled protobuf, to accommodate building for other systems
+# without protobuf installed.
+# pkg_search_module(PROTOBUF protobuf)
 if(BLOATY_ENABLE_RE2)
   if(RE2_FOUND)
     MESSAGE(STATUS "System re2 found, using")
@@ -123,6 +138,7 @@
 
   set(CAPSTONE_BUILD_SHARED OFF CACHE BOOL "Build shared library" FORCE)
   set(CAPSTONE_BUILD_TESTS OFF CACHE BOOL "Build tests" FORCE)
+
   add_subdirectory(third_party/capstone)
   include_directories(third_party/capstone/include)
   set_property(TARGET capstone-static PROPERTY FOLDER "third_party")
@@ -148,19 +164,29 @@
 include_directories(.)
 include_directories(src)
 include_directories(third_party/abseil-cpp)
+include_directories(third_party/rustc-demangle/crates/capi/include)
 include_directories("${CMAKE_CURRENT_BINARY_DIR}/src")
 
 # Baseline build flags.
 if(MSVC)
   set(CMAKE_CXX_FLAGS "/EHsc /wd4018 /D_CRT_SECURE_NO_WARNINGS /DNOMINMAX")
 else()
-  set(CMAKE_CXX_FLAGS "-W -Wall -Wno-sign-compare")
+  set(CMAKE_CXX_FLAGS "-std=c++17 -W -Wall -Wno-sign-compare")
   set(CMAKE_CXX_FLAGS_DEBUG "-g1")
-  set(CMAKE_CXX_FLAGS_RELEASE "-O2")
+  set(CMAKE_CXX_FLAGS_RELEASE "-O3")
   set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g1")
   set_source_files_properties(third_party/demumble/third_party/libcxxabi/cxa_demangle.cpp PROPERTIES COMPILE_FLAGS -Wno-implicit-fallthrough)
 endif()
 
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fexceptions -ffunction-sections -fdata-sections")
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions -ffunction-sections -fdata-sections")
+
+if (APPLE)
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-dead_strip")
+else()
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections")
+endif()
+
 if(APPLE)
 elseif(UNIX)
   if(BLOATY_ENABLE_BUILDID)
@@ -199,6 +225,13 @@
       --cpp_out=${CMAKE_CURRENT_BINARY_DIR}/src
       -I${CMAKE_CURRENT_SOURCE_DIR}/src
 )
+add_custom_command(
+  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/src/report.pb.cc
+  DEPENDS protoc ${CMAKE_CURRENT_SOURCE_DIR}/src/report.proto
+  COMMAND protoc ${CMAKE_CURRENT_SOURCE_DIR}/src/report.proto
+      --cpp_out=${CMAKE_CURRENT_BINARY_DIR}/src
+      -I${CMAKE_CURRENT_SOURCE_DIR}/src
+)
 else()
 add_custom_command(
   OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/src/bloaty.pb.cc
@@ -206,6 +239,12 @@
       --cpp_out=${CMAKE_CURRENT_BINARY_DIR}/src
       -I${CMAKE_CURRENT_SOURCE_DIR}/src
 )
+add_custom_command(
+  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/src/report.pb.cc
+  COMMAND protoc ${CMAKE_CURRENT_SOURCE_DIR}/src/report.proto
+      --cpp_out=${CMAKE_CURRENT_BINARY_DIR}/src
+      -I${CMAKE_CURRENT_SOURCE_DIR}/src
+)
 endif()
 
 file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/src/bloaty_package.bloaty
@@ -216,9 +255,11 @@
     src/bloaty.h
     src/disassemble.cc
     ${CMAKE_CURRENT_BINARY_DIR}/src/bloaty.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/src/report.pb.cc
     src/dwarf.cc
     src/dwarf_constants.h
     src/elf.cc
+    src/link_map.cc
     src/macho.cc
     src/pe.cc
     third_party/lief_pe/pe_structures.h
@@ -228,6 +269,7 @@
     src/util.cc
     src/util.h
     src/webassembly.cc
+    src/write_bloaty_report.cc
     # Until Abseil has a proper CMake build system
     third_party/abseil-cpp/absl/base/internal/raw_logging.cc # Grrrr...
     third_party/abseil-cpp/absl/base/internal/throw_delegate.cc
@@ -253,6 +295,53 @@
     )
 set_property(TARGET libbloaty PROPERTY FOLDER "bloaty")
 
+# Teach CMake how to build rustc-demangle
+# RUST_TOOLCHAIN_PREFIX is the directory containing the `cargo` executable.
+if(NOT RUST_TOOLCHAIN_PREFIX)
+  find_program(CARGO_PATH cargo)
+  if(CARGO_PATH)
+    message(STATUS "Found cargo: ${CARGO_PATH}")
+  else()
+    set(HAVE_GMSH NO)
+    message(FATAL_ERROR "cargo not found. Please specify RUST_TOOLCHAIN_PREFIX")
+  endif()
+else()
+  set(CARGO_PATH ${RUST_TOOLCHAIN_PREFIX}/cargo CACHE FILEPATH "Path to the cargo executable")
+endif()
+file(
+  GLOB_RECURSE
+  RUSTC_DEMANGLE_SRC_FILES
+  "${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/*.rs"
+  "${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/*.c"
+  "${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/*.h"
+  "${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/*.toml"
+  )
+if(NOT APPLE)
+  # Rust uses libunwind, and embeds the libunwind runtime inside any static
+  # library produced from the build.
+  # Bloaty uses C++ exceptions to trigger failure. Use of C++ exceptions also
+  # requires libunwind, but specifically the version coming with GCC/Clang.
+  # Their interaction causes strange failures when compiled using GCC 9.3.
+  # To fix this, take out the unwind-related members from the Rust static lib.
+  set(RUSTC_DEMANGLE_FIXUP_ARCHIVE ar d target/release/librustc_demangle.a Unwind-EHABI.o Unwind-seh.o Unwind-sjlj.o UnwindLevel1-gcc-ext.o UnwindLevel1.o UnwindRegistersRestore.o UnwindRegistersSave.o libunwind.o)
+else()
+  # On macOS, there doesn't appear to be such problem.
+  set(RUSTC_DEMANGLE_FIXUP_ARCHIVE "")
+endif()
+add_custom_command(
+  OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/target/release/librustc_demangle.a
+  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/Cargo.toml
+  DEPENDS ${RUSTC_DEMANGLE_SRC_FILES}
+  COMMAND ${CARGO_PATH} build -p rustc-demangle-capi --release
+  COMMAND ${RUSTC_DEMANGLE_FIXUP_ARCHIVE}
+)
+add_custom_target(
+  rustc-demangle-lib
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/target/release/librustc_demangle.a
+)
+add_dependencies(libbloaty rustc-demangle-lib)
+
 if(UNIX)
   set(LIBBLOATY_LIBS libbloaty)
   if(PROTOBUF_FOUND)
@@ -277,8 +366,11 @@
   else()
     list(APPEND LIBBLOATY_LIBS zlib)
   endif()
+
+  set(LIBBLOATY_LIBS ${LIBBLOATY_LIBS} "${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/target/release/librustc_demangle.a" dl)
+
 else()
-  set(LIBBLOATY_LIBS libbloaty libprotoc capstone-static)
+  set(LIBBLOATY_LIBS libbloaty libprotoc capstone-static "${CMAKE_CURRENT_SOURCE_DIR}/third_party/rustc-demangle/target/release/librustc_demangle.a" dl)
   if(BLOATY_ENABLE_RE2)
     list(APPEND LIBBLOATY_LIBS  re2)
   endif()
@@ -306,9 +398,29 @@
   add_executable(fuzz_target tests/fuzz_target.cc)
   target_link_libraries(fuzz_target ${LIBBLOATY_LIBS} $ENV{LIB_FUZZING_ENGINE})
 else()
+  include(CheckIPOSupported)
+  check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error)
+
   add_executable(bloaty src/main.cc)
   target_link_libraries(bloaty ${LIBBLOATY_LIBS})
 
+  if(ipo_supported)
+      message(STATUS "IPO / LTO enabled")
+      set_property(TARGET bloaty PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
+  else()
+      message(STATUS "IPO / LTO not supported: <${ipo_error}>")
+  endif()
+
+  # All of this is to add -pthread, which is required by re2 (not us).
+  find_package(Threads REQUIRED)
+  if(THREADS_HAVE_PTHREAD_ARG)
+    set_property(TARGET bloaty PROPERTY COMPILE_OPTIONS "-pthread")
+    set_property(TARGET bloaty PROPERTY INTERFACE_COMPILE_OPTIONS "-pthread")
+  endif()
+  if(CMAKE_THREAD_LIBS_INIT)
+    target_link_libraries(bloaty "${CMAKE_THREAD_LIBS_INIT}")
+  endif()
+
   set_property(TARGET bloaty PROPERTY FOLDER "bloaty")
 
   if(BLOATY_ENABLE_CMAKETARGETS)
@@ -335,8 +447,10 @@
 
       set(TEST_TARGETS
           bloaty_test
+          bloaty_report_test
           bloaty_test_pe
           bloaty_misc_test
+          link_map_test
           range_map_test
           )
 
@@ -356,9 +470,11 @@
 
       file(GLOB fuzz_corpus tests/testdata/fuzz_corpus/*)
 
+      add_test(NAME link_map_test COMMAND link_map_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/testdata/link_map)
       add_test(NAME range_map_test COMMAND range_map_test)
       add_test(NAME bloaty_test_x86-64 COMMAND bloaty_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/testdata/linux-x86_64)
       add_test(NAME bloaty_test_x86 COMMAND bloaty_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/testdata/linux-x86)
+      add_test(NAME bloaty_report_test_x86-64 COMMAND bloaty_report_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/testdata/linux-x86_64)
       add_test(NAME bloaty_test_pe_x64 COMMAND bloaty_test_pe WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/testdata/PE/x64)
       add_test(NAME bloaty_test_pe_x86 COMMAND bloaty_test_pe WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/testdata/PE/x86)
       add_test(NAME bloaty_misc_test COMMAND bloaty_misc_test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/testdata/misc)
diff --git a/build_all.sh b/build_all.sh
new file mode 100755
index 0000000..7321781
--- /dev/null
+++ b/build_all.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+set -xeuf -o pipefail
+
+pushd "${0%/*}"
+
+rm -rf build
+# Mimic the LUCI environment defined in
+# https://fuchsia.googlesource.com/infra/recipes/+/refs/heads/master/recipes/bloaty.py#59
+export CC=`which clang`
+export CXX=`which clang++`
+# On LUCI, we should set the `RUST_TOOLCHAIN_PREFIX` variable.
+# For local builds, cmake will try to detect its location automatically.
+cmake . -DCMAKE_GENERATOR=Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=On -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXE_LINKER_FLAGS='-static-libstdc++ -ldl -lpthread' -Bbuild
+ninja -C build
+ninja -C build test
+
+popd
diff --git a/src/bloaty.cc b/src/bloaty.cc
index 78147ea..ac3628b 100644
--- a/src/bloaty.cc
+++ b/src/bloaty.cc
@@ -14,12 +14,14 @@
 
 #include <atomic>
 #include <cmath>
+#include <filesystem>
 #include <fstream>
 #include <iostream>
 #include <limits>
 #include <map>
 #include <memory>
 #include <mutex>
+#include <regex>
 #include <sstream>
 #include <string>
 #include <thread>
@@ -54,8 +56,9 @@
 
 #include "bloaty.h"
 #include "bloaty.pb.h"
+#include "demangle.h"
+#include "rustc_demangle.h"
 #include "re.h"
-#include "util.h"
 
 using absl::string_view;
 
@@ -74,6 +77,8 @@
 
 constexpr DataSourceDefinition data_sources[] = {
     {DataSource::kArchiveMembers, "armembers", "the .o files in a .a file"},
+    {DataSource::kAccessPattern, "accesspattern", "which regions in the file"
+                                                  "is accessed at run-time"},
     {DataSource::kCompileUnits, "compileunits",
      "source file for the .o file (translation unit). requires debug info."},
     {DataSource::kInputFiles, "inputfiles",
@@ -218,6 +223,22 @@
 }
 #endif
 
+namespace {
+
+std::string DemangleRustSymbol(std::string_view mangled) {
+  constexpr size_t kBufferSize = 8192;
+  std::unique_ptr<std::array<char, kBufferSize>> buffer =
+      std::make_unique<std::array<char, kBufferSize>>();
+  int result = rustc_demangle(mangled.data(), buffer->data(), kBufferSize);
+  if (result == 1) {
+    return std::string(buffer->data());
+  } else {
+    return "";
+  }
+}
+
+}  // namespace
+
 extern "C" char* __cxa_demangle(const char* mangled_name, char* buf, size_t* n,
                                 int* status);
 
@@ -232,12 +253,48 @@
     demangle_from.remove_prefix(1);
   }
 
+  if (absl::StartsWith(demangle_from, "_R")) {
+    // Demangle Rust symbols
+    std::string ret = DemangleRustSymbol(demangle_from);
+    if (!ret.empty()) {
+      return ret;
+    }
+  }
+
+  if (absl::StartsWith(demangle_from, "switch.table._R")) {
+    // Demangle Rust symbols for switch tables
+    demangle_from.remove_prefix(13);
+    std::string ret = DemangleRustSymbol(demangle_from);
+    if (!ret.empty()) {
+      return "switch.table." + ret;
+    }
+  }
+
+  if (absl::StartsWith(demangle_from, ".Lswitch.table._R")) {
+    // Demangle Rust symbols for switch tables, with ".L" prefix.
+    demangle_from.remove_prefix(15);
+    std::string ret = DemangleRustSymbol(demangle_from);
+    if (!ret.empty()) {
+      return "switch.table." + ret;
+    }
+  }
+
   if (source == DataSource::kShortSymbols) {
-    char demangled[1024];
+    char demangled[4096];
     if (absl::debugging_internal::Demangle(demangle_from.data(), demangled,
                                            sizeof(demangled))) {
       return std::string(demangled);
     } else {
+      // TODO(yifeit): Certain symbols have dots (".") in them. Those are not allowed.
+      // Find and remove the last "." and anything after.
+      auto pos = demangle_from.find(".");
+      if (pos != absl::string_view::npos) {
+        demangle_from.remove_suffix(demangle_from.length() - pos);
+        std::string shortened(demangle_from);
+        if (absl::debugging_internal::Demangle(shortened.c_str(), demangled, sizeof(demangled))) {
+          return std::string(demangled);
+        }
+      }
       return std::string(symbol);
     }
   } else if (source == DataSource::kFullSymbols) {
@@ -248,6 +305,20 @@
       free(demangled);
       return ret;
     } else {
+      // TODO(yifeit): Certain symbols have dots (".") in them. Those are not allowed.
+      // Find and remove the last "." and anything after.
+      auto pos = demangle_from.find(".");
+      if (pos != absl::string_view::npos) {
+        demangle_from.remove_suffix(demangle_from.length() - pos);
+        std::string shortened(demangle_from);
+        char* demangled =
+            __cxa_demangle(shortened.c_str(), NULL, NULL, NULL);
+        if (demangled) {
+          std::string ret(demangled);
+          free(demangled);
+          return ret;
+        }
+      }
       return std::string(symbol);
     }
   } else {
@@ -724,28 +795,6 @@
 
 }  // namespace
 
-void RollupOutput::Print(const OutputOptions& options, std::ostream* out) {
-  if (!source_names_.empty()) {
-    switch (options.output_format) {
-      case bloaty::OutputFormat::kPrettyPrint:
-        PrettyPrint(options, out);
-        break;
-      case bloaty::OutputFormat::kCSV:
-        PrintToCSV(out, /*tabs=*/false);
-        break;
-      case bloaty::OutputFormat::kTSV:
-        PrintToCSV(out, /*tabs=*/true);
-        break;
-      default:
-        BLOATY_UNREACHABLE();
-    }
-  }
-
-  if (!disassembly_.empty()) {
-    *out << disassembly_;
-  }
-}
-
 void RollupOutput::PrettyPrintRow(const RollupRow& row, size_t indent,
                                   const OutputOptions& options,
                                   std::ostream* out) const {
@@ -906,6 +955,7 @@
   }
 }
 
+
 // RangeMap ////////////////////////////////////////////////////////////////////
 
 constexpr uint64_t RangeSink::kUnknownSize;
@@ -1424,6 +1474,8 @@
 
   void AddFilename(const std::string& filename, bool base_file);
   void AddDebugFilename(const std::string& filename);
+  void AddLinkMapFilename(const std::string& filename);
+  void SetColdBytesFilter(const std::string& frequencies);
 
   size_t GetSourceCount() const { return sources_.size(); }
 
@@ -1492,6 +1544,8 @@
   std::vector<InputFileInfo> base_files_;
   std::map<std::string, std::string> debug_files_;
 
+  // "foo" -> "some/path/foo.map"
+  std::map<std::string, std::string> link_map_files_;
   // For allocating memory, like to decompress compressed sections.
   std::unique_ptr<google::protobuf::Arena> arena_;
 };
@@ -1502,10 +1556,27 @@
   AddBuiltInSources(data_sources, options);
 }
 
+std::string GetPathStem(const std::string& filename) {
+  std::regex filename_regex(R"([^\\\/]+(?=\.[\w]+$)|[^\\\/]+$)");
+  std::smatch m;
+  if (!std::regex_search(filename, m, filename_regex)) {
+    THROWF("Could not extract stem from $0", filename);
+  }
+  std::string stem = m.str(0);
+  return stem;
+}
+
 std::unique_ptr<ObjectFile> Bloaty::GetObjectFile(
     const std::string& filename) const {
   std::unique_ptr<InputFile> file(file_factory_.OpenFile(filename));
-  auto object_file = TryOpenELFFile(file);
+
+  std::string stem = GetPathStem(filename);
+  std::optional<std::string> link_map_file;
+  if (link_map_files_.find(stem) != link_map_files_.end()) {
+    link_map_file = link_map_files_.at(stem);
+  }
+
+  auto object_file = TryOpenELFFile(file, link_map_file);
 
   if (!object_file.get()) {
     object_file = TryOpenMachOFile(file);
@@ -1547,6 +1618,11 @@
   debug_files_[build_id] = filename;
 }
 
+void Bloaty::AddLinkMapFilename(const std::string& filename) {
+  std::string stem = GetPathStem(filename);
+  link_map_files_[stem] = filename;
+}
+
 void Bloaty::DefineCustomDataSource(const CustomDataSource& source) {
   if (source.base_data_source() == "symbols") {
     THROW(
@@ -1916,6 +1992,10 @@
   -c FILE            Load configuration from <file>.
   -d SOURCE,SOURCE   Comma-separated list of sources to scan.
   --debug-file=FILE  Use this file for debug symbols and/or symbol table.
+  --link-map-file=FILE
+                     Use this file for identifying a link map associated with
+                     a binary. The link map and the binary must share the same
+                     base name (e.g. `foo.map` and `foo`)
   -C MODE            How to demangle symbols.  Possible values are:
   --demangle=MODE      --demangle=none   no demangling, print raw symbols
                        --demangle=short  demangle, but omit arg/return types
@@ -2071,6 +2151,8 @@
       output_options->output_format = OutputFormat::kCSV;
     } else if (args.TryParseFlag("--tsv")) {
       output_options->output_format = OutputFormat::kTSV;
+    } else if (args.TryParseFlag("--pb")) {
+      output_options->output_format = OutputFormat::kProtobuf;
     } else if (args.TryParseOption("-c", &option)) {
       std::ifstream input_file(std::string(option), std::ios::in);
       if (!input_file.is_open()) {
@@ -2098,6 +2180,12 @@
       }
     } else if (args.TryParseOption("--debug-file", &option)) {
       options->add_debug_filename(std::string(option));
+    } else if (args.TryParseOption("--link-map-file", &option)) {
+      options->add_link_map_filename(std::string(option));
+    } else if (args.TryParseOption("--cold-bytes-filter", &option)) {
+      options->set_cold_bytes_filter(std::string(option));
+    } else if (args.TryParseUint64Option("--access-pattern-frame-size", &uint64_option)) {
+      options->set_access_pattern_frame_size(uint64_option);
     } else if (args.TryParseUint64Option("--debug-fileoff", &uint64_option)) {
       if (options->has_debug_fileoff()) {
         THROW("currently we only support a single debug fileoff");
@@ -2195,6 +2283,20 @@
     }
   }
 
+  if (output_options->output_format == OutputFormat::kProtobuf) {
+    if ((options->data_source_size() != 2 ||
+         options->data_source()[0] != "compileunits" ||
+         options->data_source()[1] != "symbols") &&
+        (options->data_source_size() != 3 ||
+         options->data_source()[0] != "accesspattern" ||
+         options->data_source()[1] != "compileunits" ||
+         options->data_source()[2] != "symbols"))
+    {
+      THROW("Protobuf output only supports '-d compileunits,symbols' "
+      "or '-d accesspattern,compileunits,symbols' for now");
+    }
+  }
+
   return true;
 }
 
@@ -2232,6 +2334,10 @@
     bloaty.AddDebugFilename(debug_filename);
   }
 
+  for (auto& link_map_filename : options.link_map_filename()) {
+    bloaty.AddLinkMapFilename(link_map_filename);
+  }
+
   for (const auto& custom_data_source : options.custom_data_source()) {
     bloaty.DefineCustomDataSource(custom_data_source);
   }
diff --git a/src/bloaty.h b/src/bloaty.h
index e5a1128..ecedc73 100644
--- a/src/bloaty.h
+++ b/src/bloaty.h
@@ -38,6 +38,7 @@
 #include "bloaty.pb.h"
 #include "range_map.h"
 #include "re.h"
+#include "util.h"
 
 namespace bloaty {
 
@@ -50,6 +51,7 @@
 
 enum class DataSource {
   kArchiveMembers,
+  kAccessPattern,
   kCompileUnits,
   kInlines,
   kInputFiles,
@@ -259,6 +261,9 @@
       : file_data_(std::move(file_data)), debug_file_(this) {}
   virtual ~ObjectFile() {}
 
+  // Searches for a binary string representing the identity of this object file.
+  // It is typically a hash of the unstripped object during the build.
+  // If not found, returns an empty string.
   virtual std::string GetBuildId() const = 0;
 
   // Process this file, pushing data to |sinks| as appropriate for each data
@@ -270,6 +275,10 @@
                                   DataSource symbol_source,
                                   DisassemblyInfo* info) const = 0;
 
+  virtual std::optional<std::unordered_map<std::string, std::string>> TakeSymbolToCrateMap() {
+    return std::nullopt;
+  }
+
   const InputFile& file_data() const { return *file_data_; }
 
   // Sets the debug file for |this|.  |file| must outlive this instance.
@@ -285,7 +294,7 @@
   const ObjectFile* debug_file_;
 };
 
-std::unique_ptr<ObjectFile> TryOpenELFFile(std::unique_ptr<InputFile>& file);
+std::unique_ptr<ObjectFile> TryOpenELFFile(std::unique_ptr<InputFile>& file, std::optional<std::string> link_map_file);
 std::unique_ptr<ObjectFile> TryOpenMachOFile(std::unique_ptr<InputFile>& file);
 std::unique_ptr<ObjectFile> TryOpenWebAssemblyFile(std::unique_ptr<InputFile>& file);
 std::unique_ptr<ObjectFile> TryOpenPEFile(std::unique_ptr<InputFile>& file);
@@ -387,6 +396,22 @@
 // controls what demangling mode we are using.
 std::string ItaniumDemangle(absl::string_view symbol, DataSource source);
 
+// Encode |symbol| and |crate| together into a single string.
+std::string EncodeSymbolWithCrateId(absl::string_view symbol, absl::string_view crate);
+
+struct DecodeCrateIdResult {
+  std::string symbol;
+  std::string crate;
+};
+
+inline bool operator==(const DecodeCrateIdResult& lhs, const DecodeCrateIdResult& rhs) {
+  return lhs.symbol == rhs.symbol && lhs.crate == rhs.crate;
+}
+
+// Parse out |symbol| and |crate| from a string encoded with |EncodeSymbolWithCrateId|.
+// If the string was not encoded, |crate| will be the empty string.
+DecodeCrateIdResult DecodeSymbolWithCrateId(absl::string_view symbol);
+
 
 // DualMap /////////////////////////////////////////////////////////////////////
 
@@ -442,6 +467,7 @@
   kPrettyPrint,
   kCSV,
   kTSV,
+  kProtobuf,
 };
 
 enum class ShowDomain {
@@ -467,7 +493,31 @@
   }
 
   const std::vector<std::string>& source_names() const { return source_names_; }
-  void Print(const OutputOptions& options, std::ostream* out);
+
+  void Print(const OutputOptions& options, std::ostream* out) {
+    if (!source_names_.empty()) {
+      switch (options.output_format) {
+        case bloaty::OutputFormat::kPrettyPrint:
+          PrettyPrint(options, out);
+          break;
+        case bloaty::OutputFormat::kCSV:
+          PrintToCSV(out, /*tabs=*/false);
+          break;
+        case bloaty::OutputFormat::kTSV:
+          PrintToCSV(out, /*tabs=*/true);
+          break;
+        case bloaty::OutputFormat::kProtobuf:
+          PrintToProtobuf(out);
+          break;
+        default:
+          BLOATY_UNREACHABLE();
+      }
+    }
+    if (!disassembly_.empty()) {
+      *out << disassembly_;
+    }
+  }
+
   void SetDisassembly(absl::string_view disassembly) {
     disassembly_ = std::string(disassembly);
   }
@@ -491,6 +541,7 @@
   static bool IsSame(const std::string& a, const std::string& b);
   void PrettyPrint(const OutputOptions& options, std::ostream* out) const;
   void PrintToCSV(std::ostream* out, bool tabs) const;
+  void PrintToProtobuf(std::ostream* out) const;
   void PrettyPrintRow(const RollupRow& row, size_t indent,
                       const OutputOptions& options, std::ostream* out) const;
   void PrettyPrintTree(const RollupRow& row, size_t indent,
@@ -503,6 +554,9 @@
                       std::ostream* out, bool tabs) const;
 };
 
+// Shim for `std::filesystem::path(filename).stem()`.
+std::string GetPathStem(const std::string& filename);
+
 bool ParseOptions(bool skip_unknown, int* argc, char** argv[], Options* options,
                   OutputOptions* output_options, std::string* error);
 bool BloatyMain(const Options& options, const InputFileFactory& file_factory,
diff --git a/src/bloaty.proto b/src/bloaty.proto
index 59db641..ec9fcc3 100644
--- a/src/bloaty.proto
+++ b/src/bloaty.proto
@@ -29,6 +29,27 @@
   // debug_filename will *not* have their file size counted.
   repeated string debug_filename = 10;
 
+  // Link map files to assist symbol and compile unit parsing.
+  // We will match these to files with the same base name / stem.
+  // E.g. "foo.map" will be used to analyze the executable "foo".
+  repeated string link_map_filename = 30;
+
+  // If set, reads how frequently a range of bytes is accessed in CSV format.
+  // The filter is in a sparse pattern format: assuming the frame size (see
+  // access_pattern_frame_size) is 8 KiB, given a 24 KiB file and a `0:1,2:3`
+  // pattern, it means that the first 8 KiB frame is accessed once, and the
+  // third 8 KiB frame is accessed 3 times. The second 8 KiB frame is left
+  // out, meaning it was never accessed. The frame indices in the pattern is not
+  // guaranteed to be sorted. This option works together with the
+  // `accesspattern` data source. When the `accesspattern` data source is the
+  // first data source, the protobuf output format will only output items whose
+  // region overlaps with a never-accessed frame.
+  optional string cold_bytes_filter = 31;
+
+  // If set, customizes how many bytes is in a frame as used by the pattern
+  // in cold_bytes_filter.
+  optional uint64 access_pattern_frame_size = 32 [default = 8192];
+
   // The data sources to scan in each file.  At least one data source must be
   // specified.  If more than one source is specified, the output is
   // hierarchical.
diff --git a/src/demangle.cc b/src/demangle.cc
new file mode 100644
index 0000000..284782e
--- /dev/null
+++ b/src/demangle.cc
@@ -0,0 +1,1901 @@
+// Copyright 2018 The Abseil Authors.
+//
+// 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
+//
+//      https://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.
+
+// For reference check out:
+// https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling
+//
+// Note that we only have partial C++11 support yet.
+
+#include "demangle.h"
+
+#include <cstdint>
+#include <cstdio>
+#include <limits>
+
+namespace absl {
+namespace debugging_internal {
+
+typedef struct {
+  const char *abbrev;
+  const char *real_name;
+  // Number of arguments in <expression> context, or 0 if disallowed.
+  int arity;
+} AbbrevPair;
+
+// List of operators from Itanium C++ ABI.
+static const AbbrevPair kOperatorList[] = {
+    // New has special syntax (not currently supported).
+    {"nw", "new", 0},
+    {"na", "new[]", 0},
+
+    // Works except that the 'gs' prefix is not supported.
+    {"dl", "delete", 1},
+    {"da", "delete[]", 1},
+
+    {"ps", "+", 1},  // "positive"
+    {"ng", "-", 1},  // "negative"
+    {"ad", "&", 1},  // "address-of"
+    {"de", "*", 1},  // "dereference"
+    {"co", "~", 1},
+
+    {"pl", "+", 2},
+    {"mi", "-", 2},
+    {"ml", "*", 2},
+    {"dv", "/", 2},
+    {"rm", "%", 2},
+    {"an", "&", 2},
+    {"or", "|", 2},
+    {"eo", "^", 2},
+    {"aS", "=", 2},
+    {"pL", "+=", 2},
+    {"mI", "-=", 2},
+    {"mL", "*=", 2},
+    {"dV", "/=", 2},
+    {"rM", "%=", 2},
+    {"aN", "&=", 2},
+    {"oR", "|=", 2},
+    {"eO", "^=", 2},
+    {"ls", "<<", 2},
+    {"rs", ">>", 2},
+    {"lS", "<<=", 2},
+    {"rS", ">>=", 2},
+    {"eq", "==", 2},
+    {"ne", "!=", 2},
+    {"lt", "<", 2},
+    {"gt", ">", 2},
+    {"le", "<=", 2},
+    {"ge", ">=", 2},
+    {"nt", "!", 1},
+    {"aa", "&&", 2},
+    {"oo", "||", 2},
+    {"pp", "++", 1},
+    {"mm", "--", 1},
+    {"cm", ",", 2},
+    {"pm", "->*", 2},
+    {"pt", "->", 0},  // Special syntax
+    {"cl", "()", 0},  // Special syntax
+    {"ix", "[]", 2},
+    {"qu", "?", 3},
+    {"st", "sizeof", 0},  // Special syntax
+    {"sz", "sizeof", 1},  // Not a real operator name, but used in expressions.
+    {nullptr, nullptr, 0},
+};
+
+// List of builtin types from Itanium C++ ABI.
+//
+// Invariant: only one- or two-character type abbreviations here.
+static const AbbrevPair kBuiltinTypeList[] = {
+    {"v", "void", 0},
+    {"w", "wchar_t", 0},
+    {"b", "bool", 0},
+    {"c", "char", 0},
+    {"a", "signed char", 0},
+    {"h", "unsigned char", 0},
+    {"s", "short", 0},
+    {"t", "unsigned short", 0},
+    {"i", "int", 0},
+    {"j", "unsigned int", 0},
+    {"l", "long", 0},
+    {"m", "unsigned long", 0},
+    {"x", "long long", 0},
+    {"y", "unsigned long long", 0},
+    {"n", "__int128", 0},
+    {"o", "unsigned __int128", 0},
+    {"f", "float", 0},
+    {"d", "double", 0},
+    {"e", "long double", 0},
+    {"g", "__float128", 0},
+    {"z", "ellipsis", 0},
+
+    {"De", "decimal128", 0},      // IEEE 754r decimal floating point (128 bits)
+    {"Dd", "decimal64", 0},       // IEEE 754r decimal floating point (64 bits)
+    {"Dc", "decltype(auto)", 0},
+    {"Da", "auto", 0},
+    {"Dn", "std::nullptr_t", 0},  // i.e., decltype(nullptr)
+    {"Df", "decimal32", 0},       // IEEE 754r decimal floating point (32 bits)
+    {"Di", "char32_t", 0},
+    {"Ds", "char16_t", 0},
+    {"Dh", "float16", 0},         // IEEE 754r half-precision float (16 bits)
+    {nullptr, nullptr, 0},
+};
+
+// List of substitutions Itanium C++ ABI.
+static const AbbrevPair kSubstitutionList[] = {
+    {"St", "", 0},
+    {"Sa", "allocator", 0},
+    {"Sb", "basic_string", 0},
+    // std::basic_string<char, std::char_traits<char>,std::allocator<char> >
+    {"Ss", "string", 0},
+    // std::basic_istream<char, std::char_traits<char> >
+    {"Si", "istream", 0},
+    // std::basic_ostream<char, std::char_traits<char> >
+    {"So", "ostream", 0},
+    // std::basic_iostream<char, std::char_traits<char> >
+    {"Sd", "iostream", 0},
+    {nullptr, nullptr, 0},
+};
+
+// State needed for demangling.  This struct is copied in almost every stack
+// frame, so every byte counts.
+typedef struct {
+  int mangled_idx;                   // Cursor of mangled name.
+  int out_cur_idx;                   // Cursor of output string.
+  int prev_name_idx;                 // For constructors/destructors.
+  signed int prev_name_length : 16;  // For constructors/destructors.
+  signed int nest_level : 15;        // For nested names.
+  unsigned int append : 1;           // Append flag.
+  // Note: for some reason MSVC can't pack "bool append : 1" into the same int
+  // with the above two fields, so we use an int instead.  Amusingly it can pack
+  // "signed bool" as expected, but relying on that to continue to be a legal
+  // type seems ill-advised (as it's illegal in at least clang).
+} ParseState;
+
+static_assert(sizeof(ParseState) == 4 * sizeof(int),
+              "unexpected size of ParseState");
+
+// One-off state for demangling that's not subject to backtracking -- either
+// constant data, data that's intentionally immune to backtracking (steps), or
+// data that would never be changed by backtracking anyway (recursion_depth).
+//
+// Only one copy of this exists for each call to Demangle, so the size of this
+// struct is nearly inconsequential.
+typedef struct {
+  const char *mangled_begin;  // Beginning of input string.
+  char *out;                  // Beginning of output string.
+  int out_end_idx;            // One past last allowed output character.
+  int recursion_depth;        // For stack exhaustion prevention.
+  int steps;               // Cap how much work we'll do, regardless of depth.
+  ParseState parse_state;  // Backtrackable state copied for most frames.
+} State;
+
+namespace {
+// Prevent deep recursion / stack exhaustion.
+// Also prevent unbounded handling of complex inputs.
+// absl:google3-begin(Buganizer links)
+// See also http://b/34269257 and http://b/37793125
+// absl:google3-end
+class ComplexityGuard {
+ public:
+  explicit ComplexityGuard(State *state) : state_(state) {
+    ++state->recursion_depth;
+    ++state->steps;
+  }
+  ~ComplexityGuard() { --state_->recursion_depth; }
+
+  // 256 levels of recursion seems like a reasonable upper limit on depth.
+  // 128 is not enough to demagle synthetic tests from demangle_unittest.txt:
+  // "_ZaaZZZZ..." and "_ZaaZcvZcvZ..."
+  static constexpr int kRecursionDepthLimit = 256;
+
+  // We're trying to pick a charitable upper-limit on how many parse steps are
+  // necessary to handle something that a human could actually make use of.
+  // This is mostly in place as a bound on how much work we'll do if we are
+  // asked to demangle an mangled name from an untrusted source, so it should be
+  // much larger than the largest expected symbol, but much smaller than the
+  // amount of work we can do in, e.g., a second.
+  //
+  // Some real-world symbols from an arbitrary binary started failing between
+  // 2^12 and 2^13, so we multiply the latter by an extra factor of 16 to set
+  // the limit.
+  //
+  // Spending one second on 2^17 parse steps would require each step to take
+  // 7.6us, or ~30000 clock cycles, so it's safe to say this can be done in
+  // under a second.
+  static constexpr int kParseStepsLimit = 1 << 17;
+
+  bool IsTooComplex() const {
+    return state_->recursion_depth > kRecursionDepthLimit ||
+           state_->steps > kParseStepsLimit;
+  }
+
+ private:
+  State *state_;
+};
+}  // namespace
+
+// We don't use strlen() in libc since it's not guaranteed to be async
+// signal safe.
+static size_t StrLen(const char *str) {
+  size_t len = 0;
+  while (*str != '\0') {
+    ++str;
+    ++len;
+  }
+  return len;
+}
+
+// Returns true if "str" has at least "n" characters remaining.
+static bool AtLeastNumCharsRemaining(const char *str, int n) {
+  for (int i = 0; i < n; ++i) {
+    if (str[i] == '\0') {
+      return false;
+    }
+  }
+  return true;
+}
+
+// Returns true if "str" has "prefix" as a prefix.
+static bool StrPrefix(const char *str, const char *prefix) {
+  size_t i = 0;
+  while (str[i] != '\0' && prefix[i] != '\0' && str[i] == prefix[i]) {
+    ++i;
+  }
+  return prefix[i] == '\0';  // Consumed everything in "prefix".
+}
+
+static void InitState(State *state, const char *mangled, char *out,
+                      int out_size) {
+  state->mangled_begin = mangled;
+  state->out = out;
+  state->out_end_idx = out_size;
+  state->recursion_depth = 0;
+  state->steps = 0;
+
+  state->parse_state.mangled_idx = 0;
+  state->parse_state.out_cur_idx = 0;
+  state->parse_state.prev_name_idx = 0;
+  state->parse_state.prev_name_length = -1;
+  state->parse_state.nest_level = -1;
+  state->parse_state.append = true;
+}
+
+static inline const char *RemainingInput(State *state) {
+  return &state->mangled_begin[state->parse_state.mangled_idx];
+}
+
+// Returns true and advances "mangled_idx" if we find "one_char_token"
+// at "mangled_idx" position.  It is assumed that "one_char_token" does
+// not contain '\0'.
+static bool ParseOneCharToken(State *state, const char one_char_token) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (RemainingInput(state)[0] == one_char_token) {
+    ++state->parse_state.mangled_idx;
+    return true;
+  }
+  return false;
+}
+
+// Returns true and advances "mangled_cur" if we find "two_char_token"
+// at "mangled_cur" position.  It is assumed that "two_char_token" does
+// not contain '\0'.
+static bool ParseTwoCharToken(State *state, const char *two_char_token) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (RemainingInput(state)[0] == two_char_token[0] &&
+      RemainingInput(state)[1] == two_char_token[1]) {
+    state->parse_state.mangled_idx += 2;
+    return true;
+  }
+  return false;
+}
+
+// Returns true and advances "mangled_cur" if we find any character in
+// "char_class" at "mangled_cur" position.
+static bool ParseCharClass(State *state, const char *char_class) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (RemainingInput(state)[0] == '\0') {
+    return false;
+  }
+  const char *p = char_class;
+  for (; *p != '\0'; ++p) {
+    if (RemainingInput(state)[0] == *p) {
+      ++state->parse_state.mangled_idx;
+      return true;
+    }
+  }
+  return false;
+}
+
+static bool ParseDigit(State *state, int *digit) {
+  char c = RemainingInput(state)[0];
+  if (ParseCharClass(state, "0123456789")) {
+    if (digit != nullptr) {
+      *digit = c - '0';
+    }
+    return true;
+  }
+  return false;
+}
+
+// This function is used for handling an optional non-terminal.
+static bool Optional(bool /*status*/) { return true; }
+
+// This function is used for handling <non-terminal>+ syntax.
+typedef bool (*ParseFunc)(State *);
+static bool OneOrMore(ParseFunc parse_func, State *state) {
+  if (parse_func(state)) {
+    while (parse_func(state)) {
+    }
+    return true;
+  }
+  return false;
+}
+
+// This function is used for handling <non-terminal>* syntax. The function
+// always returns true and must be followed by a termination token or a
+// terminating sequence not handled by parse_func (e.g.
+// ParseOneCharToken(state, 'E')).
+static bool ZeroOrMore(ParseFunc parse_func, State *state) {
+  while (parse_func(state)) {
+  }
+  return true;
+}
+
+// Append "str" at "out_cur_idx".  If there is an overflow, out_cur_idx is
+// set to out_end_idx+1.  The output string is ensured to
+// always terminate with '\0' as long as there is no overflow.
+static void Append(State *state, const char *const str, const int length) {
+  for (int i = 0; i < length; ++i) {
+    if (state->parse_state.out_cur_idx + 1 <
+        state->out_end_idx) {  // +1 for '\0'
+      state->out[state->parse_state.out_cur_idx++] = str[i];
+    } else {
+      // signal overflow
+      state->parse_state.out_cur_idx = state->out_end_idx + 1;
+      break;
+    }
+  }
+  if (state->parse_state.out_cur_idx < state->out_end_idx) {
+    state->out[state->parse_state.out_cur_idx] =
+        '\0';  // Terminate it with '\0'
+  }
+}
+
+// We don't use equivalents in libc to avoid locale issues.
+static bool IsLower(char c) { return c >= 'a' && c <= 'z'; }
+
+static bool IsAlpha(char c) {
+  return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+}
+
+static bool IsDigit(char c) { return c >= '0' && c <= '9'; }
+
+// Returns true if "str" is a function clone suffix.  These suffixes are used
+// by GCC 4.5.x and later versions (and our locally-modified version of GCC
+// 4.4.x) to indicate functions which have been cloned during optimization.
+// We treat any sequence (.<alpha>+.<digit>+)+ as a function clone suffix.
+static bool IsFunctionCloneSuffix(const char *str) {
+  size_t i = 0;
+  while (str[i] != '\0') {
+    // Consume a single .<alpha>+.<digit>+ sequence.
+    if (str[i] != '.' || !IsAlpha(str[i + 1])) {
+      return false;
+    }
+    i += 2;
+    while (IsAlpha(str[i])) {
+      ++i;
+    }
+    if (str[i] != '.' || !IsDigit(str[i + 1])) {
+      return false;
+    }
+    i += 2;
+    while (IsDigit(str[i])) {
+      ++i;
+    }
+  }
+  return true;  // Consumed everything in "str".
+}
+
+static bool EndsWith(State *state, const char chr) {
+  return state->parse_state.out_cur_idx > 0 &&
+         chr == state->out[state->parse_state.out_cur_idx - 1];
+}
+
+// Append "str" with some tweaks, iff "append" state is true.
+static void MaybeAppendWithLength(State *state, const char *const str,
+                                  const int length) {
+  if (state->parse_state.append && length > 0) {
+    // Append a space if the output buffer ends with '<' and "str"
+    // starts with '<' to avoid <<<.
+    if (str[0] == '<' && EndsWith(state, '<')) {
+      Append(state, " ", 1);
+    }
+    // Remember the last identifier name for ctors/dtors.
+    if (IsAlpha(str[0]) || str[0] == '_') {
+      state->parse_state.prev_name_idx = state->parse_state.out_cur_idx;
+      state->parse_state.prev_name_length = length;
+    }
+    Append(state, str, length);
+  }
+}
+
+// Appends a positive decimal number to the output if appending is enabled.
+static bool MaybeAppendDecimal(State *state, unsigned int val) {
+  // Max {32-64}-bit unsigned int is 20 digits.
+  constexpr size_t kMaxLength = 20;
+  char buf[kMaxLength];
+
+  // We can't use itoa or sprintf as neither is specified to be
+  // async-signal-safe.
+  if (state->parse_state.append) {
+    // We can't have a one-before-the-beginning pointer, so instead start with
+    // one-past-the-end and manipulate one character before the pointer.
+    char *p = &buf[kMaxLength];
+    do {  // val=0 is the only input that should write a leading zero digit.
+      *--p = (val % 10) + '0';
+      val /= 10;
+    } while (p > buf && val != 0);
+
+    // 'p' landed on the last character we set.  How convenient.
+    Append(state, p, kMaxLength - (p - buf));
+  }
+
+  return true;
+}
+
+// A convenient wrapper around MaybeAppendWithLength().
+// Returns true so that it can be placed in "if" conditions.
+static bool MaybeAppend(State *state, const char *const str) {
+  if (state->parse_state.append) {
+    int length = StrLen(str);
+    MaybeAppendWithLength(state, str, length);
+  }
+  return true;
+}
+
+// This function is used for handling nested names.
+static bool EnterNestedName(State *state) {
+  state->parse_state.nest_level = 0;
+  return true;
+}
+
+// This function is used for handling nested names.
+static bool LeaveNestedName(State *state, int16_t prev_value) {
+  state->parse_state.nest_level = prev_value;
+  return true;
+}
+
+// Disable the append mode not to print function parameters, etc.
+static bool DisableAppend(State *state) {
+  state->parse_state.append = false;
+  return true;
+}
+
+// Restore the append mode to the previous state.
+static bool RestoreAppend(State *state, bool prev_value) {
+  state->parse_state.append = prev_value;
+  return true;
+}
+
+// Increase the nest level for nested names.
+static void MaybeIncreaseNestLevel(State *state) {
+  if (state->parse_state.nest_level > -1) {
+    ++state->parse_state.nest_level;
+  }
+}
+
+// Appends :: for nested names if necessary.
+static void MaybeAppendSeparator(State *state) {
+  if (state->parse_state.nest_level >= 1) {
+    MaybeAppend(state, "::");
+  }
+}
+
+// Cancel the last separator if necessary.
+static void MaybeCancelLastSeparator(State *state) {
+  if (state->parse_state.nest_level >= 1 && state->parse_state.append &&
+      state->parse_state.out_cur_idx >= 2) {
+    state->parse_state.out_cur_idx -= 2;
+    state->out[state->parse_state.out_cur_idx] = '\0';
+  }
+}
+
+// Returns true if the identifier of the given length pointed to by
+// "mangled_cur" is anonymous namespace.
+static bool IdentifierIsAnonymousNamespace(State *state, int length) {
+  // Returns true if "anon_prefix" is a proper prefix of "mangled_cur".
+  static const char anon_prefix[] = "_GLOBAL__N_";
+  return (length > static_cast<int>(sizeof(anon_prefix) - 1) &&
+          StrPrefix(RemainingInput(state), anon_prefix));
+}
+
+// Forward declarations of our parsing functions.
+static bool ParseMangledName(State *state);
+static bool ParseEncoding(State *state);
+static bool ParseName(State *state);
+static bool ParseUnscopedName(State *state);
+static bool ParseNestedName(State *state);
+static bool ParsePrefix(State *state);
+static bool ParseUnqualifiedName(State *state);
+static bool ParseSourceName(State *state);
+static bool ParseLocalSourceName(State *state);
+static bool ParseUnnamedTypeName(State *state);
+static bool ParseNumber(State *state, int *number_out);
+static bool ParseFloatNumber(State *state);
+static bool ParseSeqId(State *state);
+static bool ParseIdentifier(State *state, int length);
+static bool ParseOperatorName(State *state, int *arity);
+static bool ParseSpecialName(State *state);
+static bool ParseCallOffset(State *state);
+static bool ParseNVOffset(State *state);
+static bool ParseVOffset(State *state);
+static bool ParseCtorDtorName(State *state);
+static bool ParseDecltype(State *state);
+static bool ParseType(State *state);
+static bool ParseCVQualifiers(State *state);
+static bool ParseBuiltinType(State *state);
+static bool ParseFunctionType(State *state);
+static bool ParseBareFunctionType(State *state);
+static bool ParseClassEnumType(State *state);
+static bool ParseArrayType(State *state);
+static bool ParsePointerToMemberType(State *state);
+static bool ParseTemplateParam(State *state);
+static bool ParseTemplateTemplateParam(State *state);
+static bool ParseTemplateArgs(State *state);
+static bool ParseTemplateArg(State *state);
+static bool ParseBaseUnresolvedName(State *state);
+static bool ParseUnresolvedName(State *state);
+static bool ParseExpression(State *state);
+static bool ParseExprPrimary(State *state);
+static bool ParseExprCastValue(State *state);
+static bool ParseLocalName(State *state);
+static bool ParseLocalNameSuffix(State *state);
+static bool ParseDiscriminator(State *state);
+static bool ParseSubstitution(State *state, bool accept_std);
+
+// Implementation note: the following code is a straightforward
+// translation of the Itanium C++ ABI defined in BNF with a couple of
+// exceptions.
+//
+// - Support GNU extensions not defined in the Itanium C++ ABI
+// - <prefix> and <template-prefix> are combined to avoid infinite loop
+// - Reorder patterns to shorten the code
+// - Reorder patterns to give greedier functions precedence
+//   We'll mark "Less greedy than" for these cases in the code
+//
+// Each parsing function changes the parse state and returns true on
+// success, or returns false and doesn't change the parse state (note:
+// the parse-steps counter increases regardless of success or failure).
+// To ensure that the parse state isn't changed in the latter case, we
+// save the original state before we call multiple parsing functions
+// consecutively with &&, and restore it if unsuccessful.  See
+// ParseEncoding() as an example of this convention.  We follow the
+// convention throughout the code.
+//
+// Originally we tried to do demangling without following the full ABI
+// syntax but it turned out we needed to follow the full syntax to
+// parse complicated cases like nested template arguments.  Note that
+// implementing a full-fledged demangler isn't trivial (libiberty's
+// cp-demangle.c has +4300 lines).
+//
+// Note that (foo) in <(foo) ...> is a modifier to be ignored.
+//
+// Reference:
+// - Itanium C++ ABI
+//   <https://mentorembedded.github.io/cxx-abi/abi.html#mangling>
+
+// <mangled-name> ::= _Z <encoding>
+static bool ParseMangledName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  return ParseTwoCharToken(state, "_Z") && ParseEncoding(state);
+}
+
+// <encoding> ::= <(function) name> <bare-function-type>
+//            ::= <(data) name>
+//            ::= <special-name>
+static bool ParseEncoding(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  // Implementing the first two productions together as <name>
+  // [<bare-function-type>] avoids exponential blowup of backtracking.
+  //
+  // Since Optional(...) can't fail, there's no need to copy the state for
+  // backtracking.
+  if (ParseName(state) && Optional(ParseBareFunctionType(state))) {
+    return true;
+  }
+
+  if (ParseSpecialName(state)) {
+    return true;
+  }
+  return false;
+}
+
+// <name> ::= <nested-name>
+//        ::= <unscoped-template-name> <template-args>
+//        ::= <unscoped-name>
+//        ::= <local-name>
+static bool ParseName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (ParseNestedName(state) || ParseLocalName(state)) {
+    return true;
+  }
+
+  // We reorganize the productions to avoid re-parsing unscoped names.
+  // - Inline <unscoped-template-name> productions:
+  //   <name> ::= <substitution> <template-args>
+  //          ::= <unscoped-name> <template-args>
+  //          ::= <unscoped-name>
+  // - Merge the two productions that start with unscoped-name:
+  //   <name> ::= <unscoped-name> [<template-args>]
+
+  ParseState copy = state->parse_state;
+  // "std<...>" isn't a valid name.
+  if (ParseSubstitution(state, /*accept_std=*/false) &&
+      ParseTemplateArgs(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Note there's no need to restore state after this since only the first
+  // subparser can fail.
+  return ParseUnscopedName(state) && Optional(ParseTemplateArgs(state));
+}
+
+// <unscoped-name> ::= <unqualified-name>
+//                 ::= St <unqualified-name>
+static bool ParseUnscopedName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (ParseUnqualifiedName(state)) {
+    return true;
+  }
+
+  ParseState copy = state->parse_state;
+  if (ParseTwoCharToken(state, "St") && MaybeAppend(state, "std::") &&
+      ParseUnqualifiedName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <ref-qualifer> ::= R // lvalue method reference qualifier
+//                ::= O // rvalue method reference qualifier
+static inline bool ParseRefQualifier(State *state) {
+  return ParseCharClass(state, "OR");
+}
+
+// <nested-name> ::= N [<CV-qualifiers>] [<ref-qualifier>] <prefix>
+//                   <unqualified-name> E
+//               ::= N [<CV-qualifiers>] [<ref-qualifier>] <template-prefix>
+//                   <template-args> E
+static bool ParseNestedName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'N') && EnterNestedName(state) &&
+      Optional(ParseCVQualifiers(state)) &&
+      Optional(ParseRefQualifier(state)) && ParsePrefix(state) &&
+      LeaveNestedName(state, copy.nest_level) &&
+      ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// This part is tricky.  If we literally translate them to code, we'll
+// end up infinite loop.  Hence we merge them to avoid the case.
+//
+// <prefix> ::= <prefix> <unqualified-name>
+//          ::= <template-prefix> <template-args>
+//          ::= <template-param>
+//          ::= <substitution>
+//          ::= # empty
+// <template-prefix> ::= <prefix> <(template) unqualified-name>
+//                   ::= <template-param>
+//                   ::= <substitution>
+static bool ParsePrefix(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  bool has_something = false;
+  while (true) {
+    MaybeAppendSeparator(state);
+    if (ParseTemplateParam(state) ||
+        ParseSubstitution(state, /*accept_std=*/true) ||
+        ParseUnscopedName(state) ||
+        // absl:google3-begin(Internal link)
+        // Lambda initializer scope:
+        // http://google3/third_party/binutils/binutils/libiberty/cp-demangle.c?l=1566&rcl=181793049
+        // absl:google3-end
+        (ParseOneCharToken(state, 'M') && ParseUnnamedTypeName(state))) {
+      has_something = true;
+      MaybeIncreaseNestLevel(state);
+      continue;
+    }
+    MaybeCancelLastSeparator(state);
+    if (has_something && ParseTemplateArgs(state)) {
+      return ParsePrefix(state);
+    } else {
+      break;
+    }
+  }
+  return true;
+}
+
+// <unqualified-name> ::= <operator-name>
+//                    ::= <ctor-dtor-name>
+//                    ::= <source-name>
+//                    ::= <local-source-name> // GCC extension; see below.
+//                    ::= <unnamed-type-name>
+static bool ParseUnqualifiedName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  return (ParseOperatorName(state, nullptr) || ParseCtorDtorName(state) ||
+          ParseSourceName(state) || ParseLocalSourceName(state) ||
+          ParseUnnamedTypeName(state));
+}
+
+// <source-name> ::= <positive length number> <identifier>
+static bool ParseSourceName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  int length = -1;
+  if (ParseNumber(state, &length) && ParseIdentifier(state, length)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <local-source-name> ::= L <source-name> [<discriminator>]
+//
+// References:
+//   https://gcc.gnu.org/bugzilla/show_bug.cgi?id=31775
+//   https://gcc.gnu.org/viewcvs?view=rev&revision=124467
+static bool ParseLocalSourceName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'L') && ParseSourceName(state) &&
+      Optional(ParseDiscriminator(state))) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <unnamed-type-name> ::= Ut [<(nonnegative) number>] _
+//                     ::= <closure-type-name>
+// <closure-type-name> ::= Ul <lambda-sig> E [<(nonnegative) number>] _
+// <lambda-sig>        ::= <(parameter) type>+
+static bool ParseUnnamedTypeName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  // Type's 1-based index n is encoded as { "", n == 1; itoa(n-2), otherwise }.
+  // Optionally parse the encoded value into 'which' and add 2 to get the index.
+  int which = -1;
+
+  // Unnamed type local to function or class.
+  if (ParseTwoCharToken(state, "Ut") && Optional(ParseNumber(state, &which)) &&
+      which <= std::numeric_limits<int>::max() - 2 &&  // Don't overflow.
+      ParseOneCharToken(state, '_')) {
+    MaybeAppend(state, "{unnamed type#");
+    MaybeAppendDecimal(state, 2 + which);
+    MaybeAppend(state, "}");
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Closure type.
+  which = -1;
+  if (ParseTwoCharToken(state, "Ul") && DisableAppend(state) &&
+      OneOrMore(ParseType, state) && RestoreAppend(state, copy.append) &&
+      ParseOneCharToken(state, 'E') && Optional(ParseNumber(state, &which)) &&
+      which <= std::numeric_limits<int>::max() - 2 &&  // Don't overflow.
+      ParseOneCharToken(state, '_')) {
+    MaybeAppend(state, "{lambda()#");
+    MaybeAppendDecimal(state, 2 + which);
+    MaybeAppend(state, "}");
+    return true;
+  }
+  state->parse_state = copy;
+
+  return false;
+}
+
+// <number> ::= [n] <non-negative decimal integer>
+// If "number_out" is non-null, then *number_out is set to the value of the
+// parsed number on success.
+static bool ParseNumber(State *state, int *number_out) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  bool negative = false;
+  if (ParseOneCharToken(state, 'n')) {
+    negative = true;
+  }
+  const char *p = RemainingInput(state);
+  uint64_t number = 0;
+  for (; *p != '\0'; ++p) {
+    if (IsDigit(*p)) {
+      number = number * 10 + (*p - '0');
+    } else {
+      break;
+    }
+  }
+  // Apply the sign with uint64 arithmetic so overflows aren't UB.  Gives
+  // "incorrect" results for out-of-range inputs, but negative values only
+  // appear for literals, which aren't printed.
+  if (negative) {
+    number = ~number + 1;
+  }
+  if (p != RemainingInput(state)) {  // Conversion succeeded.
+    state->parse_state.mangled_idx += p - RemainingInput(state);
+    if (number_out != nullptr) {
+      // Note: possibly truncate "number".
+      *number_out = number;
+    }
+    return true;
+  }
+  return false;
+}
+
+// Floating-point literals are encoded using a fixed-length lowercase
+// hexadecimal string.
+static bool ParseFloatNumber(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  const char *p = RemainingInput(state);
+  for (; *p != '\0'; ++p) {
+    if (!IsDigit(*p) && !(*p >= 'a' && *p <= 'f')) {
+      break;
+    }
+  }
+  if (p != RemainingInput(state)) {  // Conversion succeeded.
+    state->parse_state.mangled_idx += p - RemainingInput(state);
+    return true;
+  }
+  return false;
+}
+
+// The <seq-id> is a sequence number in base 36,
+// using digits and upper case letters
+static bool ParseSeqId(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  const char *p = RemainingInput(state);
+  for (; *p != '\0'; ++p) {
+    if (!IsDigit(*p) && !(*p >= 'A' && *p <= 'Z')) {
+      break;
+    }
+  }
+  if (p != RemainingInput(state)) {  // Conversion succeeded.
+    state->parse_state.mangled_idx += p - RemainingInput(state);
+    return true;
+  }
+  return false;
+}
+
+// <identifier> ::= <unqualified source code identifier> (of given length)
+static bool ParseIdentifier(State *state, int length) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (length < 0 || !AtLeastNumCharsRemaining(RemainingInput(state), length)) {
+    return false;
+  }
+  if (IdentifierIsAnonymousNamespace(state, length)) {
+    MaybeAppend(state, "(anonymous namespace)");
+  } else {
+    MaybeAppendWithLength(state, RemainingInput(state), length);
+  }
+  state->parse_state.mangled_idx += length;
+  return true;
+}
+
+// <operator-name> ::= nw, and other two letters cases
+//                 ::= cv <type>  # (cast)
+//                 ::= v  <digit> <source-name> # vendor extended operator
+static bool ParseOperatorName(State *state, int *arity) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (!AtLeastNumCharsRemaining(RemainingInput(state), 2)) {
+    return false;
+  }
+  // First check with "cv" (cast) case.
+  ParseState copy = state->parse_state;
+  if (ParseTwoCharToken(state, "cv") && MaybeAppend(state, "operator ") &&
+      EnterNestedName(state) && ParseType(state) &&
+      LeaveNestedName(state, copy.nest_level)) {
+    if (arity != nullptr) {
+      *arity = 1;
+    }
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Then vendor extended operators.
+  if (ParseOneCharToken(state, 'v') && ParseDigit(state, arity) &&
+      ParseSourceName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Other operator names should start with a lower alphabet followed
+  // by a lower/upper alphabet.
+  if (!(IsLower(RemainingInput(state)[0]) &&
+        IsAlpha(RemainingInput(state)[1]))) {
+    return false;
+  }
+  // We may want to perform a binary search if we really need speed.
+  const AbbrevPair *p;
+  for (p = kOperatorList; p->abbrev != nullptr; ++p) {
+    if (RemainingInput(state)[0] == p->abbrev[0] &&
+        RemainingInput(state)[1] == p->abbrev[1]) {
+      if (arity != nullptr) {
+        *arity = p->arity;
+      }
+      MaybeAppend(state, "operator");
+      if (IsLower(*p->real_name)) {  // new, delete, etc.
+        MaybeAppend(state, " ");
+      }
+      MaybeAppend(state, p->real_name);
+      state->parse_state.mangled_idx += 2;
+      return true;
+    }
+  }
+  return false;
+}
+
+// <special-name> ::= TV <type>
+//                ::= TT <type>
+//                ::= TI <type>
+//                ::= TS <type>
+//                ::= Tc <call-offset> <call-offset> <(base) encoding>
+//                ::= GV <(object) name>
+//                ::= T <call-offset> <(base) encoding>
+// G++ extensions:
+//                ::= TC <type> <(offset) number> _ <(base) type>
+//                ::= TF <type>
+//                ::= TJ <type>
+//                ::= GR <name>
+//                ::= GA <encoding>
+//                ::= Th <call-offset> <(base) encoding>
+//                ::= Tv <call-offset> <(base) encoding>
+//
+// Note: we don't care much about them since they don't appear in
+// stack traces.  The are special data.
+static bool ParseSpecialName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'T') && ParseCharClass(state, "VTIS") &&
+      ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "Tc") && ParseCallOffset(state) &&
+      ParseCallOffset(state) && ParseEncoding(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "GV") && ParseName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'T') && ParseCallOffset(state) &&
+      ParseEncoding(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // G++ extensions
+  if (ParseTwoCharToken(state, "TC") && ParseType(state) &&
+      ParseNumber(state, nullptr) && ParseOneCharToken(state, '_') &&
+      DisableAppend(state) && ParseType(state)) {
+    RestoreAppend(state, copy.append);
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'T') && ParseCharClass(state, "FJ") &&
+      ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "GR") && ParseName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "GA") && ParseEncoding(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'T') && ParseCharClass(state, "hv") &&
+      ParseCallOffset(state) && ParseEncoding(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <call-offset> ::= h <nv-offset> _
+//               ::= v <v-offset> _
+static bool ParseCallOffset(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'h') && ParseNVOffset(state) &&
+      ParseOneCharToken(state, '_')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'v') && ParseVOffset(state) &&
+      ParseOneCharToken(state, '_')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  return false;
+}
+
+// <nv-offset> ::= <(offset) number>
+static bool ParseNVOffset(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  return ParseNumber(state, nullptr);
+}
+
+// <v-offset>  ::= <(offset) number> _ <(virtual offset) number>
+static bool ParseVOffset(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseNumber(state, nullptr) && ParseOneCharToken(state, '_') &&
+      ParseNumber(state, nullptr)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <ctor-dtor-name> ::= C1 | C2 | C3
+//                  ::= D0 | D1 | D2
+// # GCC extensions: "unified" constructor/destructor.  See
+// # https://github.com/gcc-mirror/gcc/blob/7ad17b583c3643bd4557f29b8391ca7ef08391f5/gcc/cp/mangle.c#L1847
+//                  ::= C4 | D4
+static bool ParseCtorDtorName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'C') && ParseCharClass(state, "1234")) {
+    const char *const prev_name = state->out + state->parse_state.prev_name_idx;
+    MaybeAppendWithLength(state, prev_name,
+                          state->parse_state.prev_name_length);
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'D') && ParseCharClass(state, "0124")) {
+    const char *const prev_name = state->out + state->parse_state.prev_name_idx;
+    MaybeAppend(state, "~");
+    MaybeAppendWithLength(state, prev_name,
+                          state->parse_state.prev_name_length);
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <decltype> ::= Dt <expression> E  # decltype of an id-expression or class
+//                                   # member access (C++0x)
+//            ::= DT <expression> E  # decltype of an expression (C++0x)
+static bool ParseDecltype(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'D') && ParseCharClass(state, "tT") &&
+      ParseExpression(state) && ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  return false;
+}
+
+// <type> ::= <CV-qualifiers> <type>
+//        ::= P <type>   # pointer-to
+//        ::= R <type>   # reference-to
+//        ::= O <type>   # rvalue reference-to (C++0x)
+//        ::= C <type>   # complex pair (C 2000)
+//        ::= G <type>   # imaginary (C 2000)
+//        ::= U <source-name> <type>  # vendor extended type qualifier
+//        ::= <builtin-type>
+//        ::= <function-type>
+//        ::= <class-enum-type>  # note: just an alias for <name>
+//        ::= <array-type>
+//        ::= <pointer-to-member-type>
+//        ::= <template-template-param> <template-args>
+//        ::= <template-param>
+//        ::= <decltype>
+//        ::= <substitution>
+//        ::= Dp <type>          # pack expansion of (C++0x)
+//
+static bool ParseType(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+
+  // We should check CV-qualifers, and PRGC things first.
+  //
+  // CV-qualifiers overlap with some operator names, but an operator name is not
+  // valid as a type.  To avoid an ambiguity that can lead to exponential time
+  // complexity, refuse to backtrack the CV-qualifiers.
+  //
+  // _Z4aoeuIrMvvE
+  //  => _Z 4aoeuI        rM  v     v   E
+  //         aoeu<operator%=, void, void>
+  //  => _Z 4aoeuI r Mv v              E
+  //         aoeu<void void::* restrict>
+  //
+  // By consuming the CV-qualifiers first, the former parse is disabled.
+  if (ParseCVQualifiers(state)) {
+    const bool result = ParseType(state);
+    if (!result) state->parse_state = copy;
+    return result;
+  }
+  state->parse_state = copy;
+
+  // Similarly, these tag characters can overlap with other <name>s resulting in
+  // two different parse prefixes that land on <template-args> in the same
+  // place, such as "C3r1xI...".  So, disable the "ctor-name = C3" parse by
+  // refusing to backtrack the tag characters.
+  if (ParseCharClass(state, "OPRCG")) {
+    const bool result = ParseType(state);
+    if (!result) state->parse_state = copy;
+    return result;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "Dp") && ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'U') && ParseSourceName(state) &&
+      ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseBuiltinType(state) || ParseFunctionType(state) ||
+      ParseClassEnumType(state) || ParseArrayType(state) ||
+      ParsePointerToMemberType(state) || ParseDecltype(state) ||
+      // "std" on its own isn't a type.
+      ParseSubstitution(state, /*accept_std=*/false)) {
+    return true;
+  }
+
+  if (ParseTemplateTemplateParam(state) && ParseTemplateArgs(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Less greedy than <template-template-param> <template-args>.
+  if (ParseTemplateParam(state)) {
+    return true;
+  }
+
+  return false;
+}
+
+// <CV-qualifiers> ::= [r] [V] [K]
+// We don't allow empty <CV-qualifiers> to avoid infinite loop in
+// ParseType().
+static bool ParseCVQualifiers(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  int num_cv_qualifiers = 0;
+  num_cv_qualifiers += ParseOneCharToken(state, 'r');
+  num_cv_qualifiers += ParseOneCharToken(state, 'V');
+  num_cv_qualifiers += ParseOneCharToken(state, 'K');
+  return num_cv_qualifiers > 0;
+}
+
+// <builtin-type> ::= v, etc.  # single-character builtin types
+//                ::= u <source-name>
+//                ::= Dd, etc.  # two-character builtin types
+//
+// Not supported:
+//                ::= DF <number> _ # _FloatN (N bits)
+//
+static bool ParseBuiltinType(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  const AbbrevPair *p;
+  for (p = kBuiltinTypeList; p->abbrev != nullptr; ++p) {
+    // Guaranteed only 1- or 2-character strings in kBuiltinTypeList.
+    if (p->abbrev[1] == '\0') {
+      if (ParseOneCharToken(state, p->abbrev[0])) {
+        MaybeAppend(state, p->real_name);
+        return true;
+      }
+    } else if (p->abbrev[2] == '\0' && ParseTwoCharToken(state, p->abbrev)) {
+      MaybeAppend(state, p->real_name);
+      return true;
+    }
+  }
+
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'u') && ParseSourceName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <function-type> ::= F [Y] <bare-function-type> E
+static bool ParseFunctionType(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'F') &&
+      Optional(ParseOneCharToken(state, 'Y')) && ParseBareFunctionType(state) &&
+      ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <bare-function-type> ::= <(signature) type>+
+static bool ParseBareFunctionType(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  DisableAppend(state);
+  if (OneOrMore(ParseType, state)) {
+    RestoreAppend(state, copy.append);
+    MaybeAppend(state, "()");
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <class-enum-type> ::= <name>
+static bool ParseClassEnumType(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  return ParseName(state);
+}
+
+// <array-type> ::= A <(positive dimension) number> _ <(element) type>
+//              ::= A [<(dimension) expression>] _ <(element) type>
+static bool ParseArrayType(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'A') && ParseNumber(state, nullptr) &&
+      ParseOneCharToken(state, '_') && ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'A') && Optional(ParseExpression(state)) &&
+      ParseOneCharToken(state, '_') && ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <pointer-to-member-type> ::= M <(class) type> <(member) type>
+static bool ParsePointerToMemberType(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'M') && ParseType(state) && ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <template-param> ::= T_
+//                  ::= T <parameter-2 non-negative number> _
+static bool ParseTemplateParam(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (ParseTwoCharToken(state, "T_")) {
+    MaybeAppend(state, "?");  // We don't support template substitutions.
+    return true;
+  }
+
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'T') && ParseNumber(state, nullptr) &&
+      ParseOneCharToken(state, '_')) {
+    MaybeAppend(state, "?");  // We don't support template substitutions.
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <template-template-param> ::= <template-param>
+//                           ::= <substitution>
+static bool ParseTemplateTemplateParam(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  return (ParseTemplateParam(state) ||
+          // "std" on its own isn't a template.
+          ParseSubstitution(state, /*accept_std=*/false));
+}
+
+// <template-args> ::= I <template-arg>+ E
+static bool ParseTemplateArgs(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  DisableAppend(state);
+  if (ParseOneCharToken(state, 'I') && OneOrMore(ParseTemplateArg, state) &&
+      ParseOneCharToken(state, 'E')) {
+    RestoreAppend(state, copy.append);
+    MaybeAppend(state, "<>");
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <template-arg>  ::= <type>
+//                 ::= <expr-primary>
+//                 ::= J <template-arg>* E        # argument pack
+//                 ::= X <expression> E
+static bool ParseTemplateArg(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'J') && ZeroOrMore(ParseTemplateArg, state) &&
+      ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // There can be significant overlap between the following leading to
+  // exponential backtracking:
+  //
+  //   <expr-primary> ::= L <type> <expr-cast-value> E
+  //                 e.g. L 2xxIvE 1                 E
+  //   <type>         ==> <local-source-name> <template-args>
+  //                 e.g. L 2xx               IvE
+  //
+  // This means parsing an entire <type> twice, and <type> can contain
+  // <template-arg>, so this can generate exponential backtracking.  There is
+  // only overlap when the remaining input starts with "L <source-name>", so
+  // parse all cases that can start this way jointly to share the common prefix.
+  //
+  // We have:
+  //
+  //   <template-arg> ::= <type>
+  //                  ::= <expr-primary>
+  //
+  // First, drop all the productions of <type> that must start with something
+  // other than 'L'.  All that's left is <class-enum-type>; inline it.
+  //
+  //   <type> ::= <nested-name> # starts with 'N'
+  //          ::= <unscoped-name>
+  //          ::= <unscoped-template-name> <template-args>
+  //          ::= <local-name> # starts with 'Z'
+  //
+  // Drop and inline again:
+  //
+  //   <type> ::= <unscoped-name>
+  //          ::= <unscoped-name> <template-args>
+  //          ::= <substitution> <template-args> # starts with 'S'
+  //
+  // Merge the first two, inline <unscoped-name>, drop last:
+  //
+  //   <type> ::= <unqualified-name> [<template-args>]
+  //          ::= St <unqualified-name> [<template-args>] # starts with 'S'
+  //
+  // Drop and inline:
+  //
+  //   <type> ::= <operator-name> [<template-args>] # starts with lowercase
+  //          ::= <ctor-dtor-name> [<template-args>] # starts with 'C' or 'D'
+  //          ::= <source-name> [<template-args>] # starts with digit
+  //          ::= <local-source-name> [<template-args>]
+  //          ::= <unnamed-type-name> [<template-args>] # starts with 'U'
+  //
+  // One more time:
+  //
+  //   <type> ::= L <source-name> [<template-args>]
+  //
+  // Likewise with <expr-primary>:
+  //
+  //   <expr-primary> ::= L <type> <expr-cast-value> E
+  //                  ::= LZ <encoding> E # cannot overlap; drop
+  //                  ::= L <mangled_name> E # cannot overlap; drop
+  //
+  // By similar reasoning as shown above, the only <type>s starting with
+  // <source-name> are "<source-name> [<template-args>]".  Inline this.
+  //
+  //   <expr-primary> ::= L <source-name> [<template-args>] <expr-cast-value> E
+  //
+  // Now inline both of these into <template-arg>:
+  //
+  //   <template-arg> ::= L <source-name> [<template-args>]
+  //                  ::= L <source-name> [<template-args>] <expr-cast-value> E
+  //
+  // Merge them and we're done:
+  //   <template-arg>
+  //     ::= L <source-name> [<template-args>] [<expr-cast-value> E]
+  if (ParseLocalSourceName(state) && Optional(ParseTemplateArgs(state))) {
+    copy = state->parse_state;
+    if (ParseExprCastValue(state) && ParseOneCharToken(state, 'E')) {
+      return true;
+    }
+    state->parse_state = copy;
+    return true;
+  }
+
+  // Now that the overlapping cases can't reach this code, we can safely call
+  // both of these.
+  if (ParseType(state) || ParseExprPrimary(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'X') && ParseExpression(state) &&
+      ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <unresolved-type> ::= <template-param> [<template-args>]
+//                   ::= <decltype>
+//                   ::= <substitution>
+static inline bool ParseUnresolvedType(State *state) {
+  // No ComplexityGuard because we don't copy the state in this stack frame.
+  return (ParseTemplateParam(state) && Optional(ParseTemplateArgs(state))) ||
+         ParseDecltype(state) || ParseSubstitution(state, /*accept_std=*/false);
+}
+
+// <simple-id> ::= <source-name> [<template-args>]
+static inline bool ParseSimpleId(State *state) {
+  // No ComplexityGuard because we don't copy the state in this stack frame.
+
+  // Note: <simple-id> cannot be followed by a parameter pack; see comment in
+  // ParseUnresolvedType.
+  return ParseSourceName(state) && Optional(ParseTemplateArgs(state));
+}
+
+// <base-unresolved-name> ::= <source-name> [<template-args>]
+//                        ::= on <operator-name> [<template-args>]
+//                        ::= dn <destructor-name>
+static bool ParseBaseUnresolvedName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+
+  if (ParseSimpleId(state)) {
+    return true;
+  }
+
+  ParseState copy = state->parse_state;
+  if (ParseTwoCharToken(state, "on") && ParseOperatorName(state, nullptr) &&
+      Optional(ParseTemplateArgs(state))) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "dn") &&
+      (ParseUnresolvedType(state) || ParseSimpleId(state))) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  return false;
+}
+
+// <unresolved-name> ::= [gs] <base-unresolved-name>
+//                   ::= sr <unresolved-type> <base-unresolved-name>
+//                   ::= srN <unresolved-type> <unresolved-qualifier-level>+ E
+//                         <base-unresolved-name>
+//                   ::= [gs] sr <unresolved-qualifier-level>+ E
+//                         <base-unresolved-name>
+static bool ParseUnresolvedName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+
+  ParseState copy = state->parse_state;
+  if (Optional(ParseTwoCharToken(state, "gs")) &&
+      ParseBaseUnresolvedName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "sr") && ParseUnresolvedType(state) &&
+      ParseBaseUnresolvedName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseTwoCharToken(state, "sr") && ParseOneCharToken(state, 'N') &&
+      ParseUnresolvedType(state) &&
+      OneOrMore(/* <unresolved-qualifier-level> ::= */ ParseSimpleId, state) &&
+      ParseOneCharToken(state, 'E') && ParseBaseUnresolvedName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (Optional(ParseTwoCharToken(state, "gs")) &&
+      ParseTwoCharToken(state, "sr") &&
+      OneOrMore(/* <unresolved-qualifier-level> ::= */ ParseSimpleId, state) &&
+      ParseOneCharToken(state, 'E') && ParseBaseUnresolvedName(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  return false;
+}
+
+// <expression> ::= <1-ary operator-name> <expression>
+//              ::= <2-ary operator-name> <expression> <expression>
+//              ::= <3-ary operator-name> <expression> <expression> <expression>
+//              ::= cl <expression>+ E
+//              ::= cv <type> <expression>      # type (expression)
+//              ::= cv <type> _ <expression>* E # type (expr-list)
+//              ::= st <type>
+//              ::= <template-param>
+//              ::= <function-param>
+//              ::= <expr-primary>
+//              ::= dt <expression> <unresolved-name> # expr.name
+//              ::= pt <expression> <unresolved-name> # expr->name
+//              ::= sp <expression>         # argument pack expansion
+//              ::= sr <type> <unqualified-name> <template-args>
+//              ::= sr <type> <unqualified-name>
+// <function-param> ::= fp <(top-level) CV-qualifiers> _
+//                  ::= fp <(top-level) CV-qualifiers> <number> _
+//                  ::= fL <number> p <(top-level) CV-qualifiers> _
+//                  ::= fL <number> p <(top-level) CV-qualifiers> <number> _
+static bool ParseExpression(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (ParseTemplateParam(state) || ParseExprPrimary(state)) {
+    return true;
+  }
+
+  // Object/function call expression.
+  ParseState copy = state->parse_state;
+  if (ParseTwoCharToken(state, "cl") && OneOrMore(ParseExpression, state) &&
+      ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Function-param expression (level 0).
+  if (ParseTwoCharToken(state, "fp") && Optional(ParseCVQualifiers(state)) &&
+      Optional(ParseNumber(state, nullptr)) && ParseOneCharToken(state, '_')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Function-param expression (level 1+).
+  if (ParseTwoCharToken(state, "fL") && Optional(ParseNumber(state, nullptr)) &&
+      ParseOneCharToken(state, 'p') && Optional(ParseCVQualifiers(state)) &&
+      Optional(ParseNumber(state, nullptr)) && ParseOneCharToken(state, '_')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Parse the conversion expressions jointly to avoid re-parsing the <type> in
+  // their common prefix.  Parsed as:
+  // <expression> ::= cv <type> <conversion-args>
+  // <conversion-args> ::= _ <expression>* E
+  //                   ::= <expression>
+  //
+  // Also don't try ParseOperatorName after seeing "cv", since ParseOperatorName
+  // also needs to accept "cv <type>" in other contexts.
+  if (ParseTwoCharToken(state, "cv")) {
+    if (ParseType(state)) {
+      ParseState copy2 = state->parse_state;
+      if (ParseOneCharToken(state, '_') && ZeroOrMore(ParseExpression, state) &&
+          ParseOneCharToken(state, 'E')) {
+        return true;
+      }
+      state->parse_state = copy2;
+      if (ParseExpression(state)) {
+        return true;
+      }
+    }
+  } else {
+    // Parse unary, binary, and ternary operator expressions jointly, taking
+    // care not to re-parse subexpressions repeatedly. Parse like:
+    //   <expression> ::= <operator-name> <expression>
+    //                    [<one-to-two-expressions>]
+    //   <one-to-two-expressions> ::= <expression> [<expression>]
+    int arity = -1;
+    if (ParseOperatorName(state, &arity) &&
+        arity > 0 &&  // 0 arity => disabled.
+        (arity < 3 || ParseExpression(state)) &&
+        (arity < 2 || ParseExpression(state)) &&
+        (arity < 1 || ParseExpression(state))) {
+      return true;
+    }
+  }
+  state->parse_state = copy;
+
+  // sizeof type
+  if (ParseTwoCharToken(state, "st") && ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Object and pointer member access expressions.
+  if ((ParseTwoCharToken(state, "dt") || ParseTwoCharToken(state, "pt")) &&
+      ParseExpression(state) && ParseType(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Pointer-to-member access expressions.  This parses the same as a binary
+  // operator, but it's implemented separately because "ds" shouldn't be
+  // accepted in other contexts that parse an operator name.
+  if (ParseTwoCharToken(state, "ds") && ParseExpression(state) &&
+      ParseExpression(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Parameter pack expansion
+  if (ParseTwoCharToken(state, "sp") && ParseExpression(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  return ParseUnresolvedName(state);
+}
+
+// <expr-primary> ::= L <type> <(value) number> E
+//                ::= L <type> <(value) float> E
+//                ::= L <mangled-name> E
+//                // A bug in g++'s C++ ABI version 2 (-fabi-version=2).
+//                ::= LZ <encoding> E
+//
+// Warning, subtle: the "bug" LZ production above is ambiguous with the first
+// production where <type> starts with <local-name>, which can lead to
+// exponential backtracking in two scenarios:
+//
+// - When whatever follows the E in the <local-name> in the first production is
+//   not a name, we backtrack the whole <encoding> and re-parse the whole thing.
+//
+// - When whatever follows the <local-name> in the first production is not a
+//   number and this <expr-primary> may be followed by a name, we backtrack the
+//   <name> and re-parse it.
+//
+// Moreover this ambiguity isn't always resolved -- for example, the following
+// has two different parses:
+//
+//   _ZaaILZ4aoeuE1x1EvE
+//   => operator&&<aoeu, x, E, void>
+//   => operator&&<(aoeu::x)(1), void>
+//
+// To resolve this, we just do what GCC's demangler does, and refuse to parse
+// casts to <local-name> types.
+static bool ParseExprPrimary(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+
+  // The "LZ" special case: if we see LZ, we commit to accept "LZ <encoding> E"
+  // or fail, no backtracking.
+  if (ParseTwoCharToken(state, "LZ")) {
+    if (ParseEncoding(state) && ParseOneCharToken(state, 'E')) {
+      return true;
+    }
+
+    state->parse_state = copy;
+    return false;
+  }
+
+  // The merged cast production.
+  if (ParseOneCharToken(state, 'L') && ParseType(state) &&
+      ParseExprCastValue(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseOneCharToken(state, 'L') && ParseMangledName(state) &&
+      ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  return false;
+}
+
+// <number> or <float>, followed by 'E', as described above ParseExprPrimary.
+static bool ParseExprCastValue(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  // We have to be able to backtrack after accepting a number because we could
+  // have e.g. "7fffE", which will accept "7" as a number but then fail to find
+  // the 'E'.
+  ParseState copy = state->parse_state;
+  if (ParseNumber(state, nullptr) && ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  if (ParseFloatNumber(state) && ParseOneCharToken(state, 'E')) {
+    return true;
+  }
+  state->parse_state = copy;
+
+  return false;
+}
+
+// <local-name> ::= Z <(function) encoding> E <(entity) name> [<discriminator>]
+//              ::= Z <(function) encoding> E s [<discriminator>]
+//
+// Parsing a common prefix of these two productions together avoids an
+// exponential blowup of backtracking.  Parse like:
+//   <local-name> := Z <encoding> E <local-name-suffix>
+//   <local-name-suffix> ::= s [<discriminator>]
+//                       ::= <name> [<discriminator>]
+
+static bool ParseLocalNameSuffix(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+
+  if (MaybeAppend(state, "::") && ParseName(state) &&
+      Optional(ParseDiscriminator(state))) {
+    return true;
+  }
+
+  // Since we're not going to overwrite the above "::" by re-parsing the
+  // <encoding> (whose trailing '\0' byte was in the byte now holding the
+  // first ':'), we have to rollback the "::" if the <name> parse failed.
+  if (state->parse_state.append) {
+    state->out[state->parse_state.out_cur_idx - 2] = '\0';
+  }
+
+  return ParseOneCharToken(state, 's') && Optional(ParseDiscriminator(state));
+}
+
+static bool ParseLocalName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'Z') && ParseEncoding(state) &&
+      ParseOneCharToken(state, 'E') && ParseLocalNameSuffix(state)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <discriminator> := _ <(non-negative) number>
+static bool ParseDiscriminator(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, '_') && ParseNumber(state, nullptr)) {
+    return true;
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// <substitution> ::= S_
+//                ::= S <seq-id> _
+//                ::= St, etc.
+//
+// "St" is special in that it's not valid as a standalone name, and it *is*
+// allowed to precede a name without being wrapped in "N...E".  This means that
+// if we accept it on its own, we can accept "St1a" and try to parse
+// template-args, then fail and backtrack, accept "St" on its own, then "1a" as
+// an unqualified name and re-parse the same template-args.  To block this
+// exponential backtracking, we disable it with 'accept_std=false' in
+// problematic contexts.
+static bool ParseSubstitution(State *state, bool accept_std) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (ParseTwoCharToken(state, "S_")) {
+    MaybeAppend(state, "?");  // We don't support substitutions.
+    return true;
+  }
+
+  ParseState copy = state->parse_state;
+  if (ParseOneCharToken(state, 'S') && ParseSeqId(state) &&
+      ParseOneCharToken(state, '_')) {
+    MaybeAppend(state, "?");  // We don't support substitutions.
+    return true;
+  }
+  state->parse_state = copy;
+
+  // Expand abbreviations like "St" => "std".
+  if (ParseOneCharToken(state, 'S')) {
+    const AbbrevPair *p;
+    for (p = kSubstitutionList; p->abbrev != nullptr; ++p) {
+      if (RemainingInput(state)[0] == p->abbrev[1] &&
+          (accept_std || p->abbrev[1] != 't')) {
+        MaybeAppend(state, "std");
+        if (p->real_name[0] != '\0') {
+          MaybeAppend(state, "::");
+          MaybeAppend(state, p->real_name);
+        }
+        ++state->parse_state.mangled_idx;
+        return true;
+      }
+    }
+  }
+  state->parse_state = copy;
+  return false;
+}
+
+// Parse <mangled-name>, optionally followed by either a function-clone suffix
+// or version suffix.  Returns true only if all of "mangled_cur" was consumed.
+static bool ParseTopLevelMangledName(State *state) {
+  ComplexityGuard guard(state);
+  if (guard.IsTooComplex()) return false;
+  if (ParseMangledName(state)) {
+    if (RemainingInput(state)[0] != '\0') {
+      // Drop trailing function clone suffix, if any.
+      if (IsFunctionCloneSuffix(RemainingInput(state))) {
+        return true;
+      }
+      // Append trailing version suffix if any.
+      // ex. _Z3foo@@GLIBCXX_3.4
+      if (RemainingInput(state)[0] == '@') {
+        MaybeAppend(state, RemainingInput(state));
+        return true;
+      }
+      return false;  // Unconsumed suffix.
+    }
+    return true;
+  }
+  return false;
+}
+
+static bool Overflowed(const State *state) {
+  return state->parse_state.out_cur_idx >= state->out_end_idx;
+}
+
+}  // namespace debugging_internal
+}  // namespace absl
+
+// The demangler entry point.
+bool Demangle(const char *mangled, char *out, int out_size) {
+  using namespace absl::debugging_internal;
+  State state;
+  InitState(&state, mangled, out, out_size);
+  return ParseTopLevelMangledName(&state) && !Overflowed(&state);
+}
diff --git a/src/demangle.h b/src/demangle.h
new file mode 100644
index 0000000..823b0da
--- /dev/null
+++ b/src/demangle.h
@@ -0,0 +1,58 @@
+// Copyright 2017 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.
+// Author: satorux@google.com (Satoru Takabayashi)
+//
+// An async-signal-safe and thread-safe demangler for Itanium C++ ABI
+// (aka G++ V3 ABI).
+// The demangler is implemented to be used in async signal handlers to
+// symbolize stack traces.  We cannot use libstdc++'s
+// abi::__cxa_demangle() in such signal handlers since it's not async
+// signal safe (it uses malloc() internally).
+//
+// Note that this demangler doesn't support full demangling.  More
+// specifically, it doesn't print types of function parameters and
+// types of template arguments.  It just skips them.  However, it's
+// still very useful to extract basic information such as class,
+// function, constructor, destructor, and operator names.
+//
+// See the implementation note in demangle.cc if you are interested.
+//
+// Example:
+//
+// | Mangled Name  | The Demangler | abi::__cxa_demangle()
+// |---------------|---------------|-----------------------
+// | _Z1fv         | f()           | f()
+// | _Z1fi         | f()           | f(int)
+// | _Z3foo3bar    | foo()         | foo(bar)
+// | _Z1fIiEvi     | f<>()         | void f<int>(int)
+// | _ZN1N1fE      | N::f          | N::f
+// | _ZN3Foo3BarEv | Foo::Bar()    | Foo::Bar()
+// | _Zrm1XS_"     | operator%()   | operator%(X, X)
+// | _ZN3FooC1Ev   | Foo::Foo()    | Foo::Foo()
+// | _Z1fSs        | f()           | f(std::basic_string<char,
+// |               |               |   std::char_traits<char>,
+// |               |               |   std::allocator<char> >)
+//
+// See the unit test for more examples.
+//
+// Note: we might want to write demanglers for ABIs other than Itanium
+// C++ ABI in the future.
+//
+#ifndef BASE_DEMANGLE_H_
+#define BASE_DEMANGLE_H_
+// Demangle "mangled".  On success, return true and write the
+// demangled symbol name to "out".  Otherwise, return false.
+// "out" is modified even if demangling is unsuccessful.
+bool Demangle(const char *mangled, char *out, int out_size);
+#endif  // BASE_DEMANGLE_H_
diff --git a/src/elf.cc b/src/elf.cc
index 9e8c4b4..9195261 100644
--- a/src/elf.cc
+++ b/src/elf.cc
@@ -15,13 +15,15 @@
 #include <algorithm>
 #include <string>
 #include <iostream>
+#include <fstream>
+#include <sstream>
 #include "absl/numeric/int128.h"
 #include "absl/strings/escaping.h"
 #include "absl/strings/string_view.h"
 #include "absl/strings/substitute.h"
 #include "third_party/freebsd_elf/elf.h"
 #include "bloaty.h"
-#include "util.h"
+#include "link_map.h"
 
 #include <assert.h>
 #include <limits.h>
@@ -140,6 +142,10 @@
         : elf_(&section.elf()), remaining_(section.contents()) {
       Next();
     }
+    NoteIter(const Segment& segment, const ElfFile* elf)
+        : elf_(elf), remaining_(segment.contents()) {
+      Next();
+    }
 
     bool IsDone() const { return done_; }
     uint32_t type() const { return type_; }
@@ -1234,8 +1240,27 @@
 
 class ElfObjectFile : public ObjectFile {
  public:
-  ElfObjectFile(std::unique_ptr<InputFile> file)
-      : ObjectFile(std::move(file)) {}
+  ElfObjectFile(std::unique_ptr<InputFile> file, std::optional<std::string> link_map_file)
+      : ObjectFile(std::move(file)) {
+    if (link_map_file.has_value()) {
+      std::ifstream infile(*link_map_file);
+      std::string link_map;
+
+      // Strip comments and empty lines.
+      for (std::string line; getline(infile, line);) {
+        if (line.empty()) continue;
+        if (line[0] == '#') continue;
+        link_map += line;
+        link_map += '\n';
+      }
+
+      absl::StripLeadingAsciiWhitespace(&link_map);
+      absl::StripTrailingAsciiWhitespace(&link_map);
+
+      link_map_symbols_ = bloaty_link_map::ParseLldLinkMap(link_map);
+      link_map_sections_ = bloaty_link_map::ParseLldLinkMapSections(link_map);
+    }
+  }
 
   std::string GetBuildId() const override {
     if (IsObjectFile(file_data().data())) {
@@ -1245,6 +1270,7 @@
 
     ElfFile elf(file_data().data());
     assert(elf.IsOpen());
+    // Search for a build-id section.
     for (Elf64_Xword i = 1; i < elf.section_count(); i++) {
       ElfFile::Section section;
       elf.ReadSection(i, &section);
@@ -1258,11 +1284,123 @@
         }
       }
     }
+    // Search for a build-id segment.
+    for (Elf64_Xword i = 0; i < elf.header().e_phnum; i++) {
+      ElfFile::Segment segment;
+      elf.ReadSegment(i, &segment);
+      const auto &header = segment.header();
+      if (header.p_type != PT_NOTE) {
+        continue;
+      }
+      for (ElfFile::NoteIter notes(segment, &elf); !notes.IsDone(); notes.Next()) {
+        if (notes.name() == "GNU" && notes.type() == NT_GNU_BUILD_ID) {
+          return std::string(notes.descriptor());
+        }
+      }
+    }
 
     // No build id section found.
     return std::string();
   }
 
+  void ReadAccessPattern(RangeSink* sink) const {
+    if (!sink->options().has_cold_bytes_filter()) {
+      THROW("need to specify cold bytes filter");
+    }
+    // Each element corresponds to |kAccessPatternFrameSize| bytes.
+    std::vector<bool> access_pattern;
+    uint64_t kAccessPatternFrameSize = sink->options().access_pattern_frame_size();
+    auto frequencies = sink->options().cold_bytes_filter();
+    size_t file_size = sink->input_file().data().size();
+    size_t num_frames = (file_size + kAccessPatternFrameSize - 1) / kAccessPatternFrameSize;
+    access_pattern.reserve(num_frames);
+    for (size_t i = 0; i < num_frames; i ++) {
+      access_pattern.push_back(false);
+    }
+    std::vector<std::string> frequencies_vec = absl::StrSplit(frequencies, ',');
+    for (const auto& part : frequencies_vec) {
+      std::vector<std::string> frame_and_count = absl::StrSplit(part, ':');
+      if (frame_and_count.size() != 2) {
+        THROWF("Invalid format in cold bytes filter: $0", part);
+      }
+      size_t frame = std::stoi(frame_and_count[0]);
+      size_t count = std::stoi(frame_and_count[1]);
+      if (frame >= access_pattern.size()) {
+        THROW("access pattern exceeded end of file");
+      }
+      if (count > 0) {
+        access_pattern[frame] = true;
+      }
+    }
+    for (size_t i = 0; i < access_pattern.size(); i++) {
+      std::string label = access_pattern[i] ? "Hot" : "Cold";
+      size_t length;
+      size_t file_size = sink->input_file().data().size();
+      if (i * kAccessPatternFrameSize > file_size) {
+        THROW("access pattern exceeded end of file");
+      }
+      if (i * kAccessPatternFrameSize + kAccessPatternFrameSize > file_size) {
+        // We're at the last frame in the ELF, and it is not fully 32 KiB.
+        length = file_size % kAccessPatternFrameSize;
+      } else {
+        length = kAccessPatternFrameSize;
+      }
+      sink->AddFileRange("access_pattern", label,
+                         i * kAccessPatternFrameSize, length);
+    }
+  }
+
+  void ReadLinkMapSymbols(RangeSink* sink) const {
+    if (!link_map_symbols_.has_value()) return;
+    const auto& symbols = *link_map_symbols_;
+    for (const auto& symbol : symbols) {
+      auto maybe_transformed_compile_unit =
+          bloaty_link_map::TransformCompileUnitForFuchsia(symbol.compile_unit);
+      auto demangled = ItaniumDemangle(symbol.name, sink->data_source());
+      if (maybe_transformed_compile_unit.has_value()) {
+        auto [transformed_compile_unit, maybe_rust_crate] =
+            *maybe_transformed_compile_unit;
+        if (maybe_rust_crate.has_value()) {
+          auto symbol_with_crate_id =
+              EncodeSymbolWithCrateId(demangled, *maybe_rust_crate);
+          sink->AddVMRange("link_map", symbol.addr, symbol.size,
+                           symbol_with_crate_id);
+          continue;
+        }
+      }
+      sink->AddVMRange("link_map", symbol.addr, symbol.size, demangled);
+    }
+
+    if (!link_map_sections_.has_value()) return;
+    const auto& sections = *link_map_sections_;
+    for (const auto& section : sections) {
+      sink->AddVMRange("link_map", section.addr, section.size,
+                       "[section " + section.name + "]");
+    }
+  }
+
+  void ReadLinkMapCompileUnits(RangeSink* sink) const {
+    if (!link_map_symbols_.has_value()) return;
+    const auto& symbols = *link_map_symbols_;
+    for (const auto& symbol : symbols) {
+      auto maybe_transformed_compile_unit =
+          bloaty_link_map::TransformCompileUnitForFuchsia(symbol.compile_unit);
+      if (maybe_transformed_compile_unit.has_value()) {
+        auto [transformed_compile_unit, maybe_rust_crate] =
+            *maybe_transformed_compile_unit;
+        sink->AddVMRange("link_map", symbol.addr, symbol.size,
+                         transformed_compile_unit);
+      }
+    }
+
+    if (!link_map_sections_.has_value()) return;
+    const auto& sections = *link_map_sections_;
+    for (const auto& section : sections) {
+      sink->AddVMRange("link_map", section.addr, section.size,
+                       "[section " + section.name + "]");
+    }
+  }
+
   void ProcessFile(const std::vector<RangeSink*>& sinks) const override {
     for (auto sink : sinks) {
       switch (sink->data_source()) {
@@ -1275,11 +1413,16 @@
         case DataSource::kRawSymbols:
         case DataSource::kShortSymbols:
         case DataSource::kFullSymbols:
+          ReadLinkMapSymbols(sink);
           ReadELFSymbols(debug_file().file_data(), sink, nullptr, false);
           break;
         case DataSource::kArchiveMembers:
           DoReadELFSections(sink, kReportByArchiveMember);
           break;
+        case DataSource::kAccessPattern: {
+          ReadAccessPattern(sink);
+          break;
+        }
         case DataSource::kCompileUnits: {
           CheckNotObject("compileunits", sink);
           SymbolTable symtab;
@@ -1295,6 +1438,7 @@
           dwarf::File dwarf;
           ReadDWARFSections(debug_file().file_data(), &dwarf, sink);
           ReadDWARFCompileUnits(dwarf, symtab, symbol_map, sink);
+          ReadLinkMapCompileUnits(sink);
           break;
         }
         case DataSource::kInlines: {
@@ -1313,6 +1457,7 @@
         case DataSource::kSegments:
         case DataSource::kSections:
         case DataSource::kArchiveMembers:
+        case DataSource::kAccessPattern:
           break;
         default:
           // Add these *after* processing all other data sources.
@@ -1377,15 +1522,23 @@
     ReadElfArchMode(file_data(), &info->arch, &info->mode);
     return true;
   }
+
+ private:
+  std::optional<std::vector<bloaty_link_map::Symbol>> link_map_symbols_ = std::nullopt;
+  std::optional<std::vector<bloaty_link_map::Section>> link_map_sections_ = std::nullopt;
 };
 
 }  // namespace
 
-std::unique_ptr<ObjectFile> TryOpenELFFile(std::unique_ptr<InputFile>& file) {
+std::unique_ptr<ObjectFile> TryOpenELFFile(std::unique_ptr<InputFile>& file,
+                                           std::optional<std::string> link_map_file) {
   ElfFile elf(file->data());
   ArFile ar(file->data());
   if (elf.IsOpen() || ar.IsOpen()) {
-    return std::unique_ptr<ObjectFile>(new ElfObjectFile(std::move(file)));
+    if (link_map_file.has_value()) {
+      std::cerr << "Using link map: " << *link_map_file << std::endl;
+    }
+    return std::unique_ptr<ObjectFile>(new ElfObjectFile(std::move(file), link_map_file));
   } else {
     return nullptr;
   }
diff --git a/src/link_map.cc b/src/link_map.cc
new file mode 100644
index 0000000..9eca7f8
--- /dev/null
+++ b/src/link_map.cc
@@ -0,0 +1,626 @@
+// 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
diff --git a/src/link_map.h b/src/link_map.h
new file mode 100644
index 0000000..38d9b8f
--- /dev/null
+++ b/src/link_map.h
@@ -0,0 +1,128 @@
+// 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.
+
+#ifndef BLOATY_LINK_MAP_H_
+#define BLOATY_LINK_MAP_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <tuple>
+#include <vector>
+
+namespace bloaty_link_map {
+
+struct Symbol {
+  std::string name;
+  std::string compile_unit;
+  std::string section;
+  uint64_t addr;
+  uint64_t size;
+};
+
+// Parses a linker map file in lld v1 format.
+//
+// This function is adapted from
+// https://chromium.googlesource.com/chromium/src.git/+/master/tools/binary_size/libsupersize/linker_map_parser.py
+//
+// Args:
+//     content: Contents of the link map, the first line of which is the header.
+//
+// Returns:
+//     A vector of symbols.
+//
+// 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
+//
+// Notes:
+// - `VMA` and `LMA` are mostly identical.
+// - In a position-independent executable, the base address is zero.
+//   Hence the minimum value of the addresses is quite small.
+// - Stripping of the binary will not relocate symbols, hence the addresses
+//   will be the same between a stripped and unstripped binary.
+std::vector<Symbol> ParseLldLinkMap(const std::string& content);
+
+struct Section {
+  std::string name;
+  uint64_t addr;
+  uint64_t size;
+};
+
+std::vector<Section> ParseLldLinkMapSections(const std::string& content);
+
+// Transform the compile unit path parsed from link maps to a format
+// easier to organize, specialized to a Fuchsia build.
+// If the compile unit could not be understood by the transformer, it will
+// return `std::nullopt`. Since bloaty can still recover some compile unit
+// information from DWARF, we do not have to cover 100% here. For Rust however,
+// this allows setting a reasonable fallback compile unit at the crate level.
+// Therefore, the output of this transform should be inserted after the
+// usual DWARF-based compile unit data source.
+//
+// Examples:
+//
+// `./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o`
+// becomes `[crate: alloc]`.
+//
+// `./exe.unstripped/component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o.rcgu.o`
+// becomes `[crate: component_manager_lib]`.
+//
+// `./exe.unstripped/component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o`
+// becomes `[crate: cm_fidl_translator]`.
+//
+// `./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o`
+// becomes `[crate: component_manager]`.
+//
+// `./exe.unstripped/component_manager.libfidl_fuchsia_io.fidl_fuchsia_io.3a1fbbbh-cgu.0.rcgu.o.rcgu.o`
+// becomes `[crate: fidl_fuchsia_io]`.
+//
+// `/usr/local/google/home/yifeit/vg/out/default/obj/third_party/rust_crates/libregex_syntax-579ced0738b0164d.rlib(libregex_syntax-579ced0738b0164d-579ced0738b0164d.regex_syntax.c02sfxfu-cgu.13.rcgu.o)`
+// becomes `[crate: regex_syntax]`.
+//
+// `/usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o`
+// becomes `../../zircon/system/ulib/c/Scrt1.cc`.
+//
+// `obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c.o`
+// becomes `fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block.fidl.tables.c`.
+//
+// `obj/zircon/public/lib/fidl_base/libfidl_base.a(libfidl_base.decoding.cc.o)`
+// becomes `../../zircon/system/ulib/fidl/decoding.cc`
+//
+// `obj/zircon/system/uapp/blobfs/blobfs.main.cc.o`
+// becomes `../../zircon/system/uapp/blobfs/main.cc`
+//
+// `obj/zircon/system/ulib/trace-provider/libtrace-provider-with-fdio.a(libtrace-provider-with-fdio.fdio_connect.cc.o)`
+// is not suppported.
+//
+// `obj/zircon/public/lib/lz4/liblz4.a(liblz4.lz4hc.c.o)` is not supported.
+std::optional<std::tuple<std::string, std::optional<std::string>>> TransformCompileUnitForFuchsia(
+    const std::string& compile_unit);
+
+}  // namespace bloaty_link_map
+
+#endif  // BLOATY_LINK_MAP_H_
diff --git a/src/report.proto b/src/report.proto
new file mode 100644
index 0000000..e4cab01
--- /dev/null
+++ b/src/report.proto
@@ -0,0 +1,41 @@
+// 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.
+
+syntax = "proto3";
+option cc_enable_arenas = true;
+
+package bloaty_report;
+
+message SizeInfo {
+  uint32 file_actual = 1;
+  uint32 vm_actual = 2;
+}
+
+message Symbol {
+  SizeInfo sizes = 1;
+  string name = 2;
+  string maybe_rust_crate = 3;
+}
+
+message CompileUnit {
+  SizeInfo sizes = 1;
+  repeated Symbol symbols = 2;
+  string name = 3;
+}
+
+message Report {
+  repeated CompileUnit compile_units = 1;
+  uint32 file_total = 2;
+  uint32 vm_total = 3;
+}
diff --git a/src/write_bloaty_report.cc b/src/write_bloaty_report.cc
new file mode 100644
index 0000000..a22fbad
--- /dev/null
+++ b/src/write_bloaty_report.cc
@@ -0,0 +1,131 @@
+// 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 <stdexcept>
+
+#include "absl/strings/match.h"
+#include "absl/strings/str_split.h"
+#include "absl/strings/substitute.h"
+
+#include "google/protobuf/arena.h"
+
+#include "bloaty.h"
+#include "report.pb.h"
+
+namespace bloaty {
+
+void RollupOutput::PrintToProtobuf(std::ostream* out) const {
+  using namespace ::bloaty_report;
+
+  google::protobuf::Arena arena;
+  Report* report = google::protobuf::Arena::CreateMessage<Report>(&arena);
+  report->set_file_total(toplevel_row_.filesize);
+  report->set_vm_total(toplevel_row_.vmsize);
+
+  ([&] {
+    // If 2 levels, assume compileunits,symbols.
+    // If 3 levels, assume accesspattern,compileunits,symbols; and pick the cold accesses.
+    auto* compile_unit_row = &toplevel_row_;
+    if (toplevel_row_.sorted_children.front().sorted_children.front().sorted_children.size() > 0) {
+      bool found_hot = false;
+      for (const auto& access : toplevel_row_.sorted_children) {
+        if (access.name == "Hot") {
+          found_hot = true;
+        }
+        if (access.name == "Cold") {
+          compile_unit_row = &access;
+          break;
+        }
+      }
+      if (compile_unit_row == &toplevel_row_) {
+        if (!found_hot) {
+          THROWF("Could not find hot/cold regions in the binary, top-level row has $0 children "
+                 "([$1, ...]), "
+                 "next level row has $2 children ([$3, ...])",
+                 toplevel_row_.sorted_children.size(),
+                 toplevel_row_.sorted_children.front().name,
+                 toplevel_row_.sorted_children.front().sorted_children.size(),
+                 toplevel_row_.sorted_children.front().sorted_children.front().name);
+        } else {
+          // No cold regions, hence nothing to output.
+          return;
+        }
+      }
+    }
+
+    for (const auto& child_row : compile_unit_row->sorted_children) {
+      CompileUnit* compile_unit = report->add_compile_units();
+      for (const auto& symbol_row : child_row.sorted_children) {
+        SizeInfo* info = google::protobuf::Arena::CreateMessage<SizeInfo>(&arena);
+        info->set_file_actual(symbol_row.filesize);
+        info->set_vm_actual(symbol_row.vmsize);
+        Symbol* symbol = compile_unit->add_symbols();
+        symbol->set_allocated_sizes(info);
+        auto decoded_symbol = DecodeSymbolWithCrateId(symbol_row.name);
+        symbol->set_name(decoded_symbol.symbol);
+        symbol->set_maybe_rust_crate(decoded_symbol.crate);
+      }
+      if (child_row.sorted_children.empty()) {
+        // Add a "fake symbol" that is the same as the compile unit,
+        // to make sizes add up.
+        SizeInfo* info = google::protobuf::Arena::CreateMessage<SizeInfo>(&arena);
+        info->set_file_actual(child_row.filesize);
+        info->set_vm_actual(child_row.vmsize);
+        Symbol* symbol = compile_unit->add_symbols();
+        symbol->set_allocated_sizes(info);
+        symbol->set_name(child_row.name);
+      }
+
+      SizeInfo* info = google::protobuf::Arena::CreateMessage<SizeInfo>(&arena);
+      info->set_file_actual(child_row.filesize);
+      info->set_vm_actual(child_row.vmsize);
+      compile_unit->set_allocated_sizes(info);
+      compile_unit->set_name(child_row.name);
+    }
+  })();
+
+  bool good = report->SerializeToOstream(out);
+  if (!good) {
+    throw std::runtime_error("Failed to serialize report");
+  }
+  out->flush();
+}
+
+namespace {
+
+const std::string kSeparator = ", in crate ";
+
+}  // namespace
+
+std::string EncodeSymbolWithCrateId(absl::string_view symbol, absl::string_view crate) {
+  // The encoding format is "$symbol, in crate $crate". Since regular C++ and
+  // Rust code is extremely unlikely to produce the word "in crate", This format
+  // is unlikely to cause ambiguities, while staying readable if bloaty is
+  // invoked from the command line.
+  return std::string(symbol) + kSeparator + std::string(crate);
+}
+
+DecodeCrateIdResult DecodeSymbolWithCrateId(absl::string_view symbol) {
+  if (absl::StrContains(symbol, kSeparator)) {
+    std::vector<std::string> parts = absl::StrSplit(symbol, kSeparator);
+    if (parts.size() != 2) {
+      throw std::runtime_error("Unexpected symbol when decoding: " + std::string(symbol));
+    }
+    return DecodeCrateIdResult{.symbol = parts[0], .crate = parts[1]};
+  } else {
+    return DecodeCrateIdResult{.symbol = std::string(symbol)};
+  }
+}
+
+}  // namespace bloaty
diff --git a/tests/bloaty_misc_test.cc b/tests/bloaty_misc_test.cc
index ef23d5a..8bee15a 100644
--- a/tests/bloaty_misc_test.cc
+++ b/tests/bloaty_misc_test.cc
@@ -51,3 +51,24 @@
   RunBloaty(args);  // Heavily multithreaded test.
   EXPECT_EQ(top_row_->filesize, file_size * 100);
 }
+
+TEST(GetPathStem, Normal) {
+  EXPECT_EQ(bloaty::GetPathStem("foo"), "foo");
+  EXPECT_EQ(bloaty::GetPathStem("foo/bar/baz"), "baz");
+  EXPECT_EQ(bloaty::GetPathStem("/foo/bar/baz"), "baz");
+  EXPECT_EQ(bloaty::GetPathStem("/foo/../bar/baz"), "baz");
+}
+
+TEST(GetPathStem, DotSuffix) {
+  EXPECT_EQ(bloaty::GetPathStem("foo.b"), "foo");
+  EXPECT_EQ(bloaty::GetPathStem("foo/bar/baz.b"), "baz");
+  EXPECT_EQ(bloaty::GetPathStem("/foo/bar/baz.b"), "baz");
+  EXPECT_EQ(bloaty::GetPathStem("/foo/../bar/baz.b"), "baz");
+}
+
+TEST(GetPathStem, DotPrefix) {
+  EXPECT_EQ(bloaty::GetPathStem(".foo"), ".foo");
+  EXPECT_EQ(bloaty::GetPathStem("foo/bar/.baz"), ".baz");
+  EXPECT_EQ(bloaty::GetPathStem("/foo/bar/.baz"), ".baz");
+  EXPECT_EQ(bloaty::GetPathStem("/foo/../bar/.baz"), ".baz");
+}
diff --git a/tests/bloaty_report_test.cc b/tests/bloaty_report_test.cc
new file mode 100644
index 0000000..d9b44e1
--- /dev/null
+++ b/tests/bloaty_report_test.cc
@@ -0,0 +1,208 @@
+// 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 "bloaty.h"
+#include "test.h"
+#include "report.pb.h"
+
+TEST_F(BloatyTest, ProtobufOutput) {
+  std::string file = "05-binary.bin";
+  uint64_t size;
+  ASSERT_TRUE(GetFileSize(file, &size));
+
+  RunBloaty({"bloaty", "-d", "compileunits,symbols", file});
+
+  // Select protobuf output.
+  std::ostringstream stream;
+  bloaty::OutputOptions options;
+  options.output_format = bloaty::OutputFormat::kProtobuf;
+  output_->Print(options, &stream);
+
+  // Check Protobuf output.
+  bloaty_report::Report report;
+  ASSERT_TRUE(report.ParseFromString(stream.str()));
+
+  // These are the internal structure of `05-binary.bin`.
+  // They shouldn't change if the test data don't change.
+  // See below for a CSV version of the same output; here we ensure that
+  // the Protobuf output matches the CSV version.
+  // See also `BloatyTest.SimpleBinary` in `bloaty_test.cc`.
+  ASSERT_EQ(21, report.compile_units().size());
+  auto& bar_o = report.compile_units()[0];
+  ASSERT_EQ("bar.o.c", bar_o.name());
+  EXPECT_NEAR(4.36, bar_o.sizes().file_actual() / 1024.0, .01);
+  EXPECT_NEAR(3.99, bar_o.sizes().vm_actual() / 1024.0, .01);
+
+  ASSERT_GT(bar_o.symbols().size(), 0);
+  auto& bar_x = bar_o.symbols()[0];
+  ASSERT_EQ("bar_x", bar_x.name());
+  EXPECT_NEAR(3.94, bar_x.sizes().file_actual() / 1024.0, .01);
+  EXPECT_NEAR(3.91, bar_x.sizes().vm_actual() / 1024.0, .01);
+
+  ASSERT_GT(bar_o.symbols().size(), 1);
+  auto& debug_info = bar_o.symbols()[1];
+  ASSERT_EQ("[section .debug_info]", debug_info.name());
+  EXPECT_EQ(169, debug_info.sizes().file_actual());
+  EXPECT_EQ(0, debug_info.sizes().vm_actual());
+
+  EXPECT_NEAR(14.1, report.file_total() / 1024.0, .1);
+  EXPECT_NEAR(10.3, report.vm_total() / 1024.0, .1);
+}
+
+TEST_F(BloatyTest, ProtobufOutputFilterHotSymbolsAllHot) {
+  std::string file = "05-binary.bin";
+  uint64_t size;
+  ASSERT_TRUE(GetFileSize(file, &size));
+
+  RunBloaty({"bloaty", "-d", "accesspattern,compileunits,symbols", file,
+             // The first 32 KiB frame is accessed 100 times.
+             "--cold-bytes-filter", "0:100",
+             "--access-pattern-frame-size", "32768"});
+
+  // Select protobuf output.
+  std::ostringstream stream;
+  bloaty::OutputOptions options;
+  options.output_format = bloaty::OutputFormat::kProtobuf;
+  output_->Print(options, &stream);
+
+  // Check Protobuf output.
+  bloaty_report::Report report;
+  ASSERT_TRUE(report.ParseFromString(stream.str()));
+
+  // Since the test binary `05-binary.bin` is smaller than 32 KiB,
+  // every symbol (except zero-filled segments synthesized at run-time)
+  // inside the file will be hot, as designated by our command line
+  // arguments. So the list of compile units should be empty.
+  ASSERT_EQ(0, report.compile_units().size());
+
+  EXPECT_NEAR(14.1, report.file_total() / 1024.0, .1);
+  EXPECT_NEAR(10.3, report.vm_total() / 1024.0, .1);
+}
+
+TEST_F(BloatyTest, ProtobufOutputFilterHotSymbols) {
+  std::string file = "05-binary.bin";
+  uint64_t size;
+  ASSERT_TRUE(GetFileSize(file, &size));
+
+  RunBloaty({"bloaty", "-d", "accesspattern,compileunits,symbols", file,
+             // Default frame size is 8 KiB.
+             // The first 8 KiB frame is accessed 100 times.
+             "--cold-bytes-filter", "0:100"});
+
+  // Select protobuf output.
+  std::ostringstream stream;
+  bloaty::OutputOptions options;
+  options.output_format = bloaty::OutputFormat::kProtobuf;
+  output_->Print(options, &stream);
+
+  // Check Protobuf output.
+  bloaty_report::Report report;
+  ASSERT_TRUE(report.ParseFromString(stream.str()));
+
+  // This test is only run on x86_64.
+  // The test binary `05-binary.bin` is around 14 KiB.
+  // We would expect to see some compile units.
+  EXPECT_NEAR(13, report.compile_units().size(), 1);
+
+  EXPECT_NEAR(14.1, static_cast<double>(size) / 1024.0, .1);
+  EXPECT_NEAR(14.1, report.file_total() / 1024.0, .1);
+  EXPECT_NEAR(10.3, report.vm_total() / 1024.0, .1);
+}
+
+TEST_F(BloatyTest, ProtobufOutputFilterHotSymbolsPatternTooLarge) {
+  std::string file = "05-binary.bin";
+  std::string errmsg = "access pattern exceeded end of file";
+  AssertBloatyFails({"bloaty", "-d", "accesspattern,compileunits,symbols", file,
+                     // Default frame size is 8 KiB.
+                     // The file is 14 KiB, so specifying the non-existent
+                     // third frame should fail.
+                     "--cold-bytes-filter", "0:100,1:100,2:100"}, errmsg);
+}
+
+// Here is a regular bloaty print-out on the same file for reference:
+//
+//    FILE SIZE        VM SIZE
+// --------------  --------------
+//  30.9%  4.36Ki  38.8%  3.99Ki    bar.o.c
+//    90.3%  3.94Ki  97.9%  3.91Ki    bar_x
+//     3.8%     169   0.0%       0    [section .debug_info]
+//     2.4%     109   1.9%      76    bar_func
+//     1.3%      58   0.0%       0    [section .debug_line]
+//     0.8%      34   0.1%       4    bar_y
+//     0.7%      31   0.0%       0    [section .debug_str]
+//     0.7%      30   0.1%       4    bar_z
+//   3.9%     564  38.8%  3.98Ki    foo.o.c
+//     5.3%      30  98.0%  3.91Ki    foo_x
+//    27.7%     156   0.0%       0    [section .debug_abbrev]
+//    26.2%     148   0.0%       0    [section .debug_info]
+//    19.3%     109   1.9%      76    foo_func
+//    10.3%      58   0.0%       0    [section .debug_line]
+//     5.9%      33   0.0%       0    [section .debug_str]
+//     5.3%      30   0.1%       4    foo_y
+//  15.5%  2.19Ki   0.0%       0    [ELF Headers]
+//    91.4%  2.00Ki   NAN%       0    [ELF Headers]
+//     5.7%     128   NAN%       0    __libc_csu_init
+//     2.9%      64   NAN%       0    _IO_stdin_used
+//  12.1%  1.71Ki   0.0%       0    [Unmapped]
+//  11.6%  1.64Ki   0.0%       0    [section .symtab]
+//    78.6%  1.29Ki   NAN%       0    [section .symtab]
+//    17.1%     288   NAN%       0    __libc_csu_init
+//     1.4%      24   NAN%       0    _IO_stdin_used
+//     1.4%      24   NAN%       0    __libc_csu_fini
+//     1.4%      24   NAN%       0    completed.6973
+//   4.1%     593   5.6%     593    [LOAD #2 [RX]]
+//   3.9%     561   0.0%       0    [section .strtab]
+//    53.5%     300   NAN%       0    [section .strtab]
+//    38.3%     215   NAN%       0    __libc_csu_init
+//     2.9%      16   NAN%       0    __libc_csu_fini
+//     2.7%      15   NAN%       0    _IO_stdin_used
+//     2.7%      15   NAN%       0    completed.6973
+//   3.2%     464   4.4%     464    [section .dynamic]
+//   3.0%     430   3.9%     405    [20 Others]
+//   2.5%     356   3.4%     356    [section .text]
+//    71.1%     253  71.1%     253    [section .text]
+//    28.4%     101  28.4%     101    __libc_csu_init
+//     0.6%       2   0.6%       2    __libc_csu_fini
+//   2.3%     328   0.0%       0    [section .shstrtab]
+//   1.5%     220   0.4%      46    main.o.c
+//    37.3%      82   0.0%       0    [section .debug_info]
+//    34.1%      75 100.0%      46    main
+//    26.8%      59   0.0%       0    [section .debug_line]
+//     1.8%       4   0.0%       0    [section .debug_str]
+//   1.5%     212   2.0%     212    [section .eh_frame]
+//    54.7%     116  54.7%     116    [section .eh_frame]
+//    34.0%      72  34.0%      72    __libc_csu_init
+//    11.3%      24  11.3%      24    __libc_csu_fini
+//   1.0%     144   0.0%       0    [section .debug_aranges]
+//   0.7%     101   0.0%       0    [section .debug_abbrev]
+//   0.6%      93   0.0%       0
+//   0.5%      72   0.7%      72    [section .dynsym]
+//   0.4%      56   0.5%      56    [section .dynstr]
+//   0.2%      24   0.5%      52    [LOAD #3 [RW]]
+//   0.3%      48   0.5%      48    [section .plt]
+//   0.3%      48   0.5%      48    [section .rela.plt]
+//    50.0%      24  50.0%      24    [section .rela.plt]
+//    50.0%      24  50.0%      24    __libc_csu_init
+// 100.0%  14.1Ki 100.0%  10.3Ki    TOTAL
+
+TEST_F(BloatyTest, EncodeSymbolWithCrate) {
+  EXPECT_EQ(bloaty::EncodeSymbolWithCrateId("foo", "bar"), "foo, in crate bar");
+}
+
+TEST_F(BloatyTest, DecodeSymbolWithCrate) {
+  auto expected1 = bloaty::DecodeCrateIdResult{.symbol = "foo", .crate = ""};
+  EXPECT_EQ(bloaty::DecodeSymbolWithCrateId("foo"), expected1);
+  auto expected2 = bloaty::DecodeCrateIdResult{.symbol = "foo", .crate = "bar"};
+  EXPECT_EQ(bloaty::DecodeSymbolWithCrateId("foo, in crate bar"), expected2);
+}
diff --git a/tests/link_map_test.cc b/tests/link_map_test.cc
new file mode 100644
index 0000000..e729fcf
--- /dev/null
+++ b/tests/link_map_test.cc
@@ -0,0 +1,971 @@
+// 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 <fstream>
+#include <tuple>
+
+#include "bloaty.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+TEST(TransformCompileUnitForFuchsiaTest, RustLibraryCrate1) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "./exe.unstripped/"
+      "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "[crate: alloc]");
+  ASSERT_EQ(*std::get<1>(*result), "alloc");
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, RustLibraryCrate2) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "./exe.unstripped/"
+      "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o.rcgu."
+      "o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "[crate: component_manager_lib]");
+  ASSERT_EQ(*std::get<1>(*result), "component_manager_lib");
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, RustLibraryCrate3) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "./exe.unstripped/"
+      "component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "[crate: cm_fidl_translator]");
+  ASSERT_EQ(*std::get<1>(*result), "cm_fidl_translator");
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, RustLibraryCrate4) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "./exe.unstripped/"
+      "component_manager.libfidl_fuchsia_io.fidl_fuchsia_io.3a1fbbbh-cgu.0.rcgu.o.rcgu.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "[crate: fidl_fuchsia_io]");
+  ASSERT_EQ(*std::get<1>(*result), "fidl_fuchsia_io");
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, RustBinaryCrate) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "[crate: component_manager]");
+  ASSERT_EQ(*std::get<1>(*result), "component_manager");
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, RustRLibCrate) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "/usr/local/google/home/yifeit/vg/out/default/obj/third_party/rust_crates/"
+      "libregex_syntax-579ced0738b0164d.rlib(libregex_syntax-579ced0738b0164d-579ced0738b0164d."
+      "regex_syntax.c02sfxfu-cgu.13.rcgu.o)");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "[crate: regex_syntax]");
+  ASSERT_EQ(*std::get<1>(*result), "regex_syntax");
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, ZirconLib) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "/usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/"
+      "c/crt1.Scrt1.cc.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "../../zircon/system/ulib/c/Scrt1.cc");
+  ASSERT_EQ(std::get<1>(*result), std::nullopt);
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, Fidling1) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/"
+      "fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result),
+            "fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block.fidl.tables.c");
+  ASSERT_EQ(std::get<1>(*result), std::nullopt);
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, Fidling2) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia/hardware/block/c/"
+      "fuchsia.hardware.block_c_client.fidl.client.c.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result),
+            "fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia/hardware/block/c/fidl.client.c");
+  ASSERT_EQ(std::get<1>(*result), std::nullopt);
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, Fidling3) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "obj/out/default/fidling/gen/sdk/fidl/fuchsia.security.resource/fuchsia/security/resource/"
+      "llcpp/fuchsia.security.resource_llcpp.fidl.cc.o");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(
+      std::get<0>(*result),
+      "fidling/gen/sdk/fidl/fuchsia.security.resource/fuchsia/security/resource/llcpp/fidl.cc");
+  ASSERT_EQ(std::get<1>(*result), std::nullopt);
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, ZirconFidlLib) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia(
+      "obj/zircon/public/lib/fidl_base/libfidl_base.a(libfidl_base.decoding.cc.o)");
+  ASSERT_TRUE(result.has_value());
+  ASSERT_EQ(std::get<0>(*result), "../../zircon/system/ulib/fidl/decoding.cc");
+  ASSERT_EQ(std::get<1>(*result), std::nullopt);
+}
+
+TEST(TransformCompileUnitForFuchsiaTest, PassthroughUnknown) {
+  auto result = bloaty_link_map::TransformCompileUnitForFuchsia("foobar");
+  ASSERT_FALSE(result.has_value());
+}
+
+std::string LoadLinkMapFile(const std::string& name) {
+  std::ifstream infile(name);
+  std::string link_map;
+
+  // Strip comments and empty lines.
+  for (std::string line; getline(infile, line);) {
+    if (line.empty()) continue;
+    if (line[0] == '#') continue;
+    link_map += line;
+    link_map += '\n';
+  }
+
+  absl::StripLeadingAsciiWhitespace(&link_map);
+  absl::StripTrailingAsciiWhitespace(&link_map);
+
+  return link_map;
+}
+
+TEST(LinkMapTest, Empty) {
+  const std::string link_map = "     VMA      LMA     Size Align Out     In      Symbol\n";
+  auto symbols = bloaty_link_map::ParseLldLinkMap(link_map);
+  ASSERT_EQ(symbols.size(), 0);
+}
+
+TEST(LinkMapTest, ExampleCpp) {
+  std::string link_map = LoadLinkMapFile("example_cpp.map");
+  absl::StripLeadingAsciiWhitespace(&link_map);
+  absl::StripTrailingAsciiWhitespace(&link_map);
+
+  auto symbols = bloaty_link_map::ParseLldLinkMap(link_map);
+
+  struct Golden {
+    uint64_t addr;
+    uint64_t size;
+    std::string name;
+    std::string compile_unit;
+    std::string section;
+  };
+  std::vector<Golden> goldens = {
+      {0x4380, 0x14, "main", "obj/zircon/system/uapp/blobfs/blobfs.main.cc.o", ".rodata"},
+      {0x4394, 0x8a18, "** lld merge strings", "", ".rodata"},
+      {0xcdb0, 0xa8, "** lld merge strings", "", ".rodata"},
+      {0xce58, 0xc, "HandlefifonullableTable",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/"
+       "fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c.o",
+       ".rodata"},
+      {0xce64, 0xc, "HandlevmononnullableTable",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/"
+       "fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c.o",
+       ".rodata"},
+      {0xce70, 0xc, "HandleresourcenonnullableTable",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.security.resource/"
+       "fuchsia.security.resource_tables.fuchsia.security.resource.fidl.tables.c.o",
+       ".rodata"},
+      {0xce7c, 0xa, "_ZN5llcpp7fuchsia2io8NodeInfo9reset_ptrEON4fidl12tracking_ptrIvEE",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/fuchsia/io/llcpp/"
+       "fuchsia.io_llcpp.fidl.cc.o",
+       ".rodata"},
+      {0xce86, 0x2b, "fidl::kErrorWriteFailed",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/fuchsia/io/llcpp/"
+       "fuchsia.io_llcpp.fidl.cc.o",
+       ".rodata"},
+      {0xceb4, 0xc, "Request15fuchsia_io_NodenonnullableTable",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/"
+       "fuchsia.io_tables.fuchsia.io.fidl.tables.c.o",
+       ".rodata"},
+      {0xcec0, 0x8, "String4096nonnullableTable",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/"
+       "fuchsia.io_tables.fuchsia.io.fidl.tables.c.o",
+       ".rodata"},
+      {0xd030, 0x100, "** lld merge strings", "", ".rodata"},
+      {0xd130, 0xc, "HandleeventnonnullableTable",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.fs/"
+       "fuchsia.fs_tables.fuchsia.fs.fidl.tables.c.o",
+       ".rodata"},
+      {0xd13c, 0x8, "String32nonnullableTable",
+       "obj/out/default/fidling/gen/sdk/fidl/fuchsia.fs/"
+       "fuchsia.fs_tables.fuchsia.fs.fidl.tables.c.o",
+       ".rodata"},
+      {0xfb38, 0x9c, "LZ4HC_compress_generic_internal.clTable",
+       "obj/zircon/public/lib/lz4/liblz4.a(liblz4.lz4hc.c.o)", ".rodata"},
+      {0xfbd4, 0x10, "XXH32_finalize", "obj/zircon/public/lib/lz4/liblz4.a(liblz4.xxhash.c.o)",
+       ".rodata"},
+      {0xfbe4, 0x27, "kServicePath",
+       "obj/zircon/system/ulib/trace-provider/"
+       "libtrace-provider-with-fdio.a(libtrace-provider-with-fdio.fdio_connect.cc.o)",
+       ".rodata"},
+      {0x21000, 0x14, "_start",
+       "/usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/"
+       "c/crt1.Scrt1.cc.o",
+       ".text"},
+      {0x21014, 0x494, "main", "obj/zircon/system/uapp/blobfs/blobfs.main.cc.o", ".text"},
+      {0x3a04c, 0x8, "non-virtual thunk to blobfs::TransactionManager::~TransactionManager()",
+       "obj/zircon/system/ulib/blobfs/libblobfs.a(libblobfs.blobfs.cc.o)", ".text"},
+      {0x108120, 0xdc, "trace::internal::Session::~Session()",
+       "obj/zircon/system/ulib/trace-provider/"
+       "libtrace-provider-with-fdio.a(libtrace-provider-with-fdio.session.cc.o)",
+       ".text"},
+
+  };
+
+  // for (const auto& sym : symbols) {
+  //   std::cout << std::hex << "{0x" << sym.addr << ", 0x" << sym.size << ", \"" << sym.name
+  //             << "\", \"" << sym.compile_unit << "\", \"" << sym.section << "\"}," << std::endl;
+  // }
+
+  ASSERT_EQ(symbols.size(), goldens.size());
+  for (size_t i = 0; i < symbols.size(); i++) {
+    ASSERT_EQ(symbols[i].addr, goldens[i].addr);
+    ASSERT_EQ(symbols[i].size, goldens[i].size);
+    ASSERT_EQ(symbols[i].name, goldens[i].name);
+    ASSERT_EQ(symbols[i].compile_unit, goldens[i].compile_unit);
+    ASSERT_EQ(symbols[i].section, goldens[i].section);
+  }
+}
+
+TEST(LinkMapTest, ExampleRust) {
+  std::string link_map = LoadLinkMapFile("example_rust.map");
+  absl::StripLeadingAsciiWhitespace(&link_map);
+  absl::StripTrailingAsciiWhitespace(&link_map);
+
+  auto symbols = bloaty_link_map::ParseLldLinkMap(link_map);
+
+  struct Golden {
+    uint64_t addr;
+    uint64_t size;
+    std::string name;
+    std::string compile_unit;
+    std::string section;
+  };
+  std::vector<Golden> goldens = {
+      {0x2800, 0x27, ".Lanon.745d157954bc19b57d1f47812cead630.3",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2828, 0x0, ".Lanon.745d157954bc19b57d1f47812cead630.5",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2828, 0x33, ".Lanon.745d157954bc19b57d1f47812cead630.13",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2885, 0x11, "anon.745d157954bc19b57d1f47812cead630",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2896, 0x3, ".Lanon.745d157954bc19b57d1f47812cead630.23",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2899, 0x16, ".Lanon.745d157954bc19b57d1f47812cead630.26",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x292a, 0xd, "anon.745d157954bc19b57d1f47812cead630",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2937, 0x5, "anon.745d157954bc19b57d1f47812cead630",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x293c, 0x5, "anon.745d157954bc19b57d1f47812cead630",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2950, 0xa90, "** lld merge strings", "", ".rodata"},
+      {0x33e0, 0x29, ".Lanon.256afffd819e3254289014381fd650e5.2",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x346d, 0x0, ".Lanon.256afffd819e3254289014381fd650e5.14",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x3470, 0x38, ".Lanon.256afffd819e3254289014381fd650e5.16",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x34a8, 0x1b08, "** lld merge strings", "", ".rodata"},
+      {0x4fb0, 0x3, ".Lanon.256afffd819e3254289014381fd650e5.18",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x5070, 0x2780, "** lld merge strings", "", ".rodata"},
+      {0x77f0, 0x6, ".Lanon.256afffd819e3254289014381fd650e5.26",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x77f8, 0x118, ".Lanon.256afffd819e3254289014381fd650e5.28",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x7910, 0xc, "anon.256afffd819e3254289014381fd650e5",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x791c, 0x6, "anon.256afffd819e3254289014381fd650e5",
+       "./exe.unstripped/"
+       "component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x7922, 0x10,
+       "_RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeNtCsbDqzXfLQacH_15fidl_fuchsia_"
+       "io11FileRequestECs4fqI2P2rA04_17component_manager",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o", ".rodata"},
+      {0x7950, 0xb1,
+       "_RNvXsc_NtCsbDqzXfLQacH_17fuchsia_component6serverINtB5_9ServiceFsINtNtB5_"
+       "7service10ServiceObjuEENtNtCseXumDWplyBO_12futures_core6stream6Stream9poll_"
+       "nextCs4fqI2P2rA04_17component_manager",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o", ".rodata"},
+      {0x7a08, 0x380, "** lld merge strings", "", ".rodata"},
+      {0x7d88, 0x2b, ".Lanon.dda71605b8fec89c031783be146b7025.1",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o", ".rodata"},
+      {0x8cf1, 0x4e, ".Lanon.8db4583f006f833985ea18d2ab89bab7.1",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.7.rcgu.o", ".rodata"},
+      {0x8d3f, 0x1f, "anon.8db4583f006f833985ea18d2ab89bab7",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.7.rcgu.o", ".rodata"},
+      {0x8d5e, 0x25, ".Lanon.a1c52c190a58d1713a23e7a7bdb0f9c0.2",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.8.rcgu.o", ".rodata"},
+      {0x8d83, 0x7,
+       "_RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeINtNvNtB4_6future14from_"
+       "generator9GenFutureNCNvMNtNtNtCsbDqzXfLQacH_21component_manager_"
+       "lib5model6events6sourceNtB1s_11EventSource3new0EECs4fqI2P2rA04_17component_manager",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o", ".rodata"},
+      {0x8d8a, 0x8,
+       "_RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeINtNvNtB4_6future14from_"
+       "generator9GenFutureNCNvMs2_NtNtCsbDqzXfLQacH_21component_manager_lib5model5realmNtB1v_"
+       "5Realm19lock_resolved_state0EECs4fqI2P2rA04_17component_manager",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o", ".rodata"},
+      {0x8d92, 0x16,
+       "_RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeINtNvNtB4_6future14from_"
+       "generator9GenFutureNCNvMs_NtCsbDqzXfLQacH_21component_manager_lib19builtin_"
+       "environmentNtB1u_18BuiltinEnvironment3new0EECs4fqI2P2rA04_17component_manager",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o", ".rodata"},
+      {0x8da8, 0x4,
+       "_RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeNtCsbDqzXfLQacH_7cm_"
+       "rust10ExposeDeclECs4fqI2P2rA04_17component_manager",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o", ".rodata"},
+      {0x8dac, 0x6,
+       "_RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeNtCsbDqzXfLQacH_7cm_"
+       "rust7UseDeclECs4fqI2P2rA04_17component_manager",
+       "./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o", ".rodata"},
+      {0x91e3, 0x4, "_ZN4core3fmt9Formatter3pad17h9c055cdb18c92cc1E",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x91e7, 0x4, "_ZN4core3fmt9Formatter19pad_formatted_parts17heb4f002989281314E",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x9214, 0x29, "_ZN41_$LT$char$u20$as$u20$core..fmt..Debug$GT$3fmt17hb14158a7386f6b1aE",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x925c, 0x6f,
+       "_ZN61_$LT$core..str..EscapeDebug$u20$as$u20$core..fmt..Display$GT$3fmt17hf1238c3db5de3de5E",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x92d0, 0x0, "anon.6741f024a682c8cb8e59ecab4fc7e9ed.12.llvm.9325873315546439775",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x92d0, 0x1, ".Lanon.6741f024a682c8cb8e59ecab4fc7e9ed.14",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x92d1, 0x1d, ".Lanon.6741f024a682c8cb8e59ecab4fc7e9ed.27",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x9344, 0x28, "core::num::flt2dec::strategy::dragon::POW10::he881f1848ad745b6",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x936c, 0x28, "core::num::flt2dec::strategy::dragon::TWOPOW10::h8ee033ba72605fde",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x9394, 0x8, "core::num::flt2dec::strategy::dragon::POW10TO16::hd008c9b35d67c443",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x939c, 0x10, "core::num::flt2dec::strategy::dragon::POW10TO32::h94bb3ddee75ca692",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x93ac, 0x1c, "core::num::flt2dec::strategy::dragon::POW10TO64::h2607b29da45ca92b",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0xebf8, 0x28, ".Lanon.31de557278eced41e2a6dce0e33ed7ec.9",
+       "./exe.unstripped/"
+       "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0xec20, 0x72, ".Lanon.31de557278eced41e2a6dce0e33ed7ec.15",
+       "./exe.unstripped/"
+       "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0xec92, 0x2e, "anon.31de557278eced41e2a6dce0e33ed7ec",
+       "./exe.unstripped/"
+       "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0xecc0, 0x13, "anon.31de557278eced41e2a6dce0e33ed7ec",
+       "./exe.unstripped/"
+       "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0xecd3, 0x4a, ".Lanon.31de557278eced41e2a6dce0e33ed7ec.34",
+       "./exe.unstripped/"
+       "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0x2e20c, 0xc, ".Lanon.93519e6636017643a07f78e678f4f11e.3501",
+       "./exe.unstripped/"
+       "component_manager.libidna-5c3c4f537fb8313f-5c3c4f537fb8313f.idna.axcknxfw-cgu.15.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0x2e218, 0xc, ".Lanon.93519e6636017643a07f78e678f4f11e.3502",
+       "./exe.unstripped/"
+       "component_manager.libidna-5c3c4f537fb8313f-5c3c4f537fb8313f.idna.axcknxfw-cgu.15.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0x2e224, 0xc, ".Lanon.93519e6636017643a07f78e678f4f11e.3503",
+       "./exe.unstripped/"
+       "component_manager.libidna-5c3c4f537fb8313f-5c3c4f537fb8313f.idna.axcknxfw-cgu.15.rcgu.o."
+       "rcgu.o",
+       ".rodata"},
+      {0x2f4ca, 0x1b, "anon.7cbed09a5c2e2961b1298b053de6d71a",
+       "./exe.unstripped/component_manager.libio_util.io_util.3a1fbbbh-cgu.3.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2f4e5, 0x28, ".Lanon.2dc76abe0f971e71acd588990ffc8513.0",
+       "./exe.unstripped/component_manager.libio_util.io_util.3a1fbbbh-cgu.8.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x2f50d, 0x2b, ".Lanon.6f9bafb4adb671d7ac9f8408e86ff31a.0",
+       "./exe.unstripped/"
+       "component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o",
+       ".rodata"},
+      {0x3aacc, 0x134,
+       "_ZN9libunwind17DwarfInstructionsINS_17LocalAddressSpaceENS_15Registers_"
+       "arm64EE18evaluateExpressionEmRS1_RKS2_m",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".rodata"},
+      {0x3ac00, 0x39,
+       "_ZN9libunwind10CFI_ParserINS_17LocalAddressSpaceEE8parseCIEERS1_mPNS2_8CIE_InfoE",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".rodata"},
+      {0x3ac39, 0xc, "_ZN9libunwind14EHHeaderParserINS_17LocalAddressSpaceEE17getTableEntrySizeEh",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".rodata"},
+      {0xb2640, 0x14, "_start",
+       "/usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/"
+       "c/crt1.Scrt1.cc.o",
+       ".text"},
+      {0xb2654, 0x1c, "_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$::fmt::h12b31caec1d188c4",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0xb2670, 0xb4, "_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$::fmt::h268a568299531efb",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0xcd52c, 0x98,
+       "core::num::_$LT$impl$u20$core..str..FromStr$u20$for$u20$usize$GT$::from_str::"
+       "h4398f35a1c0b4be7",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0xcd5c4, 0xe4,
+       "_$LT$core..num..TryFromIntError$u20$as$u20$core..fmt..Debug$GT$::fmt::h10047a113409f418",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0xcd6a8, 0x34,
+       "core::fmt::float::_$LT$impl$u20$core..fmt..Display$u20$for$u20$f64$GT$::fmt::"
+       "hac033855e28d5eb4",
+       "./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0xe2554, 0x10, "OUTLINED_FUNCTION_359",
+       "./exe.unstripped/"
+       "component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0xe2564, 0x10, "OUTLINED_FUNCTION_360",
+       "./exe.unstripped/"
+       "component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0xeb888, 0x40,
+       "_RINvNtNtCs6QHBvYLDz7W_4core4iter8adapters15process_resultsINtB2_3MapINtNtCsiEMmsMe2HZG_"
+       "5alloc3vec8IntoIterNtNtCsbDqzXfLQacH_7cm_json2cm8ResolverENCNvXs_CsbDqzXfLQacH_18cm_fidl_"
+       "translatorINtB17_3VecB1G_EINtB2q_6CmIntoIB2Z_NtCsbDqzXfLQacH_17fidl_fuchsia_"
+       "sys212ResolverDeclEE7cm_into0EB3x_NtB1K_5ErrorNCINvXsx_NtB6_6resultINtB4V_6ResultB3s_B4A_"
+       "EINtNtNtB4_6traits7collect12FromIteratorIB58_B3x_B4A_EE9from_iterBU_E0B3s_EB2q_",
+       "./exe.unstripped/"
+       "component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.4.rcgu.o.rcgu.o",
+       ".text"},
+      {0x197340, 0x70,
+       "_RINvNtCsevZhND7KDbE_9hashbrown3raw16calculate_layoutTjINtNtCsiEMmsMe2HZG_"
+       "5alloc4sync3ArcDNtNtCsbDqzXfLQacH_13fuchsia_async8executor14PacketReceiverEL_EEEB1t_",
+       "./exe.unstripped/"
+       "component_manager.libfuchsia_async.fuchsia_async.3a1fbbbh-cgu.1.rcgu.o.rcgu.o",
+       ".text"},
+      {0x200800, 0x38,
+       "std::sys_common::thread_info::THREAD_INFO::__getit::__KEY::h836843d10e59cada",
+       "./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0x200840, 0x18,
+       "std::panicking::update_panic_count::PANIC_COUNT::__getit::__KEY::h69785e8fa31a63e6",
+       "./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o",
+       ".text"},
+      {0x1ff610, 0x30, ".Lanon.745d157954bc19b57d1f47812cead630.1",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x1ff640, 0x20, ".Lanon.745d157954bc19b57d1f47812cead630.2",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x1ff660, 0x18, ".Lanon.745d157954bc19b57d1f47812cead630.4",
+       "./exe.unstripped/"
+       "component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x202a88, 0x20, "anon.e57b3f2ae8b5861992cfe341e01c75d6",
+       "./exe.unstripped/"
+       "component_manager.libcm_fidl_validator.cm_fidl_validator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x2110a8, 0x18, ".Lanon.6f9bafb4adb671d7ac9f8408e86ff31a.3",
+       "./exe.unstripped/"
+       "component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x211108, 0x10, "anon.6f9bafb4adb671d7ac9f8408e86ff31a",
+       "./exe.unstripped/"
+       "component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x211118, 0x38, "anon.6f9bafb4adb671d7ac9f8408e86ff31a",
+       "./exe.unstripped/"
+       "component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x211150, 0x20, "anon.54c490f3ebdb496a1657b64fb6de7217",
+       "./exe.unstripped/"
+       "component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.10.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x211170, 0x18, ".Lanon.39cf32991562f2df00749835baa6fdc6.1",
+       "./exe.unstripped/"
+       "component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.11.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x215018, 0x18, "anon.d8e73c4ab2af7c41d0ca8802142f134e",
+       "./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o",
+       ".data.rel.ro"},
+      {0x215030, 0x88,
+       "vtable for libunwind::UnwindCursor<libunwind::LocalAddressSpace, "
+       "libunwind::Registers_arm64>",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".data.rel.ro"},
+      {0x2150b8, 0x310, ".Lswitch.table._ZN9libunwind15Registers_arm6415getRegisterNameEi",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".data.rel.ro"},
+      {0x225848, 0x10, "_RNvCsi1CgODtnl2B_3log6LOGGER",
+       "./exe.unstripped/"
+       "component_manager.liblog-51a8548556518bd3-51a8548556518bd3.log.7ncv9mgr-cgu.0.rcgu.o.rcgu."
+       "o",
+       ".data"},
+      {0x225858, 0x8, "std::thread::ThreadId::new::COUNTER::hb3a413a29c0f81f6",
+       "./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o",
+       ".data"},
+      {0x225860, 0x38, "std::io::stdio::stderr::INSTANCE::hb24941292040ef06",
+       "./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o",
+       ".data"},
+      {0x2258c8, 0x8, "libunwind::DwarfFDECache<libunwind::LocalAddressSpace>::_bufferEnd",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".data"},
+      {0x225c68, 0x20,
+       "_RNvNvNvXs15_CsbDqzXfLQacH_7cm_rustNtBa_13DATA_TYPENAMENtNtNtCs6QHBvYLDz7W_"
+       "4core3ops5deref5Deref5deref11___stability4LAZY",
+       "./exe.unstripped/component_manager.libcm_rust.cm_rust.3a1fbbbh-cgu.0.rcgu.o.rcgu.o",
+       ".bss"},
+      {0x225c88, 0x20,
+       "_RNvNvNvXs17_CsbDqzXfLQacH_7cm_rustNtBa_14CACHE_TYPENAMENtNtNtCs6QHBvYLDz7W_"
+       "4core3ops5deref5Deref5deref11___stability4LAZY",
+       "./exe.unstripped/component_manager.libcm_rust.cm_rust.3a1fbbbh-cgu.0.rcgu.o.rcgu.o",
+       ".bss"},
+      {0x225ca8, 0x20,
+       "_RNvNvNvXs19_CsbDqzXfLQacH_7cm_rustNtBa_13META_TYPENAMENtNtNtCs6QHBvYLDz7W_"
+       "4core3ops5deref5Deref5deref11___stability4LAZY",
+       "./exe.unstripped/component_manager.libcm_rust.cm_rust.3a1fbbbh-cgu.0.rcgu.o.rcgu.o",
+       ".bss"},
+      {0x225cc8, 0x38,
+       "_RNvNvNvXs0_NtNtNtCsbDqzXfLQacH_21component_manager_lib5model6events14source_factoryNtB9_"
+       "25EVENT_SOURCE_SERVICE_PATHNtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___"
+       "stability4LAZY",
+       "./exe.unstripped/"
+       "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.12.rcgu.o."
+       "rcgu.o",
+       ".bss"},
+      {0x225d00, 0x38,
+       "_RNvNvNvXs2_NtNtNtCsbDqzXfLQacH_21component_manager_lib5model6events14source_factoryNtB9_"
+       "30EVENT_SOURCE_SYNC_SERVICE_PATHNtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___"
+       "stability4LAZY",
+       "./exe.unstripped/"
+       "component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.12.rcgu.o."
+       "rcgu.o",
+       ".bss"},
+      {0x2262ac, 0x8, "libunwind::DwarfFDECache<libunwind::LocalAddressSpace>::_lock",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+      {0x2262b8, 0x800, "libunwind::DwarfFDECache<libunwind::LocalAddressSpace>::_initialBuffer",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+      {0x226ab8, 0x1, "logAPIs::checked",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+      {0x226abc, 0x1, "logAPIs::log",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+      {0x226abd, 0x1, "logUnwinding::checked",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+      {0x226ac0, 0x1, "logUnwinding::log",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+      {0x226ac1, 0x1, "logDWARF::checked",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+      {0x226ac4, 0x1, "logDWARF::log",
+       "/tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o)", ".bss"},
+  };
+
+  // for (const auto& sym : symbols) {
+  //   std::cout << std::hex << "{0x" << sym.addr << ", 0x" << sym.size << ", \"" << sym.name
+  //             << "\", \"" << sym.compile_unit << "\", \"" << sym.section << "\"}," << std::endl;
+  // }
+
+  ASSERT_EQ(symbols.size(), goldens.size());
+  for (size_t i = 0; i < symbols.size(); i++) {
+    ASSERT_EQ(symbols[i].addr, goldens[i].addr);
+    ASSERT_EQ(symbols[i].size, goldens[i].size);
+    ASSERT_EQ(symbols[i].name, goldens[i].name);
+    ASSERT_EQ(symbols[i].compile_unit, goldens[i].compile_unit);
+    ASSERT_EQ(symbols[i].section, goldens[i].section);
+  }
+}
+
+TEST(LinkMapTest, SuperSizeGoldens) {
+  std::ifstream infile("test_lld-lto_v1.map");
+  std::string link_map;
+  for (std::string line; getline(infile, line);) {
+    if (line.empty()) continue;
+    if (line[0] == '#') continue;
+    link_map += line;
+    link_map += '\n';
+  }
+
+  absl::StripLeadingAsciiWhitespace(&link_map);
+  absl::StripTrailingAsciiWhitespace(&link_map);
+
+  struct Golden {
+    uint64_t addr;
+    uint64_t size;
+    std::string name;
+    std::string compile_unit;
+    std::string section;
+  };
+  std::vector<Golden> goldens = {
+      {0x213200, 0x4, "v8_Default_embedded_blob_size_", "obj/v8/v8_external_snapshot/embedded.o",
+       ".rodata"},
+      {0x213210, 0x10, "pmmp",
+       "obj/third_party/ffmpeg/libffmpeg_internal.a(ffmpeg_internal/fft_neon.o)", ".rodata"},
+      {0x213220, 0x10, "mppm",
+       "obj/third_party/ffmpeg/libffmpeg_internal.a(ffmpeg_internal/fft_neon.o)", ".rodata"},
+      {0x213230, 0xc0, "std::__ndk1::(anonymous namespace)::small_primes",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(hash.o)",
+       ".rodata"},
+      {0x2132f0, 0xc0, "std::__ndk1::(anonymous namespace)::indices",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(hash.o)",
+       ".rodata"},
+      {0x2133b0, 0x4, "std::__ndk1::ios_base::boolalpha",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133b4, 0x4, "std::__ndk1::ios_base::dec",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133b8, 0x4, "std::__ndk1::ios_base::fixed",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133bc, 0x4, "std::__ndk1::ios_base::hex",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133c0, 0x4, "std::__ndk1::ios_base::internal",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133c4, 0x4, "std::__ndk1::ios_base::left",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133c8, 0x4, "std::__ndk1::ios_base::oct",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133cc, 0x4, "std::__ndk1::ios_base::right",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133d0, 0x4, "std::__ndk1::ios_base::scientific",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133d4, 0x4, "std::__ndk1::ios_base::showbase",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133d8, 0x4, "std::__ndk1::ios_base::showpoint",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133dc, 0x4, "std::__ndk1::ios_base::showpos",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133e0, 0x4, "std::__ndk1::ios_base::skipws",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133e4, 0x4, "std::__ndk1::ios_base::unitbuf",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133e8, 0x4, "std::__ndk1::ios_base::uppercase",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133ec, 0x4, "std::__ndk1::ios_base::adjustfield",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133f0, 0x4, "std::__ndk1::ios_base::basefield",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133f4, 0x4, "std::__ndk1::ios_base::floatfield",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133f8, 0x4, "std::__ndk1::ios_base::badbit",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x2133fc, 0x4, "std::__ndk1::ios_base::eofbit",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213400, 0x4, "std::__ndk1::ios_base::failbit",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213404, 0x4, "std::__ndk1::ios_base::goodbit",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213408, 0x4, "std::__ndk1::ios_base::app",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x21340c, 0x4, "std::__ndk1::ios_base::ate",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213410, 0x4, "std::__ndk1::ios_base::binary",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213414, 0x4, "std::__ndk1::ios_base::in",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213418, 0x4, "std::__ndk1::ios_base::out",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x21341c, 0x4, "std::__ndk1::ios_base::trunc",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213420, 0x1d, "typeinfo name for std::__ndk1::ios_base::failure",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213440, 0x15, "typeinfo name for std::__ndk1::ios_base",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213460, 0x21, "typeinfo name for std::__ndk1::__iostream_category",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x213490, 0x2d, "_ZTSNSt6__ndk19basic_iosIcNS_11char_traitsIcEEEE",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".rodata"},
+      {0x21368b, 0x2c378e, "** lld merge strings", "", ".rodata"},
+      {0x4d6e20, 0x1b, "typeinfo name for std::__ndk1::__stdinbuf<wchar_t>",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(iostream.o)",
+       ".rodata"},
+      {0x4d7920, 0x266c, "** lld merge strings", "", ".rodata"},
+      {0x4d9f90, 0x18, "typeinfo name for std::__ndk1::ctype_base",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(locale.o)",
+       ".rodata"},
+      {0x4d9fb0, 0x1a, "typeinfo name for std::__ndk1::codecvt_base",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(locale.o)",
+       ".rodata"},
+      {0x503eb3, 0x1, "network::mojom::CookieManagerProxy_SetCanonicalCookie_Message::kMessageTag",
+       "", ".rodata"},
+      {0x503eb4, 0x1,
+       "network::mojom::CookieManagerProxy_DeleteCanonicalCookie_Message::kMessageTag", "",
+       ".rodata"},
+      {0x82f000, 0x40, "", "obj/third_party/boringssl/boringssl_asm/chacha-armv4.o", ".text"},
+      {0x82f040, 0x3f0, "ChaCha20_ctr32", "obj/third_party/boringssl/boringssl_asm/chacha-armv4.o",
+       ".text"},
+      {0x82f440, 0x89c, "ChaCha20_neon", "obj/third_party/boringssl/boringssl_asm/chacha-armv4.o",
+       ".text"},
+      {0x82fce0, 0x540, "AES_Te", "obj/third_party/boringssl/boringssl_asm/aes-armv4.o", ".text"},
+      {0x830220, 0x60, "aes_nohw_encrypt", "obj/third_party/boringssl/boringssl_asm/aes-armv4.o",
+       ".text"},
+      {0x830280, 0x1d8, "_armv4_AES_encrypt", "obj/third_party/boringssl/boringssl_asm/aes-armv4.o",
+       ".text"},
+      {0x830460, 0x2a0, "aes_nohw_set_encrypt_key",
+       "obj/third_party/boringssl/boringssl_asm/aes-armv4.o", ".text"},
+      {0x830700, 0x20, "aes_nohw_set_decrypt_key",
+       "obj/third_party/boringssl/boringssl_asm/aes-armv4.o", ".text"},
+      {0x830720, 0x124, "AES_set_enc2dec_key",
+       "obj/third_party/boringssl/boringssl_asm/aes-armv4.o", ".text"},
+      {0x830860, 0x500, "AES_Td", "obj/third_party/boringssl/boringssl_asm/aes-armv4.o", ".text"},
+      {0x830d60, 0x60, "aes_nohw_decrypt", "obj/third_party/boringssl/boringssl_asm/aes-armv4.o",
+       ".text"},
+      {0x830dc0, 0x1f8, "_armv4_AES_decrypt", "obj/third_party/boringssl/boringssl_asm/aes-armv4.o",
+       ".text"},
+      {0x831000, 0x40, "", "obj/third_party/boringssl/boringssl_asm/aesv8-armx32.o", ".text"},
+      {0x831040, 0x218, "aes_hw_set_encrypt_key",
+       "obj/third_party/boringssl/boringssl_asm/aesv8-armx32.o", ".text"},
+      {0x831260, 0x60, "aes_hw_set_decrypt_key",
+       "obj/third_party/boringssl/boringssl_asm/aesv8-armx32.o", ".text"},
+      {0x8312c0, 0x50, "aes_hw_encrypt", "obj/third_party/boringssl/boringssl_asm/aesv8-armx32.o",
+       ".text"},
+      {0x831320, 0x50, "aes_hw_decrypt", "obj/third_party/boringssl/boringssl_asm/aesv8-armx32.o",
+       ".text"},
+      {0x83c640, 0x2e60, "v8_Default_embedded_blob_data_", "obj/v8/v8_external_snapshot/embedded.o",
+       ".text"},
+      {0x83f4a0, 0x320, "Builtins_RecordWrite", "obj/v8/v8_external_snapshot/embedded.o", ".text"},
+      {0x83f7c0, 0x40, "Builtins_AdaptorWithExitFrame", "obj/v8/v8_external_snapshot/embedded.o",
+       ".text"},
+      {0x83f800, 0x40, "Builtins_AdaptorWithBuiltinExitFrame",
+       "obj/v8/v8_external_snapshot/embedded.o", ".text"},
+      {0x83f840, 0x100, "Builtins_ArgumentsAdaptorTrampoline",
+       "obj/v8/v8_external_snapshot/embedded.o", ".text"},
+      {0x83f940, 0x100, "Builtins_CallFunction_ReceiverIsNullOrUndefined",
+       "obj/v8/v8_external_snapshot/embedded.o", ".text"},
+      {0x83fa40, 0x140, "Builtins_CallFunction_ReceiverIsNotNullOrUndefined",
+       "obj/v8/v8_external_snapshot/embedded.o", ".text"},
+      {0x83fb80, 0x180, "Builtins_CallFunction_ReceiverIsAny",
+       "obj/v8/v8_external_snapshot/embedded.o", ".text"},
+      {0x83fd00, 0x137820, "Builtins_CallBoundFunction", "obj/v8/v8_external_snapshot/embedded.o",
+       ".text"},
+      {0x977520, 0x18, "PushAllRegisters",
+       "obj/third_party/blink/renderer/platform/heap/asm/asm/SaveRegisters_arm.o", ".text"},
+      {0x97c834, 0x4dc, "vpx_convolve8_avg_horiz_filter_type1_neon",
+       "obj/third_party/libvpx/libvpx_assembly_arm.a(libvpx_assembly_arm/"
+       "vpx_convolve8_avg_horiz_filter_type1_neon.asm.o)",
+       ".text"},
+      {0x99cad8, 0xfc, "sinh",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libandroid_support.a(e_sinh.o)",
+       ".text"},
+      {0x1401788, 0xa4, "$_21::operator()(FamilyData*, char const*, char const**) const", "",
+       ".text"},
+      {0x1401820, 0x10, "UnlikelyFunc", "", ".text"},
+      {0x1401850, 0x10, "StartUpFunc", "", ".text"},
+      {0x2d4c000, 0x4, "v8_Default_embedded_blob_", "obj/v8/v8_external_snapshot/embedded.o",
+       ".data"},
+      {0x2d4c004, 0x4, "__dso_handle",
+       "../../third_party/android_ndk/platforms/android-16/arch-arm/usr/lib/crtbegin_so.o",
+       ".data"},
+      {0x2d69000, 0x3c, "fft_tab_vfp",
+       "obj/third_party/ffmpeg/libffmpeg_internal.a(ffmpeg_internal/fft_vfp.o)", ".data.rel.ro"},
+      {0x2d6903c, 0x3c, "fft_tab_neon",
+       "obj/third_party/ffmpeg/libffmpeg_internal.a(ffmpeg_internal/fft_neon.o)", ".data.rel.ro"},
+      {0x2d69078, 0x40,
+       "vtable for std::__ndk1::basic_streambuf<wchar_t, std::__ndk1::char_traits<wchar_t> >",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d690b8, 0x28,
+       "vtable for std::__ndk1::basic_istream<char, std::__ndk1::char_traits<char> >",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d690e0, 0x28,
+       "vtable for std::__ndk1::basic_istream<wchar_t, std::__ndk1::char_traits<wchar_t> >",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d69108, 0x28,
+       "vtable for std::__ndk1::basic_ostream<char, std::__ndk1::char_traits<char> >",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d69130, 0x28,
+       "vtable for std::__ndk1::basic_ostream<wchar_t, std::__ndk1::char_traits<wchar_t> >",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d69158, 0x24, "vtable for std::__ndk1::__iostream_category",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d6917c, 0x14, "vtable for std::__ndk1::ios_base::failure",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d69190, 0x10, "vtable for std::__ndk1::ios_base",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d691a0, 0xc, "typeinfo for std::__ndk1::ios_base::failure",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d691ac, 0x8, "typeinfo for std::__ndk1::ios_base",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2d691b4, 0xc, "typeinfo for std::__ndk1::__iostream_category",
+       "../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/"
+       "libc++_static.a(ios.o)",
+       ".data.rel.ro"},
+      {0x2f56000, 0x4, "WebRtcSpl_CrossCorrelation", "", ".bss"},
+      {0x2f56004, 0x4, "WebRtcSpl_DownsampleFast", "", ".bss"},
+      {0x2f56008, 0x4, "WebRtcSpl_MaxAbsValueW16", "", ".bss"},
+      {0x0, 0x1000, "** ", "", ".part.end"},
+  };
+
+  auto symbols = bloaty_link_map::ParseLldLinkMap(link_map);
+  // for (const auto& sym : symbols) {
+  //   std::cout << std::hex << "{0x" << sym.addr << ", 0x" << sym.size << ", \"" << sym.name
+  //             << "\", \"" << sym.compile_unit << "\", \"" << sym.section << "\"}," << std::endl;
+  // }
+
+  ASSERT_EQ(symbols.size(), goldens.size());
+  for (size_t i = 0; i < symbols.size(); i++) {
+    ASSERT_EQ(symbols[i].addr, goldens[i].addr);
+    ASSERT_EQ(symbols[i].size, goldens[i].size);
+    ASSERT_EQ(symbols[i].name, goldens[i].name);
+    ASSERT_EQ(symbols[i].compile_unit, goldens[i].compile_unit);
+    ASSERT_EQ(symbols[i].section, goldens[i].section);
+  }
+}
diff --git a/tests/testdata/link_map/example_cpp.map b/tests/testdata/link_map/example_cpp.map
new file mode 100644
index 0000000..ff80ed1
--- /dev/null
+++ b/tests/testdata/link_map/example_cpp.map
@@ -0,0 +1,103 @@
+             VMA              LMA     Size Align Out     In      Symbol
+             2a8              2a8        8     1 .interp
+             2a8              2a8        8     1         <internal>:(.interp)
+             2b0              2b0       18     4 .note.gnu.build-id
+             2b0              2b0       18     4         <internal>:(.note.gnu.build-id)
+             2c8              2c8     12f0     8 .dynsym
+             2c8              2c8     12f0     8         <internal>:(.dynsym)
+            15b8             15b8       1c     8 .gnu.hash
+            15b8             15b8       1c     8         <internal>:(.gnu.hash)
+            15d8             15d8      1e0     8 .dynamic
+            15d8             15d8      1e0     8         <internal>:(.dynamic)
+            17b8             17b8     143c     1 .dynstr
+            17b8             17b8     143c     1         <internal>:(.dynstr)
+            2bf8             2bf8      540     8 .rela.dyn
+            2bf8             2bf8      540     8         <internal>:(.rela.dyn)
+            3138             3138      150     8 .relr.dyn
+            3138             3138      150     8         <internal>:(.relr.dyn)
+            3288             3288     10f8     8 .rela.plt
+            3288             3288     10f8     8         <internal>:(.rela.plt)
+            4380             4380     b88b    16 .rodata
+            4380             4380       14     1         obj/zircon/system/uapp/blobfs/blobfs.main.cc.o:(.rodata.main)
+            4380             4380        0     1                 $d.1
+            4394             4394     8a18     1         <internal>:(.rodata)
+            cdb0             cdb0       a8     8         <internal>:(.rodata)
+            ce58             ce58        c     4         obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c.o:(.rodata.HandlefifonullableTable)
+            ce58             ce58        0     1                 $d.45
+            ce58             ce58        c     1                 HandlefifonullableTable
+            ce64             ce64        c     4         obj/out/default/fidling/gen/sdk/fidl/fuchsia.hardware.block/fuchsia.hardware.block_tables.fuchsia.hardware.block.fidl.tables.c.o:(.rodata.HandlevmononnullableTable)
+            ce64             ce64        0     1                 $d.46
+            ce64             ce64        c     1                 HandlevmononnullableTable
+            ce70             ce70        c     4         obj/out/default/fidling/gen/sdk/fidl/fuchsia.security.resource/fuchsia.security.resource_tables.fuchsia.security.resource.fidl.tables.c.o:(.rodata.HandleresourcenonnullableTable)
+            ce70             ce70        0     1                 $d.5
+            ce70             ce70        c     1                 HandleresourcenonnullableTable
+            ce7c             ce7c        a     1         obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/fuchsia/io/llcpp/fuchsia.io_llcpp.fidl.cc.o:(.rodata._ZN5llcpp7fuchsia2io8NodeInfo9reset_ptrEON4fidl12tracking_ptrIvEE)
+            ce7c             ce7c        0     1                 $d.1175
+            ce86             ce86       2b     1         obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/fuchsia/io/llcpp/fuchsia.io_llcpp.fidl.cc.o:(.rodata._ZN4fidlL17kErrorWriteFailedE)
+            ce86             ce86        0     1                 $d.1404
+            ce86             ce86       2b     1                 fidl::kErrorWriteFailed
+            ceb4             ceb4        c     4         obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/fuchsia.io_tables.fuchsia.io.fidl.tables.c.o:(.rodata.Request15fuchsia_io_NodenonnullableTable)
+            ceb4             ceb4        0     1                 $d.251
+            ceb4             ceb4        c     1                 Request15fuchsia_io_NodenonnullableTable
+            cec0             cec0        8     4         obj/out/default/fidling/gen/sdk/fidl/fuchsia.io/fuchsia.io_tables.fuchsia.io.fidl.tables.c.o:(.rodata.String4096nonnullableTable)
+            cec0             cec0        0     1                 $d.253
+            cec0             cec0        8     1                 String4096nonnullableTable
+            d030             d030      100    16         <internal>:(.rodata)
+            d130             d130        c     4         obj/out/default/fidling/gen/sdk/fidl/fuchsia.fs/fuchsia.fs_tables.fuchsia.fs.fidl.tables.c.o:(.rodata.HandleeventnonnullableTable)
+            d130             d130        0     1                 $d.22
+            d130             d130        c     1                 HandleeventnonnullableTable
+            d13c             d13c        8     4         obj/out/default/fidling/gen/sdk/fidl/fuchsia.fs/fuchsia.fs_tables.fuchsia.fs.fidl.tables.c.o:(.rodata.String32nonnullableTable)
+            d13c             d13c        0     1                 $d.23
+            d13c             d13c        8     1                 String32nonnullableTable
+            fb38             fb38       9c     4         obj/zircon/public/lib/lz4/liblz4.a(liblz4.lz4hc.c.o):(.rodata.LZ4HC_compress_generic_internal.clTable)
+            fb38             fb38        0     1                 $d.39
+            fb38             fb38       9c     1                 LZ4HC_compress_generic_internal.clTable
+            fbd4             fbd4       10     1         obj/zircon/public/lib/lz4/liblz4.a(liblz4.xxhash.c.o):(.rodata.XXH32_finalize)
+            fbd4             fbd4        0     1                 $d.22
+            fbe4             fbe4       27     1         obj/zircon/system/ulib/trace-provider/libtrace-provider-with-fdio.a(libtrace-provider-with-fdio.fdio_connect.cc.o):(.rodata._ZL12kServicePath)
+            fbe4             fbe4        0     1                 $d.1
+            fbe4             fbe4       27     1                 kServicePath
+            fc0c             fc0c     361c     4 .eh_frame_hdr
+            fc0c             fc0c     361c     4         <internal>:(.eh_frame_hdr)
+           13228            13228     d63c     8 .eh_frame
+           13228            13228       14     1         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.eh_frame+0x0)
+           13260            13260       54     1         obj/zircon/system/uapp/blobfs/blobfs.main.cc.o:(.eh_frame+0x14)
+           13320            13320       2c     1         obj/out/default/fidling/gen/sdk/fidl/fuchsia.security.resource/fuchsia/security/resource/llcpp/fuchsia.security.resource_llcpp.fidl.cc.o:(.eh_frame+0x34)
+           207d8            207d8       1c     1         obj/zircon/system/ulib/trace-provider/libtrace-provider-with-fdio.a(libtrace-provider-with-fdio.utils.cc.o):(.eh_frame+0x14)
+           20818            20818       14     1         ../../out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/sync/libsync.a(sync._sources.completion.c.o):(.eh_frame+0x54)
+           20848            20848       14     1         ../../prebuilt/third_party/clang/linux-x64/lib/clang/11.0.0/lib/aarch64-unknown-fuchsia/libclang_rt.builtins.a(udivmodti4.c.obj):(.eh_frame+0x14)
+           21000            21000    e84f0     8 .text
+           21000            21000       14     8         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.text._start)
+           21000            21000        0     1                 $x.0
+           21000            21000       14     1                 _start
+           21014            21014      494     4         obj/zircon/system/uapp/blobfs/blobfs.main.cc.o:(.text.main)
+           21014            21014        0     1                 $x.0
+           21014            21014      494     1                 main
+           3a04c            3a04c        8     4         obj/zircon/system/ulib/blobfs/libblobfs.a(libblobfs.blobfs.cc.o):(.text._ZThn8_N6blobfs18TransactionManagerD1Ev)
+           3a04c            3a04c        0     1                 $x.83
+           3a04c            3a04c        8     1                 non-virtual thunk to blobfs::TransactionManager::~TransactionManager()
+          108120           108120       dc     4         obj/zircon/system/ulib/trace-provider/libtrace-provider-with-fdio.a(libtrace-provider-with-fdio.session.cc.o):(.text._ZN5trace8internal7SessionD2Ev)
+          108120           108120        0     1                 $x.1
+          108120           108120       dc     1                 trace::internal::Session::~Session()
+               0                0   d41f13     1 .debug_loc
+               0                0       39     1         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.debug_loc)
+               0                0        0     1                 $d.1         108120           108120       dc     1                 trace::internal::Session::~Session()
+          d411ac           d411ac      458     1         ../../out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/sync/libsync.a(sync._sources.completion.c.o):(.debug_loc)
+          d411ac           d411ac        0     1                 $d.6
+          d41604           d41604       50     1         ../../prebuilt/third_party/clang/linux-x64/lib/clang/11.0.0/lib/aarch64-unknown-fuchsia/libclang_rt.builtins.a(udivti3.c.obj):(.debug_loc)
+          d41604           d41604        0     1                 $d.1
+          d41654           d41654      8bf     1         ../../prebuilt/third_party/clang/linux-x64/lib/clang/11.0.0/lib/aarch64-unknown-fuchsia/libclang_rt.builtins.a(udivmodti4.c.obj):(.debug_loc)
+          d41654           d41654        0     1                 $d.1
+               0                0    52a34     1 .debug_abbrev
+               0                0       6d     1         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.debug_abbrev)
+               0                0        0     1                 $d.2
+             7fa              7fa      93f     1         obj/out/default/fidling/gen/sdk/fidl/fuchsia.security.resource/fuchsia/security/resource/llcpp/fuchsia.security.resource_llcpp.fidl.cc.o:(.debug_abbrev)
+             7fa              7fa        0     1                 $d.40
+          3323a0           3323a0       30     1         ../../prebuilt/third_party/clang/linux-x64/lib/clang/11.0.0/lib/aarch64-unknown-fuchsia/libclang_rt.builtins.a(udivmodti4.c.obj):(.debug_ranges)
+          3323a0           3323a0        0     1                 $d.4
+               0                0    2ed88     8 .symtab
+               0                0    2ed88     8         <internal>:(.symtab)
+               0                0      127     1 .shstrtab
+               0                0      127     1         <internal>:(.shstrtab)
+               0                0    3b51c     1 .strtab
+               0                0    3b51c     1         <internal>:(.strtab)
diff --git a/tests/testdata/link_map/example_rust.map b/tests/testdata/link_map/example_rust.map
new file mode 100644
index 0000000..fda29bc
--- /dev/null
+++ b/tests/testdata/link_map/example_rust.map
@@ -0,0 +1,272 @@
+             VMA              LMA     Size Align Out     In      Symbol
+             2e0              2e0        8     1 .interp
+             2e0              2e0        8     1         <internal>:(.interp)
+             2e8              2e8       18     4 .note.gnu.build-id
+             2e8              2e8       18     4         <internal>:(.note.gnu.build-id)
+             300              300      ac8     8 .dynsym
+             300              300      ac8     8         <internal>:(.dynsym)
+             dc8              dc8       1c     8 .gnu.hash
+             dc8              dc8       1c     8         <internal>:(.gnu.hash)
+             de8              de8      170     8 .dynamic
+             de8              de8      170     8         <internal>:(.dynamic)
+             f58              f58      821     1 .dynstr
+             f58              f58      821     1         <internal>:(.dynstr)
+            1780             1780       48     8 .rela.dyn
+            1780             1780       48     8         <internal>:(.rela.dyn)
+            17c8             17c8      5b8     8 .relr.dyn
+            17c8             17c8      5b8     8         <internal>:(.relr.dyn)
+            1d80             1d80      a80     8 .rela.plt
+            1d80             1d80      a80     8         <internal>:(.rela.plt)
+            2800             2800    38445    16 .rodata
+            2800             2800       27     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.745d157954bc19b57d1f47812cead630.3)
+            2800             2800        0     1                 $d.27
+            2828             2828        0     8         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.745d157954bc19b57d1f47812cead630.5)
+            2828             2828       33     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.745d157954bc19b57d1f47812cead630.13)
+            2828             2828        0     1                 $d.30
+            2885             2885       11     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata.anon.745d157954bc19b57d1f47812cead630.19.llvm.1460520995096272372)
+            2885             2885        0     1                 $d.34
+            2885             2885       11     1                 anon.745d157954bc19b57d1f47812cead630.19.llvm.1460520995096272372
+            2896             2896        3     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.745d157954bc19b57d1f47812cead630.23)
+            2896             2896        0     1                 $d.36
+            2899             2899       16     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.745d157954bc19b57d1f47812cead630.26)
+            2899             2899        0     1                 $d.37
+            292a             292a        d     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata.anon.745d157954bc19b57d1f47812cead630.56.llvm.1460520995096272372)
+            292a             292a        0     1                 $d.57
+            292a             292a        d     1                 anon.745d157954bc19b57d1f47812cead630.56.llvm.1460520995096272372
+            2937             2937        5     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata.anon.745d157954bc19b57d1f47812cead630.57.llvm.1460520995096272372)
+            2937             2937        0     1                 $d.58
+            2937             2937        5     1                 anon.745d157954bc19b57d1f47812cead630.57.llvm.1460520995096272372
+            293c             293c        5     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.rodata.anon.745d157954bc19b57d1f47812cead630.59.llvm.1460520995096272372)
+            293c             293c        0     1                 $d.60
+            293c             293c        5     1                 anon.745d157954bc19b57d1f47812cead630.59.llvm.1460520995096272372
+            2950             2950      a90    16         <internal>:(.rodata)
+            33e0             33e0       29     1         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.256afffd819e3254289014381fd650e5.2)
+            33e0             33e0        0     1                 $d.20
+            346d             346d        0     1         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.256afffd819e3254289014381fd650e5.14)
+            3470             3470       38     8         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.256afffd819e3254289014381fd650e5.16)
+            3470             3470        0     1                 $d.25
+            34a8             34a8     1b08     4         <internal>:(.rodata)
+            4fb0             4fb0        3     1         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.256afffd819e3254289014381fd650e5.18)
+            4fb0             4fb0        0     1                 $d.27
+            5070             5070     2780     8         <internal>:(.rodata)
+            77f0             77f0        6     1         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.256afffd819e3254289014381fd650e5.26)
+            77f0             77f0        0     1                 $d.34
+            77f8             77f8      118     8         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.256afffd819e3254289014381fd650e5.28)
+            77f8             77f8        0     1                 $d.36
+            7910             7910        c     1         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata.anon.256afffd819e3254289014381fd650e5.29.llvm.13894642946385378479)
+            7910             7910        0     1                 $d.37
+            7910             7910        c     1                 anon.256afffd819e3254289014381fd650e5.29.llvm.13894642946385378479
+            791c             791c        6     1         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.rodata.anon.256afffd819e3254289014381fd650e5.30.llvm.13894642946385378479)
+            791c             791c        0     1                 $d.38
+            791c             791c        6     1                 anon.256afffd819e3254289014381fd650e5.30.llvm.13894642946385378479
+            7922             7922       10     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o:(.rodata._RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeNtCsbDqzXfLQacH_15fidl_fuchsia_io11FileRequestECs4fqI2P2rA04_17component_manager)
+            7922             7922        0     1                 $d.13
+            7950             7950       b1     2         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o:(.rodata._RNvXsc_NtCsbDqzXfLQacH_17fuchsia_component6serverINtB5_9ServiceFsINtNtB5_7service10ServiceObjuEENtNtCseXumDWplyBO_12futures_core6stream6Stream9poll_nextCs4fqI2P2rA04_17component_manager)
+            7950             7950        0     1                 $d.44
+            7a08             7a08      380     8         <internal>:(.rodata)
+            7d88             7d88       2b     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.0.rcgu.o:(.rodata..Lanon.dda71605b8fec89c031783be146b7025.1)
+            7d88             7d88        0     1                 $d.101
+            8cf1             8cf1       4e     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.7.rcgu.o:(.rodata..Lanon.8db4583f006f833985ea18d2ab89bab7.1)
+            8cf1             8cf1        0     1                 $d.47
+            8d3f             8d3f       1f     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.7.rcgu.o:(.rodata.anon.8db4583f006f833985ea18d2ab89bab7.4.llvm.7664684806704912226)
+            8d3f             8d3f        0     1                 $d.50
+            8d3f             8d3f       1f     1                 anon.8db4583f006f833985ea18d2ab89bab7.4.llvm.7664684806704912226
+            8d5e             8d5e       25     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.8.rcgu.o:(.rodata..Lanon.a1c52c190a58d1713a23e7a7bdb0f9c0.2)
+            8d5e             8d5e        0     1                 $d.17
+            8d83             8d83        7     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o:(.rodata._RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeINtNvNtB4_6future14from_generator9GenFutureNCNvMNtNtNtCsbDqzXfLQacH_21component_manager_lib5model6events6sourceNtB1s_11EventSource3new0EECs4fqI2P2rA04_17component_manager)
+            8d83             8d83        0     1                 $d.56
+            8d8a             8d8a        8     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o:(.rodata._RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeINtNvNtB4_6future14from_generator9GenFutureNCNvMs2_NtNtCsbDqzXfLQacH_21component_manager_lib5model5realmNtB1v_5Realm19lock_resolved_state0EECs4fqI2P2rA04_17component_manager)
+            8d8a             8d8a        0     1                 $d.59
+            8d92             8d92       16     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o:(.rodata._RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeINtNvNtB4_6future14from_generator9GenFutureNCNvMs_NtCsbDqzXfLQacH_21component_manager_lib19builtin_environmentNtB1u_18BuiltinEnvironment3new0EECs4fqI2P2rA04_17component_manager)
+            8d92             8d92        0     1                 $d.64
+            8da8             8da8        4     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o:(.rodata._RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeNtCsbDqzXfLQacH_7cm_rust10ExposeDeclECs4fqI2P2rA04_17component_manager)
+            8da8             8da8        0     1                 $d.74
+            8dac             8dac        6     1         ./exe.unstripped/component_manager.component_manager.7rcbfp3g-cgu.9.rcgu.o:(.rodata._RINvNtCs6QHBvYLDz7W_4core3ptr13drop_in_placeNtCsbDqzXfLQacH_7cm_rust7UseDeclECs4fqI2P2rA04_17component_manager)
+            8dac             8dac        0     1                 $d.81
+            91e3             91e3        4     1         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN4core3fmt9Formatter3pad17h9c055cdb18c92cc1E)
+            91e3             91e3        0     1                 $d.54
+            91e7             91e7        4     1         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN4core3fmt9Formatter19pad_formatted_parts17heb4f002989281314E)
+            91e7             91e7        0     1                 $d.56
+            9214             9214       29     1         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN41_$LT$char$u20$as$u20$core..fmt..Debug$GT$3fmt17hb14158a7386f6b1aE)
+            9214             9214        0     1                 $d.73
+            925c             925c       6f     1         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN61_$LT$core..str..EscapeDebug$u20$as$u20$core..fmt..Display$GT$3fmt17hf1238c3db5de3de5E)
+            925c             925c        0     1                 $d.130
+            92d0             92d0        0     8         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata.anon.6741f024a682c8cb8e59ecab4fc7e9ed.12.llvm.9325873315546439775)
+            92d0             92d0        0     1                 anon.6741f024a682c8cb8e59ecab4fc7e9ed.12.llvm.9325873315546439775
+            92d0             92d0        1     1         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.6741f024a682c8cb8e59ecab4fc7e9ed.14)
+            92d0             92d0        0     1                 $d.133
+            92d1             92d1       1d     1         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata..Lanon.6741f024a682c8cb8e59ecab4fc7e9ed.27)
+            92d1             92d1        0     1                 $d.134
+            9344             9344       28     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN4core3num7flt2dec8strategy6dragon5POW1017he881f1848ad745b6E)
+            9344             9344        0     1                 $d.142
+            9344             9344       28     1                 core::num::flt2dec::strategy::dragon::POW10::he881f1848ad745b6
+            936c             936c       28     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN4core3num7flt2dec8strategy6dragon8TWOPOW1017h8ee033ba72605fdeE)
+            936c             936c        0     1                 $d.143
+            936c             936c       28     1                 core::num::flt2dec::strategy::dragon::TWOPOW10::h8ee033ba72605fde
+            9394             9394        8     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN4core3num7flt2dec8strategy6dragon9POW10TO1617hd008c9b35d67c443E)
+            9394             9394        0     1                 $d.144
+            9394             9394        8     1                 core::num::flt2dec::strategy::dragon::POW10TO16::hd008c9b35d67c443
+            939c             939c       10     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN4core3num7flt2dec8strategy6dragon9POW10TO3217h94bb3ddee75ca692E)
+            939c             939c        0     1                 $d.145
+            939c             939c       10     1                 core::num::flt2dec::strategy::dragon::POW10TO32::h94bb3ddee75ca692
+            93ac             93ac       1c     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.rodata._ZN4core3num7flt2dec8strategy6dragon9POW10TO6417h2607b29da45ca92bE)
+            93ac             93ac        0     1                 $d.146
+            93ac             93ac       1c     1                 core::num::flt2dec::strategy::dragon::POW10TO64::h2607b29da45ca92b
+            ebf8             ebf8       28     1         ./exe.unstripped/component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o.rcgu.o:(.rodata..Lanon.31de557278eced41e2a6dce0e33ed7ec.9)
+            ebf8             ebf8        0     1                 $d.392
+            ec20             ec20       1c     1         ./exe.unstripped/component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o.rcgu.o:(.rodata..Lanon.31de557278eced41e2a6dce0e33ed7ec.15)
+            ec20             ec20        0     1                 $d.393
+            ec92             ec92       2e     1                 anon.31de557278eced41e2a6dce0e33ed7ec.20.llvm.11955293494338827311
+            ecc0             ecc0       13     1         ./exe.unstripped/component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o.rcgu.o:(.rodata.anon.31de557278eced41e2a6dce0e33ed7ec.33.llvm.13709288095524858756)
+            ecc0             ecc0        0     1                 $d.400
+            ecc0             ecc0       13     1                 anon.31de557278eced41e2a6dce0e33ed7ec.33.llvm.13709288095524858756
+            ecd3             ecd3       4a     1         ./exe.unstripped/component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.2.rcgu.o.rcgu.o:(.rodata..Lanon.31de557278eced41e2a6dce0e33ed7ec.34)
+            ecd3             ecd3        0     1                 $d.401
+           2e20c            2e20c        c     4         ./exe.unstripped/component_manager.libidna-5c3c4f537fb8313f-5c3c4f537fb8313f.idna.axcknxfw-cgu.15.rcgu.o.rcgu.o:(.rodata..Lanon.93519e6636017643a07f78e678f4f11e.3501)
+           2e20c            2e20c        0     1                 $d.656
+           2e218            2e218        c     4         ./exe.unstripped/component_manager.libidna-5c3c4f537fb8313f-5c3c4f537fb8313f.idna.axcknxfw-cgu.15.rcgu.o.rcgu.o:(.rodata..Lanon.93519e6636017643a07f78e678f4f11e.3502)
+           2e218            2e218        0     1                 $d.657
+           2e224            2e224        c     4         ./exe.unstripped/component_manager.libidna-5c3c4f537fb8313f-5c3c4f537fb8313f.idna.axcknxfw-cgu.15.rcgu.o.rcgu.o:(.rodata..Lanon.93519e6636017643a07f78e678f4f11e.3503)
+           2e224            2e224        0     1                 $d.658
+           2f4ca            2f4ca       1b     1         ./exe.unstripped/component_manager.libio_util.io_util.3a1fbbbh-cgu.3.rcgu.o.rcgu.o:(.rodata.anon.7cbed09a5c2e2961b1298b053de6d71a.14.llvm.4649089991547526960)
+           2f4ca            2f4ca        0     1                 $d.33
+           2f4ca            2f4ca       1b     1                 anon.7cbed09a5c2e2961b1298b053de6d71a.14.llvm.4649089991547526960
+           2f4e5            2f4e5       28     1         ./exe.unstripped/component_manager.libio_util.io_util.3a1fbbbh-cgu.8.rcgu.o.rcgu.o:(.rodata..Lanon.2dc76abe0f971e71acd588990ffc8513.0)
+           2f4e5            2f4e5        0     1                 $d.16
+           2f50d            2f50d       2b     1         ./exe.unstripped/component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o:(.rodata..Lanon.6f9bafb4adb671d7ac9f8408e86ff31a.0)
+           2f50d            2f50d        0     1                 $d.8
+           3aacc            3aacc      134     2         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.rodata._ZN9libunwind17DwarfInstructionsINS_17LocalAddressSpaceENS_15Registers_arm64EE18evaluateExpressionEmRS1_RKS2_m)
+           3aacc            3aacc        0     1                 $d.44
+           3ac00            3ac00       39     1         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.rodata._ZN9libunwind10CFI_ParserINS_17LocalAddressSpaceEE8parseCIEERS1_mPNS2_8CIE_InfoE)
+           3ac00            3ac00        0     1                 $d.51
+           3ac39            3ac39        c     1         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.rodata._ZN9libunwind14EHHeaderParserINS_17LocalAddressSpaceEE17getTableEntrySizeEh)
+           3ac39            3ac39        0     1                 $d.53
+           3ac45            3ac45       22     1 .debug_gdb_scripts
+           3ac45            3ac45       22     1         <internal>:(.debug_gdb_scripts)
+           3ac68            3ac68    17624     4 .eh_frame_hdr
+           3ac68            3ac68    17624     4         <internal>:(.eh_frame_hdr)
+           52290            52290    503ac     8 .eh_frame
+           52290            52290       14     1         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.eh_frame+0x0)
+           522a8            522a8       1c     1         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.eh_frame+0x14)
+           522c8            522c8       14     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.eh_frame+0x14)
+           522e0            522e0       34     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.eh_frame+0x28)
+           a2620            a2620       14     1         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.eh_frame+0x5ec)
+           b2640            b2640   13c78c     8 .text
+           b2640            b2640       14     8         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.text._start)
+           b2640            b2640        0     1                 $x.0
+           b2640            b2640       14     1                 _start
+           b2654            b2654       1c     4         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.text._ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17h12b31caec1d188c4E)
+           b2654            b2654        0     1                 $x.0
+           b2654            b2654       1c     1                 _$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$::fmt::h12b31caec1d188c4
+           b2670            b2670       b4     4         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.text._ZN42_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$3fmt17h268a568299531efbE)
+           b2670            b2670        0     1                 $x.1
+           b2670            b2670       b4     1                 _$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$::fmt::h268a568299531efb
+           cd52c            cd52c       98     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.text._ZN4core3num52_$LT$impl$u20$core..str..FromStr$u20$for$u20$u64$GT$8from_str17he51bc12ed2f79990E)
+           cd52c            cd52c        0     1                 $x.102
+           cd52c            cd52c       98     1                 core::num::_$LT$impl$u20$core..str..FromStr$u20$for$u20$u64$GT$::from_str::he51bc12ed2f79990
+           cd52c            cd52c       98     1                 core::num::_$LT$impl$u20$core..str..FromStr$u20$for$u20$usize$GT$::from_str::h4398f35a1c0b4be7
+           cd5c4            cd5c4       e4     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.text._ZN63_$LT$core..num..TryFromIntError$u20$as$u20$core..fmt..Debug$GT$3fmt17h10047a113409f418E)
+           cd5c4            cd5c4        0     1                 $x.103
+           cd5c4            cd5c4       e4     1                 _$LT$core..num..TryFromIntError$u20$as$u20$core..fmt..Debug$GT$::fmt::h10047a113409f418
+           cd6a8            cd6a8       34     4         ./exe.unstripped/component_manager.core-6a4b1e7af979d229.core.3ocx6h1n-cgu.0.rcgu.o.rcgu.o:(.text._ZN4core3fmt5float52_$LT$impl$u20$core..fmt..Display$u20$for$u20$f64$GT$3fmt17hac033855e28d5eb4E)
+           cd6a8            cd6a8        0     1                 $x.104
+           cd6a8            cd6a8       34     1                 core::fmt::float::_$LT$impl$u20$core..fmt..Display$u20$for$u20$f64$GT$::fmt::hac033855e28d5eb4
+           e2554            e2554       10     4         ./exe.unstripped/component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o:(.text.OUTLINED_FUNCTION_359)
+           e2554            e2554        0     1                 $x.752
+           e2554            e2554       10     1                 OUTLINED_FUNCTION_359
+           e2564            e2564       10     4         ./exe.unstripped/component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o:(.text.OUTLINED_FUNCTION_360)
+           e2564            e2564        0     1                 $x.753
+           e2564            e2564       10     1                 OUTLINED_FUNCTION_360
+           eb888            eb888       40     4         ./exe.unstripped/component_manager.libcm_fidl_translator.cm_fidl_translator.3a1fbbbh-cgu.4.rcgu.o.rcgu.o:(.text._RINvNtNtCs6QHBvYLDz7W_4core4iter8adapters15process_resultsINtB2_3MapINtNtCsiEMmsMe2HZG_5alloc3vec8IntoIterNtNtCsbDqzXfLQacH_7cm_json2cm8ResolverENCNvXs_CsbDqzXfLQacH_18cm_fidl_translatorINtB17_3VecB1G_EINtB2q_6CmIntoIB2Z_NtCsbDqzXfLQacH_17fidl_fuchsia_sys212ResolverDeclEE7cm_into0EB3x_NtB1K_5ErrorNCINvXsx_NtB6_6resultINtB4V_6ResultB3s_B4A_EINtNtNtB4_6traits7collect12FromIteratorIB58_B3x_B4A_EE9from_iterBU_E0B3s_EB2q_)
+           eb888            eb888        0     1                 $x.15
+           eb888            eb888       40     1                 _RINvNtNtCs6QHBvYLDz7W_4core4iter8adapters15process_resultsINtB2_3MapINtNtCsiEMmsMe2HZG_5alloc3vec8IntoIterNtNtCsbDqzXfLQacH_7cm_json2cm8ResolverENCNvXs_CsbDqzXfLQacH_18cm_fidl_translatorINtB17_3VecB1G_EINtB2q_6CmIntoIB2Z_NtCsbDqzXfLQacH_17fidl_fuchsia_sys212ResolverDeclEE7cm_into0EB3x_NtB1K_5ErrorNCINvXsx_NtB6_6resultINtB4V_6ResultB3s_B4A_EINtNtNtB4_6traits7collect12FromIteratorIB58_B3x_B4A_EE9from_iterBU_E0B3s_EB2q_
+          197340           197340       70     4         ./exe.unstripped/component_manager.libfuchsia_async.fuchsia_async.3a1fbbbh-cgu.1.rcgu.o.rcgu.o:(.text._RINvNtCsevZhND7KDbE_9hashbrown3raw16calculate_layoutTjINtNtCsiEMmsMe2HZG_5alloc4sync3ArcDNtNtCsbDqzXfLQacH_13fuchsia_async8executor14PacketReceiverEL_EEEB1t_)
+          197340           197340        0     1                 $x.6
+          197340           197340       70     1                 _RINvNtCsevZhND7KDbE_9hashbrown3raw16calculate_layoutTjINtNtCsiEMmsMe2HZG_5alloc4sync3ArcDNtNtCsbDqzXfLQacH_13fuchsia_async8executor14PacketReceiverEL_EEEB1t_
+          200800           200800       38    32         ./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o:(.tbss._ZN3std10sys_common11thread_info11THREAD_INFO7__getit5__KEY17h836843d10e59cadaE.llvm.8019338592007173243)
+            1300             1300       38     1                 std::sys_common::thread_info::THREAD_INFO::__getit::__KEY::h836843d10e59cada (.llvm.8019338592007173243)
+          200800           200800        0     1                 $d.517
+          200840           200840       18    16         ./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o:(.tbss._ZN3std9panicking18update_panic_count11PANIC_COUNT7__getit5__KEY17h69785e8fa31a63e6E.llvm.8019338592007173243)
+            1340             1340       18     1                 std::panicking::update_panic_count::PANIC_COUNT::__getit::__KEY::h69785e8fa31a63e6 (.llvm.8019338592007173243)
+          200840           200840        0     1                 $d.557
+          1ff610           1ff610    15db8     8 .data.rel.ro
+          1ff610           1ff610       30     8         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.data.rel.ro..Lanon.745d157954bc19b57d1f47812cead630.1)
+          1ff610           1ff610        0     1                 $d.25
+          1ff640           1ff640       20     8         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.data.rel.ro..Lanon.745d157954bc19b57d1f47812cead630.2)
+          1ff640           1ff640        0     1                 $d.26
+          1ff660           1ff660       18     8         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.data.rel.ro..Lanon.745d157954bc19b57d1f47812cead630.4)
+          1ff660           1ff660        0     1                 $d.28
+          202a88           202a88       20     8         ./exe.unstripped/component_manager.libcm_fidl_validator.cm_fidl_validator.3a1fbbbh-cgu.0.rcgu.o.rcgu.o:(.data.rel.ro.anon.e57b3f2ae8b5861992cfe341e01c75d6.1.llvm.9882057474999025597)
+          202a88           202a88        0     1                 $d.287
+          202a88           202a88       20     1                 anon.e57b3f2ae8b5861992cfe341e01c75d6.1.llvm.9882057474999025597
+          2110a8           2110a8       18     8         ./exe.unstripped/component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o:(.data.rel.ro..Lanon.6f9bafb4adb671d7ac9f8408e86ff31a.3)
+          2110a8           2110a8        0     1                 $d.10
+          211108           211108       10     8         ./exe.unstripped/component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o:(.data.rel.ro.anon.6f9bafb4adb671d7ac9f8408e86ff31a.9.llvm.14732610969729869220)
+          211108           211108        0     1                 $d.16
+          211108           211108       10     1                 anon.6f9bafb4adb671d7ac9f8408e86ff31a.9.llvm.14732610969729869220
+          211118           211118       38     8         ./exe.unstripped/component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.1.rcgu.o.rcgu.o:(.data.rel.ro.anon.6f9bafb4adb671d7ac9f8408e86ff31a.12.llvm.14732610969729869220)
+          211118           211118        0     1                 $d.19
+          211118           211118       38     1                 anon.6f9bafb4adb671d7ac9f8408e86ff31a.12.llvm.14732610969729869220
+          211150           211150       20     8         ./exe.unstripped/component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.10.rcgu.o.rcgu.o:(.data.rel.ro.anon.54c490f3ebdb496a1657b64fb6de7217.0.llvm.16392173952262690328)
+          211150           211150        0     1                 $d.7
+          211150           211150       20     1                 anon.54c490f3ebdb496a1657b64fb6de7217.0.llvm.16392173952262690328
+          211170           211170       18     8         ./exe.unstripped/component_manager.liblibrary_loader.library_loader.3a1fbbbh-cgu.11.rcgu.o.rcgu.o:(.data.rel.ro..Lanon.39cf32991562f2df00749835baa6fdc6.1)
+          211170           211170        0     1                 $d.19
+          215018           215018       18     8         ./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o:(.data.rel.ro.anon.d8e73c4ab2af7c41d0ca8802142f134e.759.llvm.8019338592007173243)
+          215018           215018        0     1                 $d.556
+          215018           215018       18     1                 anon.d8e73c4ab2af7c41d0ca8802142f134e.759.llvm.8019338592007173243
+          215030           215030       88     8         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.data.rel.ro._ZTVN9libunwind12UnwindCursorINS_17LocalAddressSpaceENS_15Registers_arm64EEE)
+          215030           215030        0     1                 $d.58
+          215030           215030       88     1                 vtable for libunwind::UnwindCursor<libunwind::LocalAddressSpace, libunwind::Registers_arm64>
+          2150b8           2150b8      310     8         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.data.rel.ro..Lswitch.table._ZN9libunwind15Registers_arm6415getRegisterNameEi)
+          2150b8           2150b8        0     1                 $d.64
+          2153c8           2153c8      480     8 .got
+          2153c8           2153c8      480     8         <internal>:(.got)
+          225848           225848       88     8 .data
+          225848           225848       10     8         ./exe.unstripped/component_manager.liblog-51a8548556518bd3-51a8548556518bd3.log.7ncv9mgr-cgu.0.rcgu.o.rcgu.o:(.data._RNvCsi1CgODtnl2B_3log6LOGGER.llvm.13428694738827280366)
+          225848           225848        0     1                 $d.13
+          225848           225848       10     1                 _RNvCsi1CgODtnl2B_3log6LOGGER.llvm.13428694738827280366
+          225858           225858        8     8         ./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o:(.data._ZN3std6thread8ThreadId3new7COUNTER17hb3a413a29c0f81f6E)
+          225858           225858        0     1                 $d.306
+          225858           225858        8     1                 std::thread::ThreadId::new::COUNTER::hb3a413a29c0f81f6
+          225860           225860       38     8         ./exe.unstripped/component_manager.std-7a72ffd2f3a7a812.std.a0uwjzoe-cgu.0.rcgu.o.rcgu.o:(.data._ZN3std2io5stdio6stderr8INSTANCE17hb24941292040ef06E)
+          225860           225860        0     1                 $d.363
+          225860           225860       38     1                 std::io::stdio::stderr::INSTANCE::hb24941292040ef06
+          2258c8           2258c8        8     8         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.data._ZN9libunwind13DwarfFDECacheINS_17LocalAddressSpaceEE10_bufferEndE)
+          2258c8           2258c8        0     1                 $d.63
+          2258c8           2258c8        8     1                 libunwind::DwarfFDECache<libunwind::LocalAddressSpace>::_bufferEnd
+          2258d0           2258d0      398     8 .got.plt
+          2258d0           2258d0      398     8         <internal>:(.got.plt)
+          225c68           225c68      e5d     8 .bss
+          225c68           225c68       60     8         ./exe.unstripped/component_manager.libcm_rust.cm_rust.3a1fbbbh-cgu.0.rcgu.o.rcgu.o:(.bss..L_MergedGlobals)
+          225c68           225c68        0     1                 $d.378
+          225c68           225c68       20     1                 _RNvNvNvXs15_CsbDqzXfLQacH_7cm_rustNtBa_13DATA_TYPENAMENtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___stability4LAZY.llvm.2893117974861896834
+          225c88           225c88       20     1                 _RNvNvNvXs17_CsbDqzXfLQacH_7cm_rustNtBa_14CACHE_TYPENAMENtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___stability4LAZY.llvm.2893117974861896834
+          225ca8           225ca8       20     1                 _RNvNvNvXs19_CsbDqzXfLQacH_7cm_rustNtBa_13META_TYPENAMENtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___stability4LAZY.llvm.2893117974861896834
+          225cc8           225cc8       38     8         ./exe.unstripped/component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.12.rcgu.o.rcgu.o:(.bss._RNvNvNvXs0_NtNtNtCsbDqzXfLQacH_21component_manager_lib5model6events14source_factoryNtB9_25EVENT_SOURCE_SERVICE_PATHNtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___stability4LAZY.llvm.16599889146946869799)
+          225cc8           225cc8        0     1                 $d.347
+          225cc8           225cc8       38     1                 _RNvNvNvXs0_NtNtNtCsbDqzXfLQacH_21component_manager_lib5model6events14source_factoryNtB9_25EVENT_SOURCE_SERVICE_PATHNtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___stability4LAZY.llvm.16599889146946869799
+          225d00           225d00       38     8         ./exe.unstripped/component_manager.libcomponent_manager_lib.component_manager_lib.3a1fbbbh-cgu.12.rcgu.o.rcgu.o:(.bss._RNvNvNvXs2_NtNtNtCsbDqzXfLQacH_21component_manager_lib5model6events14source_factoryNtB9_30EVENT_SOURCE_SYNC_SERVICE_PATHNtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___stability4LAZY.llvm.16599889146946869799)
+          225d00           225d00        0     1                 $d.348
+          225d00           225d00       38     1                 _RNvNvNvXs2_NtNtNtCsbDqzXfLQacH_21component_manager_lib5model6events14source_factoryNtB9_30EVENT_SOURCE_SYNC_SERVICE_PATHNtNtNtCs6QHBvYLDz7W_4core3ops5deref5Deref5deref11___stability4LAZY.llvm.16599889146946869799
+          2262ac           2262ac        8     4         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.bss._ZN9libunwind13DwarfFDECacheINS_17LocalAddressSpaceEE5_lockE)
+          2262ac           2262ac        0     1                 $d.59
+          2262ac           2262ac        8     1                 libunwind::DwarfFDECache<libunwind::LocalAddressSpace>::_lock
+          2262b8           2262b8      800     8         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.bss._ZN9libunwind13DwarfFDECacheINS_17LocalAddressSpaceEE14_initialBufferE)
+          2262b8           2262b8        0     1                 $d.62
+          2262b8           2262b8      800     1                 libunwind::DwarfFDECache<libunwind::LocalAddressSpace>::_initialBuffer
+          226ab8           226ab8        d     4         /tmp/rustcL27BaP/libunwind-e14dabfa3d8ce62b.rlib(libunwind.o):(.bss..L_MergedGlobals)
+          226ab8           226ab8        0     1                 $d.65
+          226ab8           226ab8        1     1                 logAPIs::checked
+          226abc           226abc        1     1                 logAPIs::log
+          226abd           226abd        1     1                 logUnwinding::checked
+          226ac0           226ac0        1     1                 logUnwinding::log
+          226ac1           226ac1        1     1                 logDWARF::checked
+          226ac4           226ac4        1     1                 logDWARF::log
+               0                0   48c441     1 .debug_loc
+               0                0       39     1         /usr/local/google/home/yifeit/vg/out/default.zircon/user-arm64-clang.shlib/obj/system/ulib/c/crt1.Scrt1.cc.o:(.debug_loc)
+               0                0        0     1                 $d.1
+              39               39     6d97     1         ./exe.unstripped/component_manager.alloc-54127f36ba192482.alloc.4k1iwrm2-cgu.0.rcgu.o.rcgu.o:(.debug_loc)
+              39               39        0     1                 $d.63
+            6dd0             6dd0     280c     1         ./exe.unstripped/component_manager.backtrace-5619ad41e613872b.backtrace.e1f4vorm-cgu.0.rcgu.o.rcgu.o:(.debug_loc)
+            6dd0             6dd0        0     1                 $d.41
diff --git a/tests/testdata/link_map/test_lld-lto_v1.map b/tests/testdata/link_map/test_lld-lto_v1.map
new file mode 100644
index 0000000..9658718
--- /dev/null
+++ b/tests/testdata/link_map/test_lld-lto_v1.map
@@ -0,0 +1,410 @@
+# Test Linker map for LLD with ThinLTO, "v1" format.
+# .map files actually don't have comments and blank lines! These are added to
+# improve documentation, and are stripped by tests.
+
+# First line is needed to identify .map file type.
+     VMA      LMA     Size Align Out     In      Symbol
+# Extract sizes for every section, and symbols for selected sections only.
+# Size-only sections.
+     174      174       13     1 .interp
+     174      174       13     1         <internal>:(.interp)
+     188      188   1e2678     4 .ARM.exidx
+     188      188        8     4         obj/third_party/breakpad/libclient.a(client/breakpad_getcontext.o):(.ARM.exidx)
+     190      190        8     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(chrono.o):(.ARM.exidx.text.__clang_call_terminate)
+     190      190        0     1                 $d
+     198      198        8     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(chrono.o):(.ARM.exidx.text._ZNSt6__ndk16chrono12steady_clock3nowEv)
+     198      198        0     1                 $d
+     1a0      1a0        8     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(exception.o):(.ARM.exidx.text._ZSt18uncaught_exceptionv)
+     1a0      1a0        0     1                 $d
+    2340     2340        8     4         lto.tmp:(.ARM.exidx.text._ZN5media18CdmPromiseTemplateIJEE26RejectPromiseOnDestructionEv)
+    2348     2348        8     4         lto.tmp:(.ARM.exidx.text._ZN5media18CdmPromiseTemplateIJEED2Ev)
+  1e2800   1e2800     1870     4 .dynsym
+  1e2800   1e2800     1870     4         <internal>:(.dynsym)
+  1e4070   1e4070      30e     2 .gnu.version
+  1e4070   1e4070      30e     2         <internal>:(.gnu.version)
+  1e4380   1e4380       60     4 .gnu.version_r
+  1e4380   1e4380       60     4         <internal>:(.gnu.version_r)
+  1e43e0   1e43e0       1c     4 .gnu.hash
+  1e43e0   1e43e0       1c     4         <internal>:(.gnu.hash)
+  1e43fc   1e43fc      f6b     1 .dynstr
+  1e43fc   1e43fc      f6b     1         <internal>:(.dynstr)
+  1e5368   1e5368    2d1d5     4 .rel.dyn
+  1e5368   1e5368    2d1d5     4         <internal>:(.rel.dyn)
+  212540   212540      bd8     4 .rel.plt
+  212540   212540      bd8     4         <internal>:(.rel.plt)
+  213118   213118       1c     4 .note.crashpad.info
+  213118   213118       1c     4         obj/third_party/crashpad/crashpad/client/libclient.a(client/crashpad_info_note.o):(.note.crashpad.info)
+  213118   213118       1c     1                 CRASHPAD_NOTE
+  213124   213124        0     1                 name
+  21312d   21312d        0     1                 name_end
+  213130   213130        0     1                 desc
+  213134   213134        0     1                 desc_end
+  213134   213134       98     4 .note.android.ident
+  213134   213134       98     4         ../../third_party/android_ndk/platforms/android-16/arch-arm/usr/lib/crtbegin_so.o:(.note.android.ident)
+  213134   213134        0     1                 $d
+  213134   213134       98     1                 note_android_ident
+  213140   213140        0     1                 note_name
+  213148   213148        0     1                 note_data
+  21314c   21314c        0     1                 ndk_version
+  21318c   21318c        0     1                 ndk_build_number
+  2131cc   2131cc        0     1                 note_end
+  2131cc   2131cc       24     4 .note.gnu.build-id
+  2131cc   2131cc       24     4         <internal>:(.note.gnu.build-id)
+
+# .rodata: Extract symbols and size (read-only data).
+  213200   213200   611e4b   256 .rodata
+  213200   213200        4     1         obj/v8/v8_external_snapshot/embedded.o:(.rodata)
+  213200   213200        0     1                 v8_Default_embedded_blob_size_
+  213210   213210       20    16         obj/third_party/ffmpeg/libffmpeg_internal.a(ffmpeg_internal/fft_neon.o):(.rodata)
+  213210   213210       10     1                 pmmp
+  213220   213220       10     1                 mppm
+  213230   213230      180     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(hash.o):(.rodata)
+  213230   213230       c0     1                 std::__ndk1::(anonymous namespace)::small_primes
+  213230   213230        0     1                 $d
+  2132f0   2132f0       c0     1                 std::__ndk1::(anonymous namespace)::indices
+  2133b0   2133b0       d1    16         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.rodata)
+  2133b0   2133b0        0     1                 $d
+  2133b0   2133b0        4     1                 std::__ndk1::ios_base::boolalpha
+  2133b4   2133b4        4     1                 std::__ndk1::ios_base::dec
+  2133b8   2133b8        4     1                 std::__ndk1::ios_base::fixed
+  2133bc   2133bc        4     1                 std::__ndk1::ios_base::hex
+  2133c0   2133c0        4     1                 std::__ndk1::ios_base::internal
+  2133c4   2133c4        4     1                 std::__ndk1::ios_base::left
+  2133c8   2133c8        4     1                 std::__ndk1::ios_base::oct
+  2133cc   2133cc        4     1                 std::__ndk1::ios_base::right
+  2133d0   2133d0        4     1                 std::__ndk1::ios_base::scientific
+  2133d4   2133d4        4     1                 std::__ndk1::ios_base::showbase
+  2133d8   2133d8        4     1                 std::__ndk1::ios_base::showpoint
+  2133dc   2133dc        4     1                 std::__ndk1::ios_base::showpos
+  2133e0   2133e0        4     1                 std::__ndk1::ios_base::skipws
+  2133e4   2133e4        4     1                 std::__ndk1::ios_base::unitbuf
+  2133e8   2133e8        4     1                 std::__ndk1::ios_base::uppercase
+  2133ec   2133ec        4     1                 std::__ndk1::ios_base::adjustfield
+  2133f0   2133f0        4     1                 std::__ndk1::ios_base::basefield
+  2133f4   2133f4        4     1                 std::__ndk1::ios_base::floatfield
+  2133f8   2133f8        4     1                 std::__ndk1::ios_base::badbit
+  2133fc   2133fc        4     1                 std::__ndk1::ios_base::eofbit
+  213400   213400        4     1                 std::__ndk1::ios_base::failbit
+  213404   213404        4     1                 std::__ndk1::ios_base::goodbit
+  213408   213408        4     1                 std::__ndk1::ios_base::app
+  21340c   21340c        4     1                 std::__ndk1::ios_base::ate
+  213410   213410        4     1                 std::__ndk1::ios_base::binary
+  213414   213414        4     1                 std::__ndk1::ios_base::in
+  213418   213418        4     1                 std::__ndk1::ios_base::out
+  21341c   21341c        4     1                 std::__ndk1::ios_base::trunc
+  213420   213420       1d     1                 typeinfo name for std::__ndk1::ios_base::failure
+  213440   213440       15     1                 typeinfo name for std::__ndk1::ios_base
+  213460   213460       21     1                 typeinfo name for std::__ndk1::__iostream_category
+  213490   213490       2d    16         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.rodata._ZTSNSt6__ndk19basic_iosIcNS_11char_traitsIcEEEE)
+  213490   213490        0     1                 $d
+
+# <internal>, which is Level 2 only.
+  21368b   21368b   2c378e     1         <internal>:(.rodata)
+# Single symbol mixed with $d.
+  4d6e20   4d6e20       1b    16         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(iostream.o):(.rodata._ZTSNSt6__ndk110__stdinbufIwEE)
+  4d6e20   4d6e20        0     1                 $d
+  4d6e20   4d6e20       1b     1                 typeinfo name for std::__ndk1::__stdinbuf<wchar_t>
+
+# More <internal>.
+  4d7920   4d7920     266c     4         <internal>:(.rodata)
+  4d9f90   4d9f90       18    16         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(locale.o):(.rodata._ZTSNSt6__ndk110ctype_baseE)
+  4d9f90   4d9f90        0     1                 $d
+  4d9f90   4d9f90       18     1                 typeinfo name for std::__ndk1::ctype_base
+  4d9fb0   4d9fb0       1a    16         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(locale.o):(.rodata._ZTSNSt6__ndk112codecvt_baseE)
+  4d9fb0   4d9fb0        0     1                 $d
+  4d9fb0   4d9fb0       1a     1                 typeinfo name for std::__ndk1::codecvt_base
+# thinlto-cache at Level 2.
+  503eb3   503eb3        1     1         thinlto-cache/Thin-84596a.tmp.o:(.rodata._ZN7network5mojom45CookieManagerProxy_SetCanonicalCookie_Message11kMessageTagE)
+  503eb3   503eb3        1     1                 network::mojom::CookieManagerProxy_SetCanonicalCookie_Message::kMessageTag
+  503eb4   503eb4        1     1         thinlto-cache/Thin-84596a.tmp.o:(.rodata._ZN7network5mojom48CookieManagerProxy_DeleteCanonicalCookie_Message11kMessageTagE)
+  503eb4   503eb4        1     1                 network::mojom::CookieManagerProxy_DeleteCanonicalCookie_Message::kMessageTag
+
+# Size-only sections.
+  82504c   82504c     8ffc     4 .ARM.extab
+  82504c   82504c       40     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(chrono.o):(.ARM.extab.text._ZNSt6__ndk16chrono12steady_clock3nowEv)
+  82504c   82504c        0     1                 $d
+  825058   825058        0     1                 GCC_except_table4
+  82508c   82508c       38     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(hash.o):(.ARM.extab.text._ZNSt6__ndk112__next_primeEj)
+  82508c   82508c        0     1                 $d
+  825098   825098        0     1                 GCC_except_table0
+  8250c4   8250c4       48     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.ARM.extab.text._ZNSt6__ndk18ios_base5clearEj)
+  8250c4   8250c4        0     1                 $d
+  8250d0   8250d0        0     1                 GCC_except_table5
+
+# .text: Extract symbols and size (executable code).
+  82f000   82f000  251b7e8    64 .text
+# Multiple Level 3 symbols under Level 2. $a annotates ARM symbols (not Thumb2).
+# Also, data precede first Level 3 line.
+  82f000   82f000      cdc    32         obj/third_party/boringssl/boringssl_asm/chacha-armv4.o:(.text)
+  82f004   82f004        0     1                 $d.0
+  82f040   82f040        0     1                 $a.1
+  82f040   82f040      3f0     1                 ChaCha20_ctr32
+  82f440   82f440      89c     1                 ChaCha20_neon
+  82fce0   82fce0     130c    32         obj/third_party/boringssl/boringssl_asm/aes-armv4.o:(.text)
+  82fce0   82fce0        0     1                 $d.0
+  82fce0   82fce0      540     1                 AES_Te
+  830220   830220        0     1                 $a.1
+  830220   830220       60     1                 aes_nohw_encrypt
+  830280   830280      1d8     1                 _armv4_AES_encrypt
+  830460   830460        0     1                 _armv4_AES_set_encrypt_key
+  830460   830460      2a0     1                 aes_nohw_set_encrypt_key
+  830700   830700       20     1                 aes_nohw_set_decrypt_key
+  830720   830720        0     1                 _armv4_AES_set_enc2dec_key
+  830720   830720      124     1                 AES_set_enc2dec_key
+  830860   830860        0     1                 $d.2
+  830860   830860      500     1                 AES_Td
+  830d60   830d60        0     1                 $a.3
+  830d60   830d60       60     1                 aes_nohw_decrypt
+  830dc0   830dc0      1f8     1                 _armv4_AES_decrypt
+  830fb8   830fb8        0     1                 $d.4
+
+# Many Level 3 annotations. Also, data precede first Level 3 line.
+  831000   831000      a28    32         obj/third_party/boringssl/boringssl_asm/aesv8-armx32.o:(.text)
+  831004   831004        0     1                 $d.0
+  831040   831040        0     1                 $a.1
+  831040   831040        0     1                 .Lenc_key
+  831040   831040      218     1                 aes_hw_set_encrypt_key
+  8310b0   8310b0        0     1                 $d.2
+  8310b4   8310b4        0     1                 $a.3
+  8310f0   8310f0        0     1                 $d.4
+  8310f4   8310f4        0     1                 $a.5
+  831124   831124        0     1                 $d.6
+  831128   831128        0     1                 $a.7
+  831180   831180        0     1                 $d.8
+  831184   831184        0     1                 $a.9
+  8311f0   8311f0        0     1                 $d.10
+  8311f4   8311f4        0     1                 $a.11
+  831228   831228        0     1                 $d.12
+  83122c   83122c        0     1                 $a.13
+  831260   831260       60     1                 aes_hw_set_decrypt_key
+  831294   831294        0     1                 $d.14
+  83129c   83129c        0     1                 $a.15
+  8312b0   8312b0        0     1                 $d.16
+  8312b4   8312b4        0     1                 $a.17
+  8312c0   8312c0       50     1                 aes_hw_encrypt
+  8312d4   8312d4        0     1                 $d.18
+  8312dc   8312dc        0     1                 $a.19
+  8312e4   8312e4        0     1                 $d.20
+  8312ec   8312ec        0     1                 $a.21
+  8312f4   8312f4        0     1                 $d.22
+  8312fc   8312fc        0     1                 $a.23
+  831300   831300        0     1                 $d.24
+  831304   831304        0     1                 $a.25
+  831320   831320       50     1                 aes_hw_decrypt
+  831334   831334        0     1                 $d.26
+  83133c   83133c        0     1                 $a.27
+  831344   831344        0     1                 $d.28
+  83134c   83134c        0     1                 $a.29
+  831354   831354        0     1                 $d.30
+  83135c   83135c        0     1                 $a.31
+  831360   831360        0     1                 $d.32
+  831364   831364        0     1                 $a.33
+# ... (truncated).
+
+# Symbols with Size = 0 from assembly. Level 3 items are functions.
+  83c640   83c640   13aee0    32         obj/v8/v8_external_snapshot/embedded.o:(.text)
+  83c640   83c640        0     1                 v8_Default_embedded_blob_data_
+  83f4a0   83f4a0        0     1                 Builtins_RecordWrite
+  83f7c0   83f7c0        0     1                 Builtins_AdaptorWithExitFrame
+  83f800   83f800        0     1                 Builtins_AdaptorWithBuiltinExitFrame
+  83f840   83f840        0     1                 Builtins_ArgumentsAdaptorTrampoline
+  83f940   83f940        0     1                 Builtins_CallFunction_ReceiverIsNullOrUndefined
+  83fa40   83fa40        0     1                 Builtins_CallFunction_ReceiverIsNotNullOrUndefined
+  83fb80   83fb80        0     1                 Builtins_CallFunction_ReceiverIsAny
+  83fd00   83fd00        0     1                 Builtins_CallBoundFunction
+# ... (truncated).
+
+  977520   977520       18    16         obj/third_party/blink/renderer/platform/heap/asm/asm/SaveRegisters_arm.o:(.text)
+  977520   977520        0     1                 $a.0
+  977520   977520        0     1                 PushAllRegisters
+
+# Symbols with Size = 0 from assembly. Many Level 3 items are labels.
+  97c834   97c834      4dc     4         obj/third_party/libvpx/libvpx_assembly_arm.a(libvpx_assembly_arm/vpx_convolve8_avg_horiz_filter_type1_neon.asm.o):(.text)
+  97c834   97c834        0     1                 $a.0
+  97c834   97c834        0     1                 _vpx_convolve8_avg_horiz_filter_type1_neon
+# This is a function.
+  97c834   97c834      4dc     1                 vpx_convolve8_avg_horiz_filter_type1_neon
+# These are labels.
+  97c848   97c848        0     1                 start_loop_count
+  97c8d0   97c8d0        0     1                 outer_loop8_residual
+  97c8f0   97c8f0        0     1                 outer_loop_8
+  97c900   97c900        0     1                 inner_loop_8
+  97c9c8   97c9c8        0     1                 end_inner_loop_8
+  97c9e4   97c9e4        0     1                 end_loops
+  97c9e8   97c9e8        0     1                 outer_loop_16
+  97ca54   97ca54        0     1                 inner_loop_16
+  97cbcc   97cbcc        0     1                 epilog_16
+  97cc08   97cc08        0     1                 end_loops1
+  97cc0c   97cc0c        0     1                 outer_loop4_residual
+  97cc2c   97cc2c        0     1                 outer_loop_4
+  97cc3c   97cc3c        0     1                 inner_loop_4
+  97ccf8   97ccf8        0     1                 end_inner_loop_4
+  97cd08   97cd08        0     1                 end_func
+
+# Thumb2 symbols (indicated by odd Level 3 addresses) with aliases.
+  99cad8   99cad8       fc     8         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libandroid_support.a(e_sinh.o):(.text.sinh)
+  99cad8   99cad8        0     1                 $t
+  99cad9   99cad9       fc     1                 sinhl
+  99cad9   99cad9       fc     1                 sinh
+  99cbb8   99cbb8        0     1                 $d
+
+# On rare occasions, some symbol name start with '$'.
+ 1401788  1401788       a4     4         thinlto-cache/Thin-826b9f.tmp.o:(.text._ZNK4$_21clEP10FamilyDataPKcPS3_)
+ 1401788  1401788        0     1                 $t.78
+ 1401789  1401789       a4     1                 $_21::operator()(FamilyData*, char const*, char const**) const
+ 1401818  1401818        0     1                 $d.79
+
+# Sections prefixed with ".unlikely" or ".hot"
+ 1401820  1401820       10     4         thinlto-cache/Thin-826b9f.tmp.o:(.text.unlikely.UnlikelyFunc)
+ 1401820  1401820        0     1                 $t.78
+ 1401821  1401821       10     1                 UnlikelyFunc
+ 1401831  1401831       10     1                 UnlikelyFunc2
+ 1401841  1401841        0     1                 $d.79
+
+# Sections prefixed with ".startup"
+ 1401850  1401850       10     4         thinlto-cache/Thin-826b9f.tmp.o:(.text.startup)
+ 1401850  1401850        0     1                 $t.78
+ 1401851  1401851       10     1                 StartUpFunc
+ 1401861  1401861        0     1                 $d.79
+
+# Size-only sections.
+ 2d4a7f0  2d4a7f0     17d0    16 .plt
+ 2d4a7f0  2d4a7f0     17d0    16         <internal>:(.plt)
+
+# .data: Extract symbols and size.
+ 2d4c000  2d4c000    1cfc8     8 .data
+ 2d4c000  2d4c000        4     1         obj/v8/v8_external_snapshot/embedded.o:(.data)
+ 2d4c000  2d4c000        0     1                 v8_Default_embedded_blob_
+ 2d4c004  2d4c004        4     4         ../../third_party/android_ndk/platforms/android-16/arch-arm/usr/lib/crtbegin_so.o:(.data)
+ 2d4c004  2d4c004        0     1                 $d
+ 2d4c004  2d4c004        4     1                 __dso_handle
+
+# .data.rel.ro: Extract symbols and size.
+ 2d69000  2d69000   1eb760    16 .data.rel.ro
+ 2d69000  2d69000       3c     4         obj/third_party/ffmpeg/libffmpeg_internal.a(ffmpeg_internal/fft_vfp.o):(.data.rel.ro)
+ 2d69000  2d69000       3c     1                 fft_tab_vfp
+ 2d6903c  2d6903c       3c     4         obj/third_party/ffmpeg/libffmpeg_internal.a(ffmpeg_internal/fft_neon.o):(.data.rel.ro)
+ 2d6903c  2d6903c       3c     1                 fft_tab_neon
+ 2d69078  2d69078       40     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.data.rel.ro._ZTVNSt6__ndk115basic_streambufIwNS_11char_traitsIwEEEE)
+ 2d69078  2d69078        0     1                 $d
+ 2d69078  2d69078       40     1                 vtable for std::__ndk1::basic_streambuf<wchar_t, std::__ndk1::char_traits<wchar_t> >
+ 2d690b8  2d690b8       28     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.data.rel.ro._ZTVNSt6__ndk113basic_istreamIcNS_11char_traitsIcEEEE)
+ 2d690b8  2d690b8        0     1                 $d
+ 2d690b8  2d690b8       28     1                 vtable for std::__ndk1::basic_istream<char, std::__ndk1::char_traits<char> >
+ 2d690e0  2d690e0       28     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.data.rel.ro._ZTVNSt6__ndk113basic_istreamIwNS_11char_traitsIwEEEE)
+ 2d690e0  2d690e0        0     1                 $d
+ 2d690e0  2d690e0       28     1                 vtable for std::__ndk1::basic_istream<wchar_t, std::__ndk1::char_traits<wchar_t> >
+ 2d69108  2d69108       28     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.data.rel.ro._ZTVNSt6__ndk113basic_ostreamIcNS_11char_traitsIcEEEE)
+ 2d69108  2d69108        0     1                 $d
+ 2d69108  2d69108       28     1                 vtable for std::__ndk1::basic_ostream<char, std::__ndk1::char_traits<char> >
+ 2d69130  2d69130       28     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.data.rel.ro._ZTVNSt6__ndk113basic_ostreamIwNS_11char_traitsIwEEEE)
+ 2d69130  2d69130        0     1                 $d
+ 2d69130  2d69130       28     1                 vtable for std::__ndk1::basic_ostream<wchar_t, std::__ndk1::char_traits<wchar_t> >
+ 2d69158  2d69158       68     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(ios.o):(.data.rel.ro)
+ 2d69158  2d69158        0     1                 $d
+ 2d69158  2d69158       24     1                 vtable for std::__ndk1::__iostream_category
+ 2d6917c  2d6917c       14     1                 vtable for std::__ndk1::ios_base::failure
+ 2d69190  2d69190       10     1                 vtable for std::__ndk1::ios_base
+ 2d691a0  2d691a0        c     1                 typeinfo for std::__ndk1::ios_base::failure
+ 2d691ac  2d691ac        8     1                 typeinfo for std::__ndk1::ios_base
+ 2d691b4  2d691b4        c     1                 typeinfo for std::__ndk1::__iostream_category
+
+# Size-only sections.
+ 2f54760  2f54760        8     4 .fini_array
+ 2f54760  2f54760        4     4         ../../third_party/android_ndk/platforms/android-16/arch-arm/usr/lib/crtbegin_so.o:(.fini_array)
+ 2f54760  2f54760        0     1                 $d
+ 2f54764  2f54764        4     1         ../../third_party/android_ndk/platforms/android-16/arch-arm/usr/lib/crtend_so.o:(.fini_array)
+ 2f54768  2f54768       10     4 .init_array
+ 2f54768  2f54768        4     4         ../../third_party/android_ndk/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_static.a(iostream.o):(.init_array)
+ 2f54768  2f54768        0     1                 $d
+ 2f5476c  2f5476c        4     4         ../../third_party/gvr-android-sdk/libgvr_shim_static_arm.a(base_logging.o):(.init_array)
+ 2f54770  2f54770        4     1         ../../third_party/android_ndk/platforms/android-16/arch-arm/usr/lib/crtend_so.o:(.init_array)
+ 2f54774  2f54774        4     4         thinlto-cache/Thin-b0997c.tmp.o:(.init_array)
+ 2f54778  2f54778       f0     4 .dynamic
+ 2f54778  2f54778       f0     4         <internal>:(.dynamic)
+ 2f54868  2f54868      5ac     4 .got
+ 2f54868  2f54868      5ac     4         <internal>:(.got)
+ 2f54e14  2f54e14      5f8     4 .got.plt
+ 2f54e14  2f54e14      5f8     4         <internal>:(.got.plt)
+
+# .bss: Extract symbols and size (does not consume size on disk).
+ 2f56000  2f56000   109f7c    32 .bss
+ 2f56000  2f56000        4     4         thinlto-cache/Thin-cfd493.tmp.o:(COMMON)
+ 2f56000  2f56000        4     1                 WebRtcSpl_CrossCorrelation
+ 2f56000  2f56000        4     1                 WebRtcSpl_CrossCorrelation
+ 2f56000  2f56000        4     1                 WebRtcSpl_CrossCorrelation
+ 2f56004  2f56004        4     4         thinlto-cache/Thin-cfd493.tmp.o:(COMMON)
+ 2f56004  2f56004        4     1                 WebRtcSpl_DownsampleFast
+ 2f56004  2f56004        4     1                 WebRtcSpl_DownsampleFast
+ 2f56004  2f56004        4     1                 WebRtcSpl_DownsampleFast
+ 2f56004  2f56004        4     1                 WebRtcSpl_DownsampleFast
+ 2f56008  2f56008        4     4         thinlto-cache/Thin-cfd493.tmp.o:(COMMON)
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+ 2f56008  2f56008        4     1                 WebRtcSpl_MaxAbsValueW16
+
+# Partitions should be ignored at this point. Otherwise, their .text, .rodata,
+# etc. sections will overwrite those of the main partition.
+ 34be000  34be000       34     1 vr_partition
+ 34be000  34be000       34     1         <internal>:(vr_partition)
+ 34be034  34be034      140     1 .phdrs
+ 34be034  34be034      140     1         <internal>:(.phdrs)
+ 34be174  34be174       13     1 .interp
+ 34be174  34be174       13     1         <internal>:(.interp)
+ 34be188  34be188       1c     4 .note.crashpad.info
+ 34be188  34be188       1c     4         obj/third_party/crashpad/crashpad/client/libclient.a(client/crashpad_info_note.o):(.note.crashpad.info)
+ 34be1a4  34be1a4       98     4 .note.android.ident
+ 34be1a4  34be1a4       98     4         ../../third_party/android_ndk/platforms/android-16/arch-arm/usr/lib/crtbegin_so.o:(.note.android.ident)
+ 34be23c  34be23c       24     4 .note.gnu.build-id
+ 34be23c  34be23c       24     4         <internal>:(.note.gnu.build-id)
+ 34be260  34be260       20     4 .dynsym
+ 34be260  34be260       20     4         <internal>:(.dynsym)
+ 34be280  34be280        4     2 .gnu.version
+ 34be280  34be280        4     2         <internal>:(.gnu.version)
+ 34be284  34be284       60     4 .gnu.version_r
+ 34be284  34be284       60     4         <internal>:(.gnu.version_r)
+ 34be2e4  34be2e4       1c     4 .gnu.hash
+ 34be2e4  34be2e4       1c     4         <internal>:(.gnu.hash)
+ 34be300  34be300       6c     1 .dynstr
+ 34be300  34be300       6c     1         <internal>:(.dynstr)
+ 34be36c  34be36c      257     4 .rel.dyn
+ 34be36c  34be36c      257     4         <internal>:(.rel.dyn)
+ 34be5c4  34be5c4       10     4 .ARM.exidx
+ 34be5c4  34be5c4       10     4         <internal>:(.ARM.exidx)
+ 34be5d8  34be5d8     2428     8 .rodata
+ 34be5d8  34be5d8       10     1         thinlto-cache/Thin-5e976b.tmp.o:(.rodata._ZN2vrL17kRepositionIconIdE)
+ 34be5d8  34be5d8       10     1                 vr::kRepositionIconId
+ 34c0a00  34c0a00    2c9ec    16 .text
+ 34c0a00  34c0a00       18     4         thinlto-cache/Thin-ec84a4.tmp.o:(.text._ZN6SkFont7setSizeEf)
+ 34c0a00  34c0a00        0     1                 $t.11
+ 34c0a01  34c0a01       18     1                 SkFont::setSize(float)
+ 34c0a14  34c0a14        0     1                 $d.12
+# .part.end is unique, even if multiple lib*.so, .phdrs, etc. exist.
+       0        0     1000     1 .part.end
+       0        0     1000     1         <internal>:(.part.end)
+
+# Various .debug sections can exist, but they're omitted for simplicity.
+
+# Size-only sections.
+       0        0       23     1 .ARM.attributes
+       0        0       23     1         obj/third_party/boringssl/boringssl_asm/chacha-armv4.o:(.ARM.attributes)
+       0        0       a8     1 .comment
+       0        0       a8     1         <internal>:(.comment)
+       0        0  11c3610     4 .symtab
+       0        0  11c3610     4         <internal>:(.symtab)
+       0        0      1c4     1 .shstrtab
+       0        0      1c4     1         <internal>:(.shstrtab)
+       0        0  2f73332     1 .strtab
+       0        0  2f73332     1         <internal>:(.strtab)
+
+# PROVIDE_HIDDEN are not Level 1 symbols.
+ 2f73332        0        0     1 PROVIDE_HIDDEN ( linker_script_start_of_text = ADDR ( .text ) )
+ 2f73332        0        0     1 PROVIDE_HIDDEN ( linker_script_end_of_text = ADDR ( .text ) + SIZEOF ( .text ) )
diff --git a/third_party/demumble b/third_party/demumble
index 01098ea..0cc7805 160000
--- a/third_party/demumble
+++ b/third_party/demumble
@@ -1 +1 @@
-Subproject commit 01098eab821b33bd31b9778aea38565cd796aa85
+Subproject commit 0cc780526909546f1e41bef501e248e1cdbf2a9b
diff --git a/third_party/googletest b/third_party/googletest
index 565f1b8..dcc92d0 160000
--- a/third_party/googletest
+++ b/third_party/googletest
@@ -1 +1 @@
-Subproject commit 565f1b848215b77c3732bca345fe76a0431d8b34
+Subproject commit dcc92d0ab6c4ce022162a23566d44f673251eee4
diff --git a/third_party/protobuf b/third_party/protobuf
index bc1773c..31ebe2a 160000
--- a/third_party/protobuf
+++ b/third_party/protobuf
@@ -1 +1 @@
-Subproject commit bc1773c42c9c3c522145a3119e989e0dff2a8d54
+Subproject commit 31ebe2ac71400344a5db91ffc13c4ddfb7589f92
diff --git a/third_party/rustc-demangle b/third_party/rustc-demangle
new file mode 160000
index 0000000..c4e3ab0
--- /dev/null
+++ b/third_party/rustc-demangle
@@ -0,0 +1 @@
+Subproject commit c4e3ab004edda9fbe5e2d8149d2b62c10668d10b