[go: nahoru, domu]

[Android Unwinder] Implement ChromeAndroidUnwinder V2

This CL implements the actual unwinder based on unwind table
and unwind info implemented in previous CLs.

Design Doc:
https://docs.google.com/document/d/1IYTmGCJZoiQ242xPUZX1fATD6ivsjU1TAt_fPv74ocs/edit?usp=sharing

Bug: 1240698
Change-Id: I098e3e47389189ef3953cd72cffc15d6763d5a24
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3122347
Commit-Queue: Charlie Hu <chenleihu@google.com>
Reviewed-by: Mike Wittman <wittman@chromium.org>
Cr-Commit-Position: refs/heads/main@{#930949}
diff --git a/base/profiler/chrome_unwind_info_android.cc b/base/profiler/chrome_unwind_info_android.cc
index b5d5d20..5bf790d9 100644
--- a/base/profiler/chrome_unwind_info_android.cc
+++ b/base/profiler/chrome_unwind_info_android.cc
@@ -20,6 +20,11 @@
 
 ChromeUnwindInfoAndroid::~ChromeUnwindInfoAndroid() = default;
 ChromeUnwindInfoAndroid::ChromeUnwindInfoAndroid(
+    const ChromeUnwindInfoAndroid& other) = default;
+ChromeUnwindInfoAndroid& ChromeUnwindInfoAndroid::operator=(
+    const ChromeUnwindInfoAndroid& other) = default;
+
+ChromeUnwindInfoAndroid::ChromeUnwindInfoAndroid(
     ChromeUnwindInfoAndroid&& other) = default;
 ChromeUnwindInfoAndroid& ChromeUnwindInfoAndroid::operator=(
     ChromeUnwindInfoAndroid&& other) = default;
diff --git a/base/profiler/chrome_unwind_info_android.h b/base/profiler/chrome_unwind_info_android.h
index 6586ef2..36cc4fc 100644
--- a/base/profiler/chrome_unwind_info_android.h
+++ b/base/profiler/chrome_unwind_info_android.h
@@ -88,6 +88,9 @@
                           span<const FunctionTableEntry> function_table,
                           span<const uint32_t> page_table);
   ~ChromeUnwindInfoAndroid();
+  ChromeUnwindInfoAndroid(const ChromeUnwindInfoAndroid& other);
+  ChromeUnwindInfoAndroid& operator=(const ChromeUnwindInfoAndroid& other);
+
   ChromeUnwindInfoAndroid(ChromeUnwindInfoAndroid&& other);
   ChromeUnwindInfoAndroid& operator=(ChromeUnwindInfoAndroid&& other);
 
diff --git a/base/profiler/chrome_unwinder_android_v2.cc b/base/profiler/chrome_unwinder_android_v2.cc
index d1d42c0..b96f3d1c 100644
--- a/base/profiler/chrome_unwinder_android_v2.cc
+++ b/base/profiler/chrome_unwinder_android_v2.cc
@@ -67,6 +67,76 @@
 
 }  // namespace
 
+ChromeUnwinderAndroidV2::ChromeUnwinderAndroidV2(
+    const ChromeUnwindInfoAndroid& unwind_info,
+    uintptr_t chrome_module_base_address,
+    uintptr_t text_section_start_address)
+    : unwind_info_(unwind_info),
+      chrome_module_base_address_(chrome_module_base_address),
+      text_section_start_address_(text_section_start_address) {
+  DCHECK_GT(text_section_start_address_, chrome_module_base_address_);
+}
+
+bool ChromeUnwinderAndroidV2::CanUnwindFrom(const Frame& current_frame) const {
+  return current_frame.module &&
+         current_frame.module->GetBaseAddress() == chrome_module_base_address_;
+}
+
+UnwindResult ChromeUnwinderAndroidV2::TryUnwind(
+    RegisterContext* thread_context,
+    uintptr_t stack_top,
+    std::vector<Frame>* stack) const {
+  DCHECK(CanUnwindFrom(stack->back()));
+  do {
+    const uintptr_t pc = RegisterContextInstructionPointer(thread_context);
+    const uintptr_t instruction_offset_from_text_section_start =
+        pc - text_section_start_address_;
+
+    const absl::optional<FunctionOffsetTableIndex> function_offset_table_index =
+        GetFunctionTableIndexFromInstructionOffset(
+            unwind_info_.page_table, unwind_info_.function_table,
+            instruction_offset_from_text_section_start);
+
+    if (!function_offset_table_index) {
+      return UnwindResult::ABORTED;
+    }
+
+    const uint32_t current_unwind_instruction_index =
+        GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
+            &unwind_info_
+                 .function_offset_table[function_offset_table_index
+                                            ->function_offset_table_byte_index],
+            function_offset_table_index
+                ->instruction_offset_from_function_start);
+
+    const uint8_t* current_unwind_instruction =
+        &unwind_info_
+             .unwind_instruction_table[current_unwind_instruction_index];
+
+    UnwindInstructionResult instruction_result;
+    bool pc_was_updated = false;
+
+    do {
+      instruction_result = ExecuteUnwindInstruction(
+          current_unwind_instruction, pc_was_updated, thread_context);
+      if (RegisterContextStackPointer(thread_context) >= stack_top) {
+        return UnwindResult::ABORTED;
+      }
+    } while (instruction_result ==
+             UnwindInstructionResult::kInstructionPending);
+
+    if (instruction_result == UnwindInstructionResult::kAborted) {
+      return UnwindResult::ABORTED;
+    }
+
+    DCHECK_EQ(instruction_result, UnwindInstructionResult::kCompleted);
+    stack->emplace_back(RegisterContextInstructionPointer(thread_context),
+                        module_cache()->GetModuleForAddress(
+                            RegisterContextInstructionPointer(thread_context)));
+  } while (CanUnwindFrom(stack->back()));
+  return UnwindResult::UNRECOGNIZED_FRAME;
+}
+
 UnwindInstructionResult ExecuteUnwindInstruction(
     const uint8_t*& instruction,
     bool& pc_was_updated,
@@ -167,13 +237,12 @@
   return UnwindInstructionResult::kInstructionPending;
 }
 
-const uint8_t* GetFirstUnwindInstructionFromFunctionOffsetTableIndex(
-    const uint8_t* unwind_instruction_table,
-    const uint8_t* function_offset_table,
-    const FunctionOffsetTableIndex& index) {
-  DCHECK_GE(index.instruction_offset_from_function_start, 0);
+uintptr_t GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
+    const uint8_t* function_offset_table_entry,
+    int instruction_offset_from_function_start) {
+  DCHECK_GE(instruction_offset_from_function_start, 0);
   const uint8_t* current_function_offset_table_position =
-      &function_offset_table[index.function_offset_table_byte_index];
+      function_offset_table_entry;
 
   do {
     const uintptr_t function_offset =
@@ -185,13 +254,13 @@
     // Each function always ends at 0 offset. It is guaranteed to find an entry
     // as long as the function offset table is well-structured.
     if (function_offset <=
-        static_cast<uint32_t>(index.instruction_offset_from_function_start))
-      return &unwind_instruction_table[unwind_table_index];
+        static_cast<uint32_t>(instruction_offset_from_function_start))
+      return unwind_table_index;
 
   } while (true);
 
   NOTREACHED();
-  return nullptr;
+  return 0;
 }
 
 const absl::optional<FunctionOffsetTableIndex>
@@ -300,4 +369,4 @@
   };
 }
 
-}  // namespace base
\ No newline at end of file
+}  // namespace base
diff --git a/base/profiler/chrome_unwinder_android_v2.h b/base/profiler/chrome_unwinder_android_v2.h
index d8f63f0..5e26347 100644
--- a/base/profiler/chrome_unwinder_android_v2.h
+++ b/base/profiler/chrome_unwinder_android_v2.h
@@ -9,11 +9,38 @@
 
 #include "base/base_export.h"
 #include "base/containers/span.h"
+#include "base/profiler/chrome_unwind_info_android.h"
+#include "base/profiler/module_cache.h"
 #include "base/profiler/register_context.h"
+#include "base/profiler/unwinder.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace base {
 
+// Chrome unwinder implementation for Android, using ChromeUnwindInfoAndroid,
+// a separate binary resource. This implementation is intended to replace
+// `ChromeUnwinderAndroid`, which uses ArmCfiTable.
+class BASE_EXPORT ChromeUnwinderAndroidV2 : public Unwinder {
+ public:
+  ChromeUnwinderAndroidV2(const ChromeUnwindInfoAndroid& unwind_info,
+                          uintptr_t chrome_module_base_address,
+                          uintptr_t text_section_start_address);
+  ChromeUnwinderAndroidV2(const ChromeUnwinderAndroidV2&) = delete;
+  ChromeUnwinderAndroidV2& operator=(const ChromeUnwinderAndroidV2&) = delete;
+
+  // Unwinder:
+  bool CanUnwindFrom(const Frame& current_frame) const override;
+  UnwindResult TryUnwind(RegisterContext* thread_context,
+                         uintptr_t stack_top,
+                         std::vector<Frame>* stack) const override;
+
+ private:
+  const ChromeUnwindInfoAndroid unwind_info_;
+  const uintptr_t chrome_module_base_address_;
+  const uintptr_t text_section_start_address_;
+};
+
+// Following functions are exposed for testing purpose only.
 struct FunctionTableEntry;
 
 enum class UnwindInstructionResult {
@@ -43,27 +70,29 @@
 // table.
 struct FunctionOffsetTableIndex {
   // Number of 2-byte instructions between the instruction of interest and
-  // function_start_address.
+  // function start address.
   int instruction_offset_from_function_start;
   // The byte index of the first offset for the function in the function
   // offset table.
   uint16_t function_offset_table_byte_index;
 };
 
-// Given `FunctionOffsetTableIndex`, finds the instruction to execute on unwind
-// instruction table.
+// Given function offset table entry, finds the first unwind instruction to
+// execute in unwind instruction table.
 //
 // Arguments:
-//  unwind_instruction_table: See
-//    `ChromeUnwindInfoAndroid::unwind_instruction_table` for details.
-//  function_offset_table: See
+//  function_offset_table_entry: An entry in function offset table. See
 //    `ChromeUnwindInfoAndroid::function_offset_table` for details.
-//  index: The index used to locate an entry in `function_offset_table`.
-BASE_EXPORT const uint8_t*
-GetFirstUnwindInstructionFromFunctionOffsetTableIndex(
-    const uint8_t* unwind_instruction_table,
-    const uint8_t* function_offset_table,
-    const FunctionOffsetTableIndex& index);
+//  instruction_offset_from_function_start: Number of 2-byte instructions
+//    between the instruction of interest and function start address.
+//
+// Returns:
+//   The index of the first unwind instruction to execute in
+//   `ChromeUnwindInfoAndroid::unwind_instruction_table`.
+BASE_EXPORT uintptr_t
+GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
+    const uint8_t* function_offset_table_entry,
+    int instruction_offset_from_function_start);
 
 // Given an instruction_offset_from_text_section_start, finds the corresponding
 // `FunctionOffsetTableIndex`.
diff --git a/base/profiler/chrome_unwinder_android_v2_unittest.cc b/base/profiler/chrome_unwinder_android_v2_unittest.cc
index ba39d7e7..4d1da87 100644
--- a/base/profiler/chrome_unwinder_android_v2_unittest.cc
+++ b/base/profiler/chrome_unwinder_android_v2_unittest.cc
@@ -658,7 +658,6 @@
 
 TEST(ChromeUnwinderAndroidV2Test,
      TestFunctionOffsetTableLookupExactMatchingOffset) {
-  const uint8_t unwind_instruction_table[] = {0, 1, 2, 3, 4, 5, 6};
   const uint8_t function_offset_table[] = {
       // Function 1: [(130, 2), (128, 3), (0, 4)]
       // offset = 130
@@ -677,16 +676,13 @@
       0b00000100,
   };
 
-  EXPECT_EQ(unwind_instruction_table + 3,
-            GetFirstUnwindInstructionFromFunctionOffsetTableIndex(
-                unwind_instruction_table, function_offset_table,
-                {/* instruction_offset_from_function_start */ 128,
-                 /* function_offset_table_byte_index */ 0x0}));
+  EXPECT_EQ(3ul, GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
+                     &function_offset_table[0],
+                     /* instruction_offset_from_function_start */ 128));
 }
 
 TEST(ChromeUnwinderAndroidV2Test,
      TestFunctionOffsetTableLookupNonExactMatchingOffset) {
-  const uint8_t unwind_instruction_table[] = {0, 1, 2, 3, 4, 5, 6};
   const uint8_t function_offset_table[] = {
       // Function 1: [(130, 2), (128, 3), (0, 4)]
       // offset = 130
@@ -705,15 +701,12 @@
       0b00000100,
   };
 
-  EXPECT_EQ(unwind_instruction_table + 3,
-            GetFirstUnwindInstructionFromFunctionOffsetTableIndex(
-                unwind_instruction_table, function_offset_table,
-                {/* instruction_offset_from_function_start */ 129,
-                 /* function_offset_table_byte_index */ 0x0}));
+  EXPECT_EQ(3ul, GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
+                     &function_offset_table[0],
+                     /* instruction_offset_from_function_start */ 129));
 }
 
 TEST(ChromeUnwinderAndroidV2Test, TestFunctionOffsetTableLookupZeroOffset) {
-  const uint8_t unwind_instruction_table[] = {0, 1, 2, 3, 4, 5, 6};
   const uint8_t function_offset_table[] = {
       // Function 1: [(130, 2), (128, 3), (0, 4)]
       // offset = 130
@@ -732,11 +725,9 @@
       0b00000100,
   };
 
-  EXPECT_EQ(unwind_instruction_table + 4,
-            GetFirstUnwindInstructionFromFunctionOffsetTableIndex(
-                unwind_instruction_table, function_offset_table,
-                {/* instruction_offset_from_function_start */ 0,
-                 /* function_offset_table_byte_index */ 0x0}));
+  EXPECT_EQ(4ul, GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
+                     &function_offset_table[0],
+                     /* instruction_offset_from_function_start */ 0));
 }
 
 TEST(ChromeUnwinderAndroidV2Test, TestAddressTableLookupEntryInPage) {
@@ -957,4 +948,165 @@
     EXPECT_EQ(20ul, entry_found->function_offset_table_byte_index);
   }
 }
+
+class TestModule : public ModuleCache::Module {
+ public:
+  TestModule(uintptr_t base_address,
+             size_t size,
+             const std::string& build_id = "TestModule")
+      : base_address_(base_address), size_(size), build_id_(build_id) {}
+
+  uintptr_t GetBaseAddress() const override { return base_address_; }
+  std::string GetId() const override { return build_id_; }
+  FilePath GetDebugBasename() const override { return FilePath(); }
+  size_t GetSize() const override { return size_; }
+  bool IsNative() const override { return true; }
+
+ private:
+  const uintptr_t base_address_;
+  const size_t size_;
+  const std::string build_id_;
+};
+
+// Utility function to add a single native module during test setup. Returns
+// a pointer to the provided module.
+const ModuleCache::Module* AddNativeModule(
+    ModuleCache* cache,
+    std::unique_ptr<const ModuleCache::Module> module) {
+  const ModuleCache::Module* module_ptr = module.get();
+  cache->AddCustomNativeModule(std::move(module));
+  return module_ptr;
+}
+
+TEST(ChromeUnwinderAndroidV2Test, CanUnwindFrom) {
+  const uint32_t page_table[] = {0};
+  const FunctionTableEntry function_table[] = {{0, 0}};
+  const uint8_t function_offset_table[] = {0};
+  const uint8_t unwind_instruction_table[] = {0};
+  auto dummy_unwind_info = ChromeUnwindInfoAndroid{
+      make_span(unwind_instruction_table, 1ul),
+      make_span(function_offset_table, 1ul),
+      make_span(function_table, 1ul),
+      make_span(page_table, 1ul),
+  };
+
+  auto chrome_module =
+      std::make_unique<TestModule>(0x1000, 0x500, "ChromeModule");
+  auto non_chrome_module =
+      std::make_unique<TestModule>(0x2000, 0x500, "OtherModule");
+
+  ModuleCache module_cache;
+  ChromeUnwinderAndroidV2 unwinder(dummy_unwind_info,
+                                   chrome_module->GetBaseAddress(),
+                                   /* text_section_start_address */
+                                   chrome_module->GetBaseAddress() + 4);
+  unwinder.Initialize(&module_cache);
+
+  EXPECT_TRUE(unwinder.CanUnwindFrom({0x1100, chrome_module.get()}));
+  EXPECT_TRUE(unwinder.CanUnwindFrom({0x1000, chrome_module.get()}));
+  EXPECT_FALSE(unwinder.CanUnwindFrom({0x2100, non_chrome_module.get()}));
+  EXPECT_FALSE(unwinder.CanUnwindFrom({0x400, nullptr}));
+}
+
+void ExpectFramesEq(const std::vector<Frame>& actual,
+                    const std::vector<Frame>& expected) {
+  EXPECT_EQ(actual.size(), expected.size());
+  if (actual.size() != expected.size())
+    return;
+
+  for (size_t i = 0; i < actual.size(); i++) {
+    EXPECT_EQ(actual[i].module, expected[i].module);
+    EXPECT_EQ(actual[i].instruction_pointer, expected[i].instruction_pointer);
+  }
+}
+
+TEST(ChromeUnwinderAndroidV2Test, TryUnwind) {
+  const uint32_t page_table[] = {0, 2};
+  const size_t number_of_pages = base::size(page_table);
+  const size_t page_size = 1 << 17;
+
+  const FunctionTableEntry function_table[] = {
+      // Page 0.
+      {0, 0},     // Function 0.
+      {0x10, 4},  // Function 1. The function to unwind 2 times.
+      // Page 1.
+      {0x5, 8},    // Function 2.
+      {0x20, 12},  // Function 3.
+  };
+  const uint8_t function_offset_table[] = {
+      // Function 0.
+      0x2,
+      0,
+      0x0,
+      2,
+      // Function 1.
+      0x7f,
+      0,
+      0x0,
+      2,
+      // Function 2.
+      0x78,
+      0,
+      0x0,
+      2,
+      // Function 3.
+      0x2,
+      0,
+      0x0,
+      2,
+  };
+  const uint8_t unwind_instruction_table[] = {
+      // Offset 0: Pop r14 from stack top.
+      0b10000100,
+      0b00000000,
+      // Offset 2: COMPLETE.
+      0b10110000,
+  };
+
+  auto unwind_info = ChromeUnwindInfoAndroid{
+      make_span(unwind_instruction_table, base::size(unwind_instruction_table)),
+      make_span(function_offset_table, base::size(function_offset_table)),
+      make_span(function_table, base::size(function_table)),
+      make_span(page_table, base::size(page_table)),
+  };
+
+  ModuleCache module_cache;
+  const ModuleCache::Module* chrome_module = AddNativeModule(
+      &module_cache, std::make_unique<TestModule>(
+                         0x1000, number_of_pages * page_size, "ChromeModule"));
+
+  uintptr_t text_section_start_address = 0x1100;
+  ChromeUnwinderAndroidV2 unwinder(unwind_info, chrome_module->GetBaseAddress(),
+                                   text_section_start_address);
+
+  unwinder.Initialize(&module_cache);
+
+  // Both first_pc and second_pc lie in Function 1's address range.
+  uintptr_t first_pc = text_section_start_address + 0x20;
+  uintptr_t second_pc = text_section_start_address + page_size + 0x4;
+  // third_pc lies outside chrome_module's address range.
+  uintptr_t third_pc = text_section_start_address + 3 * page_size;
+
+  const std::vector<uintptr_t> stack_memory = {
+      third_pc,
+      0xFFFF,
+  };
+  uintptr_t stack_top =
+      reinterpret_cast<uintptr_t>(stack_memory.data() + stack_memory.size());
+
+  std::vector<Frame> unwound_frames = {{first_pc, chrome_module}};
+  RegisterContext context;
+  RegisterContextInstructionPointer(&context) = first_pc;
+  RegisterContextStackPointer(&context) =
+      reinterpret_cast<uintptr_t>(stack_memory.data());
+  context.arm_lr = second_pc;
+
+  EXPECT_EQ(UnwindResult::UNRECOGNIZED_FRAME,
+            unwinder.TryUnwind(&context, stack_top, &unwound_frames));
+  ExpectFramesEq(std::vector<Frame>({{first_pc, chrome_module},
+                                     {second_pc, chrome_module},
+                                     {third_pc, nullptr}}),
+                 unwound_frames);
+}
+
 }  // namespace base