Skip to content

Commit

Permalink
cannon: Finish emulating rest of 64-bit instructions (ethereum-optimi…
Browse files Browse the repository at this point in the history
…sm#12483)

* cannon: Finish emulating rest of 64-bit instructions

This fixes the 64-bit stubs for various instructions (except lld/scd).

* review comments; fix dmult

* add todo

* test div by zero

* add a couple more dmultu tests

* remove dead code

* cannon: Fix remaining mips64 emulation bugs

* fix 64-bit Makefile build script; review comments

* fix build script
  • Loading branch information
Inphi authored and samlaf committed Nov 10, 2024
1 parent d2557e3 commit ad0ec1e
Show file tree
Hide file tree
Showing 26 changed files with 1,009 additions and 95 deletions.
4 changes: 3 additions & 1 deletion cannon/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ elf:
make -C ./testdata/example elf

sanitize-program:
@if ! { mips-linux-gnu-objdump -d -j .text $$GUEST_PROGRAM | awk '{print $3}' | grep -Ew -m1 '(bgezal|bltzal)'; }; then \
@if ! { mips-linux-gnu-objdump -d -j .text $$GUEST_PROGRAM | awk '{print $3}' | grep -Ew -m1 '(bltzal)'; }; then \
echo "guest program is sanitized for unsupported instructions"; \
else \
echo "found unsupported instructions in the guest program"; \
Expand Down Expand Up @@ -93,6 +93,8 @@ fuzz:
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallCloneST ./mipsevm/tests
# Multi-threaded tests
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallCloneMT ./mipsevm/tests
# 64-bit tests
go test $(FUZZLDFLAGS) -tags=cannon64 -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateDmultInsn ./mipsevm/tests

.PHONY: \
cannon32-impl \
Expand Down
1 change: 1 addition & 0 deletions cannon/mipsevm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Supported 63 instructions:
| `Conditional Branch` | `beq` | Branch on equal. |
| `Conditional Branch` | `bgez` | Branch on greater than or equal to zero. |
| `Conditional Branch` | `bgtz` | Branch on greater than zero. |
| `Conditional Branch` | `bgezal` | Branch and link on greater than or equal to zero. |
| `Conditional Branch` | `blez` | Branch on less than or equal to zero. |
| `Conditional Branch` | `bltz` | Branch on less than zero. |
| `Conditional Branch` | `bne` | Branch on not equal. |
Expand Down
2 changes: 2 additions & 0 deletions cannon/mipsevm/arch/arch32.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ const (
SysLlseek = 4140
SysMinCore = 4217
SysTgkill = 4266
SysGetRLimit = 4076
SysLseek = 4019
// Profiling-related syscalls
SysSetITimer = 4104
SysTimerCreate = 4257
Expand Down
12 changes: 8 additions & 4 deletions cannon/mipsevm/arch/arch64.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ const (
AddressMask = 0xFFFFFFFFFFFFFFF8
ExtMask = 0x7

HeapStart = 0x10_00_00_00_00_00_00_00
HeapEnd = 0x60_00_00_00_00_00_00_00
ProgramBreak = 0x40_00_00_00_00_00_00_00
HighMemoryStart = 0x7F_FF_FF_FF_D0_00_00_00
// Ensure virtual address is limited to 48-bits as many user programs assume such to implement packed pointers
// limit 0x00_00_FF_FF_FF_FF_FF_FF
HeapStart = 0x00_00_10_00_00_00_00_00
HeapEnd = 0x00_00_60_00_00_00_00_00
ProgramBreak = 0x00_00_40_00_00_00_00_00
HighMemoryStart = 0x00_00_7F_FF_FF_FF_F0_00
)

// MIPS64 syscall table - https://github.com/torvalds/linux/blob/3efc57369a0ce8f76bf0804f7e673982384e4ac9/arch/mips/kernel/syscalls/syscall_n64.tbl. Generate the syscall numbers using the Makefile in that directory.
Expand Down Expand Up @@ -85,6 +87,8 @@ const (
SysLlseek = UndefinedSysNr
SysMinCore = 5026
SysTgkill = 5225
SysGetRLimit = 5095
SysLseek = 5008
// Profiling-related syscalls
SysSetITimer = 5036
SysTimerCreate = 5216
Expand Down
177 changes: 136 additions & 41 deletions cannon/mipsevm/exec/mips_instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ package exec

import (
"fmt"
"math/bits"

"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/arch"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/memory"

// TODO(#12205): MIPS64 port. Replace with a custom library
u128 "lukechampine.com/uint128"
)

const (
OpLoadLinked = 0x30
OpStoreConditional = 0x38
OpLoadLinked64 = 0x34
OpStoreConditional64 = 0x3c
OpLoadDoubleLeft = 0x1A
OpLoadDoubleRight = 0x1B

// Return address register
RegRA = 31
)

func GetInstructionDetails(pc Word, memory *memory.Memory) (insn, opcode, fun uint32) {
Expand All @@ -31,7 +34,7 @@ func ExecMipsCoreStepLogic(cpu *mipsevm.CpuScalars, registers *[32]Word, memory
if opcode == 2 || opcode == 3 {
linkReg := Word(0)
if opcode == 3 {
linkReg = 31
linkReg = RegRA
}
// Take the top bits of the next PC (its 256 MB region), and concatenate with the 26-bit offset
target := (cpu.NextPC & SignExtend(0xF0000000, 32)) | Word((insn&0x03FFFFFF)<<2)
Expand Down Expand Up @@ -77,15 +80,15 @@ func ExecMipsCoreStepLogic(cpu *mipsevm.CpuScalars, registers *[32]Word, memory
}

if (opcode >= 4 && opcode < 8) || opcode == 1 {
err = HandleBranch(cpu, registers, opcode, insn, rtReg, rs)
err = HandleBranch(cpu, registers, opcode, insn, rtReg, rs, stackTracker)
return
}

storeAddr := ^Word(0)
// memory fetch (all I-type)
// we do the load for stores also
mem := Word(0)
if opcode >= 0x20 {
if opcode >= 0x20 || opcode == OpLoadDoubleLeft || opcode == OpLoadDoubleRight {
// M[R[rs]+SignExtImm]
rs += SignExtendImmediate(insn)
addr := rs & arch.AddressMask
Expand Down Expand Up @@ -311,10 +314,10 @@ func ExecuteMipsInstruction(insn uint32, opcode uint32, fun uint32, rs, rt, mem
case 0x3C: // dsll32
assertMips64(insn)
return rt << (((insn >> 6) & 0x1f) + 32)
case 0x3E: // dsll32
case 0x3E: // dsrl32
assertMips64(insn)
return rt >> (((insn >> 6) & 0x1f) + 32)
case 0x3F: // dsll32
case 0x3F: // dsra32
assertMips64(insn)
return Word(int64(rt) >> (((insn >> 6) & 0x1f) + 32))
default:
Expand Down Expand Up @@ -347,13 +350,25 @@ func ExecuteMipsInstruction(insn uint32, opcode uint32, fun uint32, rs, rt, mem
mask := Word(arch.ExtMask - 1)
return SignExtend((mem>>(msb-uint32(rs&mask)*8))&0xFFFF, 16)
case 0x22: // lwl
val := mem << ((rs & 3) * 8)
mask := Word(uint32(0xFFFFFFFF) << ((rs & 3) * 8))
return SignExtend(((rt & ^mask)|val)&0xFFFFFFFF, 32)
if arch.IsMips32 {
val := mem << ((rs & 3) * 8)
mask := Word(uint32(0xFFFFFFFF) << ((rs & 3) * 8))
return SignExtend(((rt & ^mask)|val)&0xFFFFFFFF, 32)
} else {
// similar to the above mips32 implementation but loads are constrained to the nearest 4-byte memory word
shift := 32 - ((rs & 0x4) << 3)
w := uint32(mem >> shift)
val := uint64(w << ((rs & 3) * 8))
mask := Word(uint32(0xFFFFFFFF) << ((rs & 3) * 8))
return SignExtend(((rt & ^mask)|Word(val))&0xFFFFFFFF, 32)
}
case 0x23: // lw
// TODO(#12205): port to MIPS64
return mem
//return SignExtend((mem>>(32-((rs&0x4)<<3)))&0xFFFFFFFF, 32)
if arch.IsMips32 {
return mem
} else {
// TODO(#12562): Simplify using LoadSubWord
return SignExtend((mem>>(32-((rs&0x4)<<3)))&0xFFFFFFFF, 32)
}
case 0x24: // lbu
msb := uint32(arch.WordSize - 8) // 24 for 32-bit and 56 for 64-bit
return (mem >> (msb - uint32(rs&arch.ExtMask)*8)) & 0xFF
Expand All @@ -362,9 +377,25 @@ func ExecuteMipsInstruction(insn uint32, opcode uint32, fun uint32, rs, rt, mem
mask := Word(arch.ExtMask - 1)
return (mem >> (msb - uint32(rs&mask)*8)) & 0xFFFF
case 0x26: // lwr
val := mem >> (24 - (rs&3)*8)
mask := Word(uint32(0xFFFFFFFF) >> (24 - (rs&3)*8))
return SignExtend(((rt & ^mask)|val)&0xFFFFFFFF, 32)
if arch.IsMips32 {
val := mem >> (24 - (rs&3)*8)
mask := Word(uint32(0xFFFFFFFF) >> (24 - (rs&3)*8))
return SignExtend(((rt & ^mask)|val)&0xFFFFFFFF, 32)
} else {
// similar to the above mips32 implementation but constrained to the nearest 4-byte memory word
shift := 32 - ((rs & 0x4) << 3)
w := uint32(mem >> shift)
val := w >> (24 - (rs&3)*8)
mask := uint32(0xFFFFFFFF) >> (24 - (rs&3)*8)
lwrResult := ((uint32(rt) & ^mask) | val) & 0xFFFFFFFF
if rs&3 == 3 { // loaded bit 31
return SignExtend(Word(lwrResult), 32)
} else {
// NOTE: cannon64 implementation specific: We leave the upper word untouched
rtMask := uint64(0xFF_FF_FF_FF_00_00_00_00)
return (rt & Word(rtMask)) | Word(lwrResult)
}
}
case 0x28: // sb
msb := uint32(arch.WordSize - 8) // 24 for 32-bit and 56 for 64-bit
val := (rt & 0xFF) << (msb - uint32(rs&arch.ExtMask)*8)
Expand All @@ -378,18 +409,45 @@ func ExecuteMipsInstruction(insn uint32, opcode uint32, fun uint32, rs, rt, mem
mask := ^Word(0) ^ Word(0xFFFF<<sl)
return (mem & mask) | val
case 0x2a: // swl
// TODO(#12205): port to MIPS64
val := rt >> ((rs & 3) * 8)
mask := uint32(0xFFFFFFFF) >> ((rs & 3) * 8)
return (mem & Word(^mask)) | val
if arch.IsMips32 {
val := rt >> ((rs & 3) * 8)
mask := uint32(0xFFFFFFFF) >> ((rs & 3) * 8)
return (mem & Word(^mask)) | val
} else {
sr := (rs & 3) << 3
val := ((rt & 0xFFFFFFFF) >> sr) << (32 - ((rs & 0x4) << 3))
mask := (uint64(0xFFFFFFFF) >> sr) << (32 - ((rs & 0x4) << 3))
return (mem & Word(^mask)) | val
}
case 0x2b: // sw
// TODO(#12205): port to MIPS64
return rt
if arch.IsMips32 {
return rt
} else {
sl := 32 - ((rs & 0x4) << 3)
val := (rt & 0xFFFFFFFF) << sl
mask := Word(0xFFFFFFFFFFFFFFFF ^ uint64(0xFFFFFFFF<<sl))
return Word(mem&mask) | Word(val)
}
case 0x2e: // swr
// TODO(#12205): port to MIPS64
val := rt << (24 - (rs&3)*8)
mask := uint32(0xFFFFFFFF) << (24 - (rs&3)*8)
return (mem & Word(^mask)) | val
if arch.IsMips32 {
val := rt << (24 - (rs&3)*8)
mask := uint32(0xFFFFFFFF) << (24 - (rs&3)*8)
return (mem & Word(^mask)) | val
} else {
// similar to the above mips32 implementation but constrained to the nearest 4-byte memory word
shift := 32 - ((rs & 0x4) << 3)
w := uint32(mem >> shift)
val := rt << (24 - (rs&3)*8)
mask := uint32(0xFFFFFFFF) << (24 - (rs&3)*8)
swrResult := (w & ^mask) | uint32(val)
// merge with the untouched bytes in mem
if shift > 0 {
return (Word(swrResult) << 32) | (mem & Word(^uint32(0))) // nolint: staticcheck
} else {
memMask := uint64(0xFF_FF_FF_FF_00_00_00_00)
return (mem & Word(memMask)) | Word(swrResult & ^uint32(0))
}
}

// MIPS64
case 0x1A: // ldl
Expand Down Expand Up @@ -424,10 +482,7 @@ func ExecuteMipsInstruction(insn uint32, opcode uint32, fun uint32, rs, rt, mem
return mem
case 0x3F: // sd
assertMips64(insn)
sl := (rs & 0x7) << 3
val := rt << sl
mask := ^Word(0) << sl
return (mem & ^mask) | val
return rt
default:
panic("invalid instruction")
}
Expand All @@ -446,12 +501,13 @@ func SignExtend(dat Word, idx Word) Word {
}
}

func HandleBranch(cpu *mipsevm.CpuScalars, registers *[32]Word, opcode uint32, insn uint32, rtReg Word, rs Word) error {
func HandleBranch(cpu *mipsevm.CpuScalars, registers *[32]Word, opcode uint32, insn uint32, rtReg Word, rs Word, stackTracker StackTracker) error {
if cpu.NextPC != cpu.PC+4 {
panic("branch in delay slot")
}

shouldBranch := false
linked := false
if opcode == 4 || opcode == 5 { // beq/bne
rt := registers[rtReg]
shouldBranch = (rs == rt && opcode == 4) || (rs != rt && opcode == 5)
Expand All @@ -463,23 +519,32 @@ func HandleBranch(cpu *mipsevm.CpuScalars, registers *[32]Word, opcode uint32, i
// regimm
rtv := (insn >> 16) & 0x1F
if rtv == 0 { // bltz
shouldBranch = int32(rs) < 0
shouldBranch = arch.SignedInteger(rs) < 0
}
if rtv == 1 { // bgez
shouldBranch = int32(rs) >= 0
shouldBranch = arch.SignedInteger(rs) >= 0
}
if rtv == 0x11 { // bgezal (i.e. bal mnemonic)
shouldBranch = arch.SignedInteger(rs) >= 0
registers[RegRA] = cpu.PC + 8 // always set regardless of branch taken
linked = true
}
}

prevPC := cpu.PC
cpu.PC = cpu.NextPC // execute the delay slot first
if shouldBranch {
cpu.NextPC = prevPC + 4 + (SignExtend(Word(insn&0xFFFF), 16) << 2) // then continue with the instruction the branch jumps to.
if linked {
stackTracker.PushStack(prevPC, cpu.NextPC)
}
} else {
cpu.NextPC = cpu.NextPC + 4 // branch not taken
}
return nil
}

// HandleHiLo handles instructions that modify HI and LO registers. It also additionally handles doubleword variable shift operations
func HandleHiLo(cpu *mipsevm.CpuScalars, registers *[32]Word, fun uint32, rs Word, rt Word, storeReg Word) error {
val := Word(0)
switch fun {
Expand Down Expand Up @@ -521,22 +586,44 @@ func HandleHiLo(cpu *mipsevm.CpuScalars, registers *[32]Word, fun uint32, rs Wor
assertMips64Fun(fun)
val = Word(int64(rt) >> (rs & 0x3F))
case 0x1c: // dmult
// TODO(#12205): port to MIPS64. Is signed multiply needed for dmult
assertMips64Fun(fun)
acc := u128.From64(uint64(rs)).Mul(u128.From64(uint64(rt)))
cpu.HI = Word(acc.Hi)
cpu.LO = Word(acc.Lo)
a := int64(rs)
b := int64(rt)
negative := (a < 0) != (b < 0) // set if operands have different signs

// Handle special case for most negative value to avoid overflow in negation
absA := uint64(abs64(a))
absB := uint64(abs64(b))

hi, lo := bits.Mul64(absA, absB)
if negative {
// Two's complement negation: flip all bits and add 1
hi = ^hi
lo = ^lo
if lo == 0xFFFFFFFFFFFFFFFF {
hi++
}
lo++
}
cpu.HI = Word(hi)
cpu.LO = Word(lo)
case 0x1d: // dmultu
assertMips64Fun(fun)
acc := u128.From64(uint64(rs)).Mul(u128.From64(uint64(rt)))
cpu.HI = Word(acc.Hi)
cpu.LO = Word(acc.Lo)
hi, lo := bits.Mul64(uint64(rs), uint64(rt))
cpu.HI = Word(hi)
cpu.LO = Word(lo)
case 0x1e: // ddiv
assertMips64Fun(fun)
if rt == 0 {
panic("instruction divide by zero")
}
cpu.HI = Word(int64(rs) % int64(rt))
cpu.LO = Word(int64(rs) / int64(rt))
case 0x1f: // ddivu
assertMips64Fun(fun)
if rt == 0 {
panic("instruction divide by zero")
}
cpu.HI = rs % rt
cpu.LO = rs / rt
}
Expand Down Expand Up @@ -619,3 +706,11 @@ func calculateSubWordMaskAndOffset(addr Word, byteLength Word) (dataMask, bitOff

return dataMask, bitOffset, bitLength
}

// abs64 returns the absolute value
func abs64(x int64) int64 {
if x < 0 {
return -x
}
return x
}
Loading

0 comments on commit ad0ec1e

Please sign in to comment.