├── .npmignore ├── LICENSE ├── README.md ├── assets ├── sample.png ├── yt-jbt-lldb.png └── yt-jbt-xcode.png ├── jbt ├── jbt.py ├── package.json └── test ├── read-sync ├── index.js ├── init.lldb └── run.sh └── read-sync_g ├── index.js ├── init.lldb └── run.sh /.npmignore: -------------------------------------------------------------------------------- 1 | assets/ 2 | spikes/ 3 | test/ 4 | .npmignore 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Thorsten Lorenz. 2 | All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lldb-jbt 2 | 3 | **NOTE:** Even though this module works as is, **I am no longer improving it because a better solution is available now**. Therefore please use [llnode](https://github.com/nodejs/llnode) instead. 4 | 5 | ![assets/sample.png](assets/sample.png) 6 | 7 | ## Screencasts 8 | 9 | Debugging Node.js with lldb and jbt | Debugging Node.js with Xcode and jbt 10 | :-------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------: 11 | [![assets/yt-jbt-lldb.png](assets/yt-jbt-lldb.png)](http://youtu.be/hy9o5Crjy1A) | [![assets/yt-jbt-lldb.png](assets/yt-jbt-xcode.png)](http://youtu.be/_oMt1vCwod0) 12 | 13 | ## Installation 14 | 15 | ``` 16 | npm install -g lldb-jbt 17 | ``` 18 | 19 | **Requires Node.js version `0.11.13` or higher** and works best with a *debug* build. 20 | 21 | For more information see Node.js [build instructions](https://github.com/thlorenz/lldb-jbt/wiki/Building-Node.js). 22 | 23 | ## Usage 24 | 25 | 1. Add the script dir to your `PYTHONPATH` by running `source jbt` 26 | 2. Debug your node process with `--perf-basic-prof` flag, i.e. `lldb -- node --perf-basic-prof index.js` 27 | 3. Import the **jbt** command into lldb `command script import jbt` 28 | 4. Set a breakpoint, i.e. `b uv_fs_read` 29 | 5. When you hit the breakpoint type `jbt` to see the stack trace with JavaScript symbols resolved 30 | 31 | ## Xcode 32 | 33 | To make things work with Xcode do the following: 34 | 35 | 1. Run `jbt` to determine where `jbt.py` was installed on your machine 36 | 2. Add a `~/.lldbinit-xcode` file which will be picked up by Xcode with the below content 37 | 38 | ``` 39 | command script import 40 | ``` 41 | 42 | Now the **jbt** command will initialize itself and is accessible to you in the **lldb** console inside Xcode. 43 | 44 | ## License 45 | 46 | MIT 47 | -------------------------------------------------------------------------------- /assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/lldb-jbt/f4597342d9d491ad5bb193e121943cf1008ce886/assets/sample.png -------------------------------------------------------------------------------- /assets/yt-jbt-lldb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/lldb-jbt/f4597342d9d491ad5bb193e121943cf1008ce886/assets/yt-jbt-lldb.png -------------------------------------------------------------------------------- /assets/yt-jbt-xcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/lldb-jbt/f4597342d9d491ad5bb193e121943cf1008ce886/assets/yt-jbt-xcode.png -------------------------------------------------------------------------------- /jbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then 6 | PYTHONPATH=$DIR:$PYTHONPATH 7 | export PYTHONPATH 8 | echo "Now debug your node process with lldb and ensure to use the '--perf-basic-prof' flag." 9 | echo "In lldb run 'command script import jbt' to initialize the command." 10 | else 11 | echo "This script should be sourced." 12 | echo 13 | echo "Therefore instead run: 'source jbt'." 14 | echo 15 | echo "Alternatively do one of the following:" 16 | echo " a) Add '$DIR' to your PYTHONPATH" 17 | echo " b) Run 'command script import $DIR/jbt.py' inside lldb or an '~/.lldbinit' script." 18 | fi 19 | -------------------------------------------------------------------------------- /jbt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import lldb 3 | 4 | DEBUG = False 5 | kHeaderSize = 0 6 | 7 | LAZY_COMPILE = 'LazyCompile:' 8 | LAZY_COMPILE_LEN = len(LAZY_COMPILE) 9 | 10 | from threading import Timer 11 | 12 | class Address: 13 | def __init__(self, inst_start, name): 14 | self.decimalAddress = inst_start 15 | self.hexadecimalAddress = "0x%x" % inst_start 16 | self.name = name 17 | 18 | unresolvedAddress = Address(0, '') 19 | 20 | class Addresses: 21 | def __init__(self): 22 | self._addresses = [] 23 | self._sorted = True 24 | 25 | def __getitem__(self, key): 26 | return self._addresses[key] 27 | 28 | def getKey(self, addr): 29 | return addr.decimalAddress 30 | 31 | def push(self, val): 32 | self._addresses.append(val) 33 | self._sorted = False 34 | 35 | def sort_addresses(self): 36 | if self._sorted: return 37 | self._addresses = sorted(self._addresses, key=self.getKey) 38 | self._sorted = True 39 | 40 | def len(self): 41 | return len(self._addresses) 42 | 43 | def resolve(self, addr): 44 | self.sort_addresses() 45 | 46 | # if address is smaller than the we first one we have a symbol for bail out immediately 47 | if self.len() == 0 or addr < self._addresses[0].decimalAddress: return unresolvedAddress 48 | 49 | prev = unresolvedAddress 50 | for a in self._addresses: 51 | if addr < a.decimalAddress: return prev 52 | prev = a 53 | 54 | # fell off the end of the list so we might be inside the last symbol 55 | if addr < prev.decimalAddress + 4096: return prev 56 | 57 | return unresolvedAddress 58 | 59 | addresses = Addresses() 60 | 61 | def jit_break (frame, bp_loc, dic): 62 | 63 | # kHeaderSize is a constant and evaluating expressions is expensive, so we only do it once 64 | global kHeaderSize 65 | if kHeaderSize == 0: 66 | kHeaderSize_var = frame.EvaluateExpression('((Code*)0x0)->instruction_start()') 67 | kHeaderSize = kHeaderSize_var.GetValueAsUnsigned() 68 | 69 | # If not in debug mode instruction_start symbol is not found 70 | # error: call to a function 'v8::internal::Code::instruction_start()' ('_ZN2v88internal4Code17instruction_startEv') 71 | # that is not present in the target 72 | # In that case just go with `5f == 95`, this hopefully is close enough on any architecture 73 | if kHeaderSize == 0: 74 | kHeaderSize = 95 75 | print 'Warning, could not determine kHeaderSize since node is not running in debug mode. Using approximate instead.' 76 | if DEBUG: print 'Guessed kHeaderSize: %d' % kHeaderSize 77 | print 'Run node_g if you want to run in debug mode' 78 | else: 79 | if DEBUG: print 'Determined kHeaderSize: %d' % kHeaderSize 80 | 81 | 82 | 83 | code_var = frame.FindVariable('code') 84 | name_var = frame.FindVariable('name') 85 | length_var = frame.FindVariable('length') 86 | 87 | length = length_var.GetValueAsUnsigned() 88 | name = "%.*s" % (int(length), name_var.GetSummary().strip('"')) 89 | code = code_var.GetValueAsUnsigned() 90 | inst_start = code + kHeaderSize 91 | 92 | if name.startswith(LAZY_COMPILE): 93 | name = name[LAZY_COMPILE_LEN:] 94 | 95 | # this prints exactly what PerfBasicLogger::LogRecordedBuffer prints omitting the instruction_size since we don't need it 96 | if DEBUG: print '%x %s' % (inst_start, name) 97 | 98 | addresses.push(Address(inst_start, name)) 99 | 100 | return False 101 | 102 | def jit_bt (debugger, command, result, internal_dict): 103 | target = debugger.GetSelectedTarget() 104 | process = target.GetProcess() 105 | thread = process.GetSelectedThread() 106 | frame = thread.GetSelectedFrame() 107 | frames = thread.get_thread_frames() 108 | 109 | if addresses.len() == 0: 110 | print 'WARN: jbt is unable to resolve any JavaScript symbols since it has not collected any symbol information yet.' 111 | print ' You may use "bt" instead since jbt couldn\'t add any extra information at this point.' 112 | print ' Did you debug Node.js >= v0.11.13 with the --perf-basic-prof flag and "run" the target yet? Example: "lldb -- node_g --perf-basic-prof index.js".' 113 | return 114 | 115 | print '* thread: #%d: tid = 0x%x, %s' % (thread.GetIndexID(), thread.GetThreadID(), frame) 116 | for f in frames: 117 | star = ' ' 118 | if f.GetFrameID() == frame.GetFrameID(): star = '*' 119 | name = '%s' % f.GetFunctionName() 120 | if name != 'None': 121 | print ' %s %s' % (star, f) 122 | else: 123 | addr = f.GetPC() 124 | resolved = addresses.resolve(addr) 125 | print ' %s %s %s' % (star, f, resolved.name) 126 | 127 | def run_commands(command_interpreter, commands): 128 | return_obj = lldb.SBCommandReturnObject() 129 | for command in commands: 130 | command_interpreter.HandleCommand( command, return_obj ) 131 | if return_obj.Succeeded(): 132 | if DEBUG: print return_obj.GetOutput() 133 | else: 134 | if DEBUG: print return_obj 135 | return False 136 | return True 137 | 138 | def __lldb_init_module(debugger, internal_dict): 139 | ci = debugger.GetCommandInterpreter() 140 | 141 | def initBreakPoint(): 142 | success = run_commands(ci, [ 143 | 'breakpoint set -name v8::internal::PerfBasicLogger::LogRecordedBuffer', 144 | 'breakpoint command add -F jbt.jit_break' 145 | ]) 146 | # Keep trying to set the breakpoint until it succeeds which means we finally have a target set by the user. 147 | # This is especially important for tools like Xcode which set the target automatically. 148 | # We need to make sure we don't miss too much generated code after the target starts running, but also don't want to slow things down too much 149 | # 200ms seems like a good compromise 150 | if not success: 151 | t = Timer(0.2, initBreakPoint) 152 | t.start() 153 | 154 | initBreakPoint() 155 | 156 | debugger.HandleCommand('command script add -f jbt.jit_bt jbt') 157 | print 'The jit symbol resolver command `jbt` has been initialized and is ready for use.' 158 | 159 | 160 | ### Trouble Shooting 161 | 162 | ## slow method of determining instruction_start() -- uncomment to check against calculated one 163 | # inst_start_var = frame.EvaluateExpression('reinterpret_cast(code->instruction_start())'); 164 | # inst_start_old = int(value(inst_start_var).value, 10) 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lldb-jbt", 3 | "version": "0.3.1", 4 | "description": "Adds JavaScript symbols to lldb stack traces", 5 | "main": "index.js", 6 | "bin": { 7 | "jbt": "jbt", 8 | "jbt.py": "jbt.py" 9 | }, 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "test": "Please run below test commands manually", 15 | "test-read-sync": "./test/read-sync/run.sh", 16 | "test-read-sync_g": "./test/read-sync_g/run.sh" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/thlorenz/lldb-jbt.git" 21 | }, 22 | "keywords": [ 23 | "lldb", 24 | "jit", 25 | "backtrace", 26 | "stacktrace", 27 | "symbol", 28 | "symbols", 29 | "symbolicate" 30 | ], 31 | "author": "Thorsten Lorenz (thlorenz.com)", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/thlorenz/lldb-jbt/issues" 35 | }, 36 | "homepage": "https://github.com/thlorenz/lldb-jbt" 37 | } 38 | -------------------------------------------------------------------------------- /test/read-sync/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PORT = 9000; 4 | var http = require('http'); 5 | var fs = require('fs'); 6 | var server = http.createServer(); 7 | 8 | console.error('pid:', process.pid); 9 | 10 | server 11 | .on('request', onRequest) 12 | .on('listening', onListening) 13 | .listen(PORT); 14 | 15 | function readMe() { 16 | return fs.readFileSync(__filename, 'utf8'); 17 | } 18 | 19 | function onRequest(req, res) { 20 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 21 | var text = readMe(); 22 | res.end(text + '\r\n'); 23 | } 24 | 25 | function onListening() { 26 | console.error('HTTP server listening on port', PORT); 27 | console.error('curl localhost:%d', PORT); 28 | } 29 | -------------------------------------------------------------------------------- /test/read-sync/init.lldb: -------------------------------------------------------------------------------- 1 | target create "node" 2 | settings set -- target.run-args "--perf-basic-prof" "--nouse-inlining" "index.js" 3 | command script import "../../jbt.py" 4 | breakpoint set -name uv_fs_read -ignore 1 5 | -------------------------------------------------------------------------------- /test/read-sync/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | cd $DIR && lldb -S init.lldb 6 | -------------------------------------------------------------------------------- /test/read-sync_g/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PORT = 9000; 4 | var http = require('http'); 5 | var fs = require('fs'); 6 | var server = http.createServer(); 7 | 8 | console.error('pid:', process.pid); 9 | 10 | server 11 | .on('request', onRequest) 12 | .on('listening', onListening) 13 | .listen(PORT); 14 | 15 | function readMe() { 16 | return fs.readFileSync(__filename, 'utf8'); 17 | } 18 | 19 | function onRequest(req, res) { 20 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 21 | var text = readMe(); 22 | res.end(text + '\r\n'); 23 | } 24 | 25 | function onListening() { 26 | console.error('HTTP server listening on port', PORT); 27 | console.error('curl localhost:%d', PORT); 28 | } 29 | -------------------------------------------------------------------------------- /test/read-sync_g/init.lldb: -------------------------------------------------------------------------------- 1 | target create "node_g" 2 | settings set -- target.run-args "--perf-basic-prof" "--nouse-inlining" "index.js" 3 | command script import "../../jbt.py" 4 | breakpoint set -name uv_fs_read -condition cb==NULL -ignore 1 5 | -------------------------------------------------------------------------------- /test/read-sync_g/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | cd $DIR && lldb -S init.lldb 6 | --------------------------------------------------------------------------------