diff --git a/Makefile b/Makefile index f9513c88..d9e90a37 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ all: @echo "Please take a look at README.md" .PHONY: install-osx -install-osx: +install-osx: plugin mkdir -p ~/Library/Application\ Support/LLDB/PlugIns/ cp -rf ./llnode.dylib \ ~/Library/Application\ Support/LLDB/PlugIns/ @@ -15,7 +15,7 @@ uninstall-osx: rm ~/Library/Application\ Support/LLDB/PlugIns/llnode.dylib .PHONY: install-linux -install-linux: +install-linux: plugin mkdir -p /usr/lib/lldb/plugins cp -rf ./llnode.so /usr/lib/lldb/plugins @@ -37,6 +37,10 @@ plugin: configure node-gyp rebuild node scripts/cleanup.js +.PHONY: addon +addon: configure + node-gyp rebuild + .PHONY: _travis _travis: TEST_LLDB_BINARY="$(TEST_LLDB_BINARY)" \ diff --git a/binding.gyp b/binding.gyp index f9ae2c19..2b910077 100644 --- a/binding.gyp +++ b/binding.gyp @@ -7,7 +7,8 @@ "variables": { # gyp does not appear to let you test for undefined variables, so define # lldb_lib_dir as empty so we can test it later. - "lldb_lib_dir%": "" + "lldb_lib_dir%": "", + "lldb_lib_so%": "" }, "target_defaults": { @@ -54,7 +55,6 @@ [ "OS == 'win'", { "sources": [ "<(lldb_include_dir)/../source/Host/common/GetOptInc.cpp", - "windows/llnode.def", ], "include_dirs": [ "windows/include", @@ -77,6 +77,55 @@ "src/llv8-constants.cc", "src/llscan.cc", "src/node-constants.cc", + ], + "conditions": [ + [ "OS == 'win'", { + "sources": [ + "windows/llnode.def", + ], + }] + ] + }, { + "target_name": "addon", + "type": "loadable_module", + "include_dirs": [ + " line.includes('liblldb') && line.startsWith('/')); if (!lib) { - return { dir: undefined, name: 'lldb' }; + return { dir: undefined, name: 'lldb', so: undefined }; } console.log(`From ldd: ${lldbExe} loads ${lib}`); @@ -121,20 +121,22 @@ function getLib(lldbExe, llvmConfig) { // On Ubuntu the libraries are suffixed and installed globally const libName = path.basename(lib).match(/lib(lldb.*?)\.so/)[1]; - // TODO(joyeecheung): on RedHat there might not be a non-versioned liblldb.so + // On RedHat there might not be a non-versioned liblldb.so // in the system. It's fine in the case of plugins since the lldb executable // will load the library before loading the plugin, but we will have to link // to the versioned library file for addons. - if (!fs.existsSync(path.join(libDir, `lib${libName}.so`))) { + if (fs.existsSync(path.join(libDir, `lib${libName}.so`))) { return { - dir: undefined, - name: libName + dir: libDir, + name: libName, + so: undefined }; } return { - dir: libDir, - name: libName + dir: undefined, + name: libName, + so: lib }; } @@ -181,7 +183,9 @@ function getLldbInstallation() { const lib = getLib(lldbExe, llvmConfig); if (!lib.dir) { console.log(`Could not find non-versioned lib${lib.name}.so in the system`); - console.log(`Symbols will be resolved by the lldb executable at runtime`); + console.log('Plugin symbols will be resolved by the lldb executable ' + + 'at runtime'); + console.log(`Addon will be linked to ${lib.so}`); } else { console.log(`Found lib${lib.name}.so in ${lib.dir}`); } @@ -191,7 +195,8 @@ function getLldbInstallation() { version: lldbVersion, includeDir: includeDir, libDir: lib.dir, - libName: lib.name + libName: lib.name, + libPath: lib.so }; } diff --git a/src/addon.cc b/src/addon.cc new file mode 100644 index 00000000..74cf67c4 --- /dev/null +++ b/src/addon.cc @@ -0,0 +1,14 @@ +#include "llnode_module.h" +#include "napi.h" + +namespace llnode { + +Napi::Object InitAll(Napi::Env env, Napi::Object exports) { + Napi::Object new_exports = LLNode::Init(env, exports); + return LLNodeHeapType::Init(env, new_exports); + // return new_exports; +} + +NODE_API_MODULE(addon, InitAll) + +} // namespace llnode diff --git a/src/llnode_api.cc b/src/llnode_api.cc new file mode 100644 index 00000000..754b57ba --- /dev/null +++ b/src/llnode_api.cc @@ -0,0 +1,197 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "src/llnode_api.h" +#include "src/llscan.h" +#include "src/llv8.h" + +namespace llnode { + +LLNodeApi::LLNodeApi() + : initialized_(false), + debugger(new lldb::SBDebugger()), + target(new lldb::SBTarget()), + process(new lldb::SBProcess()), + llv8(new v8::LLV8()), + llscan(new LLScan(llv8.get())) {} +LLNodeApi::~LLNodeApi() = default; +LLNodeApi::LLNodeApi(LLNodeApi&&) = default; +LLNodeApi& LLNodeApi::operator=(LLNodeApi&&) = default; + +bool LLNodeApi::debugger_initialized_ = false; + +/* Initialize the SB API and load the core dump */ +bool LLNodeApi::Init(const char* filename, const char* executable) { + if (!LLNodeApi::debugger_initialized_) { + lldb::SBDebugger::Initialize(); + LLNodeApi::debugger_initialized_ = true; + } + + if (initialized_) { + return false; + } + + *debugger = lldb::SBDebugger::Create(); + *target = debugger->CreateTarget(executable); + if (!target->IsValid()) { + return false; + } + + *process = target->LoadCore(filename); + // Load V8 constants from postmortem data + llscan->v8()->Load(*target); + initialized_ = true; + + return true; +} + +std::string LLNodeApi::GetProcessInfo() { + lldb::SBStream info; + process->GetDescription(info); + return std::string(info.GetData()); +} + +uint32_t LLNodeApi::GetProcessID() { return process->GetProcessID(); } + +std::string LLNodeApi::GetProcessState() { + return debugger->StateAsCString(process->GetState()); +} + +uint32_t LLNodeApi::GetThreadCount() { return process->GetNumThreads(); } + +uint32_t LLNodeApi::GetFrameCount(size_t thread_index) { + lldb::SBThread thread = process->GetThreadAtIndex(thread_index); + if (!thread.IsValid()) { + return 0; + } + return thread.GetNumFrames(); +} + +// TODO: should return a class with +// functionName, directory, file, complieUnitDirectory, compileUnitFile +std::string LLNodeApi::GetFrame(size_t thread_index, size_t frame_index) { + lldb::SBThread thread = process->GetThreadAtIndex(thread_index); + lldb::SBFrame frame = thread.GetFrameAtIndex(frame_index); + lldb::SBSymbol symbol = frame.GetSymbol(); + + std::string result; + char buf[4096]; + if (symbol.IsValid()) { + snprintf(buf, sizeof(buf), "Native: %s", frame.GetFunctionName()); + result += buf; + + lldb::SBModule module = frame.GetModule(); + lldb::SBFileSpec moduleFileSpec = module.GetFileSpec(); + snprintf(buf, sizeof(buf), " [%s/%s]", moduleFileSpec.GetDirectory(), + moduleFileSpec.GetFilename()); + result += buf; + + lldb::SBCompileUnit compileUnit = frame.GetCompileUnit(); + lldb::SBFileSpec compileUnitFileSpec = compileUnit.GetFileSpec(); + if (compileUnitFileSpec.GetDirectory() != nullptr || + compileUnitFileSpec.GetFilename() != nullptr) { + snprintf(buf, sizeof(buf), "\n\t [%s: %s]", + compileUnitFileSpec.GetDirectory(), + compileUnitFileSpec.GetFilename()); + result += buf; + } + } else { + // V8 frame + llnode::Error err; + llnode::v8::JSFrame v8_frame(llscan->v8(), + static_cast(frame.GetFP())); + std::string frame_str = v8_frame.Inspect(true, err); + + // Skip invalid frames + if (err.Fail() || frame_str.size() == 0 || frame_str[0] == '<') { + if (frame_str[0] == '<') { + snprintf(buf, sizeof(buf), "Unknown: %s", frame_str.c_str()); + result += buf; + } else { + result += "???"; + } + } else { + // V8 symbol + snprintf(buf, sizeof(buf), "JavaScript: %s", frame_str.c_str()); + result += buf; + } + } + return result; +} + +void LLNodeApi::ScanHeap() { + lldb::SBCommandReturnObject result; + // Initial scan to create the JavaScript object map + // TODO: make it possible to create multiple instances + // of llscan and llnode + if (!llscan->ScanHeapForObjects(*target, result)) { + return; + } + object_types.clear(); + + // Load the object types into a vector + for (const auto& kv : llscan->GetMapsToInstances()) { + object_types.push_back(kv.second); + } + + // Sort by instance count + std::sort(object_types.begin(), object_types.end(), + TypeRecord::CompareInstanceCounts); +} + +uint32_t LLNodeApi::GetTypeCount() { return object_types.size(); } + +std::string LLNodeApi::GetTypeName(size_t type_index) { + if (object_types.size() <= type_index) { + return ""; + } + return object_types[type_index]->GetTypeName(); +} + +uint32_t LLNodeApi::GetTypeInstanceCount(size_t type_index) { + if (object_types.size() <= type_index) { + return 0; + } + return object_types[type_index]->GetInstanceCount(); +} + +uint32_t LLNodeApi::GetTypeTotalSize(size_t type_index) { + if (object_types.size() <= type_index) { + return 0; + } + return object_types[type_index]->GetTotalInstanceSize(); +} + +std::set* LLNodeApi::GetTypeInstances(size_t type_index) { + if (object_types.size() <= type_index) { + return nullptr; + } + return &(object_types[type_index]->GetInstances()); +} + +std::string LLNodeApi::GetObject(uint64_t address) { + v8::Value v8_value(llscan->v8(), address); + v8::Value::InspectOptions inspect_options; + inspect_options.detailed = true; + inspect_options.length = 16; + + llnode::Error err; + std::string result = v8_value.Inspect(&inspect_options, err); + if (err.Fail()) { + return "Failed to get object"; + } + return result; +} +} // namespace llnode diff --git a/src/llnode_api.h b/src/llnode_api.h new file mode 100644 index 00000000..570349e9 --- /dev/null +++ b/src/llnode_api.h @@ -0,0 +1,72 @@ +// C++ wrapper API for lldb and llnode APIs + +#ifndef SRC_LLNODE_API_H_ +#define SRC_LLNODE_API_H_ + +#include +#include +#include +#include + +namespace lldb { +class SBDebugger; +class SBTarget; +class SBProcess; + +} // namespace lldb + +namespace llnode { + +class LLScan; +class TypeRecord; + +namespace v8 { +class LLV8; +} + +class LLNodeApi { + public: + // TODO(joyeecheung): a status class for inspection error + + LLNodeApi(); + ~LLNodeApi(); + LLNodeApi(LLNodeApi&&); + LLNodeApi& operator=(LLNodeApi&&); + + bool Init(const char* filename, const char* executable); + bool IsInitialized() { return initialized_; } + + // TODO(joyeecheung): make this a struct + std::string GetProcessInfo(); + uint32_t GetProcessID(); + // TODO(joyeecheung): make this a struct + std::string GetProcessState(); + uint32_t GetThreadCount(); + uint32_t GetFrameCount(size_t thread_index); + // TODO(joyeecheung): make this a struct + std::string GetFrame(size_t thread_index, size_t frame_index); + void ScanHeap(); + // Must be called after ScanHeap; + uint32_t GetTypeCount(); + std::string GetTypeName(size_t type_index); + uint32_t GetTypeInstanceCount(size_t type_index); + uint32_t GetTypeTotalSize(size_t type_index); + std::set* GetTypeInstances(size_t type_index); + // TODO(joyeecheung): templatize all the `Inspect` in llv8.h to + // return structured data + std::string GetObject(uint64_t address); + + private: + bool initialized_; + static bool debugger_initialized_; + std::unique_ptr debugger; + std::unique_ptr target; + std::unique_ptr process; + std::unique_ptr llv8; + std::unique_ptr llscan; + std::vector object_types; +}; + +} // namespace llnode + +#endif // SRC_LLNODE_API_H_ diff --git a/src/llnode_module.cc b/src/llnode_module.cc new file mode 100644 index 00000000..071efb17 --- /dev/null +++ b/src/llnode_module.cc @@ -0,0 +1,298 @@ +// Javascript module API for llnode/lldb +#include +#include + +#include "src/llnode_api.h" +#include "src/llnode_module.h" + +namespace llnode { + +using Napi::Array; +using Napi::CallbackInfo; +using Napi::Function; +using Napi::FunctionReference; +using Napi::HandleScope; +using Napi::Number; +using Napi::Object; +using Napi::ObjectReference; +using Napi::Persistent; +using Napi::Reference; +using Napi::String; +using Napi::Symbol; +using Napi::TypeError; +using Napi::Value; + +FunctionReference LLNode::constructor; + +template +bool HasInstance(Object obj) { + return obj.InstanceOf(T::constructor.Value()); +} + +Object LLNode::Init(Napi::Env env, Object exports) { + HandleScope scope(env); + + Function func = DefineClass( + env, "LLNode", + { + InstanceMethod("getProcessInfo", &LLNode::GetProcessInfo), + InstanceMethod("getProcessObject", &LLNode::GetProcessObject), + InstanceMethod("getHeapTypes", &LLNode::GetHeapTypes), + InstanceMethod("getObjectAtAddress", &LLNode::GetObjectAtAddress), + }); + + constructor = Persistent(func); + constructor.SuppressDestruct(); + + exports.Set("fromCoredump", + Function::New(env, LLNode::FromCoreDump, "fromCoredump")); + + exports.Set("LLNode", func); + return exports; +} + +LLNode::LLNode(const CallbackInfo& args) + : ObjectWrap(args), + heap_initialized_(false), + api_(new llnode::LLNodeApi){}; + +LLNode::~LLNode() {} + +Value LLNode::FromCoreDump(const CallbackInfo& args) { + Napi::Env env = args.Env(); + + if (!args[0].IsString() || !args[1].IsString()) { + TypeError::New(env, "Must be called as fromCoreDump(filename, executable)") + .ThrowAsJavaScriptException(); + return env.Null(); + } + + Napi::Object llnode_obj = constructor.New({}); + LLNode* obj = ObjectWrap::Unwrap(llnode_obj); + obj->heap_initialized_ = false; + std::string filename = args[0].As(); + std::string executable = args[1].As(); + bool ret = obj->api_->Init(filename.c_str(), executable.c_str()); + + if (!ret) { + TypeError::New(env, "Failed to load coredump").ThrowAsJavaScriptException(); + return env.Null(); + } + + return llnode_obj; +} + +#define CHECK_INITIALIZED(api, env) \ + if (!api->IsInitialized()) { \ + TypeError::New(env, "LLNode has not been initialized") \ + .ThrowAsJavaScriptException(); \ + return env.Null(); \ + } + +Value LLNode::GetProcessInfo(const CallbackInfo& args) { + CHECK_INITIALIZED(this->api_, args.Env()) + + return String::New(args.Env(), this->api_->GetProcessInfo()); +} + +Value LLNode::GetProcessObject(const CallbackInfo& args) { + Napi::Env env = args.Env(); + CHECK_INITIALIZED(this->api_, env) + + uint32_t pid = this->api_->GetProcessID(); + std::string state = this->api_->GetProcessState(); + uint32_t thread_count = this->api_->GetThreadCount(); + + Object result = Object::New(env); + + result.Set(String::New(env, "pid"), Number::New(env, pid)); + result.Set(String::New(env, "state"), String::New(env, state)); + result.Set(String::New(env, "threadCount"), Number::New(env, thread_count)); + + Array thread_list = Array::New(env); + for (size_t i = 0; i < thread_count; i++) { + Object thread = Object::New(env); + thread.Set(String::New(env, "threadId"), Number::New(env, i)); + uint32_t frame_count = this->api_->GetFrameCount(i); + thread.Set(String::New(env, "frameCount"), Number::New(env, frame_count)); + + Array frame_list = Array::New(env); + for (size_t j = 0; j < frame_count; j++) { + Object frame = Object::New(env); + std::string frame_str = this->api_->GetFrame(i, j); + frame.Set(String::New(env, "function"), String::New(env, frame_str)); + frame_list.Set(j, frame); + } + + thread.Set(String::New(env, "frames"), frame_list); + thread_list.Set(i, thread); + } + + result.Set(String::New(env, "threads"), thread_list); + + return result; +} + +Value LLNode::GetHeapTypes(const CallbackInfo& args) { + Napi::Env env = args.Env(); + CHECK_INITIALIZED(this->api_, env) + Object llnode_obj = args.This().As(); + + // Initialize the heap and the type iterators + if (!this->heap_initialized_) { + this->api_->ScanHeap(); + this->heap_initialized_ = true; + } + + uint32_t type_count = this->api_->GetTypeCount(); + Array type_list = Array::New(env); + for (size_t i = 0; i < type_count; i++) { + Object type_obj = LLNodeHeapType::constructor.New( + {llnode_obj, Number::New(env, static_cast(i))}); + type_list.Set(i, type_obj); + } + + return type_list; +} + +Object LLNode::GetObjectAtAddress(Napi::Env env, uint64_t addr) { + Object result = Object::New(env); + + char buf[20]; + snprintf(buf, sizeof(buf), "0x%016" PRIx64, addr); + result.Set(String::New(env, "address"), String::New(env, buf)); + + std::string value = this->api_->GetObject(addr); + result.Set(String::New(env, "value"), String::New(env, value)); + + return result; +} + +// TODO: create JS object to introspect core dump +// process/threads/frames +Value LLNode::GetObjectAtAddress(const CallbackInfo& args) { + Napi::Env env = args.Env(); + CHECK_INITIALIZED(this->api_, env) + + if (!args[0].IsString()) { + TypeError::New(env, "First argument must be a string") + .ThrowAsJavaScriptException(); + return env.Null(); + } + + std::string address_str = args[0].As(); + if (address_str[0] != '0' || address_str[1] != 'x' || + address_str.size() > 18) { + TypeError::New(env, "Invalid address").ThrowAsJavaScriptException(); + return env.Null(); + } + + uint64_t addr = std::strtoull(address_str.c_str(), nullptr, 16); + Object result = this->GetObjectAtAddress(args.Env(), addr); + return result; +} + +FunctionReference LLNodeHeapType::constructor; + +Object LLNodeHeapType::Init(Napi::Env env, Object exports) { + HandleScope scope(env); + + Function func = DefineClass(env, "LLNodeHeapType", {}); + + constructor = Persistent(func); + constructor.SuppressDestruct(); + + exports.Set(String::New(env, "LLNodeHeapType"), func); + exports.Set(String::New(env, "nextInstance"), + Function::New(env, LLNodeHeapType::NextInstance, "nextInstance")); + return exports; +} + +LLNodeHeapType::~LLNodeHeapType() {} + +LLNodeHeapType::LLNodeHeapType(const CallbackInfo& args) + : ObjectWrap(args) { + Napi::Env env = args.Env(); + if (!args[0].IsObject() || !HasInstance(args[0].As())) { + TypeError::New(env, "First argument must be a LLNode instance") + .ThrowAsJavaScriptException(); + return; + } + + if (!args[1].IsNumber()) { + TypeError::New(env, "Second argument must be a number") + .ThrowAsJavaScriptException(); + return; + } + + Object llnode_obj = args[0].As(); + LLNode* llnode_ptr = ObjectWrap::Unwrap(llnode_obj); + uint32_t index = args[1].As().Uint32Value(); + + this->llnode_ = Persistent(llnode_obj); + this->instances_initialized_ = false; + this->current_instance_index_ = 0; + + this->type_name_ = llnode_ptr->api_->GetTypeName(index); + this->type_index_ = index; + this->type_ins_count_ = llnode_ptr->api_->GetTypeInstanceCount(index); + this->type_total_size_ = llnode_ptr->api_->GetTypeTotalSize(index); + + Object instance = args.This().As(); + + String typeName = String::New(env, "typeName"); + String instanceCount = String::New(env, "instanceCount"); + String totalSize = String::New(env, "totalSize"); + + instance.Set(typeName, String::New(env, this->type_name_)); + instance.Set(instanceCount, Number::New(env, this->type_ins_count_)); + instance.Set(totalSize, Number::New(env, this->type_total_size_)); + instance.Set(String::New(env, "llnode"), llnode_obj); +} + +LLNode* LLNodeHeapType::llnode() { + return ObjectWrap::Unwrap(this->llnode_.Value()); +} + +void LLNodeHeapType::InitInstances() { + std::set* instances_set = + this->llnode()->api_->GetTypeInstances(this->type_index_); + this->current_instance_index_ = 0; + + for (const uint64_t& addr : *instances_set) { + this->type_instances_.push_back(addr); + } + + this->type_ins_count_ = this->type_instances_.size(); + this->instances_initialized_ = true; +} + +bool LLNodeHeapType::HasMoreInstances() { + return current_instance_index_ < type_ins_count_; +} + +Value LLNodeHeapType::NextInstance(const CallbackInfo& args) { + Napi::Env env = args.Env(); + if (!args[0].IsObject() || + !HasInstance(args[0].As())) { + TypeError::New(env, "First argument must be a LLNoteHeapType instance") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + + LLNodeHeapType* obj = + ObjectWrap::Unwrap(args[0].As()); + if (!obj->instances_initialized_) { + obj->InitInstances(); + } + + if (!obj->HasMoreInstances()) { + return env.Undefined(); + } + + uint64_t addr = obj->type_instances_[obj->current_instance_index_++]; + Object result = obj->llnode()->GetObjectAtAddress(env, addr); + return result; +} + +} // namespace llnode diff --git a/src/llnode_module.h b/src/llnode_module.h new file mode 100644 index 00000000..ed19dbd1 --- /dev/null +++ b/src/llnode_module.h @@ -0,0 +1,72 @@ +#ifndef SRC_LLNODE_API_MODULE_H +#define SRC_LLNODE_API_MODULE_H + +#include +#include + +namespace llnode { + +class LLNodeApi; +class LLNodeHeapType; + +class LLNode : public Napi::ObjectWrap { + friend class LLNodeHeapType; + + public: + static Napi::Object Init(Napi::Env env, Napi::Object exports); + static Napi::FunctionReference constructor; + LLNode(const Napi::CallbackInfo& args); + ~LLNode(); + + private: + LLNode() = delete; + + static Napi::Value FromCoreDump(const Napi::CallbackInfo& args); + + Napi::Value GetProcessInfo(const Napi::CallbackInfo& args); + Napi::Value GetProcessObject(const Napi::CallbackInfo& args); + Napi::Value GetHeapTypes(const Napi::CallbackInfo& args); + Napi::Value GetObjectAtAddress(const Napi::CallbackInfo& args); + + bool heap_initialized_; + + protected: + Napi::Object GetObjectAtAddress(Napi::Env env, uint64_t addr); + std::unique_ptr api_; +}; + +class LLNodeHeapType : public Napi::ObjectWrap { + friend class LLNode; + + public: + static Napi::Object Init(Napi::Env env, Napi::Object exports); + LLNodeHeapType(const Napi::CallbackInfo& args); + ~LLNodeHeapType(); + + static Napi::Value NextInstance(const Napi::CallbackInfo& args); + + static Napi::FunctionReference constructor; + + private: + LLNodeHeapType() = delete; + + void InitInstances(); + bool HasMoreInstances(); + LLNode* llnode(); + + // For getting objects + Napi::ObjectReference llnode_; + + std::vector type_instances_; + bool instances_initialized_; + size_t current_instance_index_; + + std::string type_name_; + size_t type_index_; + uint32_t type_ins_count_; + uint32_t type_total_size_; +}; + +} // namespace llnode + +#endif