64-bit Compatibility

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 value
  • ARGSIZE_I - 32-bit integer
  • ARGSIZE_Q - 64-bit integer
  • ARGSIZE_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 a LIR_i2q.
  • ins_u2p() - On 32-bit platforms, does nothing. On 64-bit platforms, performs a LIR_u2q.
  • ins_p2i() - On 32-bit platforms, does nothing. On 64-bit platforms, performs a LIR_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 LIns
  • stobj_get_dslot - Returns jsval-width LIns
  • stobj_set_dslot - Stores jsval-width LIns
  • stobj_set_fslot - Stores jsval-width LIns
  • box_jsval - Returns jsval-width LIns
  • unbox_jsval - Expects jsval-width LIns