Repository: cilium/ebpf
Pull request #1193 (2023-11-01 08:00:14)

Add new ISAv4 instructions
The [proposal](https://lore.kernel.org/bpf/4bfe98be-5333-1c7e-2f6d-42486c8ec039@meta.com/) for a new version of the eBPF instruction set has been accepted and merged in both LLVM 18 and linux kernel v6.6. So we might be seeing these new instructions in the near future.

This PR adds support for these new instructions. Before this PR we could still parse the instructions but did not recognize the variations added:

```
myfunc:
         0: SwapLE dst: r1 imm: 16
         1: SwapLE dst: r2 imm: 32
         2: SwapLE dst: r3 imm: 64
         3: LdXMode(128)B 
         4: LdXMode(128)H 
         5: LdXMode(128)W 
         6: LdXMode(128)B 
         7: LdXMode(128)H 
         8: MovReg dst: r1 src: r4
         9: MovReg dst: r2 src: r5
        10: MovReg dst: r3 src: r6
        11: Mov32Reg dst: r1 src: r3
        12: Mov32Reg dst: r2 src: r4
        13: InvalidJumpOp32Imm dst: r0 off: 0 imm: 4
        14: DivReg dst: r1 src: r3
        15: ModReg dst: r2 src: r4
        16: Div32Reg dst: r1 src: r3
        17: Mod32Reg dst: r2 src: r4
```

After this PR we correctly understand the new instructions:

```
myfunc:
        0: BSwap16 dst: r1 
        1: BSwap32 dst: r2 
        2: BSwap64 dst: r3 
        3: LdXMemSXB dst: r1 src: r4 off: 0 imm: 0
        4: LdXMemSXH dst: r2 src: r5 off: 4 imm: 0
        5: LdXMemSXW dst: r3 src: r6 off: 8 imm: 0
        6: LdXMemSXB dst: r1 src: r4 off: 0 imm: 0
        7: LdXMemSXH dst: r2 src: r5 off: 4 imm: 0
        8: SMov8Reg dst: r1 src: r4
        9: SMov16Reg dst: r2 src: r5
        10: SMov32Reg dst: r3 src: r6
        11: SMov8Reg32 dst: r1 src: r3
        12: SMov16Reg32 dst: r2 src: r4
        13: Ja32 imm: 4
        14: SDivReg dst: r1 src: r3
        15: SModReg dst: r2 src: r4
        16: SDivReg32 dst: r1 src: r3
        17: SModReg32 dst: r2 src: r4
```

Fixes: #1188 

dylandreimerink: > Basically, make OpCode wider so that we can represent all the logical opcodes. For simplicity we'd also change the various *Op to be uint16. We'd then handle converting to and from OpCode in Instruction.Marshal and Unmarshal, similar to what we do for dword loads. This way ExtendedALUOp is not required anymore and SMod / SDiv can be ALUOp. Finally we can also guarantee that InvalidOpCode really is invalid.
>
>This is basically free at runtime since we don't increase the size of Instruction. I even think that we can arrange it so that existing users won't break (besides having to throw in a uint16 cast maybe).
>
>What do you think?

Yes, that feels less hacky than what I have so far :+1: 

Patch:
diff --git a/asm/alu.go b/asm/alu.go
index 7dc5620..31b1e4c 100644
--- a/asm/alu.go
+++ b/asm/alu.go
@@ -4,23 +4,23 @@ package asm
 
 // Source of ALU / ALU64 / Branch operations
 //
-//	msb      lsb
-//	+----+-+---+
-//	|op  |S|cls|
-//	+----+-+---+
-type Source uint8
+//	msb              lsb
+//	+------------+-+---+
+//	|     op     |S|cls|
+//	+------------+-+---+
+type Source uint16
 
-const sourceMask OpCode = 0x08
+const sourceMask OpCode = 0x0008
 
 // Source bitmask
 const (
 	// InvalidSource is returned by getters when invoked
 	// on non ALU / branch OpCodes.
-	InvalidSource Source = 0xff
+	InvalidSource Source = 0xffff
 	// ImmSource src is from constant
-	ImmSource Source = 0x00
+	ImmSource Source = 0x0000
 	// RegSource src is from register
-	RegSource Source = 0x08
+	RegSource Source = 0x0008
 )
 
 // The Endianness of a byte swap instruction.
@@ -39,46 +39,56 @@ const (
 
 // ALUOp are ALU / ALU64 operations
 //
-//	msb      lsb
-//	+----+-+---+
-//	|OP  |s|cls|
-//	+----+-+---+
-type ALUOp uint8
+//	msb              lsb
+//	+-------+----+-+---+
+//	|  EXT  | OP |s|cls|
+//	+-------+----+-+---+
+type ALUOp uint16
 
-const aluMask OpCode = 0xf0
+const aluMask OpCode = 0xfff0
 
 const (
 	// InvalidALUOp is returned by getters when invoked
 	// on non ALU OpCodes
 	InvalidALUOp ALUOp = 0xff
 	// Add - addition
-	Add ALUOp = 0x00
+	Add ALUOp = 0x0000
 	// Sub - subtraction
-	Sub ALUOp = 0x10
+	Sub ALUOp = 0x0010
 	// Mul - multiplication
-	Mul ALUOp = 0x20
+	Mul ALUOp = 0x0020
 	// Div - division
-	Div ALUOp = 0x30
+	Div ALUOp = 0x0030
+	// SDiv - signed division
+	SDiv ALUOp = 0x0130
 	// Or - bitwise or
-	Or ALUOp = 0x40
+	Or ALUOp = 0x0040
 	// And - bitwise and
-	And ALUOp = 0x50
+	And ALUOp = 0x0050
 	// LSh - bitwise shift left
-	LSh ALUOp = 0x60
+	LSh ALUOp = 0x0060
 	// RSh - bitwise shift right
-	RSh ALUOp = 0x70
+	RSh ALUOp = 0x0070
 	// Neg - sign/unsign signing bit
-	Neg ALUOp = 0x80
+	Neg ALUOp = 0x0080
 	// Mod - modulo
-	Mod ALUOp = 0x90
+	Mod ALUOp = 0x0090
+	// SMod - signed modulo
+	SMod ALUOp = 0x0190
 	// Xor - bitwise xor
-	Xor ALUOp = 0xa0
+	Xor ALUOp = 0x00a0
 	// Mov - move value from one place to another
-	Mov ALUOp = 0xb0
+	Mov ALUOp = 0x00b0
+	// SMov8 - move lower 8 bits, sign extended upper bits of target
+	SMov8 ALUOp = 0x08b0
+	// SMov16 - move lower 16 bits, sign extended upper bits of target
+	SMov16 ALUOp = 0x10b0
+	// SMov32 - move lower 32 bits, sign extended upper bits of target
+	SMov32 ALUOp = 0x20b0
 	// ArSh - arithmetic shift
-	ArSh ALUOp = 0xc0
+	ArSh ALUOp = 0x00c0
 	// Swap - endian conversions
-	Swap ALUOp = 0xd0
+	Swap ALUOp = 0x00d0
 )
 
 // HostTo converts from host to another endianness.
@@ -102,6 +112,27 @@ func HostTo(endian Endianness, dst Register, size Size) Instruction {
 	}
 }
 
+// BSwap unconditionally reverses the order of bytes in a register.
+func BSwap(dst Register, size Size) Instruction {
+	var imm int64
+	switch size {
+	case Half:
+		imm = 16
+	case Word:
+		imm = 32
+	case DWord:
+		imm = 64
+	default:
+		return Instruction{OpCode: InvalidOpCode}
+	}
+
+	return Instruction{
+		OpCode:   OpCode(ALU64Class).SetALUOp(Swap),
+		Dst:      dst,
+		Constant: imm,
+	}
+}
+
 // Op returns the OpCode for an ALU operation with a given source.
 func (op ALUOp) Op(source Source) OpCode {
 	return OpCode(ALU64Class).SetALUOp(op).SetSource(source)
diff --git a/asm/alu_string.go b/asm/alu_string.go
index 72d3fe6..8ce8d8a 100644
--- a/asm/alu_string.go
+++ b/asm/alu_string.go
@@ -8,7 +8,7 @@ func _() {
 	// An "invalid array index" compiler error signifies that the constant values have changed.
 	// Re-run the stringer command to generate them again.
 	var x [1]struct{}
-	_ = x[InvalidSource-255]
+	_ = x[InvalidSource-65535]
 	_ = x[ImmSource-0]
 	_ = x[RegSource-8]
 }
@@ -25,7 +25,7 @@ func (i Source) String() string {
 		return _Source_name_0
 	case i == 8:
 		return _Source_name_1
-	case i == 255:
+	case i == 65535:
 		return _Source_name_2
 	default:
 		return "Source(" + strconv.FormatInt(int64(i), 10) + ")"
@@ -67,36 +67,46 @@ func _() {
 	_ = x[Sub-16]
 	_ = x[Mul-32]
 	_ = x[Div-48]
+	_ = x[SDiv-304]
 	_ = x[Or-64]
 	_ = x[And-80]
 	_ = x[LSh-96]
 	_ = x[RSh-112]
 	_ = x[Neg-128]
 	_ = x[Mod-144]
+	_ = x[SMod-400]
 	_ = x[Xor-160]
 	_ = x[Mov-176]
+	_ = x[SMov8-2224]
+	_ = x[SMov16-4272]
+	_ = x[SMov32-8368]
 	_ = x[ArSh-192]
 	_ = x[Swap-208]
 }
 
-const _ALUOp_name = "AddSubMulDivOrAndLShRShNegModXorMovArShSwapInvalidALUOp"
+const _ALUOp_name = "AddSubMulDivOrAndLShRShNegModXorMovArShSwapInvalidALUOpSDivSModSMov8SMov16SMov32"
 
 var _ALUOp_map = map[ALUOp]string{
-	0:   _ALUOp_name[0:3],
-	16:  _ALUOp_name[3:6],
-	32:  _ALUOp_name[6:9],
-	48:  _ALUOp_name[9:12],
-	64:  _ALUOp_name[12:14],
-	80:  _ALUOp_name[14:17],
-	96:  _ALUOp_name[17:20],
-	112: _ALUOp_name[20:23],
-	128: _ALUOp_name[23:26],
-	144: _ALUOp_name[26:29],
-	160: _ALUOp_name[29:32],
-	176: _ALUOp_name[32:35],
-	192: _ALUOp_name[35:39],
-	208: _ALUOp_name[39:43],
-	255: _ALUOp_name[43:55],
+	0:    _ALUOp_name[0:3],
+	16:   _ALUOp_name[3:6],
+	32:   _ALUOp_name[6:9],
+	48:   _ALUOp_name[9:12],
+	64:   _ALUOp_name[12:14],
+	80:   _ALUOp_name[14:17],
+	96:   _ALUOp_name[17:20],
+	112:  _ALUOp_name[20:23],
+	128:  _ALUOp_name[23:26],
+	144:  _ALUOp_name[26:29],
+	160:  _ALUOp_name[29:32],
+	176:  _ALUOp_name[32:35],
+	192:  _ALUOp_name[35:39],
+	208:  _ALUOp_name[39:43],
+	255:  _ALUOp_name[43:55],
+	304:  _ALUOp_name[55:59],
+	400:  _ALUOp_name[59:63],
+	2224: _ALUOp_name[63:68],
+	4272: _ALUOp_name[68:74],
+	8368: _ALUOp_name[74:80],
 }
 
 func (i ALUOp) String() string {
diff --git a/asm/instruction.go b/asm/instruction.go
index ef01eaa..d21a21a 100644
--- a/asm/instruction.go
+++ b/asm/instruction.go
@@ -60,6 +60,34 @@ func (ins *Instruction) Unmarshal(r io.Reader, bo binary.ByteOrder) (uint64, err
 	}
 
 	ins.Offset = int16(bo.Uint16(data[2:4]))
+
+	if ins.OpCode.Class().IsALU() {
+		switch ins.OpCode.ALUOp() {
+		case Div:
+			if ins.Offset == 1 {
+				ins.OpCode = ins.OpCode.SetALUOp(SDiv)
+				ins.Offset = 0
+			}
+		case Mod:
+			if ins.Offset == 1 {
+				ins.OpCode = ins.OpCode.SetALUOp(SMod)
+				ins.Offset = 0
+			}
+		case Mov:
+			switch ins.Offset {
+			case 8:
+				ins.OpCode = ins.OpCode.SetALUOp(SMov8)
+				ins.Offset = 0
+			case 16:
+				ins.OpCode = ins.OpCode.SetALUOp(SMov16)
+				ins.Offset = 0
+			case 32:
+				ins.OpCode = ins.OpCode.SetALUOp(SMov32)
+				ins.Offset = 0
+			}
+		}
+	}
+
 	// Convert to int32 before widening to int64
 	// to ensure the signed bit is carried over.
 	ins.Constant = int64(int32(bo.Uint32(data[4:8])))
@@ -106,6 +134,26 @@ func (ins Instruction) Marshal(w io.Writer, bo binary.ByteOrder) (uint64, error)
 		return 0, fmt.Errorf("can't marshal registers: %s", err)
 	}
 
+	if ins.OpCode.Class().IsALU() {
+		switch ins.OpCode.ALUOp() {
+		case SDiv:
+			ins.OpCode = ins.OpCode.SetALUOp(Div)
+			ins.Offset = 1
+		case SMod:
+			ins.OpCode = ins.OpCode.SetALUOp(Mod)
+			ins.Offset = 1
+		case SMov8:
+			ins.OpCode = ins.OpCode.SetALUOp(Mov)
+			ins.Offset = 8
+		case SMov16:
+			ins.OpCode = ins.OpCode.SetALUOp(Mov)
+			ins.Offset = 16
+		case SMov32:
+			ins.OpCode = ins.OpCode.SetALUOp(Mov)
+			ins.Offset = 32
+		}
+	}
+
 	data := make([]byte, InstructionSize)
 	data[0] = byte(ins.OpCode)
 	data[1] = byte(regs)
@@ -298,9 +346,9 @@ func (ins Instruction) Format(f fmt.State, c rune) {
 		goto ref
 	}
 
-	fmt.Fprintf(f, "%v ", op)
 	switch cls := op.Class(); {
 	case cls.isLoadOrStore():
+		fmt.Fprintf(f, "%v ", op)
 		switch op.Mode() {
 		case ImmMode:
 			fmt.Fprintf(f, "dst: %s imm: %d", ins.Dst, ins.Constant)
@@ -308,21 +356,30 @@ func (ins Instruction) Format(f fmt.State, c rune) {
 			fmt.Fprintf(f, "imm: %d", ins.Constant)
 		case IndMode:
 			fmt.Fprintf(f, "dst: %s src: %s imm: %d", ins.Dst, ins.Src, ins.Constant)
-		case MemMode:
+		case MemMode, MemSXMode:
 			fmt.Fprintf(f, "dst: %s src: %s off: %d imm: %d", ins.Dst, ins.Src, ins.Offset, ins.Constant)
 		case XAddMode:
 			fmt.Fprintf(f, "dst: %s src: %s", ins.Dst, ins.Src)
 		}
 
 	case cls.IsALU():
-		fmt.Fprintf(f, "dst: %s ", ins.Dst)
-		if op.ALUOp() == Swap || op.Source() == ImmSource {
+		fmt.Fprintf(f, "%v", op)
+		if op == Swap.Op(ImmSource) {
+			fmt.Fprintf(f, "%d", ins.Constant)
+		}
+
+		fmt.Fprintf(f, " dst: %s ", ins.Dst)
+		switch {
+		case op.ALUOp() == Swap:
+			break
+		case op.Source() == ImmSource:
 			fmt.Fprintf(f, "imm: %d", ins.Constant)
-		} else {
+		default:
 			fmt.Fprintf(f, "src: %s", ins.Src)
 		}
 
 	case cls.IsJump():
+		fmt.Fprintf(f, "%v ", op)
 		switch jop := op.JumpOp(); jop {
 		case Call:
 			switch ins.Src {
@@ -336,6 +393,13 @@ func (ins Instruction) Format(f fmt.State, c rune) {
 				fmt.Fprint(f, BuiltinFunc(ins.Constant))
 			}
 
+		case Ja:
+			if ins.OpCode.Class() == Jump32Class {
+				fmt.Fprintf(f, "imm: %d", ins.Constant)
+			} else {
+				fmt.Fprintf(f, "off: %d", ins.Offset)
+			}
+
 		default:
 			fmt.Fprintf(f, "dst: %s off: %d ", ins.Dst, ins.Offset)
 			if op.Source() == ImmSource {
@@ -344,6 +408,8 @@ func (ins Instruction) Format(f fmt.State, c rune) {
 				fmt.Fprintf(f, "src: %s", ins.Src)
 			}
 		}
+	default:
+		fmt.Fprintf(f, "%v ", op)
 	}
 
 ref:
@@ -772,7 +838,8 @@ func (insns Instructions) encodeFunctionReferences() error {
 		}
 
 		switch {
-		case ins.IsFunctionReference() && ins.Constant == -1:
+		case ins.IsFunctionReference() && ins.Constant == -1,
+			ins.OpCode == Ja.opCode(Jump32Class, ImmSource) && ins.Constant == -1:
 			symOffset, ok := symbolOffsets[ins.Reference()]
 			if !ok {
 				return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference(), ErrUnsatisfiedProgramReference)
diff --git a/asm/jump.go b/asm/jump.go
index 9a525b2..0c69ec9 100644
--- a/asm/jump.go
+++ b/asm/jump.go
@@ -103,13 +103,21 @@ func (op JumpOp) Reg32(dst, src Register, label string) Instruction {
 }
 
 func (op JumpOp) opCode(class Class, source Source) OpCode {
-	if op == Exit || op == Call || op == Ja {
+	if op == Exit || op == Call {
 		return InvalidOpCode
 	}
 
 	return OpCode(class).SetJumpOp(op).SetSource(source)
 }
 
+// LongJump returns a jump always instruction with a range of [-2^31, 2^31 - 1].
+func LongJump(label string) Instruction {
+	return Instruction{
+		OpCode:   Ja.opCode(Jump32Class, ImmSource),
+		Constant: -1,
+	}.WithReference(label)
+}
+
 // Label adjusts PC to the address of the label.
 func (op JumpOp) Label(label string) Instruction {
 	if op == Call {
diff --git a/asm/load_store.go b/asm/load_store.go
index 574ee37..cdb5c5c 100644
--- a/asm/load_store.go
+++ b/asm/load_store.go
@@ -24,6 +24,8 @@ const (
 	IndMode Mode = 0x40
 	// MemMode - load from memory
 	MemMode Mode = 0x60
+	// MemSXMode - load from memory, sign extension
+	MemSXMode Mode = 0x80
 	// XAddMode - add atomically across processors.
 	XAddMode Mode = 0xc0
 )
@@ -73,6 +75,11 @@ func LoadMemOp(size Size) OpCode {
 	return OpCode(LdXClass).SetMode(MemMode).SetSize(size)
 }
 
+// LoadMemSXOp returns the OpCode to load a value of given size from memory sign extended.
+func LoadMemSXOp(size Size) OpCode {
+	return OpCode(LdXClass).SetMode(MemSXMode).SetSize(size)
+}
+
 // LoadMem emits `dst = *(size *)(src + offset)`.
 func LoadMem(dst, src Register, offset int16, size Size) Instruction {
 	return Instruction{
@@ -83,6 +90,20 @@ func LoadMem(dst, src Register, offset int16, size Size) Instruction {
 	}
 }
 
+// LoadMemSX emits `dst = *(size *)(src + offset)` but sign extends dst.
+func LoadMemSX(dst, src Register, offset int16, size Size) Instruction {
+	if size == DWord {
+		return Instruction{OpCode: InvalidOpCode}
+	}
+
+	return Instruction{
+		OpCode: LoadMemSXOp(size),
+		Dst:    dst,
+		Src:    src,
+		Offset: offset,
+	}
+}
+
 // LoadImmOp returns the OpCode to load an immediate of given size.
 //
 // As of kernel 4.20, only DWord size is accepted.
diff --git a/asm/load_store_string.go b/asm/load_store_string.go
index 76d29a0..c480803 100644
--- a/asm/load_store_string.go
+++ b/asm/load_store_string.go
@@ -13,6 +13,7 @@ func _() {
 	_ = x[AbsMode-32]
 	_ = x[IndMode-64]
 	_ = x[MemMode-96]
+	_ = x[MemSXMode-128]
 	_ = x[XAddMode-192]
 }
 
@@ -21,8 +22,9 @@ const (
 	_Mode_name_1 = "AbsMode"
 	_Mode_name_2 = "IndMode"
 	_Mode_name_3 = "MemMode"
-	_Mode_name_4 = "XAddMode"
-	_Mode_name_5 = "InvalidMode"
+	_Mode_name_4 = "MemSXMode"
+	_Mode_name_5 = "XAddMode"
+	_Mode_name_6 = "InvalidMode"
 )
 
 func (i Mode) String() string {
@@ -35,10 +37,12 @@ func (i Mode) String() string {
 		return _Mode_name_2
 	case i == 96:
 		return _Mode_name_3
-	case i == 192:
+	case i == 128:
 		return _Mode_name_4
-	case i == 255:
+	case i == 192:
 		return _Mode_name_5
+	case i == 255:
+		return _Mode_name_6
 	default:
 		return "Mode(" + strconv.FormatInt(int64(i), 10) + ")"
 	}
diff --git a/asm/opcode.go b/asm/opcode.go
index 845c552..3d40849 100644
--- a/asm/opcode.go
+++ b/asm/opcode.go
@@ -66,18 +66,32 @@ func (cls Class) isJumpOrALU() bool {
 	return cls.IsJump() || cls.IsALU()
 }
 
-// OpCode is a packed eBPF opcode.
+// OpCode represents a single operation.
+// It is not a 1:1 mapping to real eBPF opcodes.
 //
-// Its encoding is defined by a Class value:
+// The encoding varies based on a 3-bit Class:
 //
-//	msb      lsb
-//	+----+-+---+
-//	| ???? |CLS|
-//	+----+-+---+
-type OpCode uint8
+//	7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+//	            ???           | CLS
+//
+// For ALUClass and ALUCLass32:
+//
+//	7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+//	           OPC          |S| CLS
+//
+// For LdClass, LdXclass, StClass and StXClass:
+//
+//	7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+//	        0       | MDE |SIZ| CLS
+//
+// For JumpClass, Jump32Class:
+//
+//	7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+//	        0       |  OPC  |S| CLS
+type OpCode uint16
 
 // InvalidOpCode is returned by setters on OpCode
-const InvalidOpCode OpCode = 0xff
+const InvalidOpCode OpCode = 0xffff
 
 // rawInstructions returns the number of BPF instructions required
 // to encode this opcode.
@@ -147,7 +161,7 @@ func (op OpCode) JumpOp() JumpOp {
 	jumpOp := JumpOp(op & jumpMask)
 
 	// Some JumpOps are only supported by JumpClass, not Jump32Class.
-	if op.Class() == Jump32Class && (jumpOp == Exit || jumpOp == Call || jumpOp == Ja) {
+	if op.Class() == Jump32Class && (jumpOp == Exit || jumpOp == Call) {
 		return InvalidJumpOp
 	}
 
@@ -234,17 +248,24 @@ func (op OpCode) String() string {
 		}
 
 	case class.IsALU():
+		if op.ALUOp() == Swap && op.Class() == ALU64Class {
+			// B to make BSwap, uncontitional byte swap
+			f.WriteString("B")
+		}
+
 		f.WriteString(op.ALUOp().String())
 
 		if op.ALUOp() == Swap {
-			// Width for Endian is controlled by Constant
-			f.WriteString(op.Endianness().String())
+			if op.Class() == ALUClass {
+				// Width for Endian is controlled by Constant
+				f.WriteString(op.Endianness().String())
+			}
 		} else {
+			f.WriteString(strings.TrimSuffix(op.Source().String(), "Source"))
+
 			if class == ALUClass {
 				f.WriteString("32")
 			}
-
-			f.WriteString(strings.TrimSuffix(op.Source().String(), "Source"))
 		}
 
 	case class.IsJump():
@@ -254,7 +275,7 @@ func (op OpCode) String() string {
 			f.WriteString("32")
 		}
 
-		if jop := op.JumpOp(); jop != Exit && jop != Call {
+		if jop := op.JumpOp(); jop != Exit && jop != Call && jop != Ja {
 			f.WriteString(strings.TrimSuffix(op.Source().String(), "Source"))
 		}
 


Test patch:
diff --git a/asm/instruction_test.go b/asm/instruction_test.go
index 41ad18a..653e607 100644
--- a/asm/instruction_test.go
+++ b/asm/instruction_test.go
@@ -339,3 +339,97 @@ type testFDer int
 func (t testFDer) FD() int {
 	return int(t)
 }
+
+func TestISAv4(t *testing.T) {
+	rawInsns := []byte{
+		0xd7, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, // r1 = bswap16 r1
+		0xd7, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, // r2 = bswap32 r2
+		0xd7, 0x03, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // r3 = bswap64 r3
+
+		0x91, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // r1 = *(s8 *)(r4 + 0x0)
+		0x89, 0x52, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // r2 = *(s16 *)(r5 + 0x4)
+		0x81, 0x63, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // r3 = *(s32 *)(r6 + 0x8)
+
+		0x91, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // r1 = *(s8 *)(r4 + 0x0)
+		0x89, 0x52, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // r2 = *(s16 *)(r5 + 0x4)
+
+		0xbf, 0x41, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // r1 = (s8)r4
+		0xbf, 0x52, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // r2 = (s16)r5
+		0xbf, 0x63, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, // r3 = (s32)r6
+
+		0xbc, 0x31, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // w1 = (s8)w3
+		0xbc, 0x42, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // w2 = (s16)w4
+
+		0x06, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, // gotol +3
+
+		0x3f, 0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // r1 s/= r3
+		0x9f, 0x42, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // r2 s%= r4
+
+		0x3c, 0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // w1 s/= w3
+		0x9c, 0x42, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // w2 s%= w4
+	}
+
+	var insns Instructions
+	err := insns.Unmarshal(bytes.NewReader(rawInsns), binary.LittleEndian)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	lines := []string{
+		"BSwap16 dst: r1 ",
+		"BSwap32 dst: r2 ",
+		"BSwap64 dst: r3 ",
+		"LdXMemSXB dst: r1 src: r4 off: 0 imm: 0",
+		"LdXMemSXH dst: r2 src: r5 off: 4 imm: 0",
+		"LdXMemSXW dst: r3 src: r6 off: 8 imm: 0",
+		"LdXMemSXB dst: r1 src: r4 off: 0 imm: 0",
+		"LdXMemSXH dst: r2 src: r5 off: 4 imm: 0",
+		"SMov8Reg dst: r1 src: r4",
+		"SMov16Reg dst: r2 src: r5",
+		"SMov32Reg dst: r3 src: r6",
+		"SMov8Reg32 dst: r1 src: r3",
+		"SMov16Reg32 dst: r2 src: r4",
+		"Ja32 imm: 3",
+		"SDivReg dst: r1 src: r3",
+		"SModReg dst: r2 src: r4",
+		"SDivReg32 dst: r1 src: r3",
+		"SModReg32 dst: r2 src: r4",
+	}
+
+	for i, ins := range insns {
+		if want, got := lines[i], fmt.Sprint(ins); want != got {
+			t.Errorf("Expected %q, got %q", want, got)
+		}
+	}
+
+	// Marshal and unmarshal again to make sure the instructions are
+	// still valid.
+	var buf bytes.Buffer
+	err = insns.Marshal(&buf, binary.LittleEndian)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !bytes.Equal(buf.Bytes(), rawInsns) {
+		t.Error("Expected instructions to be equal after marshalling")
+	}
+}
+
+func TestLongJumpPatching(t *testing.T) {
+	insns := Instructions{
+		LongJump("exit"),
+		Xor.Reg(R0, R0),
+		Xor.Reg(R0, R0),
+		Xor.Reg(R0, R0),
+		Return().WithSymbol("exit"),
+	}
+
+	err := insns.encodeFunctionReferences()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if insns[0].Constant != 3 {
+		t.Errorf("Expected offset to be 3, got %d", insns[1].Constant)
+	}
+}
diff --git a/asm/opcode_test.go b/asm/opcode_test.go
index 4e55727..f6f7673 100644
--- a/asm/opcode_test.go
+++ b/asm/opcode_test.go
@@ -22,18 +22,17 @@ func TestGetSetJumpOp(t *testing.T) {
 		})
 	}
 
-	// Exit, call and JA aren't allowed with Jump32
+	// Exit and call aren't allowed with Jump32
 	test(Jump32Class, Exit, false)
 	test(Jump32Class, Call, false)
-	test(Jump32Class, Ja, false)
 
 	// But are with Jump
 	test(JumpClass, Exit, true)
 	test(JumpClass, Call, true)
-	test(JumpClass, Ja, true)
 
 	// All other ops work
 	for _, op := range []JumpOp{
+		Ja,
 		JEq,
 		JGT,
 		JGE,