This article focuses on hacking TraceMonkey code generation (jstracer.cpp, jsregex.cpp) in ways that will work on both 32-bit and 64-bit JIT backends.
What widths are the random typedefs everywhere?
The following types or typedefs are always 64-bit on 64-bit platforms, and 32-bit on 32-bit platforms:
- Pointers
- uintptr_t, intptr_t, ptrdiff_t, (probably) size_t
- jsval
- jsuword, jsword
- Length of a string, though the actual length cannot exceed 30 bits
- JSUintPtr, JSIntPtr, JSPtrDiff, JSUptrDiff, JSSize, JSUword, JSWord (let's not use these, kthx)
The following types are 32-bit on 32-bit platforms. For all intents and purposes they are also 32-bit on 64-bit platforms:
- intN, uintN
- JSIntn, JSUintn, JSBool
General Problems with Pointers
When performing bitwise operations on pointer values, make sure that both operands are 64-bit. The compiler can implicitly sign or zero extend operands with unintended side effects. For example, consider this code:
#define POINTER_TAGBITS 3 static inline uintptr_t UnmaskPointer(uintptr_t v) { return v & ~POINTER_TAGBITS; }
The value 3 will be inverted to 0xFFFFFFFC, then zero-extended to 0x00000000FFFFFFFC - a subtle and nasty bug, assuming it is unintended. The best way to fix this is to make types explicit, such as:
const uintptr_t POINTER_TAGBITS = 3
Or by using a cast inside the macro. This sort of bug happens surprisingly often - see bug 501324, bug 512866 for example.
AMD64 Pointers
If mucking with pointers on AMD64 (or EM64-T/Intel64), it is important to keep in mind an invariant that bits 63-48 must be sign-extended from bit 47. If you use these bits to squirrel away a payload, they must be adjusted before attempting to dereference the pointer. For more information and platform specific details on virtual address widths, see this article on Wikipedia.
Builtins and Calls
When passing arguments to LirWriter::insCall()
, there are four types:
ARGSIZE_F
- floating point valueARGSIZE_I
- 32-bit integerARGSIZE_Q
- 64-bit integerARGSIZE_P
- 32-bit integer on 32-bit platforms, 64-bit integer on 64-bit platforms.
Remember to use ARGSIZE_P
where appropriate - on pointers or natively sized integers (including jsvals). Similarly, when adding types to jsbuiltins.h
, remember to use _JS_PTR
for pointer-width values.
LIR Safety
It is not immediately clear from reading LIR which opcodes should be used for 64-bit safety. If you make a mistake, there's an extremely good chance the SanityFilter in Nanojit will catch it while generating code. If this happens you will get an assert that points directly to the ill-typed LIR.
Not all pointer-width values are actually pointers. To avoid equivocating, the rest of this article will use the term "native integers". A native integer is the size used for intptr_t/uintptr_t, which is usually the width of a general-purpose register on the target CPU.
Loading and Storing Native Integers
The harder cases to detect usually involve runtime value truncation. For example, this code will not load a native integer correctly on a 64-bit machine:
struct Object { void *data; }; lir->insLoad(LIR_ld, objIns, ins->insImm(offsetof(Object, data)));
LIR_ld
is 32-bit. On 64-bit machines you must use LIR_ldq
. Luckily there is an alias that will choose the right opcode for you - LIR_ldp
:
struct Object { void *data; }; lir->insLoad(LIR_ldp, objIns, ins->insImm(offsetof(Object, data)));
When you use LirWriter::insStore
, the correct size is chosen for you automatically, based on the size of the input operands.
To insert constants, use LirWriter::insImmPtr()
for pointer types or LirWriter::insImmWord()
for integral types. Note that LirWriter::insImm()
is always 32-bit.
Quad versus Float
Internally, "quads" are 64-bit opaque values in Nanojit. They can be either floating point values or integral values. To help form well-typed LIR, there is a special opcode called LIR_float
which specifies that the associated constant value is definitely floating-point. TraceMonkey uses this to decide whether arbitrary constants in LIR are pointers or doubles. You should never use LirWriter::insImmq()
to inject a floating point value. Use LirWriter::insImmf()
instead.
Mucking with Native Integers in LIR
The following opcodes can be used to safely load, modify, and compare native integers. For such opcodes there are two forms, an "undecorated" form which is 32-bit, and a form decorated with a "q" or "qi" that is 64-bit. To make it easier to generate platform specific code, these opcodes have aliases decorated with a "p". The following table contains the most relevant opcodes:
Platform Alias | 32-bit Op | 64-bit Op |
---|---|---|
ldp |
ld |
ldq |
ldcp |
ldc |
ldcq |
piadd |
add |
qiadd |
piand |
and |
qiand |
pilsh |
lsh |
qilsh |
pirsh |
rsh |
qirsh |
pursh |
ush |
qursh |
pcmov |
cmov |
qcmov |
pior |
or |
qior |
pxor |
xor |
qxor |
addp |
iaddp |
qaddp |
peq - puge |
eq - uge |
qeq - quge |
pcall |
icall |
qcall |
The 32-bit versions have the following inputs and outputs. i32
means "32-bit integer".
32-bit Op | Inputs | Output |
---|---|---|
ld |
i32, i32 |
i32 |
ldc |
i32, i32 |
i32 |
add |
i32, i32 |
i32 |
and |
i32, i32 |
i32 |
lsh |
i32, i32 |
i32 |
rsh |
i32, i32 |
i32 |
ush |
i32 |
|
cmov |
i32, i32, i32 |
i32 |
or |
i32, i32 |
i32 |
xor |
i32, i32 |
i32 |
iaddp |
i32, i32 |
i32 |
eg - uge |
i32, i32 |
i32 |
icall |
N/A |
i32 |
The 64-bit versions have the following inputs and outputs. i64
means "64-bit integer".
64-bit Op | Inputs | Output |
---|---|---|
ldq |
i64, i32 |
i64 |
ldcq |
i64, i32 |
i64 |
qiadd |
i64, i64 |
i64 |
qiand |
i64, i64 |
i64 |
qilsh |
i64, i32 |
i64 |
qirsh |
i64, i32 |
i64 |
qursh |
i64, i32 |
i64 |
qcmov |
i32, i64, i64 |
i32 |
qior |
i64, i64 |
i64 |
qixor |
i64, i64 |
i64 |
qiaddp |
i64, i64 |
i64 |
qeq - quge |
i64, i64 |
i32 |
qcall |
N/A |
i64 |
Extending or Truncating Native Integers
Sometimes it is necessary to reduce a native integer to a 32-bit integer (for example, array or string lengths in TraceMonkey) or extend a 32-bit integer to a native integer.
There are three such opcodes:
LIR_i2q
- Sign-extends a 32-bit integer to a 64-bit integer.LIR_u2q
- Zero-extends a 32-bit integer to a 64-bit integer.LIR_qlo
- Truncates a 64-bit integer to its lower 32 bits.
There are three helper functions in LirWriter
:
ins_i2p
()
- On 32-bit platforms, does nothing. On 64-bit platforms, performs aLIR_i2q
.ins_u2p()
- On 32-bit platforms, does nothing. On 64-bit platforms, performs aLIR_u2q
.ins_p2i()
- On 32-bit platforms, does nothing. On 64-bit platforms, performs aLIR_qlo
.
A common use for extending values is to perform advanced addressing. For example, this code tries to load an index from an integer array, but it is not portable:
lir->insLoad(LIR_ldp, lir->ins2(LIR_piadd, arrayIns, lir->ins2i(LIR_mul, indexIns, sizeof(int)) ), 0);
The SanityFilter will assert on a 64-bit platform because LIR_piadd
(which will be LIR_qiadd
) needs both operands to be 64-bit. The correct code is, assuming the index is unsigned:
lir->insLoad(LIR_ldp, lir->ins2(LIR_piadd, arrayIns, lir->ins_u2p(lir->ins2i(LIR_mul, indexIns, sizeof(int))) ), 0);
What's Pointer-Width in TraceRecorder?
stobj_get_fslot
- Returns jsval-width LInsstobj_get_dslot
- Returns jsval-width LInsstobj_set_dslot
- Stores jsval-width LInsstobj_set_fslot
- Stores jsval-width LInsbox_jsval
- Returns jsval-width LInsunbox_jsval
- Expects jsval-width LIns