v8/tools/debug_helper/get-object-properties.cc
Seth Brenith dcb828b46f [tools] Add in-object properties to debug_helper
Until now, the in-object properties on JSObject have been invisible to
tools using the postmortem debugging library. With this change, those
tools will get enough information to show a flat list of property
values. This is still less powerful than the runtime printers, which can
show the corresponding key for each value, but it's a big step up from
manually inspecting memory.

This change basically requires a reimplementation of
Map::GetInObjectProperties for postmortem debugging. I'm not
enthusiastic about duplicating this logic, but it's pretty small and I
don't see any good alternatives.

As a drive-by cleanup, I moved some inline string literals into a batch
of constexpr char arrays.

Bug: v8:9376
Change-Id: Ia24c05f6e823086babaa07882d0d320ab9a225db
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1930174
Reviewed-by: Jakob Gruber <jgruber@chromium.org>
Commit-Queue: Seth Brenith <seth.brenith@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#65183}
2019-11-26 16:56:26 +00:00

539 lines
21 KiB
C++

// Copyright 2019 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 <sstream>
#include "debug-helper-internal.h"
#include "heap-constants.h"
#include "include/v8-internal.h"
#include "src/common/ptr-compr-inl.h"
#include "src/objects/string-inl.h"
#include "src/strings/unicode-inl.h"
#include "torque-generated/class-debug-readers-tq.h"
namespace i = v8::internal;
namespace v8_debug_helper_internal {
constexpr char kObject[] = "v8::internal::Object";
constexpr char kTaggedValue[] = "v8::internal::TaggedValue";
constexpr char kSmi[] = "v8::internal::Smi";
constexpr char kHeapObject[] = "v8::internal::HeapObject";
#ifdef V8_COMPRESS_POINTERS
constexpr char kObjectAsStoredInHeap[] = "v8::internal::TaggedValue";
#else
constexpr char kObjectAsStoredInHeap[] = "v8::internal::Object";
#endif
std::string AppendAddressAndType(const std::string& brief, uintptr_t address,
const char* type) {
std::stringstream brief_stream;
brief_stream << "0x" << std::hex << address << " <" << type << ">";
return brief.empty() ? brief_stream.str()
: brief + " (" + brief_stream.str() + ")";
}
std::string JoinWithSpace(const std::string& a, const std::string& b) {
return a.empty() || b.empty() ? a + b : a + " " + b;
}
struct TypedObject {
TypedObject(d::TypeCheckResult type_check_result,
std::unique_ptr<TqObject> object)
: type_check_result(type_check_result), object(std::move(object)) {}
// How we discovered the object's type, or why we failed to do so.
d::TypeCheckResult type_check_result;
// Pointer to some TqObject subclass, representing the most specific known
// type for the object.
std::unique_ptr<TqObject> object;
// Collection of other guesses at more specific types than the one represented
// by |object|.
std::vector<TypedObject> possible_types;
};
TypedObject GetTypedObjectByHint(uintptr_t address,
std::string type_hint_string) {
#define TYPE_NAME_CASE(ClassName, ...) \
if (type_hint_string == "v8::internal::" #ClassName) { \
return {d::TypeCheckResult::kUsedTypeHint, \
std::make_unique<Tq##ClassName>(address)}; \
}
TORQUE_INSTANCE_CHECKERS_SINGLE_FULLY_DEFINED(TYPE_NAME_CASE)
TORQUE_INSTANCE_CHECKERS_RANGE_FULLY_DEFINED(TYPE_NAME_CASE)
STRING_CLASS_TYPES(TYPE_NAME_CASE)
#undef TYPE_NAME_CASE
return {d::TypeCheckResult::kUnknownTypeHint,
std::make_unique<TqHeapObject>(address)};
}
TypedObject GetTypedObjectForString(uintptr_t address, i::InstanceType type,
d::TypeCheckResult type_source) {
class StringGetDispatcher : public i::AllStatic {
public:
#define DEFINE_METHOD(ClassName) \
static inline TypedObject Handle##ClassName( \
uintptr_t address, d::TypeCheckResult type_source) { \
return {type_source, std::make_unique<Tq##ClassName>(address)}; \
}
STRING_CLASS_TYPES(DEFINE_METHOD)
#undef DEFINE_METHOD
static inline TypedObject HandleInvalidString(
uintptr_t address, d::TypeCheckResult type_source) {
return {d::TypeCheckResult::kUnknownInstanceType,
std::make_unique<TqString>(address)};
}
};
return i::StringShape(type)
.DispatchToSpecificTypeWithoutCast<StringGetDispatcher, TypedObject>(
address, type_source);
}
TypedObject GetTypedObjectByInstanceType(uintptr_t address,
i::InstanceType type,
d::TypeCheckResult type_source) {
switch (type) {
#define INSTANCE_TYPE_CASE(ClassName, INSTANCE_TYPE) \
case i::INSTANCE_TYPE: \
return {type_source, std::make_unique<Tq##ClassName>(address)};
TORQUE_INSTANCE_CHECKERS_SINGLE_FULLY_DEFINED(INSTANCE_TYPE_CASE)
#undef INSTANCE_TYPE_CASE
default:
// Special case: concrete subtypes of String are not included in the
// main instance type list because they use the low bits of the instance
// type enum as flags.
if (type <= i::LAST_STRING_TYPE) {
return GetTypedObjectForString(address, type, type_source);
}
#define INSTANCE_RANGE_CASE(ClassName, FIRST_TYPE, LAST_TYPE) \
if (type >= i::FIRST_TYPE && type <= i::LAST_TYPE) { \
return {type_source, std::make_unique<Tq##ClassName>(address)}; \
}
TORQUE_INSTANCE_CHECKERS_RANGE_FULLY_DEFINED(INSTANCE_RANGE_CASE)
#undef INSTANCE_RANGE_CASE
return {d::TypeCheckResult::kUnknownInstanceType,
std::make_unique<TqHeapObject>(address)};
}
}
TypedObject GetTypedHeapObject(uintptr_t address, d::MemoryAccessor accessor,
const char* type_hint,
const d::HeapAddresses& heap_addresses) {
auto heap_object = std::make_unique<TqHeapObject>(address);
Value<uintptr_t> map_ptr = heap_object->GetMapValue(accessor);
if (map_ptr.validity != d::MemoryAccessResult::kOk) {
// If we can't read the Map pointer from the object, then we likely can't
// read anything else, so there's not any point in attempting to use the
// type hint. Just return a failure.
return {map_ptr.validity == d::MemoryAccessResult::kAddressNotValid
? d::TypeCheckResult::kObjectPointerInvalid
: d::TypeCheckResult::kObjectPointerValidButInaccessible,
std::move(heap_object)};
}
Value<i::InstanceType> type =
TqMap(map_ptr.value).GetInstanceTypeValue(accessor);
if (type.validity == d::MemoryAccessResult::kOk) {
return GetTypedObjectByInstanceType(address, type.value,
d::TypeCheckResult::kUsedMap);
}
// We can't read the Map, so check whether it is in the list of known Maps,
// as another way to get its instance type.
KnownInstanceType known_map_type =
FindKnownMapInstanceType(map_ptr.value, heap_addresses);
if (known_map_type.confidence == KnownInstanceType::Confidence::kHigh) {
DCHECK_EQ(known_map_type.types.size(), 1);
return GetTypedObjectByInstanceType(address, known_map_type.types[0],
d::TypeCheckResult::kKnownMapPointer);
}
// Create a basic result that says that the object is a HeapObject and we
// couldn't read its Map.
TypedObject result = {
type.validity == d::MemoryAccessResult::kAddressNotValid
? d::TypeCheckResult::kMapPointerInvalid
: d::TypeCheckResult::kMapPointerValidButInaccessible,
std::move(heap_object)};
// If a type hint is available, it may give us something more specific than
// HeapObject. However, a type hint of Object would be even less specific, so
// we'll only use the type hint if it's a subclass of HeapObject.
if (type_hint != nullptr) {
TypedObject hint_result = GetTypedObjectByHint(address, type_hint);
if (result.object->IsSuperclassOf(hint_result.object.get())) {
result = std::move(hint_result);
}
}
// If low-confidence results are available from known Maps, include them only
// if they don't contradict the primary type and would provide some additional
// specificity.
for (const i::InstanceType type_guess : known_map_type.types) {
TypedObject guess_result = GetTypedObjectByInstanceType(
address, type_guess, d::TypeCheckResult::kKnownMapPointer);
if (result.object->IsSuperclassOf(guess_result.object.get())) {
result.possible_types.push_back(std::move(guess_result));
}
}
return result;
}
// An object visitor that accumulates the first few characters of a string.
class ReadStringVisitor : public TqObjectVisitor {
public:
ReadStringVisitor(d::MemoryAccessor accessor,
const d::HeapAddresses& heap_addresses)
: accessor_(accessor),
heap_addresses_(heap_addresses),
index_(0),
limit_(INT32_MAX),
done_(false) {}
// Returns the result as UTF-8 once visiting is complete.
std::string GetString() {
std::vector<char> result(
string_.size() * unibrow::Utf16::kMaxExtraUtf8BytesForOneUtf16CodeUnit);
unsigned write_index = 0;
int prev_char = unibrow::Utf16::kNoPreviousCharacter;
for (size_t read_index = 0; read_index < string_.size(); ++read_index) {
uint16_t character = string_[read_index];
write_index +=
unibrow::Utf8::Encode(result.data() + write_index, character,
prev_char, /*replace_invalid=*/true);
prev_char = character;
}
return {result.data(), write_index};
}
template <typename T>
void ReadSeqString(const T* object) {
int32_t length = GetOrFinish(object->GetLengthValue(accessor_));
for (; index_ < length && index_ < limit_ && !done_; ++index_) {
char16_t c = static_cast<char16_t>(
GetOrFinish(object->GetCharsValue(accessor_, index_)));
if (!done_) AddCharacter(c);
}
}
void VisitSeqOneByteString(const TqSeqOneByteString* object) override {
ReadSeqString(object);
}
void VisitSeqTwoByteString(const TqSeqTwoByteString* object) override {
ReadSeqString(object);
}
void VisitConsString(const TqConsString* object) override {
uintptr_t first_address = GetOrFinish(object->GetFirstValue(accessor_));
if (done_) return;
auto first =
GetTypedHeapObject(first_address, accessor_, nullptr, heap_addresses_)
.object;
first->Visit(this);
if (done_) return;
int32_t first_length = GetOrFinish(
static_cast<TqString*>(first.get())->GetLengthValue(accessor_));
uintptr_t second = GetOrFinish(object->GetSecondValue(accessor_));
if (done_) return;
IndexModifier modifier(this, -first_length, -first_length);
GetTypedHeapObject(second, accessor_, nullptr, heap_addresses_)
.object->Visit(this);
}
void VisitSlicedString(const TqSlicedString* object) override {
uintptr_t parent = GetOrFinish(object->GetParentValue(accessor_));
int32_t length = GetOrFinish(object->GetLengthValue(accessor_));
int32_t offset = i::PlatformSmiTagging::SmiToInt(
GetOrFinish(object->GetOffsetValue(accessor_)));
if (done_) return;
int32_t limit_adjust = offset + length - limit_;
IndexModifier modifier(this, offset, limit_adjust < 0 ? limit_adjust : 0);
GetTypedHeapObject(parent, accessor_, nullptr, heap_addresses_)
.object->Visit(this);
}
void VisitThinString(const TqThinString* object) override {
uintptr_t actual = GetOrFinish(object->GetActualValue(accessor_));
if (done_) return;
GetTypedHeapObject(actual, accessor_, nullptr, heap_addresses_)
.object->Visit(this);
}
void VisitExternalString(const TqExternalString* object) override {
// TODO(v8:9376): External strings are very common and important when
// attempting to print the source of a function in the browser. For now
// we're just ignoring them, but eventually we'll want some kind of
// mechanism where the user of this library can provide a callback function
// that fetches data from external strings.
AddEllipsisAndFinish();
}
void VisitObject(const TqObject* object) override {
// If we fail to find a specific type for a sub-object within a cons string,
// sliced string, or thin string, we will end up here.
AddEllipsisAndFinish();
}
private:
// Unpacks a value that was fetched from the debuggee. If the value indicates
// that it couldn't successfully fetch memory, then prevents further work.
template <typename T>
T GetOrFinish(Value<T> value) {
if (value.validity != d::MemoryAccessResult::kOk) {
AddEllipsisAndFinish();
}
return value.value;
}
void AddEllipsisAndFinish() {
if (!done_) {
string_ += u"...";
done_ = true;
}
}
void AddCharacter(char16_t c) {
if (string_.size() >= kMaxCharacters) {
AddEllipsisAndFinish();
} else {
string_.push_back(c);
}
}
// Temporarily adds offsets to both index_ and limit_, to handle ConsString
// and SlicedString.
class IndexModifier {
public:
IndexModifier(ReadStringVisitor* that, int32_t index_adjust,
int32_t limit_adjust)
: that_(that),
index_adjust_(index_adjust),
limit_adjust_(limit_adjust) {
that_->index_ += index_adjust_;
that_->limit_ += limit_adjust_;
}
~IndexModifier() {
that_->index_ -= index_adjust_;
that_->limit_ -= limit_adjust_;
}
private:
ReadStringVisitor* that_;
int32_t index_adjust_;
int32_t limit_adjust_;
DISALLOW_COPY_AND_ASSIGN(IndexModifier);
};
static constexpr int kMaxCharacters = 80; // How many characters to print.
std::u16string string_; // Result string.
d::MemoryAccessor accessor_;
const d::HeapAddresses& heap_addresses_;
int32_t index_; // Index of next char to read.
int32_t limit_; // Don't read past this index (set by SlicedString).
bool done_; // Whether to stop further work.
};
// An object visitor that supplies extra information for some types.
class AddInfoVisitor : public TqObjectVisitor {
public:
// Returns a descriptive string and a list of properties for the given object.
// Both may be empty, and are meant as an addition to, not a replacement for,
// the Torque-generated data about the object.
static std::pair<std::string, std::vector<std::unique_ptr<ObjectProperty>>>
Visit(const TqObject* object, d::MemoryAccessor accessor,
const d::HeapAddresses& heap_addresses) {
AddInfoVisitor visitor(accessor, heap_addresses);
object->Visit(&visitor);
return {std::move(visitor.brief_), std::move(visitor.properties_)};
}
void VisitString(const TqString* object) override {
ReadStringVisitor visitor(accessor_, heap_addresses_);
object->Visit(&visitor);
brief_ = "\"" + visitor.GetString() + "\"";
}
void VisitJSObject(const TqJSObject* object) override {
// JSObject and its subclasses can be followed directly by an array of
// property values. The start and end offsets of those values are described
// by a pair of values in its Map.
auto map_ptr = object->GetMapValue(accessor_);
if (map_ptr.validity != d::MemoryAccessResult::kOk) {
return; // Can't read the JSObject. Nothing useful to do.
}
TqMap map(map_ptr.value);
// On JSObject instances, this value is the start of in-object properties.
// The constructor function index option is only for primitives.
auto start_offset =
map.GetInObjectPropertiesStartOrConstructorFunctionIndexValue(
accessor_);
// The total size of the object in memory. This may include over-allocated
// expansion space that doesn't correspond to any user-accessible property.
auto instance_size = map.GetInstanceSizeInWordsValue(accessor_);
if (start_offset.validity != d::MemoryAccessResult::kOk ||
instance_size.validity != d::MemoryAccessResult::kOk) {
return; // Can't read the Map. Nothing useful to do.
}
int num_properties = instance_size.value - start_offset.value;
if (num_properties > 0) {
properties_.push_back(std::make_unique<ObjectProperty>(
"in-object properties", kObjectAsStoredInHeap, kObject,
object->GetMapAddress() + start_offset.value * i::kTaggedSize,
num_properties, i::kTaggedSize,
std::vector<std::unique_ptr<StructProperty>>(),
d::PropertyKind::kArrayOfKnownSize));
}
}
private:
AddInfoVisitor(d::MemoryAccessor accessor,
const d::HeapAddresses& heap_addresses)
: accessor_(accessor), heap_addresses_(heap_addresses) {}
// Inputs used by this visitor:
d::MemoryAccessor accessor_;
const d::HeapAddresses& heap_addresses_;
// Outputs generated by this visitor:
// A brief description of the object.
std::string brief_;
// A list of extra properties to append after the automatic ones that are
// created for all Torque-defined class fields.
std::vector<std::unique_ptr<ObjectProperty>> properties_;
};
std::unique_ptr<ObjectPropertiesResult> GetHeapObjectPropertiesNotCompressed(
uintptr_t address, d::MemoryAccessor accessor, const char* type_hint,
const d::HeapAddresses& heap_addresses) {
// Regardless of whether we can read the object itself, maybe we can find its
// pointer in the list of known objects.
std::string brief = FindKnownObject(address, heap_addresses);
TypedObject typed =
GetTypedHeapObject(address, accessor, type_hint, heap_addresses);
auto props = typed.object->GetProperties(accessor);
// Use the AddInfoVisitor to get any extra properties or descriptive text that
// can't be directly derived from Torque class definitions.
auto extra_info =
AddInfoVisitor::Visit(typed.object.get(), accessor, heap_addresses);
brief = JoinWithSpace(brief, extra_info.first);
props.insert(props.end(), std::make_move_iterator(extra_info.second.begin()),
std::make_move_iterator(extra_info.second.end()));
brief = AppendAddressAndType(brief, address, typed.object->GetName());
// Convert the low-confidence guessed types to a list of strings as expected
// for the response.
std::vector<std::string> guessed_types;
for (const auto& guess : typed.possible_types) {
guessed_types.push_back(guess.object->GetName());
}
return std::make_unique<ObjectPropertiesResult>(
typed.type_check_result, brief, typed.object->GetName(), std::move(props),
std::move(guessed_types));
}
std::unique_ptr<ObjectPropertiesResult> GetHeapObjectPropertiesMaybeCompressed(
uintptr_t address, d::MemoryAccessor memory_accessor,
d::HeapAddresses heap_addresses, const char* type_hint) {
// Try to figure out the heap range, for pointer compression (this is unused
// if pointer compression is disabled).
uintptr_t any_uncompressed_ptr = 0;
if (!IsPointerCompressed(address)) any_uncompressed_ptr = address;
if (any_uncompressed_ptr == 0)
any_uncompressed_ptr = heap_addresses.any_heap_pointer;
if (any_uncompressed_ptr == 0)
any_uncompressed_ptr = heap_addresses.map_space_first_page;
if (any_uncompressed_ptr == 0)
any_uncompressed_ptr = heap_addresses.old_space_first_page;
if (any_uncompressed_ptr == 0)
any_uncompressed_ptr = heap_addresses.read_only_space_first_page;
FillInUnknownHeapAddresses(&heap_addresses, any_uncompressed_ptr);
if (any_uncompressed_ptr == 0) {
// We can't figure out the heap range. Just check for known objects.
std::string brief = FindKnownObject(address, heap_addresses);
brief = AppendAddressAndType(brief, address, kTaggedValue);
return std::make_unique<ObjectPropertiesResult>(
d::TypeCheckResult::kUnableToDecompress, brief, kTaggedValue);
}
address = EnsureDecompressed(address, any_uncompressed_ptr);
return GetHeapObjectPropertiesNotCompressed(address, memory_accessor,
type_hint, heap_addresses);
}
std::unique_ptr<ObjectPropertiesResult> GetObjectProperties(
uintptr_t address, d::MemoryAccessor memory_accessor,
const d::HeapAddresses& heap_addresses, const char* type_hint) {
if (static_cast<uint32_t>(address) == i::kClearedWeakHeapObjectLower32) {
return std::make_unique<ObjectPropertiesResult>(
d::TypeCheckResult::kWeakRef, "cleared weak ref", kHeapObject);
}
bool is_weak = (address & i::kHeapObjectTagMask) == i::kWeakHeapObjectTag;
if (is_weak) {
address &= ~i::kWeakHeapObjectMask;
}
if (i::Internals::HasHeapObjectTag(address)) {
std::unique_ptr<ObjectPropertiesResult> result =
GetHeapObjectPropertiesMaybeCompressed(address, memory_accessor,
heap_addresses, type_hint);
if (is_weak) {
result->Prepend("weak ref to ");
}
return result;
}
// For smi values, construct a response with a description representing the
// untagged value.
int32_t value = i::PlatformSmiTagging::SmiToInt(address);
std::stringstream stream;
stream << value << " (0x" << std::hex << value << ")";
return std::make_unique<ObjectPropertiesResult>(d::TypeCheckResult::kSmi,
stream.str(), kSmi);
}
} // namespace v8_debug_helper_internal
namespace di = v8_debug_helper_internal;
extern "C" {
V8_DEBUG_HELPER_EXPORT d::ObjectPropertiesResult*
_v8_debug_helper_GetObjectProperties(uintptr_t object,
d::MemoryAccessor memory_accessor,
const d::HeapAddresses& heap_addresses,
const char* type_hint) {
return di::GetObjectProperties(object, memory_accessor, heap_addresses,
type_hint)
.release()
->GetPublicView();
}
V8_DEBUG_HELPER_EXPORT void _v8_debug_helper_Free_ObjectPropertiesResult(
d::ObjectPropertiesResult* result) {
std::unique_ptr<di::ObjectPropertiesResult> ptr(
static_cast<di::ObjectPropertiesResultExtended*>(result)->base);
}
}