SpiderMonkey is Mozilla's JavaScript engine written in C and C++. The Debugger server uses its API to do everything from setting breakpoints to receiving pause events.
It's fairly easy to build the engine and get started. The MDN docs have a detailed installation guide, but here is a quick version:
Quick Version:
Set your mozconfig to this:
mk_add_options MOZ_OBJDIR=./objdir-frontend
mk_add_options MOZ_MAKE_FLAGS="-j6 -s"
Then move into the js/src directory, start building the shell and get a ☕.
cd <gecko>/js/src
autoconf-2.13
# This name should end with "_OPT.OBJ" to make the version control system ignore it.
mkdir build_OPT.OBJ
cd build_OPT.OBJ
../configure --enable-debug --disable-optimize
# Use "mozmake" on Windows
makeWhen it is done, you can run the shell and start hacking!
./js/src/js
The shell is useful for inspecting engine-level debug information. The mdn docs list all of the commands.
One useful command is dis, which disassembles the JavaScript bytecode. This is useful for seeing the offsets for individual bytecode instructions and determining where a breakpoint will be set, or what will happen when a program steps.
➜ build_OPT.OBJ ./js/src/js
js> dis("f()")
loc op
----- --
main:
00000: getgname "f" # f
00005: gimplicitthis "f" # f THIS
00010: call 0 # f(...)
00013: setrval #
00014: retrval #
Source notes:
ofs line pc delta desc args
---- ---- ----- ------ -------- ------
0: 1 14 [ 14] xdelta
1: 1 14 [ 0] colspan 3
Once you've built the JS Shell, you can begin running the JIT Tests. These are pure gold:
- they're written in JS
- they document the Debugger's API
Examples:
| Test | Description |
|---|---|
| Frame-onStep-01 | Test that onStep fires often enough to see all four values of a. |
| breakpoint-01 | Basic breakpoint test. |
| breakpoint-resume-01 | A breakpoint handler hit method can return {throw: exc} to throw an exception. |
Lets take a look at one test breakpoint-01 and see what it is doing
var g = newGlobal();
var dbg = Debugger(g);
dbg.onDebuggerStatement = function(frame) {
g.s += "0";
var line0 = frame.script.getOffsetLocation(frame.offset).lineNumber;
var offs = frame.script.getLineOffsets(line0 + 2);
for (var i = 0; i < offs.length; i++)
frame.script.setBreakpoint(offs[i], {
hit: frame => {
g.s += "1";
}
});
};
g.s = "";
g.eval(
"debugger;\n" +
"s += 'a';\n" + // line0 + 1
"s += 'b';\n"
); // line0 + 2
assertEq(g.s, "0a1b");The first thing it does is set up a new global and debugger g and dbg.
Almost every test starts this way
var g = newGlobal();
var dbg = Debugger(g);The next thing it does is set an onDebuggerStatement callback.
This is a convenient way to pause early in the test and have a script we can do stuff with. In this example, the test gets the 2nd line's bytecode offsets and sets a breakpoint for each offset.
Remember, we are testing breakpoints here :)
dbg.onDebuggerStatement = function(frame) {
g.s = "0";
var line0 = frame.script.getOffsetLocation(frame.offset).lineNumber;
var offs = frame.script.getLineOffsets(line0 + 2);
for (var i = 0; i < offs.length; i++)
frame.script.setBreakpoint(offs[i], {
hit: frame => {
g.s += "1";
}
});
};Next, we invoke g.eval with a small program. Most tests have an eval because it is the easiest way to run something.
g.s = "";
g.eval(
"debugger;\n" +
"s += 'a';\n" + // line0 + 1
"s += 'b';\n"
); // line0 + 2If this program runs without pausing, then at the end, s will have the value ab;
because we set an onDebuggerStatement, however, interesting things will happen:
- when the debugger statement is hit,
swill become '0' - when the next statement is invoked,
swill become '0a' - when the breakpoint on the second line is hit,
sbecomes '0a1' - when the statement completes,
sbecomes '0a1b'.
When everything is done, will finally check the value of s!
assertEq(g.s, "0a1b");And now we know that the breakpoint is hit!
If you're interested in seeing how the Debugger API is implemented,
the best place to start is is debugger.cpp. From there, you will find other files like EnvironmentObject.cpp which implements Debugger.Environment.
Pro Tips: