Commit f4bb38c3 authored by Peter Marshall's avatar Peter Marshall Committed by Commit Bot

[tools] Add an API for unwinding the V8 stack

This API allows the embedder to provide a stack and PC, FP and
SP registers. V8 will then attempt to unwind the stack to the C++ frame
that called into JS. This API is signal-safe, meaning it does not call
any signal-unsafe OS functions or read/write any V8 state.

Bug: v8:8116

Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng
Change-Id: I7e3e73753b711737020b6a5f11946096658afa6f
Reviewed-on: https://chromium-review.googlesource.com/c/1186724
Commit-Queue: Peter Marshall <petermarshall@chromium.org>
Reviewed-by: 's avatarYang Guo <yangguo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#57749}
parent d1d35504
......@@ -2627,6 +2627,7 @@ v8_source_set("v8_base") {
"src/unicode.h",
"src/unoptimized-compilation-info.cc",
"src/unoptimized-compilation-info.h",
"src/unwinder.cc",
"src/uri.cc",
"src/uri.h",
"src/utils-inl.h",
......
......@@ -1822,8 +1822,18 @@ struct SampleInfo {
};
struct MemoryRange {
const void* start;
size_t length_in_bytes;
const void* start = nullptr;
size_t length_in_bytes = 0;
};
struct JSEntryStub {
MemoryRange code;
};
struct UnwindState {
MemoryRange code_range;
MemoryRange embedded_code_range;
JSEntryStub js_entry_stub;
};
/**
......@@ -8137,13 +8147,9 @@ class V8_EXPORT Isolate {
void GetCodeRange(void** start, size_t* length_in_bytes);
/**
* Returns a memory range containing the code for V8's embedded functions
* (e.g. builtins) which are shared across isolates.
*
* If embedded builtins are disabled, then the memory range will be a null
* pointer with 0 length.
* Returns the UnwindState necessary for use with the Unwinder API.
*/
MemoryRange GetEmbeddedCodeRange();
UnwindState GetUnwindState();
/** Set the callback to invoke in case of fatal errors. */
void SetFatalErrorHandler(FatalErrorCallback that);
......@@ -9313,6 +9319,51 @@ class V8_EXPORT Locker {
internal::Isolate* isolate_;
};
/**
* Various helpers for skipping over V8 frames in a given stack.
*
* The unwinder API is only supported on the x64 architecture.
*/
class V8_EXPORT Unwinder {
public:
/**
* Attempt to unwind the stack to the most recent C++ frame. This function is
* signal-safe and does not access any V8 state and thus doesn't require an
* Isolate.
*
* The unwinder needs to know the location of the JS Entry Stub (a piece of
* code that is run when C++ code calls into generated JS code). This is used
* for edge cases where the current frame is being constructed or torn down
* when the stack sample occurs.
*
* The unwinder also needs the virtual memory range of all possible V8 code
* objects. There are two ranges required - the heap code range and the range
* for code embedded in the binary. The V8 API provides all required inputs
* via an UnwindState object through the Isolate::GetUnwindState() API. These
* values will not change after Isolate initialization, so the same
* |unwind_state| can be used for multiple calls.
*
* \param unwind_state Input state for the Isolate that the stack comes from.
* \param register_state The current registers. This is an in-out param that
* will be overwritten with the register values after unwinding, on success.
* \param stack_base Currently unused.
*
* \return True on success.
*/
static bool TryUnwindV8Frames(const UnwindState& unwind_state,
RegisterState* register_state,
const void* stack_base);
/**
* Whether the PC is within the V8 code range represented by code_range or
* embedded_code_range in |unwind_state|.
*
* If this returns false, then calling UnwindV8Frames() with the same PC
* and unwind_state will always fail. If it returns true, then unwinding may
* (but not necessarily) be successful.
*/
static bool PCIsInV8(const UnwindState& unwind_state, void* pc);
};
// --- Implementation ---
......
......@@ -8812,10 +8812,24 @@ void Isolate::GetCodeRange(void** start, size_t* length_in_bytes) {
*length_in_bytes = code_range.size();
}
MemoryRange Isolate::GetEmbeddedCodeRange() {
UnwindState Isolate::GetUnwindState() {
UnwindState unwind_state;
void* code_range_start;
GetCodeRange(&code_range_start, &unwind_state.code_range.length_in_bytes);
unwind_state.code_range.start = code_range_start;
i::Isolate* isolate = reinterpret_cast<i::Isolate*>(this);
return {reinterpret_cast<const void*>(isolate->embedded_blob()),
isolate->embedded_blob_size()};
unwind_state.embedded_code_range.start =
reinterpret_cast<const void*>(isolate->embedded_blob());
unwind_state.embedded_code_range.length_in_bytes =
isolate->embedded_blob_size();
i::Code js_entry = isolate->heap()->js_entry_code();
unwind_state.js_entry_stub.code.start =
reinterpret_cast<const void*>(js_entry->InstructionStart());
unwind_state.js_entry_stub.code.length_in_bytes = js_entry->InstructionSize();
return unwind_state;
}
#define CALLBACK_SETTER(ExternalName, Type, InternalName) \
......
// Copyright 2018 the V8 project 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 "include/v8.h"
#include "src/frame-constants.h"
#include "src/globals.h"
namespace v8 {
namespace {
bool PCIsInCodeRange(const v8::MemoryRange& code_range, void* pc) {
// Given that the length of the memory range is in bytes and it is not
// necessarily aligned, we need to do the pointer arithmetic in byte* here.
const i::byte* pc_as_byte = reinterpret_cast<i::byte*>(pc);
const i::byte* start = reinterpret_cast<const i::byte*>(code_range.start);
const i::byte* end = start + code_range.length_in_bytes;
return pc_as_byte >= start && pc_as_byte < end;
}
bool IsInUnsafeJSEntryRange(const v8::JSEntryStub& js_entry_stub, void* pc) {
return PCIsInCodeRange(js_entry_stub.code, pc);
// TODO(petermarshall): We can be more precise by checking whether we are
// in the JSEntryStub but after frame setup and before frame teardown, in
// which case we are safe to unwind the stack. For now, we bail out if the PC
// is anywhere within the JSEntryStub.
}
i::Address Load(i::Address address) {
return *reinterpret_cast<i::Address*>(address);
}
void* GetReturnAddressFromFP(void* fp) {
return reinterpret_cast<void*>(
Load(reinterpret_cast<i::Address>(fp) +
i::CommonFrameConstants::kCallerPCOffset));
}
void* GetCallerFPFromFP(void* fp) {
return reinterpret_cast<void*>(
Load(reinterpret_cast<i::Address>(fp) +
i::CommonFrameConstants::kCallerFPOffset));
}
void* GetCallerSPFromFP(void* fp) {
return reinterpret_cast<void*>(reinterpret_cast<i::Address>(fp) +
i::CommonFrameConstants::kCallerSPOffset);
}
} // namespace
bool Unwinder::TryUnwindV8Frames(const UnwindState& unwind_state,
RegisterState* register_state,
const void* stack_base) {
void* pc = register_state->pc;
if (PCIsInV8(unwind_state, pc) &&
!IsInUnsafeJSEntryRange(unwind_state.js_entry_stub, pc)) {
void* current_fp = register_state->fp;
// Peek at the return address that the caller pushed. If it's in V8, then we
// assume the caller frame is a JS frame and continue to unwind.
void* next_pc = GetReturnAddressFromFP(current_fp);
while (PCIsInV8(unwind_state, next_pc)) {
current_fp = GetCallerFPFromFP(current_fp);
next_pc = GetReturnAddressFromFP(current_fp);
}
register_state->sp = GetCallerSPFromFP(current_fp);
register_state->fp = GetCallerFPFromFP(current_fp);
register_state->pc = next_pc;
return true;
}
return false;
}
bool Unwinder::PCIsInV8(const UnwindState& unwind_state, void* pc) {
return pc && (PCIsInCodeRange(unwind_state.code_range, pc) ||
PCIsInCodeRange(unwind_state.embedded_code_range, pc));
}
} // namespace v8
......@@ -230,6 +230,7 @@ v8_source_set("cctest_sources") {
"test-unbound-queue.cc",
"test-unboxed-doubles.cc",
"test-unscopables-hidden-prototype.cc",
"test-unwinder.cc",
"test-usecounters.cc",
"test-utils.cc",
"test-version.cc",
......
......@@ -477,4 +477,10 @@
'test-dtoa/*': [SKIP],
}], # variant == no_wasm_traps
##############################################################################
# The stack unwinder API is only supported on x64.
['arch != x64', {
'test-unwinder/*': [SKIP]
}],
]
......@@ -29092,12 +29092,13 @@ TEST(TestSetWasmThreadsEnabledCallback) {
CHECK(i_isolate->AreWasmThreadsEnabled(i_context));
}
TEST(TestGetEmbeddedCodeRange) {
TEST(TestGetUnwindState) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
v8::MemoryRange builtins_range = isolate->GetEmbeddedCodeRange();
v8::UnwindState unwind_state = isolate->GetUnwindState();
v8::MemoryRange builtins_range = unwind_state.embedded_code_range;
// Check that each off-heap builtin is within the builtins code range.
if (i::FLAG_embedded_builtins) {
......@@ -29116,6 +29117,11 @@ TEST(TestGetEmbeddedCodeRange) {
CHECK_EQ(nullptr, builtins_range.start);
CHECK_EQ(0, builtins_range.length_in_bytes);
}
v8::JSEntryStub js_entry_stub = unwind_state.js_entry_stub;
CHECK_EQ(i_isolate->heap()->js_entry_code()->InstructionStart(),
reinterpret_cast<i::Address>(js_entry_stub.code.start));
}
TEST(MicrotaskContextShouldBeNativeContext) {
This diff is collapsed.
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