IonMonkey/OnStackInvalidation
Overview
On-Stack invalidation in IonMonkey is the mechanism by which the following situation is handled:
- Ion JITcode is entered.
- The JITcode calls a C++ function
- The C++ code does something that causes the Ion JITcode to become invalid.
- The C++ code returns to an invalid JITcode return address.
When the C++ code returns to the JITcode, it cannot continue executing as normal. It must immediately jump to a handler that bails out from the JITcode and returns execution to the C++ code that called the JITcode.
To handle this, whenever Ion JITcode calls out to C++ code, it reserves some space after the point in the JITcode where the call is made. When an IonScript is invalidated while active (i.e. it's on the stack), the patch address corresponding to the current active return address must be calculated, and it must be patched to jump to an invalidation epilogue to perform the bailout.
The invalidation epilogue needs to have access to the register state at the time of the invalidation, as well as a Snapshot describing which registers and stack locations corresponding to which values in the virtual interpreter stack. It then uses this information to re-create the proper javascript interpreter frames corresponding to the point in the jitcode where the on-stack invalidation was entered.
The following documents the major touchpoints in the code which implement the behaviour necessary to do this.
InvalidateActivation
ion::InvalidateActivation iterates through each IonFrame within the activation. Every ion frame contains a CalleeToken that identifies either the script or the function corresponding to that Ion frame.
This token is used to retreive the JSScript corresponding to the frame, and the JSScript's corresponding IonScript is retreived and checked to see if it's been marked invalidated. If so, the script's active return point needs to be patched to jump to the invalidation epilogue.
Every call that can lead to an OSI is associated with a Safepoint (created at compile time). This safepoint is indexed to the return address of the specific call that it corresponds to. This return address is available as the address returned to by the callee frame of the active invalidated ion frame.
The return address is used to lookup the SafepointIndex for the call from the IonScript. The safepoint index contains the location in the code that needs to be patched.
This location is retrieved, and patched with a jump to the IonScript's invalidateEpilogue code.
That covers the actions necessary when an IonScript gets invalidated.
InvalidateEpilogue
Once the C++ code that caused the invalidation finishes execution and returns to Ion JITcode, it will immediately jump to the invalidateEpilogue. This epilogue needs to figure out the stack layout, re-create the proper JS interpreter stack corresponding to the JITcode, then return to the caller of the invalidated JITcode.
This procedure prevents the execution of any on-stack JITcode that has been invalidated.
For every IonScript there exists a corresponding invalidateEpilogue that's generated when the IonScript is generated. The generation of this code is in generateInvalidateEpilogue in ion/shared/CodeGenerator-x86-shared.cpp (and the corresponding ARM code generator file).
The per-script epilogue assembly code is a thin veneer that:
- Pushes the relevant JSScript pointer onto the stack.
- Calls a common invalidation thunk.
The common invalidation thunk is generated by IonCompartment::getOrCreateInvalidationThunk in ion/IonCompartment.h, which is a singleton-creation wrapper around IonCompartment::generateInvalidator, which is different for each architecture. The x86 implementation of this method is located in ion/x86/Trampoline-x86.cpp (see the other relevant trampoline files for the implementations for those architectures).
The behaviour of the common invalidation code is described below (for x86 only, but the other architectures should be similar):
- pop off 1 word from the stack. - This is to eliminate the return pointer to the script-specific invalidation epilogue code, which got pushed on when the common invalidation chunk was called.
- push all registers onto the stack, remember stack pointer - This is to construct an InvalidationBailoutStack structure on the stack.
- reserve sizeof(size_t) on the stack, remember stack pointer - To reserve space for the size of the frame.
- call the C++ function InvalidationBailout in ion/Bailouts.cpp, passing the above two pointers.