Commit 868f8fd4 authored by Kunshan Wang's avatar Kunshan Wang

Use frame cursors in stack introspection.

Introduced the framecursorref type. This is to solve performance problem
when the stack is very deep. The API is still stateless, but a frame
cursor is mutable. A frame cursor can only be created from a stack in
the READY<Ts> state, may iterate downwards, and must be explicitly
closed. Concurrent introspection that races with stack modification (pop
frames/push frames/resuming a stack) has undefined behaviour.

Also used "resumption point" to simplify the description of stack
binding. The semantics is not changed. In the concrete syntax,
resumption points are "beginning of function", "OSR point instruction"
or "the appropriate place for native function". The Mu cases can be
mapped to the resumption points in the formal model: entry of the %entry
block, and the exit of the block where the OSR point instruction is in.
parent d99118c3
......@@ -367,57 +367,73 @@ Stack introspection
::
[0x254]@uvm.meta.cur_func (%stack: stackref, %frame: int<32>) -> int<32>
[0x255]@uvm.meta.cur_func_Ver (%stack: stackref, %frame: int<32>) -> int<32>
[0x256]@uvm.meta.cur_inst (%stack: stackref, %frame: int<32>) -> int<32>
[0x257]@uvm.meta.dump_keepalives (%stack: stackref, %frame: int<32>) -> @uvm.meta.refs.r
[0x254]@uvm.meta.new_cursor (%stack: stackref) -> framecursorref
[0x255]@uvm.meta.next_frame (%cursor: framecursorref)
[0x256]@uvm.meta.copy_cursor (%cursor: framecursorref) -> framecursorref
[0x257]@uvm.meta.close_cursor (%cursor: framecursorref)
``%stack`` and ``%frame`` selects a frame in the stack ``%stack``. 0 is the top,
1 is the next top, ... ``%stack`` must not be null.
In all cases, ``cursor`` and ``stack`` cannot be ``NULL``.
- ``cur_func`` returns the ID of the current function of ``%frame`` in
``%stack``. Returns 0 if the frame is native.
- ``new_cursor`` allocates a frame cursor, referring to the top frame of
``%stack``. Returns the frame cursor reference.
- ``cur_func_ver`` returns the ID of the current function version of ``%frame``
in ``%stack``. Returns 0 if the frame is native, or the function of the frame
is undefined.
- ``next_frame`` moves the frame cursor so that it refers to the frame below its
current frame.
- ``cur_inst`` returns the ID of the current instruction of ``%frame`` in
``%stack``. Returns 0 if the frame is just created, its function is undefined,
or the frame is native.
- ``copy_cursor`` allocates a frame cursor which refers to the same frame as
``%cursor``. Returns the frame cursor reference.
- ``close_cursor`` deallocates the cursor.
::
[0x258]@uvm.meta.cur_func (%cursor: framecursorref) -> int<32>
[0x259]@uvm.meta.cur_func_Ver (%cursor: framecursorref) -> int<32>
[0x25a]@uvm.meta.cur_inst (%cursor: framecursorref) -> int<32>
[0x25b]@uvm.meta.dump_keepalives (%cursor: framecursorref) -> @uvm.meta.refs.r
These functions operate on the frame referred by ``%cursor``. In all cases,
``%cursor`` cannot be ``NULL``.
- ``cur_func`` returns the ID of the frame. Returns 0 if the frame is native.
- ``cur_func_ver`` returns the ID of the current function version of the frame.
Returns 0 if the frame is native, or the function of the frame is undefined.
- ``cur_inst`` returns the ID of the current instruction of the frame. Returns 0
if the frame is just created, its function is undefined, or the frame is
native.
- ``dump_keepalives`` dumps the values of the keep-alive variables of the
current instruction in the selected frame. If the function is undefined, the
arguments are the keep-alive variables. Cannot be used on native frames. The
return value is a list of object references, each of which refers to an object
which has type *T* and contains value *v*, where *T* and *v* are the type and
the value of the corresponding keep-alive variable, respectively.
current instruction in the frame. If the function is undefined, the arguments
are the keep-alive variables. Cannot be used on native frames. The return
value is a list of object references, each of which refers to an object which
has type *T* and contains value *v*, where *T* and *v* are the type and the
value of the corresponding keep-alive variable, respectively.
On-stack replacement
--------------------
::
[0x258]@uvm.meta.pop_frame (%stack: stackref)
[0x259]@uvm.meta.push_frame <[sig]> (%stack: stackref, %func: funcref<sig>)
[0x25c]@uvm.meta.pop_frames_to (%cursor: framecursorref)
[0x25d]@uvm.meta.push_frame <[sig]> (%stack: stackref, %func: funcref<sig>)
``%stack`` and ``%func`` must not be ``NULL``.
``%cursor``, ``%stack`` and ``%func`` must not be ``NULL``.
- ``pop_frame`` pops the top frame of the stack ``%stack``.
- ``pop_frames_to`` pops all frames above ``%cursor``.
- ``push_frame`` creates a new frame on top of the stack ``%stack`` for the Mu
function ``%func``. ``%func`` must have the signature ``sig``.
The previous top frame must be in the **READY<Ts>** state for some Ts. The
return type of ``%func`` must be *Ts*.
- ``push_frame`` creates a new frame on top of the stack ``%stack`` for the
current version of the Mu function ``%func``. ``%func`` must have the
signature ``sig``.
Watchpoint operations
---------------------
::
[0x25a]@uvm.meta.enable_watchpoint (%wpid: int<32>)
[0x25b]@uvm.meta.disable_watchpoint (%wpid: int<32>)
[0x25e]@uvm.meta.enable_watchpoint (%wpid: int<32>)
[0x25f]@uvm.meta.disable_watchpoint (%wpid: int<32>)
- ``enable_watchpoint`` enables all watchpoints of watchpoint ID ``%wpid``.
- ``disenable_watchpoint`` disables all watchpoints of watchpoint ID ``%wpid``.
......@@ -427,7 +443,7 @@ Trap handling
::
[0x25c]@uvm.meta.set_trap_handler (%handler: funcref<@uvm.meta.trap_handler.sig>, %userdata: ref<void>)
[0x260]@uvm.meta.set_trap_handler (%handler: funcref<@uvm.meta.trap_handler.sig>, %userdata: ref<void>)
This instruction registers a trap handler. ``%handler`` is the function to be
called and ``%userdata`` will be their last argument when called.
......
......@@ -361,8 +361,8 @@ The ``cur_func_ver`` function, in addition to returning the function version
ID, it may also return 0 if the selected frame is a native frame. (Multiple
native frames are counted as one between two Mu frames.)
The ``pop_frame`` function has implementation-defined behaviours when popping
native frames.
The ``pop_frames_to`` function has implementation-defined behaviours when
popping native frames.
When rebinding a thread to a stack with a value, and the top frame is on a call
site (native or Mu), the value associated with the rebinding is the return value
......
......@@ -111,8 +111,8 @@ pushing a frame READY<Ts> -> READY<Us> N/A
Stack and Thread Creation
=========================
Mu stacks and Mu threads can be created by Mu instructions ``@uvm.new_stack`` and
``NEWTHREAD``, or the API function ``new_stack`` and ``new_thread``.
Mu stacks and Mu threads can be created by Mu instructions ``@uvm.new_stack``
and ``NEWTHREAD``, or the API function ``new_stack`` and ``new_thread``.
When a stack is created, a Mu function must be provided. The stack will contain
a frame created for the current version of the function (as seen by the current
......@@ -125,8 +125,8 @@ thread because of concurrency and the memory model). This frame is called the
stack-bottom frame. They are implementation-specific details.
The stack-bottom frame (and also the stack) is in the **READY<Ts>** state, where
Ts are the parameter types of the stack-bottom function. When bound (explained
later), it continues from the beginning of its stack-bottom function.
Ts are the parameter types of the stack-bottom function. The resumption point is
the beginning of the function version.
When a thread is created, a stack must be provided as its **initial stack**.
Creating a thread binds the thread to the stack, passing values or raising
......@@ -171,6 +171,59 @@ shall be performed on the stack:
It gives undefined behaviour if the stack is not in the expected state.
Resumption Point
----------------
A frame in the **READY<Ts>** state has a **resumption point**. The resumption
point determines how the received values and the received exception are
processed when binding to a thread.
For a Mu frame, the resumption point is either the beginning of a function
version, or an OSR point instruction in the function version.
- In the former case, the *Ts* in **READY<Ts>** are the parameters of the
function (also the entry block). Received values are bound to the parameters
and the execution continues from the beginning of the entry block. Received
exception is re-thrown.
- In the latter case, the *Ts* types are determined by the concrete
instructions. Specifically, *Ts* are the return types for ``CALL`` and
``CCALL``, and are explicitly specified for ``TRAP``, ``WATCHPOINT`` and
``SWAPSTACK``. The received values are bound to the results of the OSR point
instruction. The received exception is handle by the instruction or re-thrown
depending on the instruction.
Undefined Mu functions behaves as defined in `Mu IR <uvm-ir.rest>`__.
Native frames can only enter the **READY<Ts>** state when it calls back to Mu.
Thus the resumption point is where it will continue after the native-to-Mu call
returns. The received values are the return values to the native function.
Throwing exceptions to native frames has implementation-defined behaviour.
Mu gives the client a unified model of stack binding. The binding operation is
only aware of the *Ts* types of the **READY<Ts>** state, but oblivious of the
resumption point. Therefore it can resume any **READY<Ts>** stack in the same
way, whether the resumption point is the beginning of a function, a call site, a
trap or a swap-stack instruction, or a native frame.
Note to the Mu implementers: swap-stack, Mu-to-Mu calls and Mu-native calls
may all have different calling conventions, but the implementation must
present a unified "resumption protocol" to the client: all stack-binding
operations work on all OSR point instructions, as long as the *Ts* in
*READY<Ts>* match the passed values. In practice, some "adapter" frames may
need to be inserted to convert one convention to another, but these frames
must not be seen by the client. This implies that some API functions
(especially stack introspection) must "lie" to the client about the presence
of such frames.
For example, on x86_64, assume ``SWAPSTACK`` passes values to the other
stack via rdi and rsi, but ``RET`` returns values to the caller via rax and
rdx. If, during OSR, a frame pausing on the ``CALL`` instruction becomes the
top frame of a stack, then the Mu implementation must also create some glue
code and an adapter frame above that frame, so that when a thread SWAP-STACK
to this stack, values passed in rdi and rsi can be moved to rax and rdx,
respectively. This "adapter" frame must also recover callee-saved registers.
Unbinding
---------
......@@ -182,7 +235,7 @@ be performed on the stack:
An unbinding operation can **leave the stack** with a return types *Ts*. In this
case, the state of its top frame changes from **ACTIVE** to **READY<Ts>** for
some given *Ts*.
some given *Ts*. The instruction becomes the resumption point of the frame.
An unbinding operation can **kill the stack**. In this case, the state of all
frames of the stack changes from **ACTIVE** to **DEAD**. Specifically the
......@@ -218,20 +271,95 @@ stack.
The Mu may change the value of ``stackref`` type to ``NULL`` if the stack it
refers to is in the **DEAD** state.
Stack Introspection
===================
Stacks in the **READY<Ts>** state can be introspected. Stacks in other states
cannot.
The stack introspection API uses **frame cursors**. A *frame cursor* is a
mutable opaque structure allocated by Mu. It refers to a Mu frame, and also
keeps implementation-dependent states necessary to iterate through frames in a
stack.
Note: The reason why it is mutable is that the cursor may be big. The states
to be kept is specific to the implementation. Generally speaking, the more
callee-saved registers there are, the bigger the cursor is. Allocating a new
structure whenever moving down a frame may not scale for deep stacks.
The ``new_cursor`` API call allocates a frame cursor that refers to the top
frame of a given stack, and returns a ``framecursorref`` that refers to the
cursor. Then the client can use the ``next_frame`` API to move the cursor to the
frame below. The ``copy_cursor`` copies the given frame cursor. The original
frame cursor and the copied cursor can move down independently. This is useful
when the client wishes to iterate through the stack in different paces. The
``close_cursor`` API closes the frame cursor and deallocates its resources.
It has undefined behaviour if a stack is bound to a thread or a stack is killed
while there are frame cursors to its frames not closed. It has undefined
behaviour if ``next_frame`` goes below the bottom frame.
Note: There are several reasons why it needs explicit closing.
* It forces the client to avoid racing stack modification and stack
introspection.
* It will not force the Mu implementation to use a particular way to
allocate such cursors. The Mu implementation can use malloc and free. If
the implementation uses garbage collection for such cursors, it can still
treat the ``close_cursor`` operation as a no-op.
* An alternative is to close all related cursors automatically when the
stack is re-bound. But that will involve one extra check for every
swap-stack operation, which may be much more frequent than stack
introspection, which is usually only used in exceptional cases.
The ``cur_func``, ``cur_func_ver``, ``cur_inst`` and ``dump_keepalives`` API
calls take a ``framecursorref`` as argument, and returns the ID of the function,
function version or current instruction of the frame, or dumps the keep-alive
variables of the current instruction of the frame.
Multiple threads may introspect the stack concurrently as long as there is no
concurrent modification using the on-stack replacement API (see below). However,
it has undefined behaviour to operate on a closed frame cursor.
These operations can also be performed by their equivalent common instructions
``@uvm.meta.*``.
On-stack Replacement
====================
The client can pop and push frames from or to a stack. The ``pop_frame`` API
function pops the top frame from a stack. The ``push_frame`` function creates a
new frame on the top of the stack, using the current version (as seen by the
current thread) of a given function and the given arguments.
The client can pop and push frames from or to a stack.
After popping a frame from a stack, the next stack below the top becomes the new
top frame. Then the state of the stack become the state of the new top frame.
The ``pop_frames_to`` API function takes a ``framecursorref`` as argument. It
will pop all frames above the frame cursor, and the frame of the cursor becomes
the new top frame.
After pushing a frame to a stack, the new top frame is in the **READY<Ts>**
state, where Ts is the argument types of the new function. When rebound, it
continues from the beginning of the function.
Popping native frames has implementation-defined behaviour. It has undefined
behaviour if a frame is popped but there are frame cursors referring to that
frame.
The ``push_frame`` API function takes a ``stackref`` and a ``funcref`` as
arguments. It creates a new frame on the top of the stack, using the current
version (as seen by the current thread) of the given function. The resumption
point is the beginning of the function version. The return types of the function
must match the *Ts* of the state of the previous frame, which must be
**READY<Ts>**.
There are equivalent common instructions in the IR, too.
It has undefined behaviour if
- there are two API calls or equivalent instructions executed by two threads,
and
- one is ``new_cursor``, ``next_frame``, ``cur_func``, ``cur_func_ver``,
``cur_inst``, ``dump_keepalives``, ``pop_frames_to`` or ``push_frame``, and
- the other is ``pop_frames_to`` or ``push_frame``, and
- neither happens before the other.
After popping or pushing frames, the state of the stack become the state of the
new top frame, which must be **READY<Ts>** for some *Ts*. The stack can be
bound.
NOTE: For the ease of Mu implementation, the new function must continue
from the beginning rather than an arbitrary instruction in the middle.
......@@ -254,30 +382,6 @@ continues from the beginning of the function.
API so that execution can continue from an arbitrary basic block of an
arbitrary function version, rather than just the beginning of a function.
Mu gives the client a unified model of stack and frame states. As long as a
stack is in the **READY<Ts>** state, it can be resumed by binding a thread to
it, passing values of types *Ts*. The binding operation is oblivious of the
current instruction the top frame is pausing at, i.e. it can be any OSR point
instructions: ``CALL``, ``TRAP``, ``WATCHPOINT``, ``CCALL`` and ``SWAPSTACK``.
Note to the Mu implementers: swap-stack, Mu-to-Mu calls and Mu-native calls
may all have different calling conventions, but the implementation must
present a unified "resumption protocol" to the client: all stack-binding
operations work on all OSR point instructions, as long as the *Ts* in
*READY<Ts>* match the passed values. In practice, some "adapter" frames may
need to be inserted to convert one convention to another, but these frames
must not be seen by the client. This implies that some API functions
(especially stack introspection) must "lie" to the client about the presence
of such frames.
For example, on x86_64, assume ``SWAPSTACK`` passes values to the other
stack via rdi and rsi, but ``RET`` returns values to the caller via rax and
rdx. If, during OSR, a frame pausing on the ``CALL`` instruction becomes the
top frame of a stack, then the Mu implementation must also create some glue
code and an adapter frame above that frame, so that when a thread SWAP-STACK
to this stack, values passed in rdi and rsi can be moved to rax and rdx,
respectively. This "adapter" frame must also recover callee-saved registers.
Futex
=====
......
......@@ -46,6 +46,7 @@ The following type constructors are available in Mu:
- funcref < sig >
- threadref
- stackref
- framecursorref
- tagref64
- vector < T length >
- uptr < T >
......@@ -60,7 +61,8 @@ There are several kinds of types.
* ``float`` and ``double`` are **floating point types**.
* ``ref`` and ``weakref`` are **object referenct types**.
* ``ref``, ``iref`` and ``weakref`` are **reference types**.
* ``funcref``, ``threadref`` and ``stackref`` are **opaque reference types**.
* ``funcref``, ``threadref``, ``stackref`` and ``framecursorref`` are **opaque
reference types**.
* *Reference types* and *opaque reference types* are **general reference types**.
* ``int``, ``float``, ``double``, *general reference types* and ``tagref64`` are
**scalar types**.
......@@ -71,8 +73,8 @@ There are several kinds of types.
* ``hybrid`` is the only **variable-length type**. All other types are
**fixed-length types**.
* ``uptr`` and ``ufuncptr`` are **pointer types**.
* ``int``, ``ref``, ``iref``, ``funcref``, ``stackref``, ``threadref`` and
pointer types are **EQ-comparable types**.
* ``int``, ``ref``, ``iref``, ``funcref``, ``stackref``, ``threadref``,
``framecursorref`` and pointer types are **EQ-comparable types**.
* ``int``, ``iref`` and pointer types are **ULT-comparable types**.
* ``ref<T>`` is the **strong variant** of ``weakref<T>``; ``weakref<T>`` is the
**weak variant** of ``ref<T>``. All other types are the strong variant or weak
......@@ -101,8 +103,9 @@ following:
the ``ufuncptr`` type is not well-formed.
* All other types are not native-safe. (Specifically, they are ``ref``,
``iref``, ``weakref``, ``funcref``, ``threadref``, ``stackref`` as well as
``struct``, ``array`` or ``hybrid`` that contains them.)
``iref``, ``weakref``, ``funcref``, ``threadref``, ``stackref``,
``framecursorref`` as well as ``struct``, ``array`` or ``hybrid`` that
contains them.)
Integer Type
------------
......@@ -539,13 +542,25 @@ A ``NULL`` value of a ``funcref`` type does not refer to any function.
| 0x0D |
+------+
``threadref`` and ``stackref`` are opaque reference types to Mu threads and Mu
stacks, respectively. They are not interchangeable with reference types. Only
some special instructions (e.g. ``@uvm.new_stack``, ``NEWTHREAD``) or API
calls can operate on them.
``framecursorref``
Both ``threadref`` and ``stackref`` values can be ``NULL``, which does not refer
to any threads or stacks.
+------+
| opct |
+======+
| 0x12 |
+------+
``threadref``, ``stackref`` and ``framecursorref`` are opaque reference types to
Mu threads, Mu stacks and frame cursors, respectively. They are not
interchangeable with reference types. Only some special instructions (e.g.
``@uvm.new_stack``, ``NEWTHREAD``, ``@uvm.meta.new_cursor``) or API calls can
operate on them.
All opaque reference values can be ``NULL``, which does not refer to anything.
A frame cursor is an internal structure which is used by the stack introspection
API to iterate through stack frames. Its content is mutable but opaque. See
`Threads and Stacks <threads-stacks.rest>`__ for more details.
Tagged Reference
----------------
......
......@@ -113,6 +113,7 @@ opaque **handles**. Those handles are defined as::
typedef void *MuFuncRefValue; // funcref<sig>
typedef void *MuThreadRefValue; // threadref
typedef void *MuStackRefValue; // stackref
typedef void *MuFCRefValue; // framecursorref
typedef void *MuTagRef64Value; // tagref64
typedef void *MuUPtrValue; // uptr
typedef void *MuUFPValue; // ufuncptr
......@@ -709,26 +710,48 @@ Stack introspection
::
MuID (*cur_func )(MuCtx *ctx, MuStackRefValue stack, int frame);
MuID (*cur_func_ver )(MuCtx *ctx, MuStackRefValue stack, int frame);
MuID (*cur_inst )(MuCtx *ctx, MuStackRefValue stack, int frame);
void (*dump_keepalives)(MuCtx *ctx, MuStackRefValue stack, int frame, MuValue *results);
MuFCRefValue (*new_cursor )(MuCtx *ctx, MuStackRefValue stack);
void (*next_frame )(MuCtx *ctx, MuFCRefValue cursor);
MuFCRefValue (*copy_cursor )(MuCtx *ctx, MuFCRefValue cursor);
void (*close_cursor)(MuCtx *ctx, MuFCRefValue cursor);
* ``cur_func`` returns the ID of the current function of ``frame``
in ``stack``. Returns 0 if the frame is native.
In all cases, ``cursor`` and ``stack`` cannot be ``NULL``.
* ``cur_func_ver`` returns the ID of the current function version of ``frame``
in ``stack``. Returns 0 if the frame is native, or the function of the frame
is undefined.
* ``new_cursor`` allocates a frame cursor, referring to the top frame of
``stack``. Returns the frame cursor reference.
* ``cur_inst`` returns the ID of the current instruction of ``frame`` in
``stack``. Returns 0 if the frame is just created, its function is undefined,
or the frame is native.
* ``next_frame`` moves the frame cursor so that it refers to the frame below its
current frame.
* ``copy_cursor`` allocates a frame cursor which refers to the same frame as
``cursor``. Returns the frame cursor reference.
* ``close_cursor`` deallocates the cursor.
::
MuID (*cur_func )(MuCtx *ctx, MuFCRefValue cursor);
MuID (*cur_func_ver )(MuCtx *ctx, MuFCRefValue cursor);
MuID (*cur_inst )(MuCtx *ctx, MuFCRefValue cursor);
void (*dump_keepalives)(MuCtx *ctx, MuFCRefValue cursor, MuValue *results);
These functions operate on the frame referred by ``cursor``. In all cases,
``cursor`` cannot be the Mu value ``NULL``, and ``results`` cannot be the C
pointer ``NULL``.
* ``cur_func`` returns the ID of the frame. Returns 0 if the frame is native.
* ``cur_func_ver`` returns the ID of the current function version of the frame.
Returns 0 if the frame is native, or the function of the frame is undefined.
* ``cur_inst`` returns the ID of the current instruction of the frame. Returns 0
if the frame is just created, its function is undefined, or the frame is
native.
* ``dump_keepalives`` dumps the values of the keep-alive variables of the
current instruction in the selected frame. If the function is undefined, the
arguments are the keep-alive variables. Cannot be used on native frames. As
many handles as the keep-alive variables are written in the ``results`` array.
current instruction of the frame. If the function is undefined, the arguments
are the keep-alive variables. Cannot be used on native frames. As many
handles as the keep-alive variables are written in the ``results`` array.
..
......@@ -736,10 +759,6 @@ Stack introspection
variables, so the client can always know how long the ``results`` array
should be allocated.
The frame is the ``frame``-th frame below the top frame, starting with 0. (If
``frame`` is 0, it is the top frame. 1 is the frame below the top. 2 is the one
below 1, and so on.)
..
For Lua users: The debug interface provides many similar introspection
......@@ -757,26 +776,21 @@ On-stack replacement
::
void (*pop_frame )(MuCtx *ctx, MuStackRefValue stack);
void (*push_frame)(MuCtx *ctx, MuStackRefValue stack, MuFuncRefValue func);
* ``pop_frame`` pops the top frame of the stack. Popping native frames has
implementation-defined behaviour.
void (*pop_frames_to)(MuCtx *ctx, MuFCRefValue cursor);
void (*push_frame )(MuCtx *ctx, MuStackRefValue stack, MuFuncRefValue func);
* ``push_frame`` creates a new frame on the top of ``stack`` for function
``func``.
``cursor``, ``stack`` and ``func`` cannot be ``NULL``.
The previous top frame must be in the **READY<Ts>** state for some Ts. The
return type of ``%func`` must be *Ts*.
* ``pop_frames_to`` pops all frames above ``cursor``.
After ``push_frame``, the new top frame is in the **READY<Ts>** state where Ts
are the argument types of ``func``. When the stack is rebound, it continues
from the beginning of ``func``. The return value or exceptions from ``func``
are received by the previous frame.
* ``push_frame`` creates a new frame on the top of ``stack`` for the current
version of function ``func``.
..
For JVM users: ``pop_frame`` is similar to the ``PopFrame`` JVM TI function.
For JVM users: ``pop_frames_to`` is similar to the ``PopFrame`` JVM TI
function, but the resumption point is immediately after the call site, not
immediately before.
Tagged reference operations
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment