[debugger] Add to process handle extraction.

Converts the unwinder to using the ThreadHandle and ProcessHandle abstractions.

Converts thread state to a new class to allow this to be abtracted better.

Removes the process_info file and moves the ELF utilities from there
into a new file. This new file now uses the ProcessHandle abstractions
which will allow the module extration to be tested in the future,
although there are still no tests for this older code.

Creates a new GeneralRegisters container to provide cross-architecture
accesors for the important registers. Use this when relevant and move
the getter/setters of the general registers from the ArchProvider to the
ThreadHandle.

Updates changed function signatures to prefer to return values rather
than use out params, and use references rather than pointers for out
params (as specified by updated style guide).

Change-Id: I3e64b760b134b1ed74e2f3aa6aed6b0c1bbadf5d
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/404359
Testability-Review: Brett Wilson <brettw@google.com>
Commit-Queue: Brett Wilson <brettw@google.com>
Reviewed-by: Dangyi Liu <dangyi@google.com>
diff --git a/src/developer/debug/debug_agent/BUILD.gn b/src/developer/debug/debug_agent/BUILD.gn
index 8ce0f28..096974a 100644
--- a/src/developer/debug/debug_agent/BUILD.gn
+++ b/src/developer/debug/debug_agent/BUILD.gn
@@ -34,6 +34,10 @@
     "debugged_process.h",
     "debugged_thread.cc",
     "debugged_thread.h",
+    "elf_utils.cc",
+    "elf_utils.h",
+    "general_registers.cc",
+    "general_registers.h",
     "hardware_breakpoint.cc",
     "hardware_breakpoint.h",
     "limbo_provider.cc",
@@ -43,8 +47,6 @@
     "process_breakpoint.cc",
     "process_breakpoint.h",
     "process_handle.h",
-    "process_info.cc",
-    "process_info.h",
     "remote_api.h",
     "remote_api_adapter.cc",
     "remote_api_adapter.h",
diff --git a/src/developer/debug/debug_agent/arch.h b/src/developer/debug/debug_agent/arch.h
index 43ac471..d412460 100644
--- a/src/developer/debug/debug_agent/arch.h
+++ b/src/developer/debug/debug_agent/arch.h
@@ -38,12 +38,6 @@
 
   // Thread Management -----------------------------------------------------------------------------
 
-  // Read/write general-purpose registers.
-  virtual zx_status_t ReadGeneralState(const zx::thread& handle,
-                                       zx_thread_state_general_regs* regs) const = 0;
-  virtual zx_status_t WriteGeneralState(const zx::thread& handle,
-                                        const zx_thread_state_general_regs& regs) = 0;
-
   // Read/write debug registers.
   virtual zx_status_t ReadDebugState(const zx::thread& handle,
                                      zx_thread_state_debug_regs* regs) const = 0;
diff --git a/src/developer/debug/debug_agent/arch_provider_fuchsia.cc b/src/developer/debug/debug_agent/arch_provider_fuchsia.cc
index 7e23733..348f9be 100644
--- a/src/developer/debug/debug_agent/arch_provider_fuchsia.cc
+++ b/src/developer/debug/debug_agent/arch_provider_fuchsia.cc
@@ -9,18 +9,6 @@
 namespace debug_agent {
 namespace arch {
 
-zx_status_t ArchProviderFuchsia::ReadGeneralState(const zx::thread& thread,
-                                                  zx_thread_state_general_regs* regs) const {
-  return thread.read_state(ZX_THREAD_STATE_GENERAL_REGS, regs,
-                           sizeof(zx_thread_state_general_regs));
-}
-
-zx_status_t ArchProviderFuchsia::WriteGeneralState(const zx::thread& thread,
-                                                   const zx_thread_state_general_regs& regs) {
-  return thread.write_state(ZX_THREAD_STATE_GENERAL_REGS, &regs,
-                            sizeof(zx_thread_state_general_regs));
-}
-
 zx_status_t ArchProviderFuchsia::ReadDebugState(const zx::thread& thread,
                                                 zx_thread_state_debug_regs* regs) const {
   return thread.read_state(ZX_THREAD_STATE_DEBUG_REGS, regs, sizeof(zx_thread_state_debug_regs));
diff --git a/src/developer/debug/debug_agent/arch_provider_fuchsia.h b/src/developer/debug/debug_agent/arch_provider_fuchsia.h
index ea0b857..9dd29a1 100644
--- a/src/developer/debug/debug_agent/arch_provider_fuchsia.h
+++ b/src/developer/debug/debug_agent/arch_provider_fuchsia.h
@@ -15,10 +15,6 @@
 class ArchProviderFuchsia : public ArchProvider {
  public:
   // ArchProvider implementation.
-  zx_status_t ReadGeneralState(const zx::thread& handle,
-                               zx_thread_state_general_regs* regs) const override;
-  zx_status_t WriteGeneralState(const zx::thread& handle,
-                                const zx_thread_state_general_regs& regs) override;
   zx_status_t ReadDebugState(const zx::thread& handle,
                              zx_thread_state_debug_regs* regs) const override;
   zx_status_t WriteDebugState(const zx::thread& handle,
diff --git a/src/developer/debug/debug_agent/debug_agent.cc b/src/developer/debug/debug_agent/debug_agent.cc
index 0a8de6f..d491dc3 100644
--- a/src/developer/debug/debug_agent/debug_agent.cc
+++ b/src/developer/debug/debug_agent/debug_agent.cc
@@ -22,7 +22,6 @@
 #include "src/developer/debug/debug_agent/debugged_thread.h"
 #include "src/developer/debug/debug_agent/object_provider.h"
 #include "src/developer/debug/debug_agent/process_breakpoint.h"
-#include "src/developer/debug/debug_agent/process_info.h"
 #include "src/developer/debug/debug_agent/system_info.h"
 #include "src/developer/debug/debug_agent/thread_exception.h"
 #include "src/developer/debug/debug_agent/zircon_process_handle.h"
@@ -182,10 +181,8 @@
     auto threads = proc->GetThreads();
     process_record.threads.reserve(threads.size());
     for (auto* thread : threads) {
-      debug_ipc::ThreadRecord thread_record;
-      thread->FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kNone, nullptr,
-                               &thread_record);
-      process_record.threads.emplace_back(std::move(thread_record));
+      process_record.threads.emplace_back(
+          thread->GetThreadRecord(debug_ipc::ThreadRecord::StackAmount::kNone));
     }
 
     reply->processes.emplace_back(std::move(process_record));
@@ -345,7 +342,7 @@
   if (found == procs_.end())
     return;
 
-  found->second->FillThreadRecords(&reply->threads);
+  reply->threads = found->second->GetThreadRecords();
 }
 
 void DebugAgent::OnReadMemory(const debug_ipc::ReadMemoryRequest& request,
@@ -438,7 +435,7 @@
                                 debug_ipc::ThreadStatusReply* reply) {
   DebuggedThread* thread = GetDebuggedThread(request.process_koid, request.thread_koid);
   if (thread) {
-    thread->FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kFull, nullptr, &reply->record);
+    reply->record = thread->GetThreadRecord(debug_ipc::ThreadRecord::StackAmount::kFull);
   } else {
     // When the thread is not found the thread record is set to "dead".
     reply->record.process_koid = request.process_koid;
diff --git a/src/developer/debug/debug_agent/debugged_process.cc b/src/developer/debug/debug_agent/debugged_process.cc
index 28a6ccb..4fcfbe7 100644
--- a/src/developer/debug/debug_agent/debugged_process.cc
+++ b/src/developer/debug/debug_agent/debugged_process.cc
@@ -16,7 +16,6 @@
 #include "src/developer/debug/debug_agent/hardware_breakpoint.h"
 #include "src/developer/debug/debug_agent/object_provider.h"
 #include "src/developer/debug/debug_agent/process_breakpoint.h"
-#include "src/developer/debug/debug_agent/process_info.h"
 #include "src/developer/debug/debug_agent/software_breakpoint.h"
 #include "src/developer/debug/debug_agent/watchpoint.h"
 #include "src/developer/debug/debug_agent/zircon_thread_exception.h"
@@ -191,12 +190,11 @@
       thread->Suspend(true);
       thread->set_client_state(DebuggedThread::ClientState::kPaused);
 
-      // The Suspend call could have failed though most failures should be
-      // rare (perhaps we raced with the thread being destroyed). Either way,
-      // send our current knowledge of the thread's state.
-      debug_ipc::ThreadRecord record;
-      thread->FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal, nullptr, &record);
-      reply->threads.push_back(std::move(record));
+      // The Suspend call could have failed though most failures should be rare (perhaps we raced
+      // with the thread being destroyed). Either way, send our current knowledge of the thread's
+      // state.
+      reply->threads.push_back(
+          thread->GetThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal));
     }
     // Could be not found if there is a race between the thread exiting and
     // the client sending the request.
@@ -212,7 +210,7 @@
       thread->set_client_state(DebuggedThread::ClientState::kPaused);
     }
 
-    FillThreadRecords(&reply->threads);
+    reply->threads = GetThreadRecords();
   }
 }
 
@@ -306,12 +304,11 @@
   }
 }
 
-void DebuggedProcess::FillThreadRecords(std::vector<debug_ipc::ThreadRecord>* threads) {
-  for (const auto& pair : threads_) {
-    debug_ipc::ThreadRecord record;
-    pair.second->FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal, nullptr, &record);
-    threads->push_back(std::move(record));
-  }
+std::vector<debug_ipc::ThreadRecord> DebuggedProcess::GetThreadRecords() const {
+  std::vector<debug_ipc::ThreadRecord> result;
+  for (const auto& pair : threads_)
+    result.push_back(pair.second->GetThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal));
+  return result;
 }
 
 bool DebuggedProcess::RegisterDebugState() {
@@ -374,7 +371,7 @@
   // Notify the client of any libraries.
   debug_ipc::NotifyModules notify;
   notify.process_koid = koid_;
-  GetModulesForProcess(handle(), dl_debug_addr_, &notify.modules);
+  notify.modules = process_handle_->GetModules(dl_debug_addr_);
   notify.stopped_thread_koids = std::move(paused_thread_koids);
 
   DEBUG_LOG(Process) << LogPreamble(this) << "Sending modules.";
@@ -608,8 +605,7 @@
 
   threads_.erase(exception_info.tid);
 
-  // Notify the client. Can't call FillThreadRecord since the thread doesn't
-  // exist any more.
+  // Notify the client. Can't call GetThreadRecord since the thread doesn't exist any more.
   debug_ipc::NotifyThread notify;
   notify.record.process_koid = exception_info.pid;
   notify.record.thread_koid = exception_info.tid;
@@ -642,7 +638,7 @@
 void DebuggedProcess::OnModules(debug_ipc::ModulesReply* reply) {
   // Modules can only be read after the debug state is set.
   if (dl_debug_addr_)
-    GetModulesForProcess(handle(), dl_debug_addr_, &reply->modules);
+    reply->modules = process_handle_->GetModules(dl_debug_addr_);
 }
 
 void DebuggedProcess::OnWriteMemory(const debug_ipc::WriteMemoryRequest& request,
diff --git a/src/developer/debug/debug_agent/debugged_process.h b/src/developer/debug/debug_agent/debugged_process.h
index 692cd8b..202c242 100644
--- a/src/developer/debug/debug_agent/debugged_process.h
+++ b/src/developer/debug/debug_agent/debugged_process.h
@@ -122,9 +122,8 @@
   // thread state according to the underlying zircon truth.
   void PopulateCurrentThreads();
 
-  // Appends the information for all current threads. This writes minimal
-  // stacks.
-  void FillThreadRecords(std::vector<debug_ipc::ThreadRecord>* threads);
+  // Returns the information for all current threads. This gets minimal stacks.
+  std::vector<debug_ipc::ThreadRecord> GetThreadRecords() const;
 
   // Attempts to load the debug_state_ value from the
   // ZX_PROP_PROCESS_DEBUG_ADDR of the debugged process. Returns true if it
diff --git a/src/developer/debug/debug_agent/debugged_thread.cc b/src/developer/debug/debug_agent/debugged_thread.cc
index 7c61730..6655a70 100644
--- a/src/developer/debug/debug_agent/debugged_thread.cc
+++ b/src/developer/debug/debug_agent/debugged_thread.cc
@@ -19,7 +19,6 @@
 #include "src/developer/debug/debug_agent/hardware_breakpoint.h"
 #include "src/developer/debug/debug_agent/object_provider.h"
 #include "src/developer/debug/debug_agent/process_breakpoint.h"
-#include "src/developer/debug/debug_agent/process_info.h"
 #include "src/developer/debug/debug_agent/software_breakpoint.h"
 #include "src/developer/debug/debug_agent/unwind.h"
 #include "src/developer/debug/debug_agent/watchpoint.h"
@@ -153,36 +152,34 @@
   exception.type = arch_provider_->DecodeExceptionType(*this, exception_info.type);
   arch_provider_->FillExceptionRecord(thread_handle_->GetNativeHandle(), &exception.exception);
 
-  zx_thread_state_general_regs regs;
-  zx_status_t status = arch_provider_->ReadGeneralState(thread_handle_->GetNativeHandle(), &regs);
-  if (status != ZX_OK) {
+  std::optional<GeneralRegisters> regs = thread_handle_->GetGeneralRegisters();
+  if (!regs) {
     // This can happen, for example, if the thread was killed during the time the exception message
     // was waiting to be delivered to us.
-    FX_LOGS(WARNING) << "Could not read registers from thread: " << zx_status_get_string(status);
+    FX_LOGS(WARNING) << "Could not read registers from thread.";
     return;
   }
 
-  DEBUG_LOG(Thread) << ThreadPreamble(this) << "Exception @ 0x" << std::hex
-                    << *arch::IPInRegs(&regs) << std::dec << ": "
-                    << ExceptionTypeToString(exception_info.type) << " -> "
+  DEBUG_LOG(Thread) << ThreadPreamble(this) << "Exception @ 0x" << std::hex << regs->ip()
+                    << std::dec << ": " << ExceptionTypeToString(exception_info.type) << " -> "
                     << debug_ipc::ExceptionTypeToString(exception.type);
 
   switch (exception.type) {
     case debug_ipc::ExceptionType::kSingleStep:
-      return HandleSingleStep(&exception, &regs);
+      return HandleSingleStep(&exception, *regs);
     case debug_ipc::ExceptionType::kSoftware:
-      return HandleSoftwareBreakpoint(&exception, &regs);
+      return HandleSoftwareBreakpoint(&exception, *regs);
     case debug_ipc::ExceptionType::kHardware:
-      return HandleHardwareBreakpoint(&exception, &regs);
+      return HandleHardwareBreakpoint(&exception, *regs);
     case debug_ipc::ExceptionType::kWatchpoint:
-      return HandleWatchpoint(&exception, &regs);
+      return HandleWatchpoint(&exception, *regs);
     case debug_ipc::ExceptionType::kNone:
     case debug_ipc::ExceptionType::kLast:
       break;
     // TODO(donosoc): Should synthetic be general or invalid?
     case debug_ipc::ExceptionType::kSynthetic:
     default:
-      return HandleGeneralException(&exception, &regs);
+      return HandleGeneralException(&exception, *regs);
   }
 
   FX_NOTREACHED() << "Invalid exception notification type: "
@@ -195,7 +192,7 @@
 }
 
 void DebuggedThread::HandleSingleStep(debug_ipc::NotifyException* exception,
-                                      zx_thread_state_general_regs* regs) {
+                                      const GeneralRegisters& regs) {
   if (current_breakpoint_) {
     DEBUG_LOG(Thread) << ThreadPreamble(this) << "Ending single stepped over 0x" << std::hex
                       << current_breakpoint_->address();
@@ -237,7 +234,7 @@
   // When stepping in a range, automatically continue as long as we're
   // still in range.
   if (run_mode_ == debug_ipc::ResumeRequest::How::kStepInRange &&
-      *arch::IPInRegs(regs) >= step_in_range_begin_ && *arch::IPInRegs(regs) < step_in_range_end_) {
+      regs.ip() >= step_in_range_begin_ && regs.ip() < step_in_range_end_) {
     DEBUG_LOG(Thread) << ThreadPreamble(this) << "Stepping in range. Continuing.";
     ResumeForRunMode();
     return;
@@ -248,13 +245,13 @@
 }
 
 void DebuggedThread::HandleGeneralException(debug_ipc::NotifyException* exception,
-                                            zx_thread_state_general_regs* regs) {
+                                            const GeneralRegisters& regs) {
   SendExceptionNotification(exception, regs);
 }
 
 void DebuggedThread::HandleSoftwareBreakpoint(debug_ipc::NotifyException* exception,
-                                              zx_thread_state_general_regs* regs) {
-  auto on_stop = UpdateForSoftwareBreakpoint(regs, &exception->hit_breakpoints);
+                                              GeneralRegisters& regs) {
+  auto on_stop = UpdateForSoftwareBreakpoint(regs, exception->hit_breakpoints);
   switch (on_stop) {
     case OnStop::kIgnore:
       return;
@@ -272,27 +269,27 @@
 }
 
 void DebuggedThread::HandleHardwareBreakpoint(debug_ipc::NotifyException* exception,
-                                              zx_thread_state_general_regs* regs) {
+                                              GeneralRegisters& regs) {
   uint64_t breakpoint_address =
-      arch_provider_->BreakpointInstructionForHardwareExceptionAddress(*arch::IPInRegs(regs));
+      arch_provider_->BreakpointInstructionForHardwareExceptionAddress(regs.ip());
   HardwareBreakpoint* found_bp = process_->FindHardwareBreakpoint(breakpoint_address);
   if (found_bp) {
-    UpdateForHitProcessBreakpoint(debug_ipc::BreakpointType::kHardware, found_bp, regs,
-                                  &exception->hit_breakpoints);
+    UpdateForHitProcessBreakpoint(debug_ipc::BreakpointType::kHardware, found_bp,
+                                  exception->hit_breakpoints);
   } else {
-    // Hit a hw debug exception that doesn't belong to any ProcessBreakpoint.
-    // This is probably a race between the removal and the exception handler.
-    *arch::IPInRegs(regs) = breakpoint_address;
+    // Hit a hw debug exception that doesn't belong to any ProcessBreakpoint. This is probably a
+    // race between the removal and the exception handler.
+    regs.set_ip(breakpoint_address);
   }
 
-  // The ProcessBreakpoint could've been deleted if it was a one-shot, so must
-  // not be derefereced below this.
+  // The ProcessBreakpoint could've been deleted if it was a one-shot, so must not be derefereced
+  // below this.
   found_bp = nullptr;
   SendExceptionNotification(exception, regs);
 }
 
 void DebuggedThread::HandleWatchpoint(debug_ipc::NotifyException* exception,
-                                      zx_thread_state_general_regs* regs) {
+                                      const GeneralRegisters& regs) {
   auto [range, slot] = arch_provider_->InstructionForWatchpointHit(*this);
   DEBUG_LOG(Thread) << "Found watchpoint hit at 0x" << std::hex << range.ToString() << " on slot "
                     << std::dec << slot;
@@ -314,15 +311,15 @@
   }
 
   // TODO(donosoc): Plumb in R/RW types.
-  UpdateForHitProcessBreakpoint(watchpoint->Type(), watchpoint, regs, &exception->hit_breakpoints);
+  UpdateForHitProcessBreakpoint(watchpoint->Type(), watchpoint, exception->hit_breakpoints);
   // The ProcessBreakpoint could'be been deleted, so we cannot use it anymore.
   watchpoint = nullptr;
   SendExceptionNotification(exception, regs);
 }
 
 void DebuggedThread::SendExceptionNotification(debug_ipc::NotifyException* exception,
-                                               zx_thread_state_general_regs* regs) {
-  FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal, regs, &exception->thread);
+                                               const GeneralRegisters& regs) {
+  exception->thread = GetThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal, regs);
 
   // Keep the thread suspended for the client.
 
@@ -402,7 +399,7 @@
 
 bool DebuggedThread::WaitForSuspension(zx::time deadline) {
   // The thread could already be suspended. This bypasses a wait cycle in that case.
-  if (thread_handle_->GetState() == ZX_THREAD_STATE_SUSPENDED)
+  if (thread_handle_->GetState().state == debug_ipc::ThreadRecord::State::kSuspended)
     return true;  // Already suspended, success.
 
   // This function is complex because a thread in an exception state can't be suspended (ZX-3772).
@@ -424,7 +421,7 @@
   zx_status_t status = ZX_OK;
   do {
     // Before waiting, check the thread state from the kernel because of queue described above.
-    if (thread_handle_->GetState() == ZX_THREAD_STATE_BLOCKED_EXCEPTION)
+    if (thread_handle_->GetState().is_blocked_on_exception())
       return true;
 
     zx_signals_t observed;
@@ -439,46 +436,37 @@
 
 // Note that everything in this function is racy because the thread state can change at any time,
 // even while processing an exception (an external program can kill it out from under us).
-void DebuggedThread::FillThreadRecord(debug_ipc::ThreadRecord::StackAmount stack_amount,
-                                      const zx_thread_state_general_regs* optional_regs,
-                                      debug_ipc::ThreadRecord* record) const {
-  *record = thread_handle_->GetThreadRecord();
+debug_ipc::ThreadRecord DebuggedThread::GetThreadRecord(
+    debug_ipc::ThreadRecord::StackAmount stack_amount, std::optional<GeneralRegisters> regs) const {
+  debug_ipc::ThreadRecord record = thread_handle_->GetThreadRecord();
 
   // Unwind the stack if requested. This requires the registers which are available when suspended
   // or blocked in an exception.
-  if ((record->state == debug_ipc::ThreadRecord::State::kSuspended ||
-       (record->state == debug_ipc::ThreadRecord::State::kBlocked &&
-        record->blocked_reason == debug_ipc::ThreadRecord::BlockedReason::kException)) &&
+  if ((record.state == debug_ipc::ThreadRecord::State::kSuspended ||
+       (record.state == debug_ipc::ThreadRecord::State::kBlocked &&
+        record.blocked_reason == debug_ipc::ThreadRecord::BlockedReason::kException)) &&
       stack_amount != debug_ipc::ThreadRecord::StackAmount::kNone) {
     // Only record this when we actually attempt to query the stack.
-    record->stack_amount = stack_amount;
+    record.stack_amount = stack_amount;
 
     // The registers are required, fetch them if the caller didn't provide.
-    zx_thread_state_general_regs queried_regs;  // Storage for fetched regs.
-    zx_thread_state_general_regs* regs = nullptr;
-    if (!optional_regs) {
-      if (arch_provider_->ReadGeneralState(thread_handle_->GetNativeHandle(), &queried_regs) ==
-          ZX_OK)
-        regs = &queried_regs;
-    } else {
-      // We don't change the values here but *InRegs below returns mutable
-      // references so we need a mutable pointer.
-      regs = const_cast<zx_thread_state_general_regs*>(optional_regs);
-    }
+    if (!regs)
+      regs = thread_handle_->GetGeneralRegisters();  // Note this could still fail.
 
     if (regs) {
-      // Minimal stacks are 2 (current frame and calling one). Full stacks max
-      // out at 256 to prevent edge cases, especially around corrupted stacks.
+      // Minimal stacks are 2 (current frame and calling one). Full stacks max out at 256 to prevent
+      // edge cases, especially around corrupted stacks.
       uint32_t max_stack_depth =
           stack_amount == debug_ipc::ThreadRecord::StackAmount::kMinimal ? 2 : 256;
 
-      UnwindStack(arch_provider_.get(), process_->handle(), process_->dl_debug_addr(),
-                  thread_handle_->GetNativeHandle(), *regs, max_stack_depth, &record->frames);
+      UnwindStack(arch_provider_.get(), process_->process_handle(), process_->dl_debug_addr(),
+                  thread_handle(), *regs, max_stack_depth, &record.frames);
     }
   } else {
     // Didn't bother querying the stack.
-    record->stack_amount = debug_ipc::ThreadRecord::StackAmount::kNone;
+    record.stack_amount = debug_ipc::ThreadRecord::StackAmount::kNone;
   }
+  return record;
 }
 
 std::vector<debug_ipc::Register> DebuggedThread::ReadRegisters(
@@ -514,7 +502,7 @@
 void DebuggedThread::SendThreadNotification() const {
   DEBUG_LOG(Thread) << ThreadPreamble(this) << "Sending starting notification.";
   debug_ipc::NotifyThread notify;
-  FillThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal, nullptr, &notify.record);
+  notify.record = GetThreadRecord(debug_ipc::ThreadRecord::StackAmount::kMinimal);
 
   debug_ipc::MessageWriter writer;
   debug_ipc::WriteNotifyThread(debug_ipc::MsgHeader::Type::kNotifyThreadStarting, notify, &writer);
@@ -527,11 +515,11 @@
 }
 
 DebuggedThread::OnStop DebuggedThread::UpdateForSoftwareBreakpoint(
-    zx_thread_state_general_regs* regs, std::vector<debug_ipc::BreakpointStats>* hit_breakpoints) {
+    GeneralRegisters& regs, std::vector<debug_ipc::BreakpointStats>& hit_breakpoints) {
   // Get the correct address where the CPU is after hitting a breakpoint
   // (this is architecture specific).
   uint64_t breakpoint_address =
-      arch_provider_->BreakpointInstructionForSoftwareExceptionAddress(*arch::IPInRegs(regs));
+      arch_provider_->BreakpointInstructionForSoftwareExceptionAddress(regs.ip());
 
   SoftwareBreakpoint* found_bp = process_->FindSoftwareBreakpoint(breakpoint_address);
   if (found_bp) {
@@ -548,27 +536,19 @@
       return OnStop::kResume;
     }
 
-    UpdateForHitProcessBreakpoint(debug_ipc::BreakpointType::kSoftware, found_bp, regs,
-                                  hit_breakpoints);
+    UpdateForHitProcessBreakpoint(debug_ipc::BreakpointType::kSoftware, found_bp, hit_breakpoints);
 
     // The found_bp could have been deleted if it was a one-shot, so must
     // not be dereferenced below this.
     found_bp = nullptr;
   } else {
-    // Hit a software breakpoint that doesn't correspond to any current
-    // breakpoint.
+    // Hit a software breakpoint that doesn't correspond to any current breakpoint.
     if (arch_provider_->IsBreakpointInstruction(process_->handle(), breakpoint_address)) {
-      // The breakpoint is a hardcoded instruction in the program code. In
-      // this case we want to continue from the following instruction since
-      // the breakpoint instruction will never go away.
-      *arch::IPInRegs(regs) =
-          arch_provider_->NextInstructionForSoftwareExceptionAddress(*arch::IPInRegs(regs));
-      zx_status_t status =
-          arch_provider_->WriteGeneralState(thread_handle_->GetNativeHandle(), *regs);
-      if (status != ZX_OK) {
-        fprintf(stderr, "Warning: could not update IP on thread, error = %d.",
-                static_cast<int>(status));
-      }
+      // The breakpoint is a hardcoded instruction in the program code. In this case we want to
+      // continue from the following instruction since the breakpoint instruction will never go
+      // away.
+      regs.set_ip(arch_provider_->NextInstructionForSoftwareExceptionAddress(regs.ip()));
+      thread_handle_->SetGeneralRegisters(regs);
 
       if (!process_->dl_debug_addr() && process_->RegisterDebugState()) {
         DEBUG_LOG(Thread) << ThreadPreamble(this) << "Found ld.so breakpoint. Sending modules.";
@@ -586,46 +566,41 @@
     } else {
       DEBUG_LOG(Thread) << ThreadPreamble(this) << "Hit non debugger SW breakpoint on 0x"
                         << std::hex << breakpoint_address;
-      // Not a breakpoint instruction. Probably the breakpoint instruction
-      // used to be ours but its removal raced with the exception handler.
-      // Resume from the instruction that used to be the breakpoint.
-      *arch::IPInRegs(regs) = breakpoint_address;
+      // Not a breakpoint instruction. Probably the breakpoint instruction used to be ours but its
+      // removal raced with the exception handler. Resume from the instruction that used to be the
+      // breakpoint.
+      regs.set_ip(breakpoint_address);
 
-      // Don't automatically continue execution here. A race for this should
-      // be unusual and maybe something weird happened that caused an
-      // exception we're not set up to handle. Err on the side of telling the
-      // user about the exception.
+      // Don't automatically continue execution here. A race for this should be unusual and maybe
+      // something weird happened that caused an exception we're not set up to handle. Err on the
+      // side of telling the user about the exception.
     }
   }
   return OnStop::kNotify;
 }
 
 void DebuggedThread::FixSoftwareBreakpointAddress(ProcessBreakpoint* process_breakpoint,
-                                                  zx_thread_state_general_regs* regs) {
+                                                  GeneralRegisters& regs) {
   // When the program hits one of our breakpoints, set the IP back to the exact address that
   // triggered the breakpoint. When the thread resumes, this is the address that it will resume
   // from (after putting back the original instruction), and will be what the client wants to
   // display to the user.
-  *arch::IPInRegs(regs) = process_breakpoint->address();
-  zx_status_t status = arch_provider_->WriteGeneralState(thread_handle_->GetNativeHandle(), *regs);
-  if (status != ZX_OK) {
-    fprintf(stderr, "Warning: could not update IP on thread, error = %d.",
-            static_cast<int>(status));
-  }
+  regs.set_ip(process_breakpoint->address());
+  thread_handle_->SetGeneralRegisters(regs);
 }
 
 void DebuggedThread::UpdateForHitProcessBreakpoint(
     debug_ipc::BreakpointType exception_type, ProcessBreakpoint* process_breakpoint,
-    zx_thread_state_general_regs* regs, std::vector<debug_ipc::BreakpointStats>* hit_breakpoints) {
+    std::vector<debug_ipc::BreakpointStats>& hit_breakpoints) {
   current_breakpoint_ = process_breakpoint;
 
-  process_breakpoint->OnHit(exception_type, hit_breakpoints);
+  process_breakpoint->OnHit(exception_type, &hit_breakpoints);
 
   // Delete any one-shot breakpoints. Since there can be multiple Breakpoints
   // (some one-shot, some not) referring to the current ProcessBreakpoint,
   // this operation could delete the ProcessBreakpoint or it could not. If it
   // does, our observer will be told and current_breakpoint_ will be cleared.
-  for (const auto& stats : *hit_breakpoints) {
+  for (const auto& stats : hit_breakpoints) {
     if (stats.should_delete)
       process_->debug_agent()->RemoveBreakpoint(stats.id);
   }
diff --git a/src/developer/debug/debug_agent/debugged_thread.h b/src/developer/debug/debug_agent/debugged_thread.h
index 31f0425..df5f15b 100644
--- a/src/developer/debug/debug_agent/debugged_thread.h
+++ b/src/developer/debug/debug_agent/debugged_thread.h
@@ -10,6 +10,7 @@
 #include <zircon/syscalls/exception.h>
 
 #include "src/developer/debug/debug_agent/arch.h"
+#include "src/developer/debug/debug_agent/general_registers.h"
 #include "src/developer/debug/debug_agent/object_provider.h"
 #include "src/developer/debug/debug_agent/thread_exception.h"
 #include "src/developer/debug/debug_agent/thread_handle.h"
@@ -18,8 +19,6 @@
 #include "src/lib/fxl/memory/ref_ptr.h"
 #include "src/lib/fxl/memory/weak_ptr.h"
 
-struct zx_thread_state_general_regs;
-
 namespace debug_agent {
 
 class DebugAgent;
@@ -146,15 +145,15 @@
   // suspended or on an exception). False if timeout or error.
   virtual bool WaitForSuspension(zx::time deadline = DefaultSuspendDeadline());
 
-  // Fills the thread status record. If full_stack is set, a full backtrace
-  // will be generated, otherwise a minimal one will be generated.
+  // Fills the thread status record. If full_stack is set, a full backtrace will be generated,
+  // otherwise a minimal one will be generated.
   //
-  // If optional_regs is non-null, it should point to the current registers of
-  // the thread. If null, these will be fetched automatically (this is an
-  // optimization for cases where the caller has already requested registers).
-  virtual void FillThreadRecord(debug_ipc::ThreadRecord::StackAmount stack_amount,
-                                const zx_thread_state_general_regs* optional_regs,
-                                debug_ipc::ThreadRecord* record) const;
+  // If the optional registers is set, will contain the current registers of the thread. If null,
+  // these will be fetched automatically (this is an optimization for cases where the caller has
+  // already requested registers).
+  debug_ipc::ThreadRecord GetThreadRecord(
+      debug_ipc::ThreadRecord::StackAmount stack_amount,
+      std::optional<GeneralRegisters> regs = std::nullopt) const;
 
   // Register reading and writing. The "write" command also returns the contents of the register
   // categories written do.
@@ -195,32 +194,34 @@
     kResume,  // The thread should be resumed from this exception.
   };
 
-  void HandleSingleStep(debug_ipc::NotifyException*, zx_thread_state_general_regs*);
-  void HandleGeneralException(debug_ipc::NotifyException*, zx_thread_state_general_regs*);
-  void HandleSoftwareBreakpoint(debug_ipc::NotifyException*, zx_thread_state_general_regs*);
-  void HandleHardwareBreakpoint(debug_ipc::NotifyException*, zx_thread_state_general_regs*);
-  void HandleWatchpoint(debug_ipc::NotifyException*, zx_thread_state_general_regs*);
+  // Some of these need to update the general registers in response to handling the exception. These
+  // ones take a non-const GeneralRegisters reference.
+  void HandleSingleStep(debug_ipc::NotifyException*, const GeneralRegisters& regs);
+  void HandleGeneralException(debug_ipc::NotifyException*, const GeneralRegisters& regs);
+  void HandleSoftwareBreakpoint(debug_ipc::NotifyException*, GeneralRegisters& regs);
+  void HandleHardwareBreakpoint(debug_ipc::NotifyException*, GeneralRegisters& regs);
+  void HandleWatchpoint(debug_ipc::NotifyException*, const GeneralRegisters& regs);
 
-  void SendExceptionNotification(debug_ipc::NotifyException*, zx_thread_state_general_regs*);
+  void SendExceptionNotification(debug_ipc::NotifyException*, const GeneralRegisters& regs);
 
-  OnStop UpdateForSoftwareBreakpoint(zx_thread_state_general_regs* regs,
-                                     std::vector<debug_ipc::BreakpointStats>* hit_breakpoints);
+  // Updates the registers and the thread state for hitting the breakpoint, and fills in the
+  // given breakpoint array for all matches.
+  OnStop UpdateForSoftwareBreakpoint(GeneralRegisters& regs,
+                                     std::vector<debug_ipc::BreakpointStats>& hit_breakpoints);
 
-  // When hitting a SW breakpoint, the PC needs to be correctly re-set depending
-  // on where the CPU leaves the PC after a SW exception.
-  void FixSoftwareBreakpointAddress(ProcessBreakpoint* process_breakpoint,
-                                    zx_thread_state_general_regs* regs);
+  // When hitting a SW breakpoint, the PC needs to be correctly re-set depending on where the CPU
+  // leaves the PC after a SW exception. This updates both the given register record and syncs it
+  // to the actual thread.
+  void FixSoftwareBreakpointAddress(ProcessBreakpoint* process_breakpoint, GeneralRegisters& regs);
 
-  // Handles an exception corresponding to a ProcessBreakpoint. All
-  // Breakpoints affected will have their updated stats added to
-  // *hit_breakpoints.
+  // Handles an exception corresponding to a ProcessBreakpoint. All Breakpoints affected will have
+  // their updated stats added to *hit_breakpoints.
   //
   // WARNING: The ProcessBreakpoint argument could be deleted in this call if it was a one-shot
   //          breakpoint, so it must not be used after this call.
   void UpdateForHitProcessBreakpoint(debug_ipc::BreakpointType exception_type,
                                      ProcessBreakpoint* process_breakpoint,
-                                     zx_thread_state_general_regs* regs,
-                                     std::vector<debug_ipc::BreakpointStats>* hit_breakpoints);
+                                     std::vector<debug_ipc::BreakpointStats>& hit_breakpoints);
 
   // Sets or clears the single step bit on the thread.
   void SetSingleStep(bool single_step);
diff --git a/src/developer/debug/debug_agent/debugged_thread_breakpoint_unittest.cc b/src/developer/debug/debug_agent/debugged_thread_breakpoint_unittest.cc
index d1d83d2..c8e362c 100644
--- a/src/developer/debug/debug_agent/debugged_thread_breakpoint_unittest.cc
+++ b/src/developer/debug/debug_agent/debugged_thread_breakpoint_unittest.cc
@@ -27,16 +27,6 @@
 
 class MockBreakpointArchProvider : public MockArchProvider {
  public:
-  zx_status_t ReadGeneralState(const zx::thread&, zx_thread_state_general_regs* r) const override {
-    *arch::IPInRegs(r) = exception_addr_;
-    return ZX_OK;
-  }
-
-  zx_status_t WriteGeneralState(const zx::thread&,
-                                const zx_thread_state_general_regs& regs) override {
-    return ZX_OK;
-  }
-
   zx_status_t GetInfo(const zx::thread&, zx_object_info_topic_t topic, void* buffer,
                       size_t buffer_size, size_t* actual, size_t* avail) const override {
     zx_info_thread* info = reinterpret_cast<zx_info_thread*>(buffer);
@@ -251,10 +241,16 @@
 
   // Set the exception information the arch provider is going to return.
   constexpr uint64_t kAddress = 0xdeadbeef;
-  mock_thread_handle->set_state(ZX_THREAD_STATE_BLOCKED_EXCEPTION);
   context.arch_provider->set_exception_addr(kAddress);
   context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kPageFault);
 
+  // The current thread address should agree with the exception.
+  GeneralRegisters regs;
+  regs.set_ip(kAddress);
+  mock_thread_handle->SetGeneralRegisters(regs);
+  mock_thread_handle->set_state(
+      ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException));
+
   // Trigger the exception.
   zx_exception_info exception_info = {};
   exception_info.pid = proc_object->koid;
@@ -301,10 +297,16 @@
 
   // Set the exception information the arch provider is going to return.
   constexpr uint64_t kAddress = 0xdeadbeef;
-  mock_thread_handle->set_state(ZX_THREAD_STATE_BLOCKED_EXCEPTION);
   context.arch_provider->set_exception_addr(kAddress);
   context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kSoftware);
 
+  // The current thread address should agree with the exception.
+  GeneralRegisters regs;
+  regs.set_ip(kAddress);
+  mock_thread_handle->SetGeneralRegisters(regs);
+  mock_thread_handle->set_state(
+      ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException));
+
   // Trigger the exception.
   zx_exception_info exception_info = {};
   exception_info.pid = proc_object->koid;
@@ -389,10 +391,16 @@
 
   // Set the exception information the arch provider is going to return.
   constexpr uint64_t kAddress = 0xdeadbeef;
-  mock_thread_handle->set_state(ZX_THREAD_STATE_BLOCKED_EXCEPTION);
   context.arch_provider->set_exception_addr(kAddress);
   context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kHardware);
 
+  // The current thread address should agree with the exception.
+  GeneralRegisters regs;
+  regs.set_ip(kAddress);
+  mock_thread_handle->SetGeneralRegisters(regs);
+  mock_thread_handle->set_state(
+      ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException));
+
   // Add a breakpoint on that address.
   constexpr uint32_t kBreakpointId = 1000;
   MockProcessDelegate process_delegate;
@@ -471,11 +479,17 @@
   // Set the exception information the arch provider is going to return.
   const uint64_t kAddress = kRange.begin();
   constexpr uint64_t kSlot = 0;
-  mock_thread_handle->set_state(ZX_THREAD_STATE_BLOCKED_EXCEPTION);
   context.arch_provider->set_exception_type(debug_ipc::ExceptionType::kWatchpoint);
   context.arch_provider->set_exception_addr(kAddress);
   context.arch_provider->set_slot(kSlot);
 
+  // The current thread address should agree with the exception.
+  GeneralRegisters regs;
+  regs.set_ip(kAddress);
+  mock_thread_handle->SetGeneralRegisters(regs);
+  mock_thread_handle->set_state(
+      ThreadHandle::State(debug_ipc::ThreadRecord::BlockedReason::kException));
+
   // Trigger the exception.
   zx_exception_info exception_info = {};
   exception_info.pid = proc_object->koid;
diff --git a/src/developer/debug/debug_agent/elf_utils.cc b/src/developer/debug/debug_agent/elf_utils.cc
new file mode 100644
index 0000000..67cc9c4d8
--- /dev/null
+++ b/src/developer/debug/debug_agent/elf_utils.cc
@@ -0,0 +1,124 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/developer/debug/debug_agent/elf_utils.h"
+
+// clang-format off
+// Included early because of conflicts.
+#include "src/lib/elflib/elflib.h"
+// clang-format on
+
+#include <link.h>
+
+#include <string>
+
+#include "src/developer/debug/debug_agent/process_handle.h"
+#include "src/developer/debug/ipc/records.h"
+
+namespace debug_agent {
+
+namespace {
+
+// Reads a null-terminated string from the given address of the given process.
+zx_status_t ReadNullTerminatedString(const ProcessHandle& process, zx_vaddr_t vaddr,
+                                     std::string* dest) {
+  // Max size of string we'll load as a sanity check.
+  constexpr size_t kMaxString = 32768;
+
+  dest->clear();
+
+  constexpr size_t kBlockSize = 256;
+  char block[kBlockSize];
+  while (dest->size() < kMaxString) {
+    size_t num_read = 0;
+    if (auto status = process.ReadMemory(vaddr, block, kBlockSize, &num_read); status != ZX_OK)
+      return status;
+
+    for (size_t i = 0; i < num_read; i++) {
+      if (block[i] == 0)
+        return ZX_OK;
+      dest->push_back(block[i]);
+    }
+
+    if (num_read < kBlockSize)
+      return ZX_OK;  // Partial read: hit the mapped memory boundary.
+    vaddr += kBlockSize;
+  }
+  return ZX_OK;
+}
+
+}  // namespace
+
+zx_status_t WalkElfModules(const ProcessHandle& process, uint64_t dl_debug_addr,
+                           std::function<bool(uint64_t base_addr, uint64_t lmap)> cb) {
+  size_t num_read = 0;
+  uint64_t lmap = 0;
+  zx_status_t status =
+      process.ReadMemory(dl_debug_addr + offsetof(r_debug, r_map), &lmap, sizeof(lmap), &num_read);
+  if (status != ZX_OK)
+    return status;
+
+  size_t module_count = 0;
+
+  // Walk the linked list.
+  constexpr size_t kMaxObjects = 512;  // Sanity threshold.
+  while (lmap != 0) {
+    if (module_count++ >= kMaxObjects)
+      return ZX_ERR_BAD_STATE;
+
+    uint64_t base;
+    if (process.ReadMemory(lmap + offsetof(link_map, l_addr), &base, sizeof(base), &num_read) !=
+        ZX_OK)
+      break;
+
+    uint64_t next;
+    if (process.ReadMemory(lmap + offsetof(link_map, l_next), &next, sizeof(next), &num_read) !=
+        ZX_OK)
+      break;
+
+    if (!cb(base, lmap))
+      break;
+
+    lmap = next;
+  }
+
+  return ZX_OK;
+}
+
+std::vector<debug_ipc::Module> GetElfModulesForProcess(const ProcessHandle& process,
+                        uint64_t dl_debug_addr) {
+  std::vector<debug_ipc::Module> modules;
+  WalkElfModules(
+      process, dl_debug_addr, [&process, &modules](uint64_t base, uint64_t lmap) {
+        debug_ipc::Module module;
+        module.base = base;
+        module.debug_address = lmap;
+
+        uint64_t str_addr;
+        size_t num_read;
+        if (process.ReadMemory(lmap + offsetof(link_map, l_name), &str_addr, sizeof(str_addr),
+                               &num_read) != ZX_OK)
+          return false;
+
+        if (ReadNullTerminatedString(process, str_addr, &module.name) != ZX_OK)
+          return false;
+
+        auto elf = elflib::ElfLib::Create([&process, base = module.base](
+                                              uint64_t offset, std::vector<uint8_t>* buf) {
+          size_t num_read = 0;
+          if (process.ReadMemory(base + offset, buf->data(), buf->size(), &num_read) != ZX_OK)
+            return false;
+          return num_read == buf->size();
+        });
+
+        if (elf)
+          module.build_id = elf->GetGNUBuildID();
+
+        modules.push_back(std::move(module));
+        return true;
+      });
+  return modules;
+}
+
+}  // namespace debug_agent
diff --git a/src/developer/debug/debug_agent/elf_utils.h b/src/developer/debug/debug_agent/elf_utils.h
new file mode 100644
index 0000000..fe53789
--- /dev/null
+++ b/src/developer/debug/debug_agent/elf_utils.h
@@ -0,0 +1,31 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <zircon/status.h>
+
+#include <stdint.h>
+
+#include <functional>
+#include <vector>
+
+namespace debug_ipc {
+struct Module;
+}
+
+namespace debug_agent {
+
+class ProcessHandle;
+
+// Iterates through all modules in the given process, calling the callback for each. The callback
+// should return true to keep iterating, false to stop now.
+zx_status_t WalkElfModules(const ProcessHandle& process, uint64_t dl_debug_addr,
+                           std::function<bool(uint64_t base_addr, uint64_t lmap)> cb);
+
+// Computes the modules for the given process.
+std::vector<debug_ipc::Module> GetElfModulesForProcess(const ProcessHandle& process,
+                        uint64_t dl_debug_addr);
+
+}  // namespace debug_agent
diff --git a/src/developer/debug/debug_agent/general_registers.cc b/src/developer/debug/debug_agent/general_registers.cc
new file mode 100644
index 0000000..04b046f
--- /dev/null
+++ b/src/developer/debug/debug_agent/general_registers.cc
@@ -0,0 +1,15 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/developer/debug/debug_agent/general_registers.h"
+
+#include "src/developer/debug/debug_agent/arch.h"
+
+namespace debug_agent {
+
+void GeneralRegisters::CopyTo(std::vector<debug_ipc::Register>& dest) const {
+  arch::ArchProvider::SaveGeneralRegs(regs_, &dest);
+}
+
+}  // namespace debug_agent
diff --git a/src/developer/debug/debug_agent/general_registers.h b/src/developer/debug/debug_agent/general_registers.h
new file mode 100644
index 0000000..4865326
--- /dev/null
+++ b/src/developer/debug/debug_agent/general_registers.h
@@ -0,0 +1,54 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_DEVELOPER_DEBUG_DEBUG_AGENT_GENERAL_REGISTERS_H_
+#define SRC_DEVELOPER_DEBUG_DEBUG_AGENT_GENERAL_REGISTERS_H_
+
+#include <zircon/syscalls/debug.h>
+
+#include <vector>
+
+namespace debug_ipc {
+struct Register;
+}
+
+namespace debug_agent {
+
+// Wrapper around the general thread registers to allow them to be accessed uniformly regardless
+// of the platform.
+class GeneralRegisters {
+ public:
+  GeneralRegisters() : regs_() {}
+  explicit GeneralRegisters(const zx_thread_state_general_regs& r) : regs_(r) {}
+
+#if defined(__x86_64__)
+  // Instruction pointer.
+  uint64_t ip() const { return regs_.rip; }
+  void set_ip(uint64_t ip) { regs_.rip = ip; }
+
+  // Stack pointer.
+  uint64_t sp() const { return regs_.rsp; }
+  void set_sp(uint64_t sp) { regs_.rsp = sp; }
+#elif defined(__aarch64__)
+  uint64_t ip() const { return regs_.pc; }
+  void set_ip(uint64_t ip) { regs_.pc = ip; }
+
+  // Stack pointer.
+  uint64_t sp() const { return regs_.sp; }
+  void set_sp(uint64_t sp) { regs_.sp = sp; }
+#endif
+
+  // Appends the current general registers to the given high-level register record.
+  void CopyTo(std::vector<debug_ipc::Register>& dest) const;
+
+  zx_thread_state_general_regs& GetNativeRegisters() { return regs_; }
+  const zx_thread_state_general_regs& GetNativeRegisters() const { return regs_; }
+
+ private:
+  zx_thread_state_general_regs regs_;
+};
+
+}  // namespace debug_agent
+
+#endif  // SRC_DEVELOPER_DEBUG_DEBUG_AGENT_GENERAL_REGISTERS_H_
diff --git a/src/developer/debug/debug_agent/mock_arch_provider.cc b/src/developer/debug/debug_agent/mock_arch_provider.cc
index 9fb1944..72c9375 100644
--- a/src/developer/debug/debug_agent/mock_arch_provider.cc
+++ b/src/developer/debug/debug_agent/mock_arch_provider.cc
@@ -8,20 +8,6 @@
 
 namespace debug_agent {
 
-zx_status_t MockArchProvider::ReadGeneralState(const zx::thread& handle,
-                                               zx_thread_state_general_regs* regs) const {
-  // Not implemented by this mock.
-  FX_NOTREACHED();
-  return ZX_ERR_NOT_SUPPORTED;
-}
-
-zx_status_t MockArchProvider::WriteGeneralState(const zx::thread& handle,
-                                                const zx_thread_state_general_regs& regs) {
-  // Not implemented by this mock.
-  FX_NOTREACHED();
-  return ZX_ERR_NOT_SUPPORTED;
-}
-
 zx_status_t MockArchProvider::ReadDebugState(const zx::thread& handle,
                                              zx_thread_state_debug_regs* regs) const {
   // Not implemented by this mock.
diff --git a/src/developer/debug/debug_agent/mock_arch_provider.h b/src/developer/debug/debug_agent/mock_arch_provider.h
index 0f32409..90833c6 100644
--- a/src/developer/debug/debug_agent/mock_arch_provider.h
+++ b/src/developer/debug/debug_agent/mock_arch_provider.h
@@ -16,10 +16,6 @@
 // the code is doing within the tests.
 class MockArchProvider : public arch::ArchProvider {
  public:
-  zx_status_t ReadGeneralState(const zx::thread& handle,
-                               zx_thread_state_general_regs* regs) const override;
-  zx_status_t WriteGeneralState(const zx::thread& handle,
-                                const zx_thread_state_general_regs& regs) override;
   zx_status_t ReadDebugState(const zx::thread& handle,
                              zx_thread_state_debug_regs* regs) const override;
   zx_status_t WriteDebugState(const zx::thread& handle,
diff --git a/src/developer/debug/debug_agent/mock_process_handle.cc b/src/developer/debug/debug_agent/mock_process_handle.cc
index 4a38f86..2c930e1 100644
--- a/src/developer/debug/debug_agent/mock_process_handle.cc
+++ b/src/developer/debug/debug_agent/mock_process_handle.cc
@@ -22,6 +22,11 @@
   return {};
 }
 
+std::vector<debug_ipc::Module> MockProcessHandle::GetModules(uint64_t dl_debug_addr) const {
+  // Not currently implemented in this mock.
+  return {};
+}
+
 zx_status_t MockProcessHandle::ReadMemory(uintptr_t address, void* buffer, size_t len,
                                           size_t* actual) const {
   auto vect = mock_memory_.ReadMemory(address, len);
diff --git a/src/developer/debug/debug_agent/mock_process_handle.h b/src/developer/debug/debug_agent/mock_process_handle.h
index c9c1b1a..60d5f04 100644
--- a/src/developer/debug/debug_agent/mock_process_handle.h
+++ b/src/developer/debug/debug_agent/mock_process_handle.h
@@ -32,6 +32,7 @@
   zx_koid_t GetKoid() const override { return process_koid_; }
   zx_status_t GetInfo(zx_info_process* info) const override;
   std::vector<debug_ipc::AddressRegion> GetAddressSpace(uint64_t address) const override;
+  std::vector<debug_ipc::Module> GetModules(uint64_t dl_debug_addr) const override;
   zx_status_t ReadMemory(uintptr_t address, void* buffer, size_t len,
                          size_t* actual) const override;
   zx_status_t WriteMemory(uintptr_t address, const void* buffer, size_t len,
diff --git a/src/developer/debug/debug_agent/mock_thread.cc b/src/developer/debug/debug_agent/mock_thread.cc
index 9a8907a..7415027 100644
--- a/src/developer/debug/debug_agent/mock_thread.cc
+++ b/src/developer/debug/debug_agent/mock_thread.cc
@@ -43,13 +43,6 @@
   return true;
 }
 
-void MockThread::FillThreadRecord(debug_ipc::ThreadRecord::StackAmount stack_amount,
-                                  const zx_thread_state_general_regs* optional_regs,
-                                  debug_ipc::ThreadRecord* record) const {
-  record->process_koid = process()->koid();
-  record->thread_koid = koid();
-}
-
 void MockThread::IncreaseSuspend() {
   suspend_count_++;
   DEBUG_LOG(Test) << "Thread " << koid() << ": Increased suspend count to " << suspend_count_;
diff --git a/src/developer/debug/debug_agent/mock_thread.h b/src/developer/debug/debug_agent/mock_thread.h
index a190ecc..db1ddbc 100644
--- a/src/developer/debug/debug_agent/mock_thread.h
+++ b/src/developer/debug/debug_agent/mock_thread.h
@@ -28,10 +28,6 @@
   bool Suspend(bool synchronous = false) override;
   bool WaitForSuspension(zx::time deadline = DefaultSuspendDeadline()) override;
 
-  void FillThreadRecord(debug_ipc::ThreadRecord::StackAmount stack_amount,
-                        const zx_thread_state_general_regs* optional_regs,
-                        debug_ipc::ThreadRecord* record) const override;
-
   bool IsSuspended() const override { return internal_suspension_ || suspend_count_ > 0; }
   bool IsInException() const override { return in_exception_; }
 
diff --git a/src/developer/debug/debug_agent/mock_thread_handle.cc b/src/developer/debug/debug_agent/mock_thread_handle.cc
index c4fc7b6..9ae67fa 100644
--- a/src/developer/debug/debug_agent/mock_thread_handle.cc
+++ b/src/developer/debug/debug_agent/mock_thread_handle.cc
@@ -6,8 +6,6 @@
 
 #include <lib/syslog/cpp/macros.h>
 
-#include "src/developer/debug/debug_agent/process_info.h"
-
 namespace debug_agent {
 
 void MockThreadHandle::SetRegisterCategory(debug_ipc::RegisterCategory cat,
@@ -77,12 +75,21 @@
   record.process_koid = process_koid_;
   record.thread_koid = thread_koid_;
   record.name = "test thread";
-  record.state = ThreadStateToEnums(GetState(), &record.blocked_reason);
+  record.state = state_.state;
+  record.blocked_reason = state_.blocked_reason;
   return record;
 }
 
 zx::suspend_token MockThreadHandle::Suspend() { return zx::suspend_token(); }
 
+std::optional<GeneralRegisters> MockThreadHandle::GetGeneralRegisters() const {
+  return general_registers_;
+}
+
+void MockThreadHandle::SetGeneralRegisters(const GeneralRegisters& regs) {
+  general_registers_ = regs;
+}
+
 std::vector<debug_ipc::Register> MockThreadHandle::ReadRegisters(
     const std::vector<debug_ipc::RegisterCategory>& cats_to_get) const {
   std::vector<debug_ipc::Register> result;
diff --git a/src/developer/debug/debug_agent/mock_thread_handle.h b/src/developer/debug/debug_agent/mock_thread_handle.h
index 79b79c5..cb2bb02 100644
--- a/src/developer/debug/debug_agent/mock_thread_handle.h
+++ b/src/developer/debug/debug_agent/mock_thread_handle.h
@@ -23,7 +23,7 @@
   MockThreadHandle(zx_koid_t process_koid, zx_koid_t thread_koid)
       : process_koid_(process_koid), thread_koid_(thread_koid) {}
 
-  void set_state(uint32_t state) { state_ = state; }
+  void set_state(State s) { state_ = s; }
 
   // Sets the values to be returned for the given register category query.
   void SetRegisterCategory(debug_ipc::RegisterCategory cat,
@@ -56,9 +56,11 @@
   const zx::thread& GetNativeHandle() const override { return null_handle_; }
   zx::thread& GetNativeHandle() override { return null_handle_; }
   zx_koid_t GetKoid() const override { return thread_koid_; }
-  uint32_t GetState() const override { return state_; }
+  State GetState() const override { return state_; }
   debug_ipc::ThreadRecord GetThreadRecord() const override;
   zx::suspend_token Suspend() override;
+  std::optional<GeneralRegisters> GetGeneralRegisters() const override;
+  void SetGeneralRegisters(const GeneralRegisters& regs) override;
   std::vector<debug_ipc::Register> ReadRegisters(
       const std::vector<debug_ipc::RegisterCategory>& cats_to_get) const override;
   std::vector<debug_ipc::Register> WriteRegisters(
@@ -79,7 +81,9 @@
 
   std::vector<debug_ipc::Register>
       registers_[static_cast<size_t>(debug_ipc::RegisterCategory::kLast)];
-  uint32_t state_ = ZX_THREAD_STATE_RUNNING;
+
+  State state_;
+  GeneralRegisters general_registers_;
 
   debug_ipc::AddressRange watchpoint_range_to_return_;
   int watchpoint_slot_to_return_ = 0;
diff --git a/src/developer/debug/debug_agent/process_handle.h b/src/developer/debug/debug_agent/process_handle.h
index e20b1b5..920d871 100644
--- a/src/developer/debug/debug_agent/process_handle.h
+++ b/src/developer/debug/debug_agent/process_handle.h
@@ -13,6 +13,7 @@
 namespace debug_ipc {
 struct AddressRegion;
 struct MemoryBlock;
+struct Module;
 }  // namespace debug_ipc
 
 namespace debug_agent {
@@ -36,6 +37,14 @@
   // that address will be returned. Otherwise all regions will be returned.
   virtual std::vector<debug_ipc::AddressRegion> GetAddressSpace(uint64_t address) const = 0;
 
+  // Returns the modules (shared libraries and the main binary) for the process. Will be empty on
+  // failure.
+  //
+  // Prefer this version to calling the elf_utils variant because this one allows mocking.
+  //
+  // TODO(brettw) consider moving dl_debug_addr to be internally managed by ZirconProcessInfo.
+  virtual std::vector<debug_ipc::Module> GetModules(uint64_t dl_debug_addr) const = 0;
+
   virtual zx_status_t ReadMemory(uintptr_t address, void* buffer, size_t len,
                                  size_t* actual) const = 0;
   virtual zx_status_t WriteMemory(uintptr_t address, const void* buffer, size_t len,
diff --git a/src/developer/debug/debug_agent/process_info.cc b/src/developer/debug/debug_agent/process_info.cc
deleted file mode 100644
index 0263391..0000000
--- a/src/developer/debug/debug_agent/process_info.cc
+++ /dev/null
@@ -1,191 +0,0 @@
-// Copyright 2018 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "src/developer/debug/debug_agent/process_info.h"
-
-// clang-format off
-// Included early because of conflicts.
-#include "src/lib/elflib/elflib.h"
-// clang-format on
-
-#include <inttypes.h>
-#include <lib/zx/thread.h>
-#include <link.h>
-#include <zircon/syscalls.h>
-#include <zircon/syscalls/object.h>
-
-#include <algorithm>
-#include <iterator>
-#include <map>
-
-#include "src/developer/debug/debug_agent/arch.h"
-#include "src/developer/debug/debug_agent/object_provider.h"
-#include <lib/syslog/cpp/macros.h>
-
-namespace debug_agent {
-
-namespace {
-
-zx_status_t WalkModules(const zx::process& process, uint64_t dl_debug_addr,
-                        std::function<bool(const zx::process&, uint64_t, uint64_t)> cb) {
-  size_t num_read = 0;
-  uint64_t lmap = 0;
-  zx_status_t status =
-      process.read_memory(dl_debug_addr + offsetof(r_debug, r_map), &lmap, sizeof(lmap), &num_read);
-  if (status != ZX_OK)
-    return status;
-
-  size_t module_count = 0;
-
-  // Walk the linked list.
-  constexpr size_t kMaxObjects = 512;  // Sanity threshold.
-  while (lmap != 0) {
-    if (module_count++ >= kMaxObjects)
-      return ZX_ERR_BAD_STATE;
-
-    uint64_t base;
-    if (process.read_memory(lmap + offsetof(link_map, l_addr), &base, sizeof(base), &num_read) !=
-        ZX_OK)
-      break;
-
-    uint64_t next;
-    if (process.read_memory(lmap + offsetof(link_map, l_next), &next, sizeof(next), &num_read) !=
-        ZX_OK)
-      break;
-
-    if (!cb(process, base, lmap))
-      break;
-
-    lmap = next;
-  }
-
-  return ZX_OK;
-}
-
-debug_ipc::ThreadRecord::BlockedReason ThreadStateBlockedReasonToEnum(uint32_t state) {
-  FX_DCHECK(ZX_THREAD_STATE_BASIC(state) == ZX_THREAD_STATE_BLOCKED);
-
-  switch (state) {
-    case ZX_THREAD_STATE_BLOCKED_EXCEPTION:
-      return debug_ipc::ThreadRecord::BlockedReason::kException;
-    case ZX_THREAD_STATE_BLOCKED_SLEEPING:
-      return debug_ipc::ThreadRecord::BlockedReason::kSleeping;
-    case ZX_THREAD_STATE_BLOCKED_FUTEX:
-      return debug_ipc::ThreadRecord::BlockedReason::kFutex;
-    case ZX_THREAD_STATE_BLOCKED_PORT:
-      return debug_ipc::ThreadRecord::BlockedReason::kPort;
-    case ZX_THREAD_STATE_BLOCKED_CHANNEL:
-      return debug_ipc::ThreadRecord::BlockedReason::kChannel;
-    case ZX_THREAD_STATE_BLOCKED_WAIT_ONE:
-      return debug_ipc::ThreadRecord::BlockedReason::kWaitOne;
-    case ZX_THREAD_STATE_BLOCKED_WAIT_MANY:
-      return debug_ipc::ThreadRecord::BlockedReason::kWaitMany;
-    case ZX_THREAD_STATE_BLOCKED_INTERRUPT:
-      return debug_ipc::ThreadRecord::BlockedReason::kInterrupt;
-    case ZX_THREAD_STATE_BLOCKED_PAGER:
-      return debug_ipc::ThreadRecord::BlockedReason::kPager;
-    default:
-      FX_NOTREACHED();
-      return debug_ipc::ThreadRecord::BlockedReason::kNotBlocked;
-  }
-}
-
-// Reads a null-terminated string from the given address of the given process.
-zx_status_t ReadNullTerminatedString(const zx::process& process, zx_vaddr_t vaddr,
-                                     std::string* dest) {
-  // Max size of string we'll load as a sanity check.
-  constexpr size_t kMaxString = 32768;
-
-  dest->clear();
-
-  constexpr size_t kBlockSize = 256;
-  char block[kBlockSize];
-  while (dest->size() < kMaxString) {
-    size_t num_read = 0;
-    zx_status_t status = process.read_memory(vaddr, block, kBlockSize, &num_read);
-    if (status != ZX_OK)
-      return status;
-
-    for (size_t i = 0; i < num_read; i++) {
-      if (block[i] == 0)
-        return ZX_OK;
-      dest->push_back(block[i]);
-    }
-
-    if (num_read < kBlockSize)
-      return ZX_OK;  // Partial read: hit the mapped memory boundary.
-    vaddr += kBlockSize;
-  }
-  return ZX_OK;
-}
-
-}  // namespace
-
-zx_status_t GetModulesForProcess(const zx::process& process, uint64_t dl_debug_addr,
-                                 std::vector<debug_ipc::Module>* modules) {
-  return WalkModules(
-      process, dl_debug_addr, [modules](const zx::process& process, uint64_t base, uint64_t lmap) {
-        debug_ipc::Module module;
-        module.base = base;
-        module.debug_address = lmap;
-
-        uint64_t str_addr;
-        size_t num_read;
-        if (process.read_memory(lmap + offsetof(link_map, l_name), &str_addr, sizeof(str_addr),
-                                &num_read) != ZX_OK)
-          return false;
-
-        if (ReadNullTerminatedString(process, str_addr, &module.name) != ZX_OK)
-          return false;
-
-        auto elf = elflib::ElfLib::Create([&process, base = module.base](
-                                              uint64_t offset, std::vector<uint8_t>* buf) {
-          size_t num_read = 0;
-
-          if (process.read_memory(base + offset, buf->data(), buf->size(), &num_read) != ZX_OK) {
-            return false;
-          }
-
-          return num_read == buf->size();
-        });
-
-        if (elf) {
-          module.build_id = elf->GetGNUBuildID();
-        }
-
-        modules->push_back(std::move(module));
-        return true;
-      });
-}
-
-debug_ipc::ThreadRecord::State ThreadStateToEnums(
-    uint32_t state, debug_ipc::ThreadRecord::BlockedReason* blocked_reason) {
-  struct Mapping {
-    uint32_t int_state;
-    debug_ipc::ThreadRecord::State enum_state;
-  };
-  static const Mapping mappings[] = {
-      {ZX_THREAD_STATE_NEW, debug_ipc::ThreadRecord::State::kNew},
-      {ZX_THREAD_STATE_RUNNING, debug_ipc::ThreadRecord::State::kRunning},
-      {ZX_THREAD_STATE_SUSPENDED, debug_ipc::ThreadRecord::State::kSuspended},
-      {ZX_THREAD_STATE_BLOCKED, debug_ipc::ThreadRecord::State::kBlocked},
-      {ZX_THREAD_STATE_DYING, debug_ipc::ThreadRecord::State::kDying},
-      {ZX_THREAD_STATE_DEAD, debug_ipc::ThreadRecord::State::kDead}};
-
-  const uint32_t basic_state = ZX_THREAD_STATE_BASIC(state);
-  *blocked_reason = debug_ipc::ThreadRecord::BlockedReason::kNotBlocked;
-
-  for (const Mapping& mapping : mappings) {
-    if (mapping.int_state == basic_state) {
-      if (mapping.enum_state == debug_ipc::ThreadRecord::State::kBlocked) {
-        *blocked_reason = ThreadStateBlockedReasonToEnum(state);
-      }
-      return mapping.enum_state;
-    }
-  }
-  FX_NOTREACHED();
-  return debug_ipc::ThreadRecord::State::kDead;
-}
-
-}  // namespace debug_agent
diff --git a/src/developer/debug/debug_agent/process_info.h b/src/developer/debug/debug_agent/process_info.h
deleted file mode 100644
index 673ccda..0000000
--- a/src/developer/debug/debug_agent/process_info.h
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2018 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef SRC_DEVELOPER_DEBUG_DEBUG_AGENT_PROCESS_INFO_H_
-#define SRC_DEVELOPER_DEBUG_DEBUG_AGENT_PROCESS_INFO_H_
-
-#include <lib/zx/process.h>
-#include <lib/zx/thread.h>
-#include <zircon/syscalls/debug.h>
-#include <zircon/types.h>
-
-#include <vector>
-
-#include "src/developer/debug/ipc/records.h"
-
-struct zx_info_process;
-
-namespace debug_agent {
-
-// Fills the given vector with the module information for the process.
-// "dl_debug_addr" is the address inside "process" of the dynamic loader's
-// debug state.
-//
-// TODO(brettw) move to ProcessHandle when the unwinder uses it.
-zx_status_t GetModulesForProcess(const zx::process& process, uint64_t dl_debug_addr,
-                                 std::vector<debug_ipc::Module>* modules);
-
-debug_ipc::ThreadRecord::State ThreadStateToEnums(
-    uint32_t state, debug_ipc::ThreadRecord::BlockedReason* blocked_reason);
-
-}  // namespace debug_agent
-
-#endif  // SRC_DEVELOPER_DEBUG_DEBUG_AGENT_PROCESS_INFO_H_
diff --git a/src/developer/debug/debug_agent/thread_handle.h b/src/developer/debug/debug_agent/thread_handle.h
index d0e806f..90d87b4 100644
--- a/src/developer/debug/debug_agent/thread_handle.h
+++ b/src/developer/debug/debug_agent/thread_handle.h
@@ -10,13 +10,36 @@
 #include <vector>
 
 #include "src/developer/debug/debug_agent/arch_helpers.h"
+#include "src/developer/debug/debug_agent/general_registers.h"
 #include "src/developer/debug/ipc/records.h"
 
 namespace debug_agent {
 
+class GeneralRegisters;
+
 // An abstract wrapper around an OS thread primitive. This abstraction is to allow mocking.
 class ThreadHandle {
  public:
+  struct State {
+    explicit State(debug_ipc::ThreadRecord::State s = debug_ipc::ThreadRecord::State::kRunning,
+                   debug_ipc::ThreadRecord::BlockedReason br =
+                       debug_ipc::ThreadRecord::BlockedReason::kNotBlocked)
+        : state(s), blocked_reason(br) {}
+
+    // Creates a blocked state with the given reason.
+    explicit State(debug_ipc::ThreadRecord::BlockedReason br)
+        : state(debug_ipc::ThreadRecord::State::kBlocked), blocked_reason(br) {}
+
+    // This state is common to check for and requires a combination of things to check.
+    bool is_blocked_on_exception() const {
+      return state == debug_ipc::ThreadRecord::State::kBlocked &&
+             blocked_reason == debug_ipc::ThreadRecord::BlockedReason::kException;
+    }
+
+    debug_ipc::ThreadRecord::State state;
+    debug_ipc::ThreadRecord::BlockedReason blocked_reason;
+  };
+
   virtual ~ThreadHandle() = default;
 
   // Access to the underlying native thread object. This is for porting purposes, ideally this
@@ -28,8 +51,7 @@
 
   virtual zx_koid_t GetKoid() const = 0;
 
-  // Returns a ZX_THREAD_STATE_* enum for the thread.
-  virtual uint32_t GetState() const = 0;
+  virtual State GetState() const = 0;
 
   // Fills in everything but the stack into the returned thread record.
   virtual debug_ipc::ThreadRecord GetThreadRecord() const = 0;
@@ -39,6 +61,10 @@
 
   // Registers -------------------------------------------------------------------------------------
 
+  // Reads and writes the general thread registers.
+  virtual std::optional<GeneralRegisters> GetGeneralRegisters() const = 0;
+  virtual void SetGeneralRegisters(const GeneralRegisters& regs) = 0;
+
   // Returns the current values of the given register categories.
   virtual std::vector<debug_ipc::Register> ReadRegisters(
       const std::vector<debug_ipc::RegisterCategory>& cats_to_get) const = 0;
diff --git a/src/developer/debug/debug_agent/unwind.cc b/src/developer/debug/debug_agent/unwind.cc
index a0df6c0..11fd162 100644
--- a/src/developer/debug/debug_agent/unwind.cc
+++ b/src/developer/debug/debug_agent/unwind.cc
@@ -12,7 +12,9 @@
 #include <ngunwind/libunwind.h>
 
 #include "src/developer/debug/debug_agent/arch.h"
-#include "src/developer/debug/debug_agent/process_info.h"
+#include "src/developer/debug/debug_agent/general_registers.h"
+#include "src/developer/debug/debug_agent/process_handle.h"
+#include "src/developer/debug/debug_agent/thread_handle.h"
 #include "src/developer/debug/third_party/libunwindstack/fuchsia/MemoryFuchsia.h"
 #include "src/developer/debug/third_party/libunwindstack/fuchsia/RegsFuchsia.h"
 #include "src/developer/debug/third_party/libunwindstack/include/unwindstack/Unwinder.h"
@@ -94,13 +96,14 @@
   return containers::array_view<NGUnwindRegisterMap>(std::begin(kGeneral), std::end(kGeneral));
 }
 
-zx_status_t UnwindStackAndroid(const zx::process& process, uint64_t dl_debug_addr,
-                               const zx::thread& thread, const zx_thread_state_general_regs& regs,
+zx_status_t UnwindStackAndroid(const ProcessHandle& process, uint64_t dl_debug_addr,
+                               const ThreadHandle& thread, const GeneralRegisters& regs,
                                size_t max_depth, std::vector<debug_ipc::StackFrame>* stack) {
+  // The modules are sorted by load address.
+  //
   // Ignore errors getting modules, the empty case can at least give the current location, and maybe
   // more if there are stack pointers.
-  ModuleVector modules;  // Sorted by load address.
-  GetModulesForProcess(process, dl_debug_addr, &modules);
+  ModuleVector modules = process.GetModules(dl_debug_addr);
   std::sort(modules.begin(), modules.end(), [](auto& a, auto& b) { return a.base < b.base; });
 
   unwindstack::Maps maps;
@@ -127,9 +130,9 @@
   }
 
   unwindstack::RegsFuchsia unwind_regs;
-  unwind_regs.Set(regs);
+  unwind_regs.Set(regs.GetNativeRegisters());
 
-  auto memory = std::make_shared<unwindstack::MemoryFuchsia>(process.get());
+  auto memory = std::make_shared<unwindstack::MemoryFuchsia>(process.GetNativeHandle().get());
 
   // Always ask for one more frame than requested so we can get the canonical frame address for the
   // frames we do return (the CFA is the previous frame's stack pointer at the time of the call).
@@ -195,21 +198,22 @@
   return 0;
 }
 
-zx_status_t UnwindStackNgUnwind(arch::ArchProvider* arch_provider, const zx::process& process,
-                                uint64_t dl_debug_addr, const zx::thread& thread,
-                                const zx_thread_state_general_regs& regs, size_t max_depth,
+zx_status_t UnwindStackNgUnwind(arch::ArchProvider* arch_provider, const ProcessHandle& process,
+                                uint64_t dl_debug_addr, const ThreadHandle& thread,
+                                const GeneralRegisters& regs, size_t max_depth,
                                 std::vector<debug_ipc::StackFrame>* stack) {
   stack->clear();
 
+  // The modules are sorted by load address.
+  //
   // Ignore errors getting modules, the empty case can at least give the current location, and maybe
   // more if there are stack pointers.
-  ModuleVector modules;  // Sorted by load address.
-  GetModulesForProcess(process, dl_debug_addr, &modules);
+  ModuleVector modules = process.GetModules(dl_debug_addr);
   std::sort(modules.begin(), modules.end(), [](auto& a, auto& b) { return a.base < b.base; });
 
   // Any of these functions can fail if the program or thread was killed out from under us.
-  unw_fuchsia_info_t* fuchsia =
-      unw_create_fuchsia(process.get(), thread.get(), &modules, &LookupDso);
+  unw_fuchsia_info_t* fuchsia = unw_create_fuchsia(
+      process.GetNativeHandle().get(), thread.GetNativeHandle().get(), &modules, &LookupDso);
   if (!fuchsia)
     return ZX_ERR_INTERNAL;
 
@@ -229,10 +233,10 @@
 
   // Top stack frame.
   debug_ipc::StackFrame frame;
-  frame.ip = *arch::IPInRegs(const_cast<zx_thread_state_general_regs*>(&regs));
-  frame.sp = *arch::SPInRegs(const_cast<zx_thread_state_general_regs*>(&regs));
+  frame.ip = regs.ip();
+  frame.sp = regs.sp();
   frame.cfa = 0;
-  arch_provider->SaveGeneralRegs(regs, &frame.regs);
+  regs.CopyTo(frame.regs);
   stack->push_back(std::move(frame));
 
   while (frame.sp >= 0x1000000 && stack->size() < max_depth + 1) {
@@ -281,9 +285,9 @@
 
 void SetUnwinderType(UnwinderType type) { unwinder_type = type; }
 
-zx_status_t UnwindStack(arch::ArchProvider* arch_provider, const zx::process& process,
-                        uint64_t dl_debug_addr, const zx::thread& thread,
-                        const zx_thread_state_general_regs& regs, size_t max_depth,
+zx_status_t UnwindStack(arch::ArchProvider* arch_provider, const ProcessHandle& process,
+                        uint64_t dl_debug_addr, const ThreadHandle& thread,
+                        const GeneralRegisters& regs, size_t max_depth,
                         std::vector<debug_ipc::StackFrame>* stack) {
   switch (unwinder_type) {
     case UnwinderType::kNgUnwind:
diff --git a/src/developer/debug/debug_agent/unwind.h b/src/developer/debug/debug_agent/unwind.h
index 378c548..ab8774e 100644
--- a/src/developer/debug/debug_agent/unwind.h
+++ b/src/developer/debug/debug_agent/unwind.h
@@ -19,14 +19,18 @@
 class ArchProvider;
 }
 
+class GeneralRegisters;
+class ProcessHandle;
+class ThreadHandle;
+
 // We're testing different unwinders, this specifies which one you want to use.
 // The unwinder type is a process-wide state.
 enum class UnwinderType { kNgUnwind, kAndroid };
 void SetUnwinderType(UnwinderType unwinder_type);
 
-zx_status_t UnwindStack(arch::ArchProvider* arch_provider, const zx::process& process,
-                        uint64_t dl_debug_addr, const zx::thread& thread,
-                        const zx_thread_state_general_regs& regs, size_t max_depth,
+zx_status_t UnwindStack(arch::ArchProvider* arch_provider, const ProcessHandle& process,
+                        uint64_t dl_debug_addr, const ThreadHandle& thread,
+                        const GeneralRegisters& regs, size_t max_depth,
                         std::vector<debug_ipc::StackFrame>* stack);
 
 }  // namespace debug_agent
diff --git a/src/developer/debug/debug_agent/unwind_unittest.cc b/src/developer/debug/debug_agent/unwind_unittest.cc
index 65e55d5..ed322a5 100644
--- a/src/developer/debug/debug_agent/unwind_unittest.cc
+++ b/src/developer/debug/debug_agent/unwind_unittest.cc
@@ -13,6 +13,8 @@
 #include <gtest/gtest.h>
 
 #include "src/developer/debug/debug_agent/arch_provider_impl.h"
+#include "src/developer/debug/debug_agent/zircon_process_handle.h"
+#include "src/developer/debug/debug_agent/zircon_thread_handle.h"
 
 namespace debug_agent {
 
@@ -25,7 +27,7 @@
 
   // Set by thread itself before thread_ready is signaled. zx::thread::native_handle doesn't seem to
   // do what we want.
-  zx::thread thread;
+  std::unique_ptr<ThreadHandle> thread;
 
   bool thread_ready = false;
   std::condition_variable thread_ready_cv;
@@ -48,7 +50,12 @@
 
 void __attribute__((noinline)) ThreadFunc1(ThreadData* data) {
   // Fill in our thread handle.
-  zx::thread::self()->duplicate(ZX_RIGHT_SAME_RIGHTS, &data->thread);
+  zx::thread handle;
+  zx::thread::self()->duplicate(ZX_RIGHT_SAME_RIGHTS, &handle);
+
+  // Here we use fake koids for the process and thread because those aren't necessary for the test.
+  data->thread = std::make_unique<ZirconThreadHandle>(std::make_shared<ArchProviderImpl>(), 1, 2,
+                                                      std::move(handle));
 
   // Put another function on the stack.
   ThreadFunc2(data);
@@ -59,14 +66,14 @@
 }
 
 // Synchronously suspends the thread. Returns a valid suspend token on success.
-zx::suspend_token SyncSuspendThread(zx::thread& thread) {
-  zx::suspend_token token;
-  zx_status_t status = thread.suspend(&token);
-  EXPECT_EQ(ZX_OK, status);
+zx::suspend_token SyncSuspendThread(ThreadHandle& thread) {
+  // TODO(brettw) add a synchronous suspend to the ThreadHandle API.
+  zx::suspend_token token = thread.Suspend();
 
   // Need long timeout when running on shared bots on QEMU.
   zx_signals_t observed = 0;
-  status = thread.wait_one(ZX_THREAD_SUSPENDED, zx::deadline_after(zx::sec(10)), &observed);
+  zx_status_t status = thread.GetNativeHandle().wait_one(
+      ZX_THREAD_SUSPENDED, zx::deadline_after(zx::sec(10)), &observed);
   EXPECT_TRUE(observed & ZX_THREAD_SUSPENDED);
   if (status != ZX_OK)
     return zx::suspend_token();
@@ -77,6 +84,11 @@
 void DoUnwindTest() {
   ArchProviderImpl arch_provider;
 
+  zx::process handle;
+  zx::process::self()->duplicate(ZX_RIGHT_SAME_RIGHTS, &handle);
+  // This uses a fake KOID since we don't need it for this test.
+  ZirconProcessHandle process(1, std::move(handle));
+
   ThreadData data;
   std::thread background(ThreadFunc1, &data);
 
@@ -88,23 +100,21 @@
       data.thread_ready_cv.wait(lock, [&data]() { return data.thread_ready; });
 
     // Thread query functions require it to be suspended.
-    zx::suspend_token suspend = SyncSuspendThread(data.thread);
+    zx::suspend_token suspend = SyncSuspendThread(*data.thread);
 
     // Get the registers for the unwinder.
-    zx_thread_state_general_regs regs;
-    zx_status_t status = data.thread.read_state(ZX_THREAD_STATE_GENERAL_REGS, &regs, sizeof(regs));
-    ASSERT_EQ(ZX_OK, status);
+    std::optional<GeneralRegisters> regs = data.thread->GetGeneralRegisters();
+    ASSERT_TRUE(regs);
 
     // The debug addr is necessary to find the unwind information.
     uintptr_t debug_addr = 0;
-    status = zx::process::self()->get_property(ZX_PROP_PROCESS_DEBUG_ADDR, &debug_addr,
-                                               sizeof(debug_addr));
+    zx_status_t status = zx::process::self()->get_property(ZX_PROP_PROCESS_DEBUG_ADDR, &debug_addr,
+                                                           sizeof(debug_addr));
     ASSERT_EQ(ZX_OK, status);
     ASSERT_NE(0u, debug_addr);
 
     // Do the unwinding.
-    status = UnwindStack(&arch_provider, *zx::process::self(), debug_addr, data.thread, regs, 16,
-                         &stack);
+    status = UnwindStack(&arch_provider, process, debug_addr, *data.thread, *regs, 16, &stack);
     ASSERT_EQ(ZX_OK, status);
 
     data.backtrace_done = true;
diff --git a/src/developer/debug/debug_agent/zircon_process_handle.cc b/src/developer/debug/debug_agent/zircon_process_handle.cc
index 497bb79..8d8589e 100644
--- a/src/developer/debug/debug_agent/zircon_process_handle.cc
+++ b/src/developer/debug/debug_agent/zircon_process_handle.cc
@@ -4,7 +4,7 @@
 
 #include "src/developer/debug/debug_agent/zircon_process_handle.h"
 
-#include "src/developer/debug/debug_agent/process_info.h"
+#include "src/developer/debug/debug_agent/elf_utils.h"
 #include "src/developer/debug/ipc/records.h"
 
 namespace debug_agent {
@@ -46,6 +46,10 @@
   return regions;
 }
 
+std::vector<debug_ipc::Module> ZirconProcessHandle::GetModules(uint64_t dl_debug_addr) const {
+  return GetElfModulesForProcess(*this, dl_debug_addr);
+}
+
 zx_status_t ZirconProcessHandle::ReadMemory(uintptr_t address, void* buffer, size_t len,
                                             size_t* actual) const {
   return process_.read_memory(address, buffer, len, actual);
diff --git a/src/developer/debug/debug_agent/zircon_process_handle.h b/src/developer/debug/debug_agent/zircon_process_handle.h
index b88634e..3c600fb 100644
--- a/src/developer/debug/debug_agent/zircon_process_handle.h
+++ b/src/developer/debug/debug_agent/zircon_process_handle.h
@@ -19,6 +19,7 @@
   zx_koid_t GetKoid() const override { return process_koid_; }
   zx_status_t GetInfo(zx_info_process* info) const override;
   std::vector<debug_ipc::AddressRegion> GetAddressSpace(uint64_t address) const override;
+  std::vector<debug_ipc::Module> GetModules(uint64_t dl_debug_addr) const override;
   zx_status_t ReadMemory(uintptr_t address, void* buffer, size_t len,
                          size_t* actual) const override;
   zx_status_t WriteMemory(uintptr_t address, const void* buffer, size_t len,
diff --git a/src/developer/debug/debug_agent/zircon_thread_handle.cc b/src/developer/debug/debug_agent/zircon_thread_handle.cc
index 041a1e8..e0ff30d 100644
--- a/src/developer/debug/debug_agent/zircon_thread_handle.cc
+++ b/src/developer/debug/debug_agent/zircon_thread_handle.cc
@@ -6,7 +6,6 @@
 
 #include <map>
 
-#include "src/developer/debug/debug_agent/process_info.h"
 #include "src/developer/debug/shared/logging/logging.h"
 #include "src/developer/debug/shared/zx_status.h"
 
@@ -18,6 +17,62 @@
 
 namespace debug_agent {
 
+namespace {
+
+debug_ipc::ThreadRecord::BlockedReason ThreadStateBlockedReasonToEnum(uint32_t state) {
+  FX_DCHECK(ZX_THREAD_STATE_BASIC(state) == ZX_THREAD_STATE_BLOCKED);
+
+  switch (state) {
+    case ZX_THREAD_STATE_BLOCKED_EXCEPTION:
+      return debug_ipc::ThreadRecord::BlockedReason::kException;
+    case ZX_THREAD_STATE_BLOCKED_SLEEPING:
+      return debug_ipc::ThreadRecord::BlockedReason::kSleeping;
+    case ZX_THREAD_STATE_BLOCKED_FUTEX:
+      return debug_ipc::ThreadRecord::BlockedReason::kFutex;
+    case ZX_THREAD_STATE_BLOCKED_PORT:
+      return debug_ipc::ThreadRecord::BlockedReason::kPort;
+    case ZX_THREAD_STATE_BLOCKED_CHANNEL:
+      return debug_ipc::ThreadRecord::BlockedReason::kChannel;
+    case ZX_THREAD_STATE_BLOCKED_WAIT_ONE:
+      return debug_ipc::ThreadRecord::BlockedReason::kWaitOne;
+    case ZX_THREAD_STATE_BLOCKED_WAIT_MANY:
+      return debug_ipc::ThreadRecord::BlockedReason::kWaitMany;
+    case ZX_THREAD_STATE_BLOCKED_INTERRUPT:
+      return debug_ipc::ThreadRecord::BlockedReason::kInterrupt;
+    case ZX_THREAD_STATE_BLOCKED_PAGER:
+      return debug_ipc::ThreadRecord::BlockedReason::kPager;
+    default:
+      FX_NOTREACHED();
+      return debug_ipc::ThreadRecord::BlockedReason::kNotBlocked;
+  }
+}
+
+ThreadHandle::State ThreadStateToEnums(uint32_t input) {
+  struct Mapping {
+    uint32_t int_state;
+    debug_ipc::ThreadRecord::State enum_state;
+  };
+  static const Mapping mappings[] = {
+      {ZX_THREAD_STATE_NEW, debug_ipc::ThreadRecord::State::kNew},
+      {ZX_THREAD_STATE_RUNNING, debug_ipc::ThreadRecord::State::kRunning},
+      {ZX_THREAD_STATE_SUSPENDED, debug_ipc::ThreadRecord::State::kSuspended},
+      {ZX_THREAD_STATE_BLOCKED, debug_ipc::ThreadRecord::State::kBlocked},
+      {ZX_THREAD_STATE_DYING, debug_ipc::ThreadRecord::State::kDying},
+      {ZX_THREAD_STATE_DEAD, debug_ipc::ThreadRecord::State::kDead}};
+
+  const uint32_t basic_state = ZX_THREAD_STATE_BASIC(input);
+  for (const Mapping& mapping : mappings) {
+    if (mapping.int_state == basic_state) {
+      if (mapping.enum_state == debug_ipc::ThreadRecord::State::kBlocked)
+        return ThreadHandle::State(mapping.enum_state, ThreadStateBlockedReasonToEnum(input));
+      return ThreadHandle::State(mapping.enum_state);
+    }
+  }
+  return ThreadHandle::State(debug_ipc::ThreadRecord::State::kDead);
+}
+
+}  // namespace
+
 ZirconThreadHandle::ZirconThreadHandle(std::shared_ptr<arch::ArchProvider> arch_provider,
                                        zx_koid_t process_koid, zx_koid_t thread_koid, zx::thread t)
     : arch_provider_(std::move(arch_provider)),
@@ -25,11 +80,11 @@
       thread_koid_(thread_koid),
       thread_(std::move(t)) {}
 
-uint32_t ZirconThreadHandle::GetState() const {
+ThreadHandle::State ZirconThreadHandle::GetState() const {
   zx_info_thread info;
   if (thread_.get_info(ZX_INFO_THREAD, &info, sizeof(info), nullptr, nullptr) == ZX_OK)
-    return info.state;
-  return ZX_THREAD_STATE_DEAD;  // Assume failures mean the thread is dead.
+    return ThreadStateToEnums(info.state);
+  return State(debug_ipc::ThreadRecord::State::kDead);  // Assume failures mean the thread is dead.
 }
 
 zx::suspend_token ZirconThreadHandle::Suspend() {
@@ -49,11 +104,25 @@
     record.name = name;
 
   // State (running, blocked, etc.).
-  record.state = ThreadStateToEnums(GetState(), &record.blocked_reason);
+  auto state = GetState();
+  record.state = state.state;
+  record.blocked_reason = state.blocked_reason;
 
   return record;
 }
 
+std::optional<GeneralRegisters> ZirconThreadHandle::GetGeneralRegisters() const {
+  zx_thread_state_general_regs r;
+  if (thread_.read_state(ZX_THREAD_STATE_GENERAL_REGS, &r, sizeof(r)) == ZX_OK)
+    return GeneralRegisters(r);
+  return std::nullopt;
+}
+
+void ZirconThreadHandle::SetGeneralRegisters(const GeneralRegisters& regs) {
+  thread_.write_state(ZX_THREAD_STATE_GENERAL_REGS, &regs.GetNativeRegisters(),
+                      sizeof(zx_thread_state_general_regs));
+}
+
 std::vector<debug_ipc::Register> ZirconThreadHandle::ReadRegisters(
     const std::vector<debug_ipc::RegisterCategory>& cats_to_get) const {
   std::vector<debug_ipc::Register> regs;
diff --git a/src/developer/debug/debug_agent/zircon_thread_handle.h b/src/developer/debug/debug_agent/zircon_thread_handle.h
index 30427f6..4400eeb 100644
--- a/src/developer/debug/debug_agent/zircon_thread_handle.h
+++ b/src/developer/debug/debug_agent/zircon_thread_handle.h
@@ -7,6 +7,8 @@
 
 #include <lib/zx/thread.h>
 
+#include <optional>
+
 #include "src/developer/debug/debug_agent/arch.h"
 #include "src/developer/debug/debug_agent/thread_handle.h"
 
@@ -23,9 +25,11 @@
   const zx::thread& GetNativeHandle() const override { return thread_; }
   zx::thread& GetNativeHandle() override { return thread_; }
   zx_koid_t GetKoid() const override { return thread_koid_; }
-  uint32_t GetState() const override;
+  State GetState() const override;
   debug_ipc::ThreadRecord GetThreadRecord() const override;
   zx::suspend_token Suspend() override;
+  std::optional<GeneralRegisters> GetGeneralRegisters() const override;
+  void SetGeneralRegisters(const GeneralRegisters& regs) override;
   std::vector<debug_ipc::Register> ReadRegisters(
       const std::vector<debug_ipc::RegisterCategory>& cats_to_get) const override;
   std::vector<debug_ipc::Register> WriteRegisters(