14. November 2019

Exceptions in v8

How do exceptions work in v8? Let’s see.

I’ll use a little program to motivate our discussion:

function foo(a) {
  if (a === true) throw new Error("Oh no.");
}

function bar(a) {
  try {
    foo(a);
  } catch(e) {
    print(e);
  }
}

bar(true);

The bytecode for foo looks like this (I cleaned up the output from --print-bytecode slightly):

Generated bytecode for function: foo
Parameter count 2
Register count 2
Frame size 8
0            StackCheck
1            LdaTrue
2            TestEqualStrict a0, [0]
5            JumpIfFalse [19] (@ 24)
7            LdaGlobal [0], [1]
10           Star r0
12           LdaConstant [1]
14           Star r1
16           Ldar r0
18           Construct r0, r1-r1, [3]
23           Throw
24           LdaUndefined
25           Return
Constant pool (size = 2)
       0: <String[#5]: Error>
       1: <String[#6]: Oh no.>
Handler Table (size = 0)

The important thing here is that if the JumpIfFalse isn’t taken, then we create an Error object, leaving it in the accumulator for the Throw bytecode. We don’t expect to ever return from that bytecode. How strange and interesting…

We’ll get right into that, but first let me show you the bytecode for bar as well, because it has a catch handler that’ll be a point of interest for us:

Generated bytecode for function: bar
Parameter count 2
Register count 4
Frame size 16
0            StackCheck
1            Mov <context>, r0
4            LdaGlobal [0], [0]
7            Star r1
9            CallUndefinedReceiver1 r1, a0, [2]
13           Jump [30] (@ 43)
15           Star r1
17           CreateCatchContext r1, [1]
20           Star r0
22           LdaTheHole
23           SetPendingMessage
24           Ldar r0
26           PushContext r1
28           LdaGlobal [2], [4]
31           Star r2
33           LdaImmutableCurrentContextSlot [2]
35           Star r3
37           CallUndefinedReceiver1 r2, r3, [6]
41           PopContext r1
43           LdaUndefined
44           Return
Constant pool (size = 3)
       0: <String[#3]: foo>
       1: <ScopeInfo CATCH_SCOPE [5]>
       2: <String[#5]: print>
Handler Table (size = 16)
  from   to       hdlr (prediction,   data)
 (   4,  13)  ->    15 (prediction=1, data=0)

Here you can see the call to foo at offset 9, then a jump down to offset 43 where we return undefined (how boring). Offsets 15 to 41 are the catch handler. The Handler Table at the bottom lays this out. It shows the try region goes from offsets 4 to 13, and the handler that corresponds to it starts at offset 15.

Very nice.

So what does the Throw bytecode do? Like all bytecodes, the implementation is provided in file interpreter-generator.cc:

// Throws the exception in the accumulator.
IGNITION_HANDLER(Throw, InterpreterAssembler) {
  TNode<Object> exception = GetAccumulator();
  TNode<Context> context = GetContext();
  CallRuntime(Runtime::kThrow, context, exception);
  // We shouldn't ever return from a throw.
  Abort(AbortReason::kUnexpectedReturnFromThrow);
  Unreachable();
}

Okay, that’s easy, we just call a runtime function. It’s nice to see the abort in here that documents our expectations. The runtime function (in runtime-internal.cc) is also simple, just returning the result of Isolate::Throw() back from C++ to the world of generated code:

RUNTIME_FUNCTION(Runtime_Throw) {
  HandleScope scope(isolate);
  DCHECK_EQ(1, args.length());
  return isolate->Throw(args[0]);
}

You’d never suspect that we just skipped over something absolutely breathtaking, but we did. Let’s return to it after a look at the end of the road, Isolate::Throw:


Object Isolate::Throw(Object raw_exception, MessageLocation* location) {
  DCHECK(!has_pending_exception());

  HandleScope scope(this);
  Handle<Object> exception(raw_exception, this);

  ...
  ... skipping over some interesting stuff to focus on the "bones" of
  ... the function.
  ...

  // Set the exception being thrown.
  set_pending_exception(*exception);
  return ReadOnlyRoots(heap()).exception();
}

The main thing that happens here is that we save the exception object in the isolate, and return a curious sentinel value in the global root set, exception(). We only indicate that there is an exception pending in the system by saving this value, and don’t do anything exotic. The crazy stuff is yet to come, awakened into hideous life by that innocuous sentinel value.

Here’s what happens now. When the runtime function returns this value to generated code, it comes into some tricky platform dependent code we call CEntryStub. This code is responsible for building a frame and calling into C++. It also recognizes pending exceptions and (gasp!) drops frames from the stack in order to call the topmost handler. Let’s have a look, now in builtins-ia32.cc, method Builtins::Generate_CEntry:

  ...
  __ call(kRuntimeCallFunctionRegister);

  // Result is in eax or edx:eax - do not destroy these registers!

  // Check result for exception sentinel.
  Label exception_returned;
  __ CompareRoot(eax, RootIndex::kException);
  __ j(equal, &exception_returned);
  ...
  // Exit the JavaScript to C++ exit frame.
  __ LeaveExitFrame(save_doubles == kSaveFPRegs, argv_mode == kArgvOnStack);
  __ ret(0);
  ...

  // Handling of exception.
  __ bind(&exception_returned);

The code above is dutifully calling the runtime function requested. In our case, it was Runtime_Throw, but this body of code is used for the many dozens of runtime functions we have. After the call, you can see we are comparing the return value with that magic exception sentinel (here it has a different name, RootIndex::kException. We’ve often got a few ways to refer to something, depending on what kind of code you’re in. All part of the fun…). If we don’t see it, we can return to the bytecode handler, builtin or stub, getting somewhat closer to user code. Otherwise, we do interesting things:

  // Ask the runtime for help to determine the handler. This will set eax to
  // contain the current pending exception, don't clobber it.
  ExternalReference find_handler =
      ExternalReference::Create(Runtime::kUnwindAndFindExceptionHandler);
  {
    FrameScope scope(masm, StackFrame::MANUAL);
    __ PrepareCallCFunction(3, eax);
    __ mov(Operand(esp, 0 * kSystemPointerSize), Immediate(0));  // argc.
    __ mov(Operand(esp, 1 * kSystemPointerSize), Immediate(0));  // argv.
    __ Move(esi,
            Immediate(ExternalReference::isolate_address(masm->isolate())));
    __ mov(Operand(esp, 2 * kSystemPointerSize), esi);
    __ CallCFunction(find_handler, 3);
  }

  // Retrieve the handler context, SP and FP.
  __ mov(esp, __ ExternalReferenceAsOperand(pending_handler_sp_address, esi));
  __ mov(ebp, __ ExternalReferenceAsOperand(pending_handler_fp_address, esi));
  __ mov(esi,
         __ ExternalReferenceAsOperand(pending_handler_context_address, esi));

First we call back into C++, searching for an exception handler. We’ll definitely have one. I skipped over a section of code in Isolate::Throw that would abort execution if there is no handler. I really enjoy what’s next: we simply set the stack pointer and the frame pointer to the appropriate values for a handler somewhere below us on the stack. What about dutifully returning from all the functions we may have between here and there?

Nope!

A few lines later we begin executing that handler like this:

  // Compute the handler entry address and jump to it.
  __ mov(edi, __ ExternalReferenceAsOperand(pending_handler_entrypoint_address,
                                            edi));
  __ jmp(edi);
}

That final curly brace is just showing that these are the last lines of the CEntry stub. What a way to go, am I right?

Now it’s a good time to remember that we have a catch handler. It’s in function bar(), and it’s address is at offset 15 in the bytecode. So whatever Runtime_UnwindAndFindExceptionHandler does, it better return precisely that information to the CEntryStub so that the stack, frame and instruction pointer can be set appropriately.

more

25. April 2017

Poking around in Speedometer

My marching orders today were:

OPPORTUNITIES FOR SMALL IMPROVEMENT IN JQUERY SPEEDOMETER BNCHMRK. STOP.
WHY CALLS TO RUNTIME KEYED LOAD? WHY CALLS TO HANDLEAPICALL? WHY? STOP.

These days I’m a manager type, more comfortable droning on in sonorous tones with made up words like “leveraging” and “embiggen,” but p’raps I can saddle up and look at the code again.

more

21. February 2015

Altering JavaScript frames

For a while I’ve been working on a project in V8 to encode type feedback into simple data structures rather than embedding it in compiled code.

more