perf: add userspace frame pointer unwinder

This change introduces the UNWIND_FRAME_POINTER option to the UnwindMode
enum, allowing users to select the frame pointer unwinder for unwinding
userspace stack traces.

github issue: https://github.com/google/perfetto/issues/907

Change-Id: I82b612a7c534f3d3adaa1b0faf28651023429325
diff --git a/Android.bp b/Android.bp
index 2341f3b..2aa73b8 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11517,6 +11517,7 @@
     name: "perfetto_src_profiling_perf_producer_unittests",
     srcs: [
         "src/profiling/perf/event_config_unittest.cc",
+        "src/profiling/perf/frame_pointer_unwinder_unittest.cc",
         "src/profiling/perf/perf_producer_unittest.cc",
         "src/profiling/perf/unwind_queue_unittest.cc",
     ],
@@ -11542,6 +11543,7 @@
 filegroup {
     name: "perfetto_src_profiling_perf_unwinding",
     srcs: [
+        "src/profiling/perf/frame_pointer_unwinder.cc",
         "src/profiling/perf/unwinding.cc",
     ],
 }
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index a87e2a7..c29ca82 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -1957,6 +1957,8 @@
     UNWIND_SKIP = 1;
     // Use libunwindstack (default):
     UNWIND_DWARF = 2;
+    // Use userspace frame pointer unwinder:
+    UNWIND_FRAME_POINTER = 3;
   }
 }
 
diff --git a/protos/perfetto/config/profiling/perf_event_config.proto b/protos/perfetto/config/profiling/perf_event_config.proto
index b02aa07..d3cc51c 100644
--- a/protos/perfetto/config/profiling/perf_event_config.proto
+++ b/protos/perfetto/config/profiling/perf_event_config.proto
@@ -217,5 +217,7 @@
     UNWIND_SKIP = 1;
     // Use libunwindstack (default):
     UNWIND_DWARF = 2;
+    // Use userspace frame pointer unwinder:
+    UNWIND_FRAME_POINTER = 3;
   }
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index df04bec..1e8bee7 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -1957,6 +1957,8 @@
     UNWIND_SKIP = 1;
     // Use libunwindstack (default):
     UNWIND_DWARF = 2;
+    // Use userspace frame pointer unwinder:
+    UNWIND_FRAME_POINTER = 3;
   }
 }
 
diff --git a/src/profiling/perf/BUILD.gn b/src/profiling/perf/BUILD.gn
index d77cb84..702b798 100644
--- a/src/profiling/perf/BUILD.gn
+++ b/src/profiling/perf/BUILD.gn
@@ -101,6 +101,8 @@
     "../common:unwind_support",
   ]
   sources = [
+    "frame_pointer_unwinder.cc",
+    "frame_pointer_unwinder.h",
     "unwind_queue.h",
     "unwinding.cc",
     "unwinding.h",
@@ -147,6 +149,7 @@
   ]
   sources = [
     "event_config_unittest.cc",
+    "frame_pointer_unwinder_unittest.cc",
     "perf_producer_unittest.cc",
     "unwind_queue_unittest.cc",
   ]
diff --git a/src/profiling/perf/event_config.cc b/src/profiling/perf/event_config.cc
index 7409c18..94d563c 100644
--- a/src/profiling/perf/event_config.cc
+++ b/src/profiling/perf/event_config.cc
@@ -273,6 +273,20 @@
   }
 }
 
+bool IsSupportedUnwindMode(
+    protos::gen::PerfEventConfig::UnwindMode unwind_mode) {
+  using protos::gen::PerfEventConfig;
+  switch (static_cast<int>(unwind_mode)) {  // cast to pacify -Wswitch-enum
+    case PerfEventConfig::UNWIND_UNKNOWN:
+    case PerfEventConfig::UNWIND_SKIP:
+    case PerfEventConfig::UNWIND_DWARF:
+    case PerfEventConfig::UNWIND_FRAME_POINTER:
+      return true;
+    default:
+      return false;
+  }
+}
+
 }  // namespace
 
 // static
@@ -371,32 +385,18 @@
   }
 
   // Callstack sampling.
-  bool user_frames = false;
   bool kernel_frames = false;
+  // Disable user_frames by default.
+  auto unwind_mode = protos::gen::PerfEventConfig::UNWIND_SKIP;
+
   TargetFilter target_filter;
   bool legacy_config = pb_config.all_cpus();  // all_cpus was mandatory before
   if (pb_config.has_callstack_sampling() || legacy_config) {
-    user_frames = true;
-
     // Userspace callstacks.
-    using protos::gen::PerfEventConfig;
-    switch (static_cast<int>(pb_config.callstack_sampling().user_frames())) {
-      case PerfEventConfig::UNWIND_UNKNOWN:
-        // default to true, both for backwards compatibility and because it's
-        // almost always what the user wants.
-        user_frames = true;
-        break;
-      case PerfEventConfig::UNWIND_SKIP:
-        user_frames = false;
-        break;
-      case PerfEventConfig::UNWIND_DWARF:
-        user_frames = true;
-        break;
-      default:
-        // enum value from the future that we don't yet know, refuse the config
-        // TODO(rsavitski): double-check that both pbzero and ::gen propagate
-        // unknown enum values.
-        return std::nullopt;
+    unwind_mode = pb_config.callstack_sampling().user_frames();
+    if (!IsSupportedUnwindMode(unwind_mode)) {
+      // enum value from the future that we don't yet know, refuse the config
+      return std::nullopt;
     }
 
     // Process scoping. Sharding parameter is supplied from outside as it is
@@ -482,7 +482,7 @@
   pe.clockid = ToClockId(pb_config.timebase().timestamp_clock());
   pe.use_clockid = true;
 
-  if (user_frames) {
+  if (IsUserFramesEnabled(unwind_mode)) {
     pe.sample_type |= PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER;
     // PERF_SAMPLE_STACK_USER:
     // Needs to be < ((u16)(~0u)), and have bottom 8 bits clear.
@@ -529,19 +529,35 @@
 
   return EventConfig(
       raw_ds_config, pe, std::move(pe_followers), timebase_event, followers,
-      user_frames, kernel_frames, std::move(target_filter),
+      kernel_frames, unwind_mode, std::move(target_filter),
       ring_buffer_pages.value(), read_tick_period_ms, samples_per_tick_limit,
       remote_descriptor_timeout_ms, pb_config.unwind_state_clear_period_ms(),
       max_enqueued_footprint_bytes, pb_config.target_installed_by());
 }
 
+// static
+bool EventConfig::IsUserFramesEnabled(
+    const protos::gen::PerfEventConfig::UnwindMode unwind_mode) {
+  using protos::gen::PerfEventConfig;
+  switch (unwind_mode) {
+    case PerfEventConfig::UNWIND_UNKNOWN:
+    // default to true, both for backwards compatibility and because it's
+    // almost always what the user wants.
+    case PerfEventConfig::UNWIND_DWARF:
+    case PerfEventConfig::UNWIND_FRAME_POINTER:
+      return true;
+    case PerfEventConfig::UNWIND_SKIP:
+      return false;
+  }
+}
+
 EventConfig::EventConfig(const DataSourceConfig& raw_ds_config,
                          const perf_event_attr& pe_timebase,
                          std::vector<perf_event_attr> pe_followers,
                          const PerfCounter& timebase_event,
                          std::vector<PerfCounter> follower_events,
-                         bool user_frames,
                          bool kernel_frames,
+                         protos::gen::PerfEventConfig::UnwindMode unwind_mode,
                          TargetFilter target_filter,
                          uint32_t ring_buffer_pages,
                          uint32_t read_tick_period_ms,
@@ -554,8 +570,8 @@
       perf_event_followers_(std::move(pe_followers)),
       timebase_event_(timebase_event),
       follower_events_(std::move(follower_events)),
-      user_frames_(user_frames),
       kernel_frames_(kernel_frames),
+      unwind_mode_(unwind_mode),
       target_filter_(std::move(target_filter)),
       ring_buffer_pages_(ring_buffer_pages),
       read_tick_period_ms_(read_tick_period_ms),
diff --git a/src/profiling/perf/event_config.h b/src/profiling/perf/event_config.h
index fc498f7..a87429d 100644
--- a/src/profiling/perf/event_config.h
+++ b/src/profiling/perf/event_config.h
@@ -31,6 +31,7 @@
 #include "perfetto/tracing/core/data_source_config.h"
 
 #include "protos/perfetto/common/perf_events.gen.h"
+#include "protos/perfetto/config/profiling/perf_event_config.gen.h"
 
 namespace perfetto {
 namespace protos {
@@ -136,9 +137,12 @@
   uint64_t max_enqueued_footprint_bytes() const {
     return max_enqueued_footprint_bytes_;
   }
-  bool sample_callstacks() const { return user_frames_ || kernel_frames_; }
-  bool user_frames() const { return user_frames_; }
+  bool sample_callstacks() const { return user_frames() || kernel_frames_; }
+  bool user_frames() const { return IsUserFramesEnabled(unwind_mode_); }
   bool kernel_frames() const { return kernel_frames_; }
+  protos::gen::PerfEventConfig::UnwindMode unwind_mode() const {
+    return unwind_mode_;
+  }
   const TargetFilter& filter() const { return target_filter_; }
   perf_event_attr* perf_attr() const {
     return const_cast<perf_event_attr*>(&perf_event_attr_);
@@ -158,13 +162,16 @@
   const DataSourceConfig& raw_ds_config() const { return raw_ds_config_; }
 
  private:
+  static bool IsUserFramesEnabled(
+      const protos::gen::PerfEventConfig::UnwindMode unwind_mode);
+
   EventConfig(const DataSourceConfig& raw_ds_config,
               const perf_event_attr& pe_timebase,
               std::vector<perf_event_attr> pe_followers,
               const PerfCounter& timebase_event,
               std::vector<PerfCounter> follower_events,
-              bool user_frames,
               bool kernel_frames,
+              protos::gen::PerfEventConfig::UnwindMode unwind_mode,
               TargetFilter target_filter,
               uint32_t ring_buffer_pages,
               uint32_t read_tick_period_ms,
@@ -187,12 +194,12 @@
   // Timebase event, which are already described by |perf_event_followers_|.
   std::vector<PerfCounter> follower_events_;
 
-  // If true, include userspace frames in sampled callstacks.
-  const bool user_frames_;
-
   // If true, include kernel frames in sampled callstacks.
   const bool kernel_frames_;
 
+  // Userspace unwinding mode.
+  const protos::gen::PerfEventConfig::UnwindMode unwind_mode_;
+
   // Parsed allow/deny-list for filtering samples.
   const TargetFilter target_filter_;
 
diff --git a/src/profiling/perf/frame_pointer_unwinder.cc b/src/profiling/perf/frame_pointer_unwinder.cc
new file mode 100644
index 0000000..6841dc6
--- /dev/null
+++ b/src/profiling/perf/frame_pointer_unwinder.cc
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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 "src/profiling/perf/frame_pointer_unwinder.h"
+
+#include <cinttypes>
+
+#include "perfetto/base/logging.h"
+
+namespace perfetto {
+namespace profiling {
+
+void FramePointerUnwinder::Unwind() {
+  if (!IsArchSupported()) {
+    PERFETTO_ELOG("Unsupported architecture: %d", arch_);
+    last_error_.code = unwindstack::ErrorCode::ERROR_UNSUPPORTED;
+    return;
+  }
+
+  if (maps_ == nullptr || maps_->Total() == 0) {
+    PERFETTO_ELOG("No maps provided");
+    last_error_.code = unwindstack::ErrorCode::ERROR_INVALID_MAP;
+    return;
+  }
+
+  PERFETTO_DCHECK(stack_size_ > 0u);
+
+  frames_.reserve(max_frames_);
+  ClearErrors();
+  TryUnwind();
+}
+
+void FramePointerUnwinder::TryUnwind() {
+  uint64_t fp = 0;
+  switch (arch_) {
+    case unwindstack::ARCH_ARM64:
+      fp = reinterpret_cast<uint64_t*>(
+          regs_->RawData())[unwindstack::Arm64Reg::ARM64_REG_R29];
+      break;
+    case unwindstack::ARCH_X86_64:
+      fp = reinterpret_cast<uint64_t*>(
+          regs_->RawData())[unwindstack::X86_64Reg::X86_64_REG_RBP];
+      break;
+    case unwindstack::ARCH_RISCV64:
+      fp = reinterpret_cast<uint64_t*>(
+          regs_->RawData())[unwindstack::Riscv64Reg::RISCV64_REG_S0];
+      break;
+    case unwindstack::ARCH_UNKNOWN:
+    case unwindstack::ARCH_ARM:
+    case unwindstack::ARCH_X86:
+        // not supported
+        ;
+  }
+  uint64_t sp = regs_->sp();
+  uint64_t pc = regs_->pc();
+  for (size_t i = 0; i < max_frames_; i++) {
+    if (!IsFrameValid(fp, sp))
+      return;
+
+    // retrive the map info and elf info
+    std::shared_ptr<unwindstack::MapInfo> map_info = maps_->Find(pc);
+    if (map_info == nullptr) {
+      last_error_.code = unwindstack::ErrorCode::ERROR_INVALID_MAP;
+      return;
+    }
+
+    unwindstack::FrameData frame;
+    frame.num = i;
+    frame.rel_pc = pc;
+    frame.pc = pc;
+    frame.map_info = map_info;
+    unwindstack::Elf* elf = map_info->GetElf(process_memory_, arch_);
+    if (elf != nullptr) {
+      uint64_t relative_pc = elf->GetRelPc(pc, map_info.get());
+      uint64_t pc_adjustment = GetPcAdjustment(relative_pc, elf, arch_);
+      frame.rel_pc = relative_pc - pc_adjustment;
+      frame.pc = pc - pc_adjustment;
+      if (!resolve_names_ ||
+          !elf->GetFunctionName(frame.rel_pc, &frame.function_name,
+                                &frame.function_offset)) {
+        frame.function_name = "";
+        frame.function_offset = 0;
+      }
+    }
+    frames_.push_back(frame);
+    // move to the next frame
+    fp = DecodeFrame(fp, &pc, &sp);
+  }
+}
+
+uint64_t FramePointerUnwinder::DecodeFrame(uint64_t fp,
+                                           uint64_t* next_pc,
+                                           uint64_t* next_sp) {
+  uint64_t next_fp;
+  if (!process_memory_->ReadFully(static_cast<uint64_t>(fp), &next_fp,
+                                  sizeof(next_fp)))
+    return 0;
+
+  uint64_t pc;
+  if (!process_memory_->ReadFully(static_cast<uint64_t>(fp + sizeof(uint64_t)),
+                                  &pc, sizeof(pc)))
+    return 0;
+
+  // Ensure there's not a stack overflow.
+  if (__builtin_add_overflow(fp, sizeof(uint64_t) * 2, next_sp))
+    return 0;
+
+  *next_pc = static_cast<uint64_t>(pc);
+  return next_fp;
+}
+
+bool FramePointerUnwinder::IsFrameValid(uint64_t fp, uint64_t sp) {
+  uint64_t align_mask = 0;
+  switch (arch_) {
+    case unwindstack::ARCH_ARM64:
+      align_mask = 0x1;
+      break;
+    case unwindstack::ARCH_X86_64:
+      align_mask = 0xf;
+      break;
+    case unwindstack::ARCH_RISCV64:
+      align_mask = 0x7;
+      break;
+    case unwindstack::ARCH_UNKNOWN:
+    case unwindstack::ARCH_ARM:
+    case unwindstack::ARCH_X86:
+        // not supported
+        ;
+  }
+
+  if (fp == 0 || fp <= sp)
+    return false;
+
+  // Ensure there's space on the stack to read two values: the caller's
+  // frame pointer and the return address.
+  uint64_t result;
+  if (__builtin_add_overflow(fp, sizeof(uint64_t) * 2, &result))
+    return false;
+
+  return result <= stack_end_ && (fp & align_mask) == 0;
+}
+
+}  // namespace profiling
+}  // namespace perfetto
diff --git a/src/profiling/perf/frame_pointer_unwinder.h b/src/profiling/perf/frame_pointer_unwinder.h
new file mode 100644
index 0000000..14534d9
--- /dev/null
+++ b/src/profiling/perf/frame_pointer_unwinder.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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 SRC_PROFILING_PERF_FRAME_POINTER_UNWINDER_H_
+#define SRC_PROFILING_PERF_FRAME_POINTER_UNWINDER_H_
+
+#include <stdint.h>
+#include <memory>
+#include <vector>
+
+#include <unwindstack/Error.h>
+#include <unwindstack/MachineArm64.h>
+#include <unwindstack/MachineRiscv64.h>
+#include <unwindstack/MachineX86_64.h>
+#include <unwindstack/Unwinder.h>
+
+namespace perfetto {
+namespace profiling {
+
+class FramePointerUnwinder {
+ public:
+  FramePointerUnwinder(size_t max_frames,
+                       unwindstack::Maps* maps,
+                       unwindstack::Regs* regs,
+                       std::shared_ptr<unwindstack::Memory> process_memory,
+                       size_t stack_size)
+      : max_frames_(max_frames),
+        maps_(maps),
+        regs_(regs),
+        process_memory_(process_memory),
+        stack_size_(stack_size),
+        arch_(regs->Arch()) {
+    stack_end_ = regs->sp() + stack_size;
+  }
+
+  FramePointerUnwinder(const FramePointerUnwinder&) = delete;
+  FramePointerUnwinder& operator=(const FramePointerUnwinder&) = delete;
+
+  void Unwind();
+
+  // Disabling the resolving of names results in the function name being
+  // set to an empty string and the function offset being set to zero.
+  void SetResolveNames(bool resolve) { resolve_names_ = resolve; }
+
+  unwindstack::ErrorCode LastErrorCode() const { return last_error_.code; }
+  uint64_t warnings() const { return warnings_; }
+
+  std::vector<unwindstack::FrameData> ConsumeFrames() {
+    std::vector<unwindstack::FrameData> frames = std::move(frames_);
+    frames_.clear();
+    return frames;
+  }
+
+  bool IsArchSupported() const {
+    return arch_ == unwindstack::ARCH_ARM64 ||
+           arch_ == unwindstack::ARCH_X86_64;
+  }
+
+  void ClearErrors() {
+    warnings_ = unwindstack::WARNING_NONE;
+    last_error_.code = unwindstack::ERROR_NONE;
+    last_error_.address = 0;
+  }
+
+ protected:
+  const size_t max_frames_;
+  unwindstack::Maps* maps_;
+  unwindstack::Regs* regs_;
+  std::vector<unwindstack::FrameData> frames_;
+  std::shared_ptr<unwindstack::Memory> process_memory_;
+  const size_t stack_size_;
+  unwindstack::ArchEnum arch_ = unwindstack::ARCH_UNKNOWN;
+  bool resolve_names_ = false;
+  size_t stack_end_;
+
+  unwindstack::ErrorData last_error_;
+  uint64_t warnings_ = 0;
+
+ private:
+  void TryUnwind();
+  // Given a frame pointer, returns the frame pointer of the calling stack
+  // frame, places the return address of the calling stack frame into
+  // `ret_addr` and stack pointer into `sp`.
+  uint64_t DecodeFrame(uint64_t fp, uint64_t* ret_addr, uint64_t* sp);
+  bool IsFrameValid(uint64_t fp, uint64_t sp);
+};
+
+}  // namespace profiling
+}  // namespace perfetto
+
+#endif  // SRC_PROFILING_PERF_FRAME_POINTER_UNWINDER_H_
diff --git a/src/profiling/perf/frame_pointer_unwinder_unittest.cc b/src/profiling/perf/frame_pointer_unwinder_unittest.cc
new file mode 100644
index 0000000..c494fd7
--- /dev/null
+++ b/src/profiling/perf/frame_pointer_unwinder_unittest.cc
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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 "src/profiling/perf/frame_pointer_unwinder.h"
+
+#include <sys/mman.h>
+#include <unwindstack/Unwinder.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace profiling {
+namespace {
+
+class RegsFake : public unwindstack::Regs {
+ public:
+  RegsFake(uint16_t total_regs)
+      : unwindstack::Regs(
+            total_regs,
+            unwindstack::Regs::Location(unwindstack::Regs::LOCATION_UNKNOWN,
+                                        0)) {
+    fake_data_ = std::make_unique<uint64_t[]>(total_regs);
+  }
+  ~RegsFake() override = default;
+
+  unwindstack::ArchEnum Arch() override { return fake_arch_; }
+  void* RawData() override { return fake_data_.get(); }
+  uint64_t pc() override { return fake_pc_; }
+  uint64_t sp() override { return fake_sp_; }
+  void set_pc(uint64_t pc) override { fake_pc_ = pc; }
+  void set_sp(uint64_t sp) override { fake_sp_ = sp; }
+
+  void set_fp(uint64_t fp) {
+    switch (fake_arch_) {
+      case unwindstack::ARCH_ARM64:
+        fake_data_[unwindstack::Arm64Reg::ARM64_REG_R29] = fp;
+        break;
+      case unwindstack::ARCH_X86_64:
+        fake_data_[unwindstack::X86_64Reg::X86_64_REG_RBP] = fp;
+        break;
+      case unwindstack::ARCH_RISCV64:
+        fake_data_[unwindstack::Riscv64Reg::RISCV64_REG_S0] = fp;
+        break;
+      case unwindstack::ARCH_UNKNOWN:
+      case unwindstack::ARCH_ARM:
+      case unwindstack::ARCH_X86:
+          // not supported
+          ;
+    }
+  }
+
+  bool SetPcFromReturnAddress(unwindstack::Memory*) override { return false; }
+
+  void IterateRegisters(std::function<void(const char*, uint64_t)>) override {}
+
+  bool StepIfSignalHandler(uint64_t,
+                           unwindstack::Elf*,
+                           unwindstack::Memory*) override {
+    return false;
+  }
+
+  void FakeSetArch(unwindstack::ArchEnum arch) { fake_arch_ = arch; }
+
+  Regs* Clone() override { return nullptr; }
+
+ private:
+  unwindstack::ArchEnum fake_arch_ = unwindstack::ARCH_UNKNOWN;
+  uint64_t fake_pc_ = 0;
+  uint64_t fake_sp_ = 0;
+  std::unique_ptr<uint64_t[]> fake_data_;
+};
+
+class MemoryFake : public unwindstack::Memory {
+ public:
+  MemoryFake() = default;
+  ~MemoryFake() override = default;
+
+  size_t Read(uint64_t addr, void* memory, size_t size) override {
+    uint8_t* dst = reinterpret_cast<uint8_t*>(memory);
+    for (size_t i = 0; i < size; i++, addr++) {
+      auto value = data_.find(addr);
+      if (value == data_.end()) {
+        return i;
+      }
+      dst[i] = value->second;
+    }
+    return size;
+  }
+
+  void SetMemory(uint64_t addr, const void* memory, size_t length) {
+    const uint8_t* src = reinterpret_cast<const uint8_t*>(memory);
+    for (size_t i = 0; i < length; i++, addr++) {
+      auto value = data_.find(addr);
+      if (value != data_.end()) {
+        value->second = src[i];
+      } else {
+        data_.insert({addr, src[i]});
+      }
+    }
+  }
+
+  void SetData8(uint64_t addr, uint8_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetData16(uint64_t addr, uint16_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetData32(uint64_t addr, uint32_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetData64(uint64_t addr, uint64_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetMemory(uint64_t addr, std::vector<uint8_t> values) {
+    SetMemory(addr, values.data(), values.size());
+  }
+
+  void SetMemory(uint64_t addr, std::string string) {
+    SetMemory(addr, string.c_str(), string.size() + 1);
+  }
+
+  void Clear() override { data_.clear(); }
+
+ private:
+  std::unordered_map<uint64_t, uint8_t> data_;
+};
+
+constexpr static uint64_t kMaxFrames = 64;
+constexpr static uint64_t kStackSize = 0xFFFFFFF;
+
+class FramePointerUnwinderTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    memory_fake_ = new MemoryFake;
+    maps_.reset(new unwindstack::Maps);
+    regs_fake_ = std::make_unique<RegsFake>(64);
+    regs_fake_->FakeSetArch(unwindstack::ARCH_X86_64);
+    process_memory_.reset(memory_fake_);
+
+    unwinder_ = std::make_unique<FramePointerUnwinder>(
+        kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize);
+  }
+
+  MemoryFake* memory_fake_;
+  std::unique_ptr<unwindstack::Maps> maps_;
+  std::unique_ptr<RegsFake> regs_fake_;
+  std::shared_ptr<unwindstack::Memory> process_memory_;
+
+  std::unique_ptr<FramePointerUnwinder> unwinder_;
+};
+
+TEST_F(FramePointerUnwinderTest, UnwindUnsupportedArch) {
+  regs_fake_->FakeSetArch(unwindstack::ARCH_UNKNOWN);
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_UNSUPPORTED);
+
+  regs_fake_->FakeSetArch(unwindstack::ARCH_X86);
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_UNSUPPORTED);
+
+  regs_fake_->FakeSetArch(unwindstack::ARCH_ARM);
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_UNSUPPORTED);
+}
+
+TEST_F(FramePointerUnwinderTest, UnwindInvalidMaps) {
+  // Set up a valid stack frame
+  regs_fake_->set_pc(0x1000);
+  regs_fake_->set_sp(0x2000);
+  memory_fake_->SetData64(0x2000, 0x3000);
+  memory_fake_->SetData64(0x2008, 0x2000);
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_INVALID_MAP);
+  EXPECT_EQ(unwinder_->ConsumeFrames().size(), 0UL);
+}
+
+TEST_F(FramePointerUnwinderTest, UnwindValidStack) {
+  regs_fake_->set_pc(0x1900);
+  regs_fake_->set_sp(0x1800);
+  regs_fake_->set_fp(0x2000);
+
+  memory_fake_->SetData64(0x2000, 0x2200);  // mock next_fp
+  memory_fake_->SetData64(0x2000 + sizeof(uint64_t),
+                          0x2100);  // mock return_address(next_pc)
+
+  memory_fake_->SetData64(0x2200, 0);
+
+  maps_->Add(0x1000, 0x12000, 0, PROT_READ | PROT_WRITE, "libmock.so");
+
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(), unwindstack::ErrorCode::ERROR_NONE);
+  EXPECT_EQ(unwinder_->ConsumeFrames().size(), 2UL);
+}
+
+}  // namespace
+}  // namespace profiling
+}  // namespace perfetto
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index 492907e..86d7b15 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -500,8 +500,12 @@
 
   // Inform unwinder of the new data source instance, and optionally start a
   // periodic task to clear its cached state.
-  unwinding_worker_->PostStartDataSource(ds_id,
-                                         ds.event_config.kernel_frames());
+  auto unwind_mode = (ds.event_config.unwind_mode() ==
+                      protos::gen::PerfEventConfig::UNWIND_FRAME_POINTER)
+                         ? Unwinder::UnwindMode::kFramePointer
+                         : Unwinder::UnwindMode::kUnwindStack;
+  unwinding_worker_->PostStartDataSource(ds_id, ds.event_config.kernel_frames(),
+                                         unwind_mode);
   if (ds.event_config.unwind_state_clear_period_ms()) {
     unwinding_worker_->PostClearCachedStatePeriodic(
         ds_id, ds.event_config.unwind_state_clear_period_ms());
diff --git a/src/profiling/perf/unwinding.cc b/src/profiling/perf/unwinding.cc
index 53023a8..7ead30f 100644
--- a/src/profiling/perf/unwinding.cc
+++ b/src/profiling/perf/unwinding.cc
@@ -25,6 +25,7 @@
 #include "perfetto/ext/base/no_destructor.h"
 #include "perfetto/ext/base/thread_utils.h"
 #include "perfetto/ext/base/utils.h"
+#include "src/profiling/perf/frame_pointer_unwinder.h"
 
 namespace {
 constexpr size_t kUnwindingMaxFrames = 1000;
@@ -43,18 +44,23 @@
 }
 
 void Unwinder::PostStartDataSource(DataSourceInstanceID ds_id,
-                                   bool kernel_frames) {
+                                   bool kernel_frames,
+                                   UnwindMode unwind_mode) {
   // No need for a weak pointer as the associated task runner quits (stops
   // running tasks) strictly before the Unwinder's destruction.
-  task_runner_->PostTask(
-      [this, ds_id, kernel_frames] { StartDataSource(ds_id, kernel_frames); });
+  task_runner_->PostTask([this, ds_id, kernel_frames, unwind_mode] {
+    StartDataSource(ds_id, kernel_frames, unwind_mode);
+  });
 }
 
-void Unwinder::StartDataSource(DataSourceInstanceID ds_id, bool kernel_frames) {
+void Unwinder::StartDataSource(DataSourceInstanceID ds_id,
+                               bool kernel_frames,
+                               UnwindMode unwind_mode) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
   PERFETTO_DLOG("Unwinder::StartDataSource(%zu)", static_cast<size_t>(ds_id));
 
-  auto it_and_inserted = data_sources_.emplace(ds_id, DataSourceState{});
+  auto it_and_inserted =
+      data_sources_.emplace(ds_id, DataSourceState{unwind_mode});
   PERFETTO_DCHECK(it_and_inserted.second);
 
   if (kernel_frames) {
@@ -297,8 +303,9 @@
           (proc_state.unwind_state.has_value()
                ? &proc_state.unwind_state.value()
                : nullptr);
-      CompletedSample unwound_sample = UnwindSample(
-          entry.sample, opt_user_state, proc_state.attempted_unwinding);
+      CompletedSample unwound_sample =
+          UnwindSample(entry.sample, opt_user_state,
+                       proc_state.attempted_unwinding, ds.unwind_mode);
       proc_state.attempted_unwinding = true;
 
       PERFETTO_METATRACE_COUNTER(TAG_PRODUCER, PROFILER_UNWIND_CURRENT_PID, 0);
@@ -334,7 +341,8 @@
 
 CompletedSample Unwinder::UnwindSample(const ParsedSample& sample,
                                        UnwindingMetadata* opt_user_state,
-                                       bool pid_unwound_before) {
+                                       bool pid_unwound_before,
+                                       UnwindMode unwind_mode) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
 
   CompletedSample ret;
@@ -375,7 +383,7 @@
     UnwindResult& operator=(UnwindResult&&) = default;
   };
   auto attempt_unwind = [&sample, unwind_state, pid_unwound_before,
-                         &overlay_memory]() -> UnwindResult {
+                         &overlay_memory, unwind_mode]() -> UnwindResult {
     metatrace::ScopedEvent m(metatrace::TAG_PRODUCER,
                              pid_unwound_before
                                  ? metatrace::PROFILER_UNWIND_ATTEMPT
@@ -384,16 +392,29 @@
     // Unwindstack clobbers registers, so make a copy in case of retries.
     auto regs_copy = std::unique_ptr<unwindstack::Regs>{sample.regs->Clone()};
 
-    unwindstack::Unwinder unwinder(kUnwindingMaxFrames, &unwind_state->fd_maps,
-                                   regs_copy.get(), overlay_memory);
+    switch (unwind_mode) {
+      case UnwindMode::kFramePointer: {
+        FramePointerUnwinder unwinder(kUnwindingMaxFrames,
+                                      &unwind_state->fd_maps, regs_copy.get(),
+                                      overlay_memory, sample.stack.size());
+        unwinder.Unwind();
+        return {unwinder.LastErrorCode(), unwinder.warnings(),
+                unwinder.ConsumeFrames()};
+      }
+      case UnwindMode::kUnwindStack: {
+        unwindstack::Unwinder unwinder(kUnwindingMaxFrames,
+                                       &unwind_state->fd_maps, regs_copy.get(),
+                                       overlay_memory);
 #if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-    unwinder.SetJitDebug(unwind_state->GetJitDebug(regs_copy->Arch()));
-    unwinder.SetDexFiles(unwind_state->GetDexFiles(regs_copy->Arch()));
+        unwinder.SetJitDebug(unwind_state->GetJitDebug(regs_copy->Arch()));
+        unwinder.SetDexFiles(unwind_state->GetDexFiles(regs_copy->Arch()));
 #endif
-    unwinder.Unwind(/*initial_map_names_to_skip=*/nullptr,
-                    /*map_suffixes_to_ignore=*/nullptr);
-    return {unwinder.LastErrorCode(), unwinder.warnings(),
-            unwinder.ConsumeFrames()};
+        unwinder.Unwind(/*initial_map_names_to_skip=*/nullptr,
+                        /*map_suffixes_to_ignore=*/nullptr);
+        return {unwinder.LastErrorCode(), unwinder.warnings(),
+                unwinder.ConsumeFrames()};
+      }
+    }
   };
 
   // first unwind attempt
diff --git a/src/profiling/perf/unwinding.h b/src/profiling/perf/unwinding.h
index 8295fb8..13a7d6d 100644
--- a/src/profiling/perf/unwinding.h
+++ b/src/profiling/perf/unwinding.h
@@ -75,6 +75,8 @@
  public:
   friend class UnwinderHandle;
 
+  enum class UnwindMode { kUnwindStack, kFramePointer };
+
   // Callbacks from the unwinder to the primary producer thread.
   class Delegate {
    public:
@@ -89,7 +91,9 @@
 
   ~Unwinder() { PERFETTO_DCHECK_THREAD(thread_checker_); }
 
-  void PostStartDataSource(DataSourceInstanceID ds_id, bool kernel_frames);
+  void PostStartDataSource(DataSourceInstanceID ds_id,
+                           bool kernel_frames,
+                           UnwindMode unwind_mode);
   void PostAdoptProcDescriptors(DataSourceInstanceID ds_id,
                                 pid_t pid,
                                 base::ScopedFile maps_fd,
@@ -144,8 +148,11 @@
 
   struct DataSourceState {
     enum class Status { kActive, kShuttingDown };
+    explicit DataSourceState(UnwindMode _unwind_mode)
+        : unwind_mode(_unwind_mode) {}
 
     Status status = Status::kActive;
+    const UnwindMode unwind_mode;
     std::map<pid_t, ProcessState> process_states;
   };
 
@@ -163,7 +170,9 @@
 
   // Marks the data source as valid and active at the unwinding stage.
   // Initializes kernel address symbolization if needed.
-  void StartDataSource(DataSourceInstanceID ds_id, bool kernel_frames);
+  void StartDataSource(DataSourceInstanceID ds_id,
+                       bool kernel_frames,
+                       UnwindMode unwind_mode);
 
   void AdoptProcDescriptors(DataSourceInstanceID ds_id,
                             pid_t pid,
@@ -184,7 +193,8 @@
 
   CompletedSample UnwindSample(const ParsedSample& sample,
                                UnwindingMetadata* opt_user_state,
-                               bool pid_unwound_before);
+                               bool pid_unwound_before,
+                               UnwindMode unwind_mode);
 
   // Returns a list of symbolized kernel frames in the sample (if any).
   std::vector<unwindstack::FrameData> SymbolizeKernelCallchain(