[lldb/ObjectFileMachO] Fetch shared cache images from our own shared cache
authorFred Riss <friss@apple.com>
Wed, 15 Jul 2020 22:00:52 +0000 (15:00 -0700)
committerFred Riss <friss@apple.com>
Thu, 16 Jul 2020 17:37:37 +0000 (10:37 -0700)
Summary:
On macOS 11, the libraries that have been integrated in the system
shared cache are not present on the filesystem anymore. LLDB was
using those files to get access to the symbols of those libraries.
LLDB can get the images from the target process memory though.

This has 2 consequences:
 - LLDB cannot load the images before the process starts, reporting
   an error if someone tries to break on a system symbol.
 - Loading the symbols by downloading the data from the inferior
   is super slow. It takes tens of seconds at the start of the
   debug session to populate the Module list.

To fix this, we can use the library images LLDB has in its own
mapping of the shared cache. Shared cache images are somewhat
special as their LINKEDIT segment is moved to the end of the cache
and thus the images are not contiguous in memory. All of this can
hidden in ObjectFileMachO.

This patch fixes a number of test failures on macOS 11 due to the
first problem described above and adds some specific unittesting
for the new SharedCache Host utilities.

Reviewers: jasonmolenda, labath

Subscribers: llvm-commits, lldb-commits

Tags: #lldb, #llvm

Differential Revision: https://reviews.llvm.org/D83023

lldb/include/lldb/Host/HostInfoBase.h
lldb/include/lldb/Host/macosx/HostInfoMacOSX.h
lldb/source/Host/macosx/objcxx/HostInfoMacOSX.mm
lldb/source/Plugins/DynamicLoader/MacOSX-DYLD/DynamicLoaderDarwin.cpp
lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp
lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.h
lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
lldb/unittests/ObjectFile/CMakeLists.txt
lldb/unittests/ObjectFile/MachO/CMakeLists.txt [new file with mode: 0644]
lldb/unittests/ObjectFile/MachO/TestObjectFileMachO.cpp [new file with mode: 0644]

index 70682c9b685ebbc19761abbd977fb6ef9a19e568..15bb168aad97f0d1de4c856ddbc2363024adec92 100644 (file)
@@ -11,6 +11,7 @@
 
 #include "lldb/Utility/ArchSpec.h"
 #include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/UUID.h"
 #include "lldb/Utility/UserIDResolver.h"
 #include "lldb/Utility/XcodeSDK.h"
 #include "lldb/lldb-enumerations.h"
@@ -24,6 +25,11 @@ namespace lldb_private {
 
 class FileSpec;
 
+struct SharedCacheImageInfo {
+  UUID uuid;
+  lldb::DataBufferSP data_sp;
+};
+
 class HostInfoBase {
 private:
   // Static class, unconstructable.
@@ -98,6 +104,13 @@ public:
   /// Return the directory containing a specific Xcode SDK.
   static llvm::StringRef GetXcodeSDKPath(XcodeSDK sdk) { return {}; }
 
+  /// Return information about module \p image_name if it is loaded in
+  /// the current process's address space.
+  static SharedCacheImageInfo
+  GetSharedCacheImageInfo(llvm::StringRef image_name) {
+    return {};
+  }
+
 protected:
   static bool ComputeSharedLibraryDirectory(FileSpec &file_spec);
   static bool ComputeSupportExeDirectory(FileSpec &file_spec);
index 3941414f8abddb53d85eaa6b9f16981d4d2942aa..ee9f12a90943fd75c8a0b03599970ba524ef603c 100644 (file)
@@ -37,6 +37,11 @@ public:
 
   /// Query xcrun to find an Xcode SDK directory.
   static llvm::StringRef GetXcodeSDKPath(XcodeSDK sdk);
+
+  /// Shared cache utilities
+  static SharedCacheImageInfo
+  GetSharedCacheImageInfo(llvm::StringRef image_name);
+
 protected:
   static bool ComputeSupportExeDirectory(FileSpec &file_spec);
   static void ComputeHostArchitectureSupport(ArchSpec &arch_32,
index 60eacb1e49b2c9d4b114c696919397f068f1b7ec..b325bd2c5b74562a9fcca5840d7cccb1c99bea4b 100644 (file)
 #include "lldb/Host/HostInfo.h"
 #include "lldb/Utility/Args.h"
 #include "lldb/Utility/Log.h"
+#include "Utility/UuidCompatibility.h"
 
 #include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/StringMap.h"
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/raw_ostream.h"
@@ -457,3 +459,64 @@ llvm::StringRef HostInfoMacOSX::GetXcodeSDKPath(XcodeSDK sdk) {
   auto it_new = g_sdk_path.insert({sdk.GetString(), GetXcodeSDK(sdk)});
   return it_new.first->second;
 }
+
+namespace {
+struct dyld_shared_cache_dylib_text_info {
+  uint64_t version; // current version 1
+  // following fields all exist in version 1
+  uint64_t loadAddressUnslid;
+  uint64_t textSegmentSize;
+  uuid_t dylibUuid;
+  const char *path; // pointer invalid at end of iterations
+  // following fields all exist in version 2
+  uint64_t textSegmentOffset; // offset from start of cache
+};
+typedef struct dyld_shared_cache_dylib_text_info
+    dyld_shared_cache_dylib_text_info;
+}
+
+extern "C" int dyld_shared_cache_iterate_text(
+    const uuid_t cacheUuid,
+    void (^callback)(const dyld_shared_cache_dylib_text_info *info));
+extern "C" uint8_t *_dyld_get_shared_cache_range(size_t *length);
+extern "C" bool _dyld_get_shared_cache_uuid(uuid_t uuid);
+
+namespace {
+class SharedCacheInfo {
+public:
+  const UUID &GetUUID() const { return m_uuid; };
+  const llvm::StringMap<SharedCacheImageInfo> &GetImages() const {
+    return m_images;
+  };
+
+  SharedCacheInfo();
+
+private:
+  llvm::StringMap<SharedCacheImageInfo> m_images;
+  UUID m_uuid;
+};
+}
+
+SharedCacheInfo::SharedCacheInfo() {
+  size_t shared_cache_size;
+  uint8_t *shared_cache_start =
+      _dyld_get_shared_cache_range(&shared_cache_size);
+  uuid_t dsc_uuid;
+  _dyld_get_shared_cache_uuid(dsc_uuid);
+  m_uuid = UUID::fromData(dsc_uuid);
+
+  dyld_shared_cache_iterate_text(
+      dsc_uuid, ^(const dyld_shared_cache_dylib_text_info *info) {
+        m_images[info->path] = SharedCacheImageInfo{
+            UUID::fromData(info->dylibUuid, 16),
+            std::make_shared<DataBufferUnowned>(
+                shared_cache_start + info->textSegmentOffset,
+                shared_cache_size - info->textSegmentOffset)};
+      });
+}
+
+SharedCacheImageInfo
+HostInfoMacOSX::GetSharedCacheImageInfo(llvm::StringRef image_name) {
+  static SharedCacheInfo g_shared_cache_info;
+  return g_shared_cache_info.GetImages().lookup(image_name);
+}
index 7b0d6f343c0300da980a10c2c956da4be4de41fc..73100434043486b567f3cff0960308ee5de73f41 100644 (file)
@@ -16,6 +16,7 @@
 #include "lldb/Core/Section.h"
 #include "lldb/Expression/DiagnosticManager.h"
 #include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
 #include "lldb/Symbol/Function.h"
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Target/ABI.h"
@@ -123,19 +124,39 @@ ModuleSP DynamicLoaderDarwin::FindTargetModuleForImageInfo(
       module_sp.reset();
   }
 
-  if (!module_sp) {
-    if (can_create) {
-      // We'll call Target::ModulesDidLoad after all the modules have been
-      // added to the target, don't let it be called for every one.
-      module_sp = target.GetOrCreateModule(module_spec, false /* notify */);
-      if (!module_sp || module_sp->GetObjectFile() == nullptr)
-        module_sp = m_process->ReadModuleFromMemory(image_info.file_spec,
-                                                    image_info.address);
-
-      if (did_create_ptr)
-        *did_create_ptr = (bool)module_sp;
+  if (module_sp || !can_create)
+    return module_sp;
+
+  if (HostInfo::GetArchitecture().IsCompatibleMatch(target.GetArchitecture())) {
+    // When debugging on the host, we are most likely using the same shared
+    // cache as our inferior. The dylibs from the shared cache might not
+    // exist on the filesystem, so let's use the images in our own memory
+    // to create the modules.
+    // Check if the requested image is in our shared cache.
+    SharedCacheImageInfo image_info =
+        HostInfo::GetSharedCacheImageInfo(module_spec.GetFileSpec().GetPath());
+
+    // If we found it and it has the correct UUID, let's proceed with
+    // creating a module from the memory contents.
+    if (image_info.uuid &&
+        (!module_spec.GetUUID() || module_spec.GetUUID() == image_info.uuid)) {
+      ModuleSpec shared_cache_spec(module_spec.GetFileSpec(), image_info.uuid,
+                                   image_info.data_sp);
+      module_sp =
+          target.GetOrCreateModule(shared_cache_spec, false /* notify */);
     }
   }
+  // We'll call Target::ModulesDidLoad after all the modules have been
+  // added to the target, don't let it be called for every one.
+  if (!module_sp)
+    module_sp = target.GetOrCreateModule(module_spec, false /* notify */);
+  if (!module_sp || module_sp->GetObjectFile() == nullptr)
+    module_sp = m_process->ReadModuleFromMemory(image_info.file_spec,
+                                                image_info.address);
+
+  if (did_create_ptr)
+    *did_create_ptr = (bool)module_sp;
+
   return module_sp;
 }
 
index 2bb4b21adeaec028642b3b357c3c6cdbdb77753d..ab1a6a8bb5f3e036d2ae848748ae57cba1f47373 100644 (file)
@@ -47,8 +47,8 @@
 
 #include "ObjectFileMachO.h"
 
-#if defined(__APPLE__) &&                                                      \
-    (defined(__arm__) || defined(__arm64__) || defined(__aarch64__))
+#if defined(__APPLE__)
+#include <TargetConditionals.h>
 // GetLLDBSharedCacheUUID() needs to call dlsym()
 #include <dlfcn.h>
 #endif
@@ -1328,6 +1328,19 @@ void ObjectFileMachO::SanitizeSegmentCommand(segment_command_64 &seg_cmd,
   if (m_length == 0 || seg_cmd.filesize == 0)
     return;
 
+  if ((m_header.flags & MH_DYLIB_IN_CACHE) && !IsInMemory()) {
+    // In shared cache images, the load commands are relative to the
+    // shared cache file, and not the the specific image we are
+    // examining. Let's fix this up so that it looks like a normal
+    // image.
+    if (strncmp(seg_cmd.segname, "__TEXT", sizeof(seg_cmd.segname)) == 0)
+      m_text_address = seg_cmd.vmaddr;
+    if (strncmp(seg_cmd.segname, "__LINKEDIT", sizeof(seg_cmd.segname)) == 0)
+      m_linkedit_original_offset = seg_cmd.fileoff;
+
+    seg_cmd.fileoff = seg_cmd.vmaddr - m_text_address;
+  }
+
   if (seg_cmd.fileoff > m_length) {
     // We have a load command that says it extends past the end of the file.
     // This is likely a corrupt file.  We don't have any way to return an error
@@ -1664,6 +1677,10 @@ void ObjectFileMachO::ProcessSegmentCommand(const load_command &load_cmd_,
     if (m_data.GetU32(&offset, &sect64.offset, num_u32s) == nullptr)
       break;
 
+    if ((m_header.flags & MH_DYLIB_IN_CACHE) && !IsInMemory()) {
+      sect64.offset = sect64.addr - m_text_address;
+    }
+
     // Keep a list of mach sections around in case we need to get at data that
     // isn't stored in the abstracted Sections.
     m_mach_sections.push_back(sect64);
@@ -2264,14 +2281,17 @@ size_t ObjectFileMachO::ParseSymtab() {
   Process *process = process_sp.get();
 
   uint32_t memory_module_load_level = eMemoryModuleLoadLevelComplete;
+  bool is_shared_cache_image = m_header.flags & MH_DYLIB_IN_CACHE;
+  bool is_local_shared_cache_image = is_shared_cache_image && !IsInMemory();
+  SectionSP linkedit_section_sp(
+      section_list->FindSectionByName(GetSegmentNameLINKEDIT()));
 
-  if (process && m_header.filetype != llvm::MachO::MH_OBJECT) {
+  if (process && m_header.filetype != llvm::MachO::MH_OBJECT &&
+      !is_local_shared_cache_image) {
     Target &target = process->GetTarget();
 
     memory_module_load_level = target.GetMemoryModuleLoadLevel();
 
-    SectionSP linkedit_section_sp(
-        section_list->FindSectionByName(GetSegmentNameLINKEDIT()));
     // Reading mach file from memory in a process or core file...
 
     if (linkedit_section_sp) {
@@ -2293,62 +2313,6 @@ size_t ObjectFileMachO::ParseSymtab() {
       strtab_addr = linkedit_load_addr + symtab_load_command.stroff -
                     linkedit_file_offset;
 
-      bool data_was_read = false;
-
-#if defined(__APPLE__) &&                                                      \
-    (defined(__arm__) || defined(__arm64__) || defined(__aarch64__))
-      if (m_header.flags & MH_DYLIB_IN_CACHE &&
-          process->GetAddressByteSize() == sizeof(void *)) {
-        // This mach-o memory file is in the dyld shared cache. If this
-        // program is not remote and this is iOS, then this process will
-        // share the same shared cache as the process we are debugging and we
-        // can read the entire __LINKEDIT from the address space in this
-        // process. This is a needed optimization that is used for local iOS
-        // debugging only since all shared libraries in the shared cache do
-        // not have corresponding files that exist in the file system of the
-        // device. They have been combined into a single file. This means we
-        // always have to load these files from memory. All of the symbol and
-        // string tables from all of the __LINKEDIT sections from the shared
-        // libraries in the shared cache have been merged into a single large
-        // symbol and string table. Reading all of this symbol and string
-        // table data across can slow down debug launch times, so we optimize
-        // this by reading the memory for the __LINKEDIT section from this
-        // process.
-
-        UUID lldb_shared_cache;
-        addr_t lldb_shared_cache_addr;
-        GetLLDBSharedCacheUUID(lldb_shared_cache_addr, lldb_shared_cache);
-        UUID process_shared_cache;
-        addr_t process_shared_cache_addr;
-        GetProcessSharedCacheUUID(process, process_shared_cache_addr,
-                                  process_shared_cache);
-        bool use_lldb_cache = true;
-        if (lldb_shared_cache.IsValid() && process_shared_cache.IsValid() &&
-            (lldb_shared_cache != process_shared_cache ||
-             process_shared_cache_addr != lldb_shared_cache_addr)) {
-          use_lldb_cache = false;
-        }
-
-        PlatformSP platform_sp(target.GetPlatform());
-        if (platform_sp && platform_sp->IsHost() && use_lldb_cache) {
-          data_was_read = true;
-          nlist_data.SetData((void *)symoff_addr, nlist_data_byte_size,
-                             eByteOrderLittle);
-          strtab_data.SetData((void *)strtab_addr, strtab_data_byte_size,
-                              eByteOrderLittle);
-          if (function_starts_load_command.cmd) {
-            const addr_t func_start_addr =
-                linkedit_load_addr + function_starts_load_command.dataoff -
-                linkedit_file_offset;
-            function_starts_data.SetData((void *)func_start_addr,
-                                         function_starts_load_command.datasize,
-                                         eByteOrderLittle);
-          }
-        }
-      }
-#endif
-
-      if (!data_was_read) {
         // Always load dyld - the dynamic linker - from memory if we didn't
         // find a binary anywhere else. lldb will not register
         // dylib/framework/bundle loads/unloads if we don't have the dyld
@@ -2379,7 +2343,7 @@ size_t ObjectFileMachO::ParseSymtab() {
             // problem. For binaries outside the shared cache, it's faster to
             // read the entire strtab at once instead of piece-by-piece as we
             // process the nlist records.
-            if ((m_header.flags & MH_DYLIB_IN_CACHE) == 0) {
+            if (!is_shared_cache_image) {
               DataBufferSP strtab_data_sp(
                   ReadMemory(process_sp, strtab_addr, strtab_data_byte_size));
               if (strtab_data_sp) {
@@ -2388,7 +2352,6 @@ size_t ObjectFileMachO::ParseSymtab() {
               }
             }
           }
-        }
         if (memory_module_load_level >= eMemoryModuleLoadLevelPartial) {
           if (function_starts_load_command.cmd) {
             const addr_t func_start_addr =
@@ -2405,6 +2368,24 @@ size_t ObjectFileMachO::ParseSymtab() {
       }
     }
   } else {
+    if (is_local_shared_cache_image) {
+      // The load commands in shared cache images are relative to the
+      // beginning of the shared cache, not the library image. The
+      // data we get handed when creating the ObjectFileMachO starts
+      // at the beginning of a specific library and spans to the end
+      // of the cache to be able to reach the shared LINKEDIT
+      // segments. We need to convert the load command offsets to be
+      // relative to the beginning of our specific image.
+      lldb::addr_t linkedit_offset = linkedit_section_sp->GetFileOffset();
+      lldb::offset_t linkedit_slide =
+          linkedit_offset - m_linkedit_original_offset;
+      symtab_load_command.symoff += linkedit_slide;
+      symtab_load_command.stroff += linkedit_slide;
+      dyld_info.export_off += linkedit_slide;
+      m_dysymtab.indirectsymoff += linkedit_slide;
+      function_starts_load_command.dataoff += linkedit_slide;
+    }
+
     nlist_data.SetData(m_data, symtab_load_command.symoff,
                        nlist_data_byte_size);
     strtab_data.SetData(m_data, symtab_load_command.stroff,
@@ -5807,8 +5788,7 @@ void ObjectFileMachO::GetLLDBSharedCacheUUID(addr_t &base_addr, UUID &uuid) {
   uuid.Clear();
   base_addr = LLDB_INVALID_ADDRESS;
 
-#if defined(__APPLE__) &&                                                      \
-    (defined(__arm__) || defined(__arm64__) || defined(__aarch64__))
+#if defined(__APPLE__)
   uint8_t *(*dyld_get_all_image_infos)(void);
   dyld_get_all_image_infos =
       (uint8_t * (*)()) dlsym(RTLD_DEFAULT, "_dyld_get_all_image_infos");
index 979e637ef6fd8aeac27e1c9b83ec158e0fc0f08f..0c1d178b1921575e5dcae8bc03ae6554840fa85f 100644 (file)
@@ -225,6 +225,8 @@ protected:
   typedef lldb_private::RangeVector<uint32_t, uint32_t> FileRangeArray;
   lldb_private::Address m_entry_point_address;
   FileRangeArray m_thread_context_offsets;
+  lldb::offset_t m_linkedit_original_offset;
+  lldb::addr_t m_text_address;
   bool m_thread_context_offsets_valid;
   lldb_private::FileSpecList m_reexported_dylibs;
   bool m_allow_assembly_emulation_unwind_plans;
index f5ec08a1a199cd9dbf7755dfccc5c76ca9812f6f..d31559bc90183337d3ccb013dc317c0131f4d613 100644 (file)
@@ -237,6 +237,30 @@ lldb_private::Status PlatformDarwin::GetSharedModuleWithLocalCache(
 
   Status err;
 
+  if (IsHost()) {
+    // When debugging on the host, we are most likely using the same shared
+    // cache as our inferior. The dylibs from the shared cache might not
+    // exist on the filesystem, so let's use the images in our own memory
+    // to create the modules.
+
+    // Check if the requested image is in our shared cache.
+    SharedCacheImageInfo image_info =
+        HostInfo::GetSharedCacheImageInfo(module_spec.GetFileSpec().GetPath());
+
+    // If we found it and it has the correct UUID, let's proceed with
+    // creating a module from the memory contents.
+    if (image_info.uuid &&
+        (!module_spec.GetUUID() || module_spec.GetUUID() == image_info.uuid)) {
+      ModuleSpec shared_cache_spec(module_spec.GetFileSpec(), image_info.uuid,
+                                   image_info.data_sp);
+      err = ModuleList::GetSharedModule(shared_cache_spec, module_sp,
+                                        module_search_paths_ptr,
+                                        old_module_sp_ptr, did_create_ptr);
+      if (module_sp)
+        return err;
+    }
+  }
+
   err = ModuleList::GetSharedModule(module_spec, module_sp,
                                     module_search_paths_ptr, old_module_sp_ptr,
                                     did_create_ptr);
index a9b42ea3199d7ce5b4668023af2c7b7344cd4f88..b5d248e3965d36235ae6022a20c202783df9c4dc 100644 (file)
@@ -1,3 +1,4 @@
 add_subdirectory(Breakpad)
 add_subdirectory(ELF)
+add_subdirectory(MachO)
 add_subdirectory(PECOFF)
diff --git a/lldb/unittests/ObjectFile/MachO/CMakeLists.txt b/lldb/unittests/ObjectFile/MachO/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b6c4225
--- /dev/null
@@ -0,0 +1,10 @@
+add_lldb_unittest(ObjectFileMachOTests
+  TestObjectFileMachO.cpp
+
+  LINK_LIBS
+    lldbPluginObjectFileMachO
+    lldbPluginSymbolFileSymtab
+    lldbCore
+    lldbUtilityHelpers
+    LLVMTestingSupport
+  )
diff --git a/lldb/unittests/ObjectFile/MachO/TestObjectFileMachO.cpp b/lldb/unittests/ObjectFile/MachO/TestObjectFileMachO.cpp
new file mode 100644 (file)
index 0000000..119be38
--- /dev/null
@@ -0,0 +1,79 @@
+//===-- ObjectFileMachOTest.cpp -------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/HostInfo.h"
+#include "Plugins/ObjectFile/Mach-O/ObjectFileMachO.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/lldb-defines.h"
+#include "gtest/gtest.h"
+
+#ifdef __APPLE__
+#include <dlfcn.h>
+#endif
+
+using namespace lldb_private;
+using namespace llvm;
+
+namespace {
+class ObjectFileMachOTest : public ::testing::Test {
+  SubsystemRAII<FileSystem, HostInfo, ObjectFileMachO> subsystems;
+};
+} // namespace
+
+#if defined(__APPLE__)
+TEST_F(ObjectFileMachOTest, ModuleFromSharedCacheInfo) {
+  SharedCacheImageInfo image_info =
+      HostInfo::GetSharedCacheImageInfo("/usr/lib/libobjc.A.dylib");
+  EXPECT_TRUE(image_info.uuid);
+  EXPECT_TRUE(image_info.data_sp);
+
+  ModuleSpec spec(FileSpec(), UUID(), image_info.data_sp);
+  lldb::ModuleSP module = std::make_shared<Module>(spec);
+  ObjectFile *OF = module->GetObjectFile();
+  ASSERT_TRUE(llvm::isa<ObjectFileMachO>(OF));
+  EXPECT_TRUE(
+      OF->GetArchitecture().IsCompatibleMatch(HostInfo::GetArchitecture()));
+  Symtab *symtab = OF->GetSymtab();
+  ASSERT_NE(symtab, nullptr);
+  void *libobjc = dlopen("/usr/lib/libobjc.A.dylib", RTLD_LAZY);
+  ASSERT_NE(libobjc, nullptr);
+
+  // This function checks that if we read something from the
+  // ObjectFile we get through the shared cache in-mmeory
+  // buffer, it matches what we get by reading directly the
+  // memory of the symbol.
+  auto check_symbol = [&](const char *sym_name) {
+    std::vector<uint32_t> symbol_indices;
+    symtab->FindAllSymbolsWithNameAndType(ConstString(sym_name),
+                                          lldb::eSymbolTypeAny, symbol_indices);
+    EXPECT_EQ(symbol_indices.size(), 1u);
+
+    Symbol *sym = symtab->SymbolAtIndex(symbol_indices[0]);
+    ASSERT_NE(sym, nullptr);
+    Address base = sym->GetAddress();
+    size_t size = sym->GetByteSize();
+    ASSERT_NE(size, 0u);
+    uint8_t buffer[size];
+    EXPECT_EQ(OF->ReadSectionData(base.GetSection().get(), base.GetOffset(),
+                                  buffer, size),
+              size);
+
+    void *sym_addr = dlsym(libobjc, sym_name);
+    ASSERT_NE(sym_addr, nullptr);
+    EXPECT_EQ(memcmp(buffer, sym_addr, size), 0);
+  };
+
+  // Read a symbol from the __TEXT segment...
+  check_symbol("objc_msgSend");
+  // ... and one from the __DATA segment
+  check_symbol("OBJC_CLASS_$_NSObject");
+}
+#endif