336 lines
11 KiB
C++
336 lines
11 KiB
C++
// Copyright 2022 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "base/profiler/frame_pointer_unwinder.h"
|
|
|
|
#include <memory>
|
|
|
|
#include "base/profiler/module_cache.h"
|
|
#include "base/profiler/stack_sampling_profiler_test_util.h"
|
|
#include "base/profiler/unwinder.h"
|
|
#include "build/buildflag.h"
|
|
#include "testing/gtest/include/gtest/gtest.h"
|
|
|
|
#if BUILDFLAG(IS_APPLE)
|
|
#include "base/mac/mac_util.h"
|
|
#endif
|
|
|
|
namespace base {
|
|
|
|
namespace {
|
|
|
|
constexpr uintptr_t kModuleStart = 0x1000;
|
|
constexpr size_t kModuleSize = 0x1000;
|
|
constexpr uintptr_t kNonNativeModuleStart = 0x4000;
|
|
|
|
// Used to construct test stacks. If `relative` is true, the value should be the
|
|
// address `offset` positions from the bottom of the stack (at 8-byte alignment)
|
|
// Otherwise, `offset` is added to the stack as an absolute address/value.
|
|
// For example, when creating a stack with bottom 0x2000, {false, 0xf00d} will
|
|
// become 0xf00d, and {true, 0x3} will become 0x2018.
|
|
struct StackEntrySpec {
|
|
bool relative;
|
|
uintptr_t offset;
|
|
};
|
|
|
|
// Enables constructing a stack buffer that has pointers to itself
|
|
// and provides convenience methods for calling the unwinder.
|
|
struct InputStack {
|
|
explicit InputStack(const std::vector<StackEntrySpec>& offsets)
|
|
: buffer(offsets.size()) {
|
|
size_t size = offsets.size();
|
|
for (size_t i = 0; i < size; ++i) {
|
|
auto spec = offsets[i];
|
|
if (spec.relative) {
|
|
buffer[i] = bottom() + (spec.offset * sizeof(uintptr_t));
|
|
} else {
|
|
buffer[i] = spec.offset;
|
|
}
|
|
}
|
|
}
|
|
uintptr_t bottom() const {
|
|
return reinterpret_cast<uintptr_t>(buffer.data());
|
|
}
|
|
uintptr_t top() const { return bottom() + buffer.size() * sizeof(uintptr_t); }
|
|
|
|
private:
|
|
std::vector<uintptr_t> buffer;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
class FramePointerUnwinderTest : public testing::Test {
|
|
protected:
|
|
FramePointerUnwinderTest() {
|
|
#if BUILDFLAG(IS_APPLE)
|
|
if (__builtin_available(iOS 12, *)) {
|
|
#else
|
|
{
|
|
#endif
|
|
unwinder_ = std::make_unique<FramePointerUnwinder>();
|
|
|
|
auto test_module =
|
|
std::make_unique<TestModule>(kModuleStart, kModuleSize);
|
|
module_ = test_module.get();
|
|
module_cache_.AddCustomNativeModule(std::move(test_module));
|
|
auto non_native_module = std::make_unique<TestModule>(
|
|
kNonNativeModuleStart, kModuleSize, false);
|
|
non_native_module_ = non_native_module.get();
|
|
std::vector<std::unique_ptr<const ModuleCache::Module>> wrapper;
|
|
wrapper.push_back(std::move(non_native_module));
|
|
module_cache()->UpdateNonNativeModules({}, std::move(wrapper));
|
|
|
|
unwinder_->Initialize(&module_cache_);
|
|
}
|
|
}
|
|
|
|
ModuleCache* module_cache() { return &module_cache_; }
|
|
ModuleCache::Module* module() { return module_; }
|
|
ModuleCache::Module* non_native_module() { return non_native_module_; }
|
|
Unwinder* unwinder() { return unwinder_.get(); }
|
|
|
|
private:
|
|
std::unique_ptr<Unwinder> unwinder_;
|
|
base::ModuleCache module_cache_;
|
|
raw_ptr<ModuleCache::Module> module_;
|
|
raw_ptr<ModuleCache::Module> non_native_module_;
|
|
};
|
|
|
|
TEST_F(FramePointerUnwinderTest, FPPointsOutsideOfStack) {
|
|
InputStack input({
|
|
{false, 0x1000},
|
|
{false, 0x1000},
|
|
{false, 0x1000},
|
|
{false, 0x1000},
|
|
{false, 0x1000},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = 0x1;
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kAborted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({{kModuleStart, module()}}), stack);
|
|
|
|
RegisterContextFramePointer(&context) = input.bottom() - sizeof(uintptr_t);
|
|
EXPECT_EQ(UnwindResult::kAborted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({{kModuleStart, module()}}), stack);
|
|
|
|
RegisterContextFramePointer(&context) = input.top();
|
|
EXPECT_EQ(UnwindResult::kAborted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({{kModuleStart, module()}}), stack);
|
|
}
|
|
|
|
TEST_F(FramePointerUnwinderTest, FPPointsToSelf) {
|
|
InputStack input({
|
|
{true, 0},
|
|
{false, kModuleStart + 0x10},
|
|
{true, 4},
|
|
{false, kModuleStart + 0x20},
|
|
{false, 0},
|
|
{false, 0},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = input.bottom();
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kAborted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({
|
|
{kModuleStart, module()},
|
|
}),
|
|
stack);
|
|
}
|
|
|
|
// Tests that two frame pointers that point to each other can't create an
|
|
// infinite loop
|
|
TEST_F(FramePointerUnwinderTest, FPCycle) {
|
|
InputStack input({
|
|
{true, 2},
|
|
{false, kModuleStart + 0x10},
|
|
{true, 0},
|
|
{false, kModuleStart + 0x20},
|
|
{true, 4},
|
|
{false, kModuleStart + 0x30},
|
|
{false, 0},
|
|
{false, 0},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = input.bottom();
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kAborted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({
|
|
{kModuleStart, module()},
|
|
{kModuleStart + 0x10, module()},
|
|
}),
|
|
stack);
|
|
}
|
|
|
|
TEST_F(FramePointerUnwinderTest, NoModuleForIP) {
|
|
uintptr_t not_in_module = kModuleStart - 0x10;
|
|
InputStack input({
|
|
{true, 2},
|
|
{false, not_in_module},
|
|
{true, 4},
|
|
{true, kModuleStart + 0x10},
|
|
{false, 0},
|
|
{false, 0},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = input.bottom();
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kAborted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(
|
|
std::vector<Frame>({{kModuleStart, module()}, {not_in_module, nullptr}}),
|
|
stack);
|
|
}
|
|
|
|
// Tests that testing that checking if there's space to read two values from the
|
|
// stack doesn't overflow.
|
|
TEST_F(FramePointerUnwinderTest, FPAdditionOverflows) {
|
|
uintptr_t will_overflow = std::numeric_limits<uintptr_t>::max() - 1;
|
|
InputStack input({
|
|
{true, 2},
|
|
{false, kModuleStart + 0x10},
|
|
{false, 0},
|
|
{false, 0},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = will_overflow;
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kAborted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({
|
|
{kModuleStart, module()},
|
|
}),
|
|
stack);
|
|
}
|
|
|
|
// Tests the happy path: a successful unwind with no non-native modules.
|
|
TEST_F(FramePointerUnwinderTest, RegularUnwind) {
|
|
InputStack input({
|
|
{true, 4}, // fp of frame 1
|
|
{false, kModuleStart + 0x20}, // ip of frame 1
|
|
{false, 0xaaaa},
|
|
{false, 0xaaaa},
|
|
{true, 8}, // fp of frame 2
|
|
{false, kModuleStart + 0x42}, // ip of frame 2
|
|
{false, 0xaaaa},
|
|
{false, 0xaaaa},
|
|
{false, 0},
|
|
{false, 1},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = input.bottom();
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kCompleted,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({
|
|
{kModuleStart, module()},
|
|
{kModuleStart + 0x20, module()},
|
|
{kModuleStart + 0x42, module()},
|
|
}),
|
|
stack);
|
|
}
|
|
|
|
// Tests that if a V8 frame is encountered, unwinding stops and
|
|
// kUnrecognizedFrame is returned to facilitate continuing with the V8 unwinder.
|
|
TEST_F(FramePointerUnwinderTest, NonNativeFrame) {
|
|
InputStack input({
|
|
{true, 4}, // fp of frame 1
|
|
{false, kModuleStart + 0x20}, // ip of frame 1
|
|
{false, 0xaaaa},
|
|
{false, 0xaaaa},
|
|
{true, 8}, // fp of frame 2
|
|
{false, kNonNativeModuleStart + 0x42}, // ip of frame 2
|
|
{false, 0xaaaa},
|
|
{false, 0xaaaa},
|
|
{true, 12}, // fp of frame 3
|
|
{false, kModuleStart + 0x10}, // ip of frame 3
|
|
{true, 0xaaaa},
|
|
{true, 0xaaaa},
|
|
{false, 0},
|
|
{false, 1},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = input.bottom();
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kUnrecognizedFrame,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
EXPECT_EQ(std::vector<Frame>({
|
|
{kModuleStart, module()},
|
|
{kModuleStart + 0x20, module()},
|
|
{kNonNativeModuleStart + 0x42, non_native_module()},
|
|
}),
|
|
stack);
|
|
}
|
|
|
|
// Tests that a V8 frame with an unaligned frame pointer correctly returns
|
|
// kUnrecognizedFrame and not kAborted.
|
|
TEST_F(FramePointerUnwinderTest, NonNativeUnaligned) {
|
|
InputStack input({
|
|
{true, 4}, // fp of frame 1
|
|
{false, kModuleStart + 0x20}, // ip of frame 1
|
|
{false, 0xaaaa},
|
|
{false, 0xaaaa},
|
|
{true, 7}, // fp of frame 2
|
|
{false, kNonNativeModuleStart + 0x42}, // ip of frame 2
|
|
{false, 0xaaaa},
|
|
{true, 10}, // fp of frame 3
|
|
{false, kModuleStart + 0x10}, // ip of frame 3
|
|
{true, 0xaaaa},
|
|
{false, 0},
|
|
{false, 1},
|
|
});
|
|
|
|
RegisterContext context;
|
|
RegisterContextStackPointer(&context) = input.bottom();
|
|
RegisterContextInstructionPointer(&context) = kModuleStart;
|
|
RegisterContextFramePointer(&context) = input.bottom();
|
|
std::vector<Frame> stack = {
|
|
Frame(RegisterContextInstructionPointer(&context), module())};
|
|
|
|
EXPECT_EQ(UnwindResult::kUnrecognizedFrame,
|
|
unwinder()->TryUnwind(&context, input.top(), &stack));
|
|
}
|
|
|
|
} // namespace base
|