Commit e8c7ab52 authored by Siddhartha's avatar Siddhartha Committed by Commit Bot

Implement stack unwinder for sampling on Android

The CFI unwinder only works for chrome stack frames. Write a hybrid
unwinder which can use libunwind for android and CFI unwinder for
chrome. Does not support unwinding in x86 and arm 64bit builds.
It supports pausing a different thread and unwinding frames on that
thread.

This will be useful for UMA sampling profiler and tracing profiler.

BUG=859260

Change-Id: I208b9b21c8f6a73a4d593f1446095da92c0cb848
Reviewed-on: https://chromium-review.googlesource.com/1132144
Commit-Queue: Siddhartha S <ssid@chromium.org>
Reviewed-by: default avatarSylvain Defresne <sdefresne@chromium.org>
Reviewed-by: default avatarMike Wittman <wittman@chromium.org>
Reviewed-by: default avatarDavid Turner <digit@chromium.org>
Cr-Commit-Position: refs/heads/master@{#579598}
parent fad4aa5a
...@@ -57,7 +57,15 @@ breakpad symbol files. ...@@ -57,7 +57,15 @@ breakpad symbol files.
*/ */
extern "C" { extern "C" {
// The address of |__executable_start| gives the start address of the
// executable or shared library. This value is used to find the offset address
// of the instruction in binary from PC.
extern char __executable_start; extern char __executable_start;
// The address |_etext| gives the end of the .text section in the binary. This
// value is more accurate than parsing the memory map since the mapped
// regions are usualy larger than the .text section.
extern char _etext; extern char _etext;
} }
...@@ -119,6 +127,16 @@ CFIBacktraceAndroid* CFIBacktraceAndroid::GetInitializedInstance() { ...@@ -119,6 +127,16 @@ CFIBacktraceAndroid* CFIBacktraceAndroid::GetInitializedInstance() {
return instance; return instance;
} }
// static
uintptr_t CFIBacktraceAndroid::executable_start_addr() {
return reinterpret_cast<uintptr_t>(&__executable_start);
}
// static
uintptr_t CFIBacktraceAndroid::executable_end_addr() {
return reinterpret_cast<uintptr_t>(&_etext);
}
CFIBacktraceAndroid::CFIBacktraceAndroid() CFIBacktraceAndroid::CFIBacktraceAndroid()
: thread_local_cfi_cache_( : thread_local_cfi_cache_(
[](void* ptr) { delete static_cast<CFICache*>(ptr); }) { [](void* ptr) { delete static_cast<CFICache*>(ptr); }) {
...@@ -128,15 +146,6 @@ CFIBacktraceAndroid::CFIBacktraceAndroid() ...@@ -128,15 +146,6 @@ CFIBacktraceAndroid::CFIBacktraceAndroid()
CFIBacktraceAndroid::~CFIBacktraceAndroid() {} CFIBacktraceAndroid::~CFIBacktraceAndroid() {}
void CFIBacktraceAndroid::Initialize() { void CFIBacktraceAndroid::Initialize() {
// The address |_etext| gives the end of the .text section in the binary. This
// value is more accurate than parsing the memory map since the mapped
// regions are usualy larger than the .text section.
executable_end_addr_ = reinterpret_cast<uintptr_t>(&_etext);
// The address of |__executable_start| gives the start address of the
// executable. This value is used to find the offset address of the
// instruction in binary from PC.
executable_start_addr_ = reinterpret_cast<uintptr_t>(&__executable_start);
// This file name is defined by extract_unwind_tables.gni. // This file name is defined by extract_unwind_tables.gni.
static constexpr char kCfiFileName[] = "assets/unwind_cfi_32"; static constexpr char kCfiFileName[] = "assets/unwind_cfi_32";
MemoryMappedFile::Region cfi_region; MemoryMappedFile::Region cfi_region;
...@@ -188,13 +197,22 @@ size_t CFIBacktraceAndroid::Unwind(const void** out_trace, size_t max_depth) { ...@@ -188,13 +197,22 @@ size_t CFIBacktraceAndroid::Unwind(const void** out_trace, size_t max_depth) {
asm volatile("mov %0, pc" : "=r"(pc)); asm volatile("mov %0, pc" : "=r"(pc));
asm volatile("mov %0, sp" : "=r"(sp)); asm volatile("mov %0, sp" : "=r"(sp));
return Unwind(pc, sp, out_trace, max_depth);
}
size_t CFIBacktraceAndroid::Unwind(uintptr_t pc,
uintptr_t sp,
const void** out_trace,
size_t max_depth) {
if (!can_unwind_stack_frames())
return 0;
// We can only unwind as long as the pc is within the chrome.so. // We can only unwind as long as the pc is within the chrome.so.
size_t depth = 0; size_t depth = 0;
while (pc > executable_start_addr_ && pc <= executable_end_addr_ && while (is_chrome_address(pc) && depth < max_depth) {
depth < max_depth) {
out_trace[depth++] = reinterpret_cast<void*>(pc); out_trace[depth++] = reinterpret_cast<void*>(pc);
// The offset of function from the start of the chrome.so binary: // The offset of function from the start of the chrome.so binary:
uintptr_t func_addr = pc - executable_start_addr_; uintptr_t func_addr = pc - executable_start_addr();
CFIRow cfi{}; CFIRow cfi{};
if (!FindCFIRowForPC(func_addr, &cfi)) if (!FindCFIRowForPC(func_addr, &cfi))
break; break;
...@@ -209,8 +227,15 @@ size_t CFIBacktraceAndroid::Unwind(const void** out_trace, size_t max_depth) { ...@@ -209,8 +227,15 @@ size_t CFIBacktraceAndroid::Unwind(const void** out_trace, size_t max_depth) {
return depth; return depth;
} }
void CFIBacktraceAndroid::AllocateCacheForCurrentThread() {
GetThreadLocalCFICache();
}
bool CFIBacktraceAndroid::FindCFIRowForPC(uintptr_t func_addr, bool CFIBacktraceAndroid::FindCFIRowForPC(uintptr_t func_addr,
CFIBacktraceAndroid::CFIRow* cfi) { CFIBacktraceAndroid::CFIRow* cfi) {
if (!can_unwind_stack_frames())
return false;
auto* cache = GetThreadLocalCFICache(); auto* cache = GetThreadLocalCFICache();
*cfi = {0}; *cfi = {0};
if (cache->Find(func_addr, cfi)) if (cache->Find(func_addr, cfi))
......
...@@ -35,6 +35,15 @@ class BASE_EXPORT CFIBacktraceAndroid { ...@@ -35,6 +35,15 @@ class BASE_EXPORT CFIBacktraceAndroid {
// on first call. // on first call.
static CFIBacktraceAndroid* GetInitializedInstance(); static CFIBacktraceAndroid* GetInitializedInstance();
// Returns true if the given program counter |pc| is mapped in chrome library.
static bool is_chrome_address(uintptr_t pc) {
return pc >= executable_start_addr() && pc < executable_end_addr();
}
// Returns the start and end address of the current library.
static uintptr_t executable_start_addr();
static uintptr_t executable_end_addr();
// Returns true if stack unwinding is possible using CFI unwind tables in apk. // Returns true if stack unwinding is possible using CFI unwind tables in apk.
// There is no need to check this before each unwind call. Will always return // There is no need to check this before each unwind call. Will always return
// the same value based on CFI tables being present in the binary. // the same value based on CFI tables being present in the binary.
...@@ -43,18 +52,28 @@ class BASE_EXPORT CFIBacktraceAndroid { ...@@ -43,18 +52,28 @@ class BASE_EXPORT CFIBacktraceAndroid {
// Returns the program counters by unwinding stack in the current thread in // Returns the program counters by unwinding stack in the current thread in
// order of latest call frame first. Unwinding works only if // order of latest call frame first. Unwinding works only if
// can_unwind_stack_frames() returns true. This function allocates memory from // can_unwind_stack_frames() returns true. This function allocates memory from
// heap for caches. For each stack frame, this method searches through the // heap for cache on the first call of the calling thread, unless
// unwind table mapped in memory to find the unwind information for function // AllocateCacheForCurrentThread() is called from the thread. For each stack
// and walks the stack to find all the return address. This only works until // frame, this method searches through the unwind table mapped in memory to
// the last function call from the chrome.so. We do not have unwind // find the unwind information for function and walks the stack to find all
// information to unwind beyond any frame outside of chrome.so. Calls to // the return address. This only works until the last function call from the
// Unwind() are thread safe and lock free, once Initialize() returns success. // chrome.so. We do not have unwind information to unwind beyond any frame
// outside of chrome.so. Calls to Unwind() are thread safe and lock free, once
// Initialize() returns success.
size_t Unwind(const void** out_trace, size_t max_depth); size_t Unwind(const void** out_trace, size_t max_depth);
private: // Same as above function, but starts from a given program counter |pc| and
FRIEND_TEST_ALL_PREFIXES(CFIBacktraceAndroidTest, TestCFICache); // stack pointer |sp|. This can be from current thread or any other thread.
FRIEND_TEST_ALL_PREFIXES(CFIBacktraceAndroidTest, TestFindCFIRow); // But the caller must make sure that the thread's stack segment is not racy
FRIEND_TEST_ALL_PREFIXES(CFIBacktraceAndroidTest, TestUnwinding); // to read.
size_t Unwind(uintptr_t pc,
uintptr_t sp,
const void** out_trace,
size_t max_depth);
// Allocates memory for CFI cache for the current thread so that Unwind()
// calls are safe for signal handlers.
void AllocateCacheForCurrentThread();
// The CFI information that correspond to an instruction. // The CFI information that correspond to an instruction.
struct CFIRow { struct CFIRow {
...@@ -71,6 +90,15 @@ class BASE_EXPORT CFIBacktraceAndroid { ...@@ -71,6 +90,15 @@ class BASE_EXPORT CFIBacktraceAndroid {
uint16_t ra_offset = 0; uint16_t ra_offset = 0;
}; };
// Finds the CFI row for the given |func_addr| in terms of offset from
// the start of the current binary. Concurrent calls are thread safe.
bool FindCFIRowForPC(uintptr_t func_addr, CFIRow* out);
private:
FRIEND_TEST_ALL_PREFIXES(CFIBacktraceAndroidTest, TestCFICache);
FRIEND_TEST_ALL_PREFIXES(CFIBacktraceAndroidTest, TestFindCFIRow);
FRIEND_TEST_ALL_PREFIXES(CFIBacktraceAndroidTest, TestUnwinding);
// A simple cache that stores entries in table using prime modulo hashing. // A simple cache that stores entries in table using prime modulo hashing.
// This cache with 500 entries already gives us 95% hit rate, and fits in a // This cache with 500 entries already gives us 95% hit rate, and fits in a
// single system page (usually 4KiB). Using a thread local cache for each // single system page (usually 4KiB). Using a thread local cache for each
...@@ -118,17 +146,8 @@ class BASE_EXPORT CFIBacktraceAndroid { ...@@ -118,17 +146,8 @@ class BASE_EXPORT CFIBacktraceAndroid {
// Finds the UNW_INDEX and UNW_DATA tables in from the CFI file memory map. // Finds the UNW_INDEX and UNW_DATA tables in from the CFI file memory map.
void ParseCFITables(); void ParseCFITables();
// Finds the CFI row for the given |func_addr| in terms of offset from
// the start of the current binary.
bool FindCFIRowForPC(uintptr_t func_addr, CFIRow* out);
CFICache* GetThreadLocalCFICache(); CFICache* GetThreadLocalCFICache();
// Details about the memory mapped region which contains the libchrome.so
// library file.
uintptr_t executable_start_addr_ = 0;
uintptr_t executable_end_addr_ = 0;
// The start address of the memory mapped unwind table asset file. Unique ptr // The start address of the memory mapped unwind table asset file. Unique ptr
// because it is replaced in tests. // because it is replaced in tests.
std::unique_ptr<MemoryMappedFile> cfi_mmap_; std::unique_ptr<MemoryMappedFile> cfi_mmap_;
......
...@@ -21,8 +21,8 @@ void* GetPC() { ...@@ -21,8 +21,8 @@ void* GetPC() {
TEST(CFIBacktraceAndroidTest, TestUnwinding) { TEST(CFIBacktraceAndroidTest, TestUnwinding) {
auto* unwinder = CFIBacktraceAndroid::GetInitializedInstance(); auto* unwinder = CFIBacktraceAndroid::GetInitializedInstance();
EXPECT_TRUE(unwinder->can_unwind_stack_frames()); EXPECT_TRUE(unwinder->can_unwind_stack_frames());
EXPECT_GT(unwinder->executable_start_addr_, 0u); EXPECT_GT(unwinder->executable_start_addr(), 0u);
EXPECT_GT(unwinder->executable_end_addr_, unwinder->executable_start_addr_); EXPECT_GT(unwinder->executable_end_addr(), unwinder->executable_start_addr());
EXPECT_GT(unwinder->cfi_mmap_->length(), 0u); EXPECT_GT(unwinder->cfi_mmap_->length(), 0u);
const size_t kMaxFrames = 100; const size_t kMaxFrames = 100;
...@@ -39,9 +39,9 @@ TEST(CFIBacktraceAndroidTest, TestUnwinding) { ...@@ -39,9 +39,9 @@ TEST(CFIBacktraceAndroidTest, TestUnwinding) {
for (size_t i = 0; i < unwind_count; ++i) { for (size_t i = 0; i < unwind_count; ++i) {
EXPECT_GT(reinterpret_cast<uintptr_t>(frames[i]), EXPECT_GT(reinterpret_cast<uintptr_t>(frames[i]),
unwinder->executable_start_addr_); unwinder->executable_start_addr());
EXPECT_LT(reinterpret_cast<uintptr_t>(frames[i]), EXPECT_LT(reinterpret_cast<uintptr_t>(frames[i]),
unwinder->executable_end_addr_); unwinder->executable_end_addr());
} }
} }
......
...@@ -15,6 +15,7 @@ import("//tools/grit/repack.gni") ...@@ -15,6 +15,7 @@ import("//tools/grit/repack.gni")
if (is_android) { if (is_android) {
import("//build/config/android/rules.gni") import("//build/config/android/rules.gni")
import("//build/config/compiler/compiler.gni")
import("//tools/v8_context_snapshot/v8_context_snapshot.gni") import("//tools/v8_context_snapshot/v8_context_snapshot.gni")
} }
...@@ -55,6 +56,12 @@ test("components_unittests") { ...@@ -55,6 +56,12 @@ test("components_unittests") {
if (is_android) { if (is_android) {
enable_multidex = true enable_multidex = true
# The tracing unittests require this for testing unwinding. See
# stack_unwinder_android_unittest.cc.
if (can_unwind_with_cfi_table && is_official_build) {
add_unwind_tables_in_apk = true
}
} }
# Add only ":unit_tests" dependencies here. If your tests have dependencies # Add only ":unit_tests" dependencies here. If your tests have dependencies
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
# found in the LICENSE file. # found in the LICENSE file.
import("//testing/test.gni") import("//testing/test.gni")
import("//build/config/compiler/compiler.gni")
config("wrap_find_exidx") {
ldflags = [ "-Wl,-wrap,dl_unwind_find_exidx" ]
}
component("tracing") { component("tracing") {
sources = [ sources = [
...@@ -21,6 +26,18 @@ component("tracing") { ...@@ -21,6 +26,18 @@ component("tracing") {
"//base", "//base",
"//ipc", "//ipc",
] ]
if (is_android && can_unwind_with_cfi_table && is_official_build) {
sources += [
"common/stack_unwinder_android.cc",
"common/stack_unwinder_android.h",
]
deps += [ "//buildtools/third_party/libunwind" ]
include_dirs = [ "//buildtools/third_party/libunwind/trunk/include" ]
# stack_unwinder_android.cc overrides the dl_unwind_find_exidx function.
all_dependent_configs = [ ":wrap_find_exidx" ]
}
} }
component("startup_tracing") { component("startup_tracing") {
...@@ -63,6 +80,10 @@ source_set("unit_tests") { ...@@ -63,6 +80,10 @@ source_set("unit_tests") {
sources += [ "common/trace_startup_config_unittest.cc" ] sources += [ "common/trace_startup_config_unittest.cc" ]
deps += [ ":startup_tracing" ] deps += [ ":startup_tracing" ]
} }
if (is_android && can_unwind_with_cfi_table && is_official_build) {
sources += [ "common/stack_unwinder_android_unittest.cc" ]
}
} }
test("tracing_perftests") { test("tracing_perftests") {
......
This diff is collapsed.
// Copyright 2018 The Chromium 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 COMPONENTS_TRACING_COMMON_STACK_UNWINDER_ANDROID_H_
#define COMPONENTS_TRACING_COMMON_STACK_UNWINDER_ANDROID_H_
#include <map>
#include "base/debug/proc_maps_linux.h"
#include "base/threading/platform_thread.h"
#include "components/tracing/tracing_export.h"
namespace tracing {
// Utility to unwind stacks for current thread on ARM devices. Contains ability
// to unwind stacks based on EHABI section in Android libraries and using the
// custom stack unwind information in Chrome. This works on top of
// base::trace_event::CFIBacktraceAndroid, which unwinds Chrome only stacks.
class TRACING_EXPORT StackUnwinderAndroid {
public:
static StackUnwinderAndroid* GetInstance();
// Intializes the unwinder for current process. It finds all loaded libraries
// in current process and also initializes CFIBacktraceAndroid, with file IO.
void Initialize();
// Unwinds stack frames for current thread and stores the program counters in
// |out_trace|, and returns the number of frames stored.
size_t TraceStack(const void** out_trace, size_t max_depth);
// Same as above function, but pauses the thread with the given |tid| and then
// unwinds. |tid| should not be current thread's.
size_t TraceStack(base::PlatformThreadId tid,
const void** out_trace,
size_t max_depth);
// Returns the end address of the memory map with given |addr|.
uintptr_t GetEndAddressOfRegion(uintptr_t addr) const;
// Returns true if the given |pc| was part of any mapped segments in the
// process.
bool IsAddressMapped(uintptr_t pc) const;
private:
StackUnwinderAndroid();
~StackUnwinderAndroid();
bool is_initialized_ = false;
// Stores all the memory mapped regions in the current process, including all
// the files mapped and anonymous regions. This data could be stale, but the
// error caused by changes in library loads would be missing stackframes and
// is acceptable.
std::vector<base::debug::MappedMemoryRegion> regions_;
};
} // namespace tracing
#endif // COMPONENTS_TRACING_COMMON_STACK_UNWINDER_ANDROID_H_
// Copyright 2018 The Chromium 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 "components/tracing/common/stack_unwinder_android.h"
#include "base/synchronization/waitable_event.h"
#include "base/task_scheduler/post_task.h"
#include "base/test/scoped_task_environment.h"
#include "base/trace_event/cfi_backtrace_android.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace tracing {
namespace {
const size_t kMaxStackFrames = 40;
class StackUnwinderTest : public testing::Test {
public:
StackUnwinderTest() : testing::Test() {}
~StackUnwinderTest() override {}
void SetUp() override {
StackUnwinderAndroid::GetInstance()->Initialize();
base::trace_event::CFIBacktraceAndroid::GetInitializedInstance()
->AllocateCacheForCurrentThread();
}
private:
DISALLOW_COPY_AND_ASSIGN(StackUnwinderTest);
base::test::ScopedTaskEnvironment scoped_task_environment_;
};
uintptr_t GetCurrentPC() {
return reinterpret_cast<uintptr_t>(__builtin_return_address(0));
}
} // namespace
TEST_F(StackUnwinderTest, UnwindCurrentThread) {
const void* frames[kMaxStackFrames];
size_t result =
StackUnwinderAndroid::GetInstance()->TraceStack(frames, kMaxStackFrames);
EXPECT_GT(result, 0u);
// Since we are starting from chrome library function (this), all the unwind
// frames will be chrome frames.
for (size_t i = 0; i < result; ++i) {
EXPECT_TRUE(
base::trace_event::CFIBacktraceAndroid::GetInitializedInstance()
->is_chrome_address(reinterpret_cast<uintptr_t>(frames[i])));
}
}
TEST_F(StackUnwinderTest, UnwindOtherThread) {
base::WaitableEvent unwind_finished_event;
auto task_runner = base::CreateSingleThreadTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
auto callback = [](base::PlatformThreadId tid,
base::WaitableEvent* unwind_finished_event,
uintptr_t test_pc) {
const void* frames[kMaxStackFrames];
size_t result = StackUnwinderAndroid::GetInstance()->TraceStack(
tid, frames, kMaxStackFrames);
EXPECT_GT(result, 0u);
for (size_t i = 0; i < result; ++i) {
uintptr_t addr = reinterpret_cast<uintptr_t>(frames[i]);
EXPECT_TRUE(StackUnwinderAndroid::GetInstance()->IsAddressMapped(addr));
}
unwind_finished_event->Signal();
};
// Post task on background thread to unwind the current thread.
task_runner->PostTask(
FROM_HERE, base::BindOnce(callback, base::PlatformThread::CurrentId(),
&unwind_finished_event, GetCurrentPC()));
// While the background thread is trying to unwind make some slow framework
// calls (malloc) so that the current thread can be stopped in framework
// library functions on stack.
// TODO(ssid): Test for reliable unwinding through non-chrome and chrome
// frames.
while (true) {
std::vector<int> temp;
temp.reserve(kMaxStackFrames);
usleep(100);
if (unwind_finished_event.IsSignaled())
break;
}
}
} // namespace tracing
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment