diff --git a/doc/guides/node-postmortem-support.md b/doc/guides/node-postmortem-support.md new file mode 100644 index 00000000000000..e29d9ca3a1fdf0 --- /dev/null +++ b/doc/guides/node-postmortem-support.md @@ -0,0 +1,72 @@ +# Postmortem Support + +Postmortem metadata are constants present in the final build which can be used +by debuggers and other tools to navigate through internal structures of software +when analyzing its memory (either on a running process or a core dump). Node +provides this metadata in its builds for V8 and Node internal structures. + + +### V8 Postmortem metadata + +V8 prefixes all postmortem constants with `v8dbg_`, and they allow inspection of +objects on the heap as well as object properties and references. V8 generates +those symbols with a script (`deps/v8/tools/gen-postmortem-metadata.py`), and +Node always includes these constants in the final build. + +### Node Debug Symbols + +Node prefixes all postmortem constants with `nodedbg_`, and they complement V8 +constants by providing ways to inspect Node-specific structures, like +`node::Environment`, `node::BaseObject` and its descendants, classes from +`src/utils.h` and others. Those constants are declared in +`src/node_postmortem_metadata.cc`, and most of them are calculated at compile +time. + +#### Calculating offset of class members + +Node constants referring to the offset of class members in memory are calculated +at compile time. Because of that, those class members must be at a fixed offset +from the start of the class. That's not a problem in most cases, but it also +means that those members should always come after any templated member on the +class definition. + +For example, if we want to add a constant with the offset for +`ReqWrap::req_wrap_queue_`, it should be defined after `ReqWrap::req_`, because +`sizeof(req_)` depends on the type of T, which means the class definition should +be like this: + +```c++ +template +class ReqWrap : public AsyncWrap { + private: + // req_wrap_queue_ comes before any templated member, which places it in a + // fixed offset from the start of the class + ListNode req_wrap_queue_; + + T req_; +}; +``` + +instead of: + +```c++ +template +class ReqWrap : public AsyncWrap { + private: + T req_; + + // req_wrap_queue_ comes after a templated member, which means it won't be in + // a fixed offset from the start of the class + ListNode req_wrap_queue_; +}; +``` + +There are also tests on `test/cctest/test_node_postmortem_metadata.cc` to make +sure all Node postmortem metadata are calculated correctly. + +## Tools and References + +* [llnode](https://github.com/nodejs/llnode): LLDB plugin +* [`mdb_v8`](https://github.com/joyent/mdb_v8): mdb plugin +* [nodejs/post-mortem](https://github.com/nodejs/post-mortem): Node.js +post-mortem working group diff --git a/node.gyp b/node.gyp index 8a7af9bfd1e50a..2b1d8b2dbf6086 100644 --- a/node.gyp +++ b/node.gyp @@ -163,6 +163,7 @@ 'src/node_http_parser.cc', 'src/node_main.cc', 'src/node_os.cc', + 'src/node_postmortem_metadata.cc', 'src/node_revert.cc', 'src/node_url.cc', 'src/node_util.cc', @@ -597,6 +598,7 @@ 'libraries': [ '<(OBJ_GEN_PATH)/node_javascript.<(OBJ_SUFFIX)', '<(OBJ_PATH)/async-wrap.<(OBJ_SUFFIX)', + '<(OBJ_PATH)/handle_wrap.<(OBJ_SUFFIX)', '<(OBJ_PATH)/env.<(OBJ_SUFFIX)', '<(OBJ_PATH)/node.<(OBJ_SUFFIX)', '<(OBJ_PATH)/node_buffer.<(OBJ_SUFFIX)', @@ -623,7 +625,9 @@ ], 'sources': [ + 'test/cctest/node_test_fixture.cc', 'test/cctest/test_base64.cc', + 'test/cctest/test_node_postmortem_metadata.cc', 'test/cctest/test_util.cc', 'test/cctest/test_url.cc' ], diff --git a/src/base-object.h b/src/base-object.h index 27251379770e2c..26cb5bee372ea7 100644 --- a/src/base-object.h +++ b/src/base-object.h @@ -44,6 +44,11 @@ class BaseObject { static inline void WeakCallback( const v8::WeakCallbackInfo& data); + // persistent_handle_ needs to be at a fixed offset from the start of the + // class because it is used by src/node_postmortem_metadata.cc to calculate + // offsets and generate debug symbols for BaseObject, which assumes that the + // position of members in memory are predictable. For more information please + // refer to `doc/guides/node-postmortem-support.md` v8::Persistent persistent_handle_; Environment* env_; }; diff --git a/src/env.h b/src/env.h index c4489857161b75..3a3bba94151416 100644 --- a/src/env.h +++ b/src/env.h @@ -574,6 +574,12 @@ class Environment { inspector::Agent inspector_agent_; #endif + // handle_wrap_queue_ and req_wrap_queue_ needs to be at a fixed offset from + // the start of the class because it is used by + // src/node_postmortem_metadata.cc to calculate offsets and generate debug + // symbols for Environment, which assumes that the position of members in + // memory are predictable. For more information please refer to + // `doc/guides/node-postmortem-support.md` HandleWrapQueue handle_wrap_queue_; ReqWrapQueue req_wrap_queue_; ListHead&); static void OnClose(uv_handle_t* handle); + // handle_wrap_queue_ needs to be at a fixed offset from the start of the + // class because it is used by src/node_postmortem_metadata.cc to calculate + // offsets and generate debug symbols for HandleWrap, which assumes that the + // position of members in memory are predictable. For more information please + // refer to `doc/guides/node-postmortem-support.md` ListNode handle_wrap_queue_; enum { kInitialized, kClosing, kClosingWithCallback, kClosed } state_; uv_handle_t* const handle_; diff --git a/src/node_postmortem_metadata.cc b/src/node_postmortem_metadata.cc new file mode 100644 index 00000000000000..af36652b41d32c --- /dev/null +++ b/src/node_postmortem_metadata.cc @@ -0,0 +1,110 @@ +// Need to import standard headers before redefining private, otherwise it +// won't compile. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace node { +// Forward declaration needed before redefining private. +int GenDebugSymbols(); +} // namespace node + + +#define private friend int GenDebugSymbols(); private + +#include "env.h" +#include "base-object-inl.h" +#include "handle_wrap.h" +#include "util-inl.h" +#include "req-wrap.h" +#include "v8abbr.h" + +#define NODEDBG_SYMBOL(Name) nodedbg_ ## Name + +// nodedbg_offset_CLASS__MEMBER__TYPE: Describes the offset to a class member. +#define NODEDBG_OFFSET(Class, Member, Type) \ + NODEDBG_SYMBOL(offset_ ## Class ## __ ## Member ## __ ## Type) + +// These are the constants describing Node internal structures. Every constant +// should use the format described above. These constants are declared as +// global integers so that they'll be present in the generated node binary. They +// also need to be declared outside any namespace to avoid C++ name-mangling. +#define NODE_OFFSET_POSTMORTEM_METADATA(V) \ + V(BaseObject, persistent_handle_, v8_Persistent_v8_Object, \ + BaseObject::persistent_handle_) \ + V(Environment, handle_wrap_queue_, Environment_HandleWrapQueue, \ + Environment::handle_wrap_queue_) \ + V(Environment, req_wrap_queue_, Environment_ReqWrapQueue, \ + Environment::req_wrap_queue_) \ + V(HandleWrap, handle_wrap_queue_, ListNode_HandleWrap, \ + HandleWrap::handle_wrap_queue_) \ + V(Environment_HandleWrapQueue, head_, ListNode_HandleWrap, \ + Environment::HandleWrapQueue::head_) \ + V(ListNode_HandleWrap, next_, uintptr_t, ListNode::next_) \ + V(ReqWrap, req_wrap_queue_, ListNode_ReqWrapQueue, \ + ReqWrap::req_wrap_queue_) \ + V(Environment_ReqWrapQueue, head_, ListNode_ReqWrapQueue, \ + Environment::ReqWrapQueue::head_) \ + V(ListNode_ReqWrap, next_, uintptr_t, ListNode>::next_) + +extern "C" { +int nodedbg_const_Environment__kContextEmbedderDataIndex__int; +uintptr_t nodedbg_offset_ExternalString__data__uintptr_t; + +#define V(Class, Member, Type, Accessor) \ + NODE_EXTERN uintptr_t NODEDBG_OFFSET(Class, Member, Type); + NODE_OFFSET_POSTMORTEM_METADATA(V) +#undef V +} + +namespace node { + +int GenDebugSymbols() { + nodedbg_const_Environment__kContextEmbedderDataIndex__int = + Environment::kContextEmbedderDataIndex; + + nodedbg_offset_ExternalString__data__uintptr_t = NODE_OFF_EXTSTR_DATA; + + #define V(Class, Member, Type, Accessor) \ + NODEDBG_OFFSET(Class, Member, Type) = OffsetOf(&Accessor); + NODE_OFFSET_POSTMORTEM_METADATA(V) + #undef V + + return 1; +} + +int debug_symbols_generated = GenDebugSymbols(); + +} // namespace node diff --git a/src/req-wrap.h b/src/req-wrap.h index 0fddae67460d6f..d5903029f20d4f 100644 --- a/src/req-wrap.h +++ b/src/req-wrap.h @@ -27,9 +27,13 @@ class ReqWrap : public AsyncWrap { protected: // req_wrap_queue_ needs to be at a fixed offset from the start of the class // because it is used by ContainerOf to calculate the address of the embedding - // ReqWrap. ContainerOf compiles down to simple, fixed pointer arithmetic. - // sizeof(req_) depends on the type of T, so req_wrap_queue_ would - // no longer be at a fixed offset if it came after req_. + // ReqWrap. ContainerOf compiles down to simple, fixed pointer arithmetic. It + // is also used by src/node_postmortem_metadata.cc to calculate offsets and + // generate debug symbols for ReqWrap, which assumes that the position of + // members in memory are predictable. sizeof(req_) depends on the type of T, + // so req_wrap_queue_ would no longer be at a fixed offset if it came after + // req_. For more information please refer to + // `doc/guides/node-postmortem-support.md` T req_; }; diff --git a/src/util-inl.h b/src/util-inl.h index 5ffe5b857f5381..cb4673ade93cab 100644 --- a/src/util-inl.h +++ b/src/util-inl.h @@ -133,13 +133,17 @@ typename ListHead::Iterator ListHead::end() const { return Iterator(const_cast*>(&head_)); } +template +constexpr uintptr_t OffsetOf(Inner Outer::*field) { + return reinterpret_cast(&(static_cast(0)->*field)); +} + template ContainerOfHelper::ContainerOfHelper(Inner Outer::*field, Inner* pointer) - : pointer_(reinterpret_cast( - reinterpret_cast(pointer) - - reinterpret_cast(&(static_cast(0)->*field)))) { -} + : pointer_( + reinterpret_cast( + reinterpret_cast(pointer) - OffsetOf(field))) {} template template diff --git a/test/cctest/node_test_fixture.cc b/test/cctest/node_test_fixture.cc index 9fc8b96445063c..a0685284c9c56f 100644 --- a/test/cctest/node_test_fixture.cc +++ b/test/cctest/node_test_fixture.cc @@ -1,2 +1,7 @@ #include #include "node_test_fixture.h" + +v8::Isolate::CreateParams NodeTestFixture::params_; +ArrayBufferAllocator NodeTestFixture::allocator_; +v8::Platform* NodeTestFixture::platform_; +uv_loop_t NodeTestFixture::current_loop; diff --git a/test/cctest/node_test_fixture.h b/test/cctest/node_test_fixture.h index 5460a26e3764ad..f0eb602aec4c93 100644 --- a/test/cctest/node_test_fixture.h +++ b/test/cctest/node_test_fixture.h @@ -4,6 +4,7 @@ #include #include "gtest/gtest.h" #include "node.h" +#include "node_internals.h" #include "env.h" #include "v8.h" #include "libplatform/libplatform.h" @@ -25,6 +26,8 @@ class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator { struct Argv { public: + Argv() : Argv("node", "-p", "process.version") {} + Argv(const char* prog, const char* arg1, const char* arg2) { int prog_len = strlen(prog) + 1; int arg1_len = strlen(arg1) + 1; @@ -36,6 +39,7 @@ struct Argv { snprintf(argv_[0] + prog_len + arg1_len, arg2_len, "%s", arg2); argv_[1] = argv_[0] + prog_len; argv_[2] = argv_[0] + prog_len + arg1_len; + nr_args_ = 3; } ~Argv() { @@ -43,41 +47,100 @@ struct Argv { free(argv_); } - char** operator *() const { + int nr_args() const { + return nr_args_; + } + + char** operator*() const { return argv_; } private: char** argv_; + int nr_args_; }; + class NodeTestFixture : public ::testing::Test { protected: - v8::Isolate::CreateParams params_; - ArrayBufferAllocator allocator_; + static v8::Isolate::CreateParams params_; + static ArrayBufferAllocator allocator_; + static v8::Platform* platform_; + static uv_loop_t current_loop; v8::Isolate* isolate_; ~NodeTestFixture() { TearDown(); } - virtual void SetUp() { + static void SetUpTestCase() { platform_ = v8::platform::CreateDefaultPlatform(); + CHECK_EQ(0, uv_loop_init(¤t_loop)); v8::V8::InitializePlatform(platform_); v8::V8::Initialize(); params_.array_buffer_allocator = &allocator_; - isolate_ = v8::Isolate::New(params_); } - virtual void TearDown() { + static void TearDownTestCase() { if (platform_ == nullptr) return; + while (uv_loop_alive(¤t_loop)) { + uv_run(¤t_loop, UV_RUN_ONCE); + } v8::V8::ShutdownPlatform(); delete platform_; platform_ = nullptr; + + uv_loop_close(¤t_loop); } - private: - v8::Platform* platform_ = nullptr; + virtual void SetUp() { + isolate_ = v8::Isolate::New(params_); + } +}; + + +class EnvironmentTestFixture : public NodeTestFixture { + public: + class Env { + public: + Env(const v8::HandleScope& handle_scope, + const Argv& argv) { + auto isolate = handle_scope.GetIsolate(); + context_ = v8::Context::New(isolate); + CHECK(!context_.IsEmpty()); + context_->Enter(); + + v8::Context::Scope context_scope(context_); + + environment_ = node::CreateEnvironment(isolate, + &NodeTestFixture::current_loop, + context_, + 1, + *argv, + argv.nr_args(), + *argv); + + CHECK_NE(nullptr, environment_); + } + + ~Env() { + node::FreeEnvironment(environment_); + context_->Exit(); + } + + node::Environment* operator*() const { + return environment_; + } + + v8::Local context() const { + return context_; + } + + private: + v8::Local context_; + node::Environment* environment_; + DISALLOW_COPY_AND_ASSIGN(Env); + }; }; #endif // TEST_CCTEST_NODE_TEST_FIXTURE_H_ diff --git a/test/cctest/test_node_postmortem_metadata.cc b/test/cctest/test_node_postmortem_metadata.cc new file mode 100644 index 00000000000000..c9be7aae9f9d66 --- /dev/null +++ b/test/cctest/test_node_postmortem_metadata.cc @@ -0,0 +1,147 @@ +#include "node_postmortem_metadata.cc" + +#include "gtest/gtest.h" +#include "node.h" +#include "node_internals.h" +#include "node_test_fixture.h" +#include "req-wrap-inl.h" +#include "v8.h" + + +class DebugSymbolsTest : public EnvironmentTestFixture {}; + + +class TestHandleWrap : public node::HandleWrap { + public: + size_t self_size() const override { return sizeof(*this); } + + TestHandleWrap(node::Environment* env, + v8::Local object, + uv_tcp_t* handle) + : node::HandleWrap(env, + object, + reinterpret_cast(handle), + node::AsyncWrap::PROVIDER_TIMERWRAP) {} +}; + + +class TestReqWrap : public node::ReqWrap { + public: + size_t self_size() const override { return sizeof(*this); } + + TestReqWrap(node::Environment* env, v8::Local object) + : node::ReqWrap(env, + object, + node::AsyncWrap::PROVIDER_TIMERWRAP) {} +}; + +TEST_F(DebugSymbolsTest, ContextEmbedderDataIndex) { + int kContextEmbedderDataIndex = node::Environment::kContextEmbedderDataIndex; + EXPECT_EQ(nodedbg_const_Environment__kContextEmbedderDataIndex__int, + kContextEmbedderDataIndex); +} + +TEST_F(DebugSymbolsTest, ExternalStringDataOffset) { + EXPECT_EQ(nodedbg_offset_ExternalString__data__uintptr_t, + NODE_OFF_EXTSTR_DATA); +} + +TEST_F(DebugSymbolsTest, BaseObjectPersistentHandle) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + + v8::Local object = v8::Object::New(isolate_); + node::BaseObject obj(*env, object); + + auto expected = reinterpret_cast(&obj.persistent()); + auto calculated = reinterpret_cast(&obj) + + nodedbg_offset_BaseObject__persistent_handle___v8_Persistent_v8_Object; + EXPECT_EQ(expected, calculated); + + obj.persistent().Reset(); // ~BaseObject() expects an empty handle. +} + + +TEST_F(DebugSymbolsTest, EnvironmentHandleWrapQueue) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + + auto expected = reinterpret_cast((*env)->handle_wrap_queue()); + auto calculated = reinterpret_cast(*env) + + nodedbg_offset_Environment__handle_wrap_queue___Environment_HandleWrapQueue; // NOLINT(whitespace/line_length) + EXPECT_EQ(expected, calculated); +} + +TEST_F(DebugSymbolsTest, EnvironmentReqWrapQueue) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + + auto expected = reinterpret_cast((*env)->req_wrap_queue()); + auto calculated = reinterpret_cast(*env) + + nodedbg_offset_Environment__req_wrap_queue___Environment_ReqWrapQueue; + EXPECT_EQ(expected, calculated); +} + +TEST_F(DebugSymbolsTest, HandleWrapList) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + + uv_tcp_t handle; + + auto obj_template = v8::FunctionTemplate::New(isolate_); + obj_template->InstanceTemplate()->SetInternalFieldCount(1); + + v8::Local object = + obj_template->GetFunction()->NewInstance(env.context()).ToLocalChecked(); + TestHandleWrap obj(*env, object, &handle); + + auto queue = reinterpret_cast((*env)->handle_wrap_queue()); + auto head = queue + + nodedbg_offset_Environment_HandleWrapQueue__head___ListNode_HandleWrap; + auto next = + head + nodedbg_offset_ListNode_HandleWrap__next___uintptr_t; + next = *reinterpret_cast(next); + + auto expected = reinterpret_cast(&obj); + auto calculated = next - + nodedbg_offset_HandleWrap__handle_wrap_queue___ListNode_HandleWrap; + EXPECT_EQ(expected, calculated); + + obj.persistent().Reset(); // ~HandleWrap() expects an empty handle. +} + +TEST_F(DebugSymbolsTest, ReqWrapList) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + + auto obj_template = v8::FunctionTemplate::New(isolate_); + obj_template->InstanceTemplate()->SetInternalFieldCount(1); + + v8::Local object = + obj_template->GetFunction()->NewInstance(env.context()).ToLocalChecked(); + TestReqWrap obj(*env, object); + + // NOTE (mmarchini): Workaround to fix failing tests on ARM64 machines with + // older GCC. Should be removed once we upgrade the GCC version used on our + // ARM64 CI machinies. + for (auto it : *(*env)->req_wrap_queue()) {} + + auto queue = reinterpret_cast((*env)->req_wrap_queue()); + auto head = queue + + nodedbg_offset_Environment_ReqWrapQueue__head___ListNode_ReqWrapQueue; + auto next = + head + nodedbg_offset_ListNode_ReqWrap__next___uintptr_t; + next = *reinterpret_cast(next); + + auto expected = reinterpret_cast(&obj); + auto calculated = + next - nodedbg_offset_ReqWrap__req_wrap_queue___ListNode_ReqWrapQueue; + EXPECT_EQ(expected, calculated); + + obj.Dispatched(); +}