| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package wasm |
| |
| import ( |
| "bytes" |
| "cmd/internal/obj" |
| "cmd/internal/objabi" |
| "cmd/internal/sys" |
| "encoding/binary" |
| "fmt" |
| "io" |
| "math" |
| ) |
| |
| var Register = map[string]int16{ |
| "SP": REG_SP, |
| "CTXT": REG_CTXT, |
| "g": REG_g, |
| "RET0": REG_RET0, |
| "RET1": REG_RET1, |
| "RET2": REG_RET2, |
| "RET3": REG_RET3, |
| "PAUSE": REG_PAUSE, |
| |
| "R0": REG_R0, |
| "R1": REG_R1, |
| "R2": REG_R2, |
| "R3": REG_R3, |
| "R4": REG_R4, |
| "R5": REG_R5, |
| "R6": REG_R6, |
| "R7": REG_R7, |
| "R8": REG_R8, |
| "R9": REG_R9, |
| "R10": REG_R10, |
| "R11": REG_R11, |
| "R12": REG_R12, |
| "R13": REG_R13, |
| "R14": REG_R14, |
| "R15": REG_R15, |
| |
| "F0": REG_F0, |
| "F1": REG_F1, |
| "F2": REG_F2, |
| "F3": REG_F3, |
| "F4": REG_F4, |
| "F5": REG_F5, |
| "F6": REG_F6, |
| "F7": REG_F7, |
| "F8": REG_F8, |
| "F9": REG_F9, |
| "F10": REG_F10, |
| "F11": REG_F11, |
| "F12": REG_F12, |
| "F13": REG_F13, |
| "F14": REG_F14, |
| "F15": REG_F15, |
| |
| "F16": REG_F16, |
| "F17": REG_F17, |
| "F18": REG_F18, |
| "F19": REG_F19, |
| "F20": REG_F20, |
| "F21": REG_F21, |
| "F22": REG_F22, |
| "F23": REG_F23, |
| "F24": REG_F24, |
| "F25": REG_F25, |
| "F26": REG_F26, |
| "F27": REG_F27, |
| "F28": REG_F28, |
| "F29": REG_F29, |
| "F30": REG_F30, |
| "F31": REG_F31, |
| |
| "PC_B": REG_PC_B, |
| } |
| |
| var registerNames []string |
| |
| func init() { |
| obj.RegisterRegister(MINREG, MAXREG, rconv) |
| obj.RegisterOpcode(obj.ABaseWasm, Anames) |
| |
| registerNames = make([]string, MAXREG-MINREG) |
| for name, reg := range Register { |
| registerNames[reg-MINREG] = name |
| } |
| } |
| |
| func rconv(r int) string { |
| return registerNames[r-MINREG] |
| } |
| |
| var unaryDst = map[obj.As]bool{ |
| ASet: true, |
| ATee: true, |
| ACall: true, |
| ACallIndirect: true, |
| ACallImport: true, |
| ABr: true, |
| ABrIf: true, |
| ABrTable: true, |
| AI32Store: true, |
| AI64Store: true, |
| AF32Store: true, |
| AF64Store: true, |
| AI32Store8: true, |
| AI32Store16: true, |
| AI64Store8: true, |
| AI64Store16: true, |
| AI64Store32: true, |
| ACALLNORESUME: true, |
| } |
| |
| var Linkwasm = obj.LinkArch{ |
| Arch: sys.ArchWasm, |
| Init: instinit, |
| Preprocess: preprocess, |
| Assemble: assemble, |
| UnaryDst: unaryDst, |
| } |
| |
| var ( |
| morestack *obj.LSym |
| morestackNoCtxt *obj.LSym |
| gcWriteBarrier *obj.LSym |
| sigpanic *obj.LSym |
| sigpanic0 *obj.LSym |
| deferreturn *obj.LSym |
| jmpdefer *obj.LSym |
| ) |
| |
| const ( |
| /* mark flags */ |
| WasmImport = 1 << 0 |
| ) |
| |
| func instinit(ctxt *obj.Link) { |
| morestack = ctxt.Lookup("runtime.morestack") |
| morestackNoCtxt = ctxt.Lookup("runtime.morestack_noctxt") |
| gcWriteBarrier = ctxt.Lookup("runtime.gcWriteBarrier") |
| sigpanic = ctxt.LookupABI("runtime.sigpanic", obj.ABIInternal) |
| sigpanic0 = ctxt.LookupABI("runtime.sigpanic", 0) // sigpanic called from assembly, which has ABI0 |
| deferreturn = ctxt.LookupABI("runtime.deferreturn", obj.ABIInternal) |
| // jmpdefer is defined in assembly as ABI0, but what we're |
| // looking for is the *call* to jmpdefer from the Go function |
| // deferreturn, so we're looking for the ABIInternal version |
| // of jmpdefer that's called by Go. |
| jmpdefer = ctxt.LookupABI(`"".jmpdefer`, obj.ABIInternal) |
| } |
| |
| func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { |
| appendp := func(p *obj.Prog, as obj.As, args ...obj.Addr) *obj.Prog { |
| if p.As != obj.ANOP { |
| p2 := obj.Appendp(p, newprog) |
| p2.Pc = p.Pc |
| p = p2 |
| } |
| p.As = as |
| switch len(args) { |
| case 0: |
| p.From = obj.Addr{} |
| p.To = obj.Addr{} |
| case 1: |
| if unaryDst[as] { |
| p.From = obj.Addr{} |
| p.To = args[0] |
| } else { |
| p.From = args[0] |
| p.To = obj.Addr{} |
| } |
| case 2: |
| p.From = args[0] |
| p.To = args[1] |
| default: |
| panic("bad args") |
| } |
| return p |
| } |
| |
| framesize := s.Func.Text.To.Offset |
| if framesize < 0 { |
| panic("bad framesize") |
| } |
| s.Func.Args = s.Func.Text.To.Val.(int32) |
| s.Func.Locals = int32(framesize) |
| |
| if s.Func.Text.From.Sym.Wrapper() { |
| // if g._panic != nil && g._panic.argp == FP { |
| // g._panic.argp = bottom-of-frame |
| // } |
| // |
| // MOVD g_panic(g), R0 |
| // Get R0 |
| // I64Eqz |
| // Not |
| // If |
| // Get SP |
| // I64ExtendI32U |
| // I64Const $framesize+8 |
| // I64Add |
| // I64Load panic_argp(R0) |
| // I64Eq |
| // If |
| // MOVD SP, panic_argp(R0) |
| // End |
| // End |
| |
| gpanic := obj.Addr{ |
| Type: obj.TYPE_MEM, |
| Reg: REGG, |
| Offset: 4 * 8, // g_panic |
| } |
| |
| panicargp := obj.Addr{ |
| Type: obj.TYPE_MEM, |
| Reg: REG_R0, |
| Offset: 0, // panic.argp |
| } |
| |
| p := s.Func.Text |
| p = appendp(p, AMOVD, gpanic, regAddr(REG_R0)) |
| |
| p = appendp(p, AGet, regAddr(REG_R0)) |
| p = appendp(p, AI64Eqz) |
| p = appendp(p, ANot) |
| p = appendp(p, AIf) |
| |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AI64ExtendI32U) |
| p = appendp(p, AI64Const, constAddr(framesize+8)) |
| p = appendp(p, AI64Add) |
| p = appendp(p, AI64Load, panicargp) |
| |
| p = appendp(p, AI64Eq) |
| p = appendp(p, AIf) |
| p = appendp(p, AMOVD, regAddr(REG_SP), panicargp) |
| p = appendp(p, AEnd) |
| |
| p = appendp(p, AEnd) |
| } |
| |
| if framesize > 0 { |
| p := s.Func.Text |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AI32Const, constAddr(framesize)) |
| p = appendp(p, AI32Sub) |
| p = appendp(p, ASet, regAddr(REG_SP)) |
| p.Spadj = int32(framesize) |
| } |
| |
| // Introduce resume points for CALL instructions |
| // and collect other explicit resume points. |
| numResumePoints := 0 |
| explicitBlockDepth := 0 |
| pc := int64(0) // pc is only incremented when necessary, this avoids bloat of the BrTable instruction |
| var tableIdxs []uint64 |
| tablePC := int64(0) |
| base := ctxt.PosTable.Pos(s.Func.Text.Pos).Base() |
| for p := s.Func.Text; p != nil; p = p.Link { |
| prevBase := base |
| base = ctxt.PosTable.Pos(p.Pos).Base() |
| switch p.As { |
| case ABlock, ALoop, AIf: |
| explicitBlockDepth++ |
| |
| case AEnd: |
| if explicitBlockDepth == 0 { |
| panic("End without block") |
| } |
| explicitBlockDepth-- |
| |
| case ARESUMEPOINT: |
| if explicitBlockDepth != 0 { |
| panic("RESUME can only be used on toplevel") |
| } |
| p.As = AEnd |
| for tablePC <= pc { |
| tableIdxs = append(tableIdxs, uint64(numResumePoints)) |
| tablePC++ |
| } |
| numResumePoints++ |
| pc++ |
| |
| case obj.ACALL: |
| if explicitBlockDepth != 0 { |
| panic("CALL can only be used on toplevel, try CALLNORESUME instead") |
| } |
| appendp(p, ARESUMEPOINT) |
| } |
| |
| p.Pc = pc |
| |
| // Increase pc whenever some pc-value table needs a new entry. Don't increase it |
| // more often to avoid bloat of the BrTable instruction. |
| // The "base != prevBase" condition detects inlined instructions. They are an |
| // implicit call, so entering and leaving this section affects the stack trace. |
| if p.As == ACALLNORESUME || p.As == obj.ANOP || p.As == ANop || p.Spadj != 0 || base != prevBase { |
| pc++ |
| if p.To.Sym == sigpanic { |
| // The panic stack trace expects the PC at the call of sigpanic, |
| // not the next one. However, runtime.Caller subtracts 1 from the |
| // PC. To make both PC and PC-1 work (have the same line number), |
| // we advance the PC by 2 at sigpanic. |
| pc++ |
| } |
| } |
| } |
| tableIdxs = append(tableIdxs, uint64(numResumePoints)) |
| s.Size = pc + 1 |
| |
| if !s.Func.Text.From.Sym.NoSplit() { |
| p := s.Func.Text |
| |
| if framesize <= objabi.StackSmall { |
| // small stack: SP <= stackguard |
| // Get SP |
| // Get g |
| // I32WrapI64 |
| // I32Load $stackguard0 |
| // I32GtU |
| |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AGet, regAddr(REGG)) |
| p = appendp(p, AI32WrapI64) |
| p = appendp(p, AI32Load, constAddr(2*int64(ctxt.Arch.PtrSize))) // G.stackguard0 |
| p = appendp(p, AI32LeU) |
| } else { |
| // large stack: SP-framesize <= stackguard-StackSmall |
| // SP <= stackguard+(framesize-StackSmall) |
| // Get SP |
| // Get g |
| // I32WrapI64 |
| // I32Load $stackguard0 |
| // I32Const $(framesize-StackSmall) |
| // I32Add |
| // I32GtU |
| |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AGet, regAddr(REGG)) |
| p = appendp(p, AI32WrapI64) |
| p = appendp(p, AI32Load, constAddr(2*int64(ctxt.Arch.PtrSize))) // G.stackguard0 |
| p = appendp(p, AI32Const, constAddr(int64(framesize)-objabi.StackSmall)) |
| p = appendp(p, AI32Add) |
| p = appendp(p, AI32LeU) |
| } |
| // TODO(neelance): handle wraparound case |
| |
| p = appendp(p, AIf) |
| p = appendp(p, obj.ACALL, constAddr(0)) |
| if s.Func.Text.From.Sym.NeedCtxt() { |
| p.To = obj.Addr{Type: obj.TYPE_MEM, Name: obj.NAME_EXTERN, Sym: morestack} |
| } else { |
| p.To = obj.Addr{Type: obj.TYPE_MEM, Name: obj.NAME_EXTERN, Sym: morestackNoCtxt} |
| } |
| p = appendp(p, AEnd) |
| } |
| |
| // record the branches targeting the entry loop and the unwind exit, |
| // their targets with be filled in later |
| var entryPointLoopBranches []*obj.Prog |
| var unwindExitBranches []*obj.Prog |
| currentDepth := 0 |
| for p := s.Func.Text; p != nil; p = p.Link { |
| switch p.As { |
| case ABlock, ALoop, AIf: |
| currentDepth++ |
| case AEnd: |
| currentDepth-- |
| } |
| |
| switch p.As { |
| case obj.AJMP: |
| jmp := *p |
| p.As = obj.ANOP |
| |
| if jmp.To.Type == obj.TYPE_BRANCH { |
| // jump to basic block |
| p = appendp(p, AI32Const, constAddr(jmp.To.Val.(*obj.Prog).Pc)) |
| p = appendp(p, ASet, regAddr(REG_PC_B)) // write next basic block to PC_B |
| p = appendp(p, ABr) // jump to beginning of entryPointLoop |
| entryPointLoopBranches = append(entryPointLoopBranches, p) |
| break |
| } |
| |
| // low-level WebAssembly call to function |
| switch jmp.To.Type { |
| case obj.TYPE_MEM: |
| if !notUsePC_B[jmp.To.Sym.Name] { |
| // Set PC_B parameter to function entry. |
| p = appendp(p, AI32Const, constAddr(0)) |
| } |
| p = appendp(p, ACall, jmp.To) |
| |
| case obj.TYPE_NONE: |
| // (target PC is on stack) |
| p = appendp(p, AI32WrapI64) |
| p = appendp(p, AI32Const, constAddr(16)) // only needs PC_F bits (16-31), PC_B bits (0-15) are zero |
| p = appendp(p, AI32ShrU) |
| |
| // Set PC_B parameter to function entry. |
| // We need to push this before pushing the target PC_F, |
| // so temporarily pop PC_F, using our REG_PC_B as a |
| // scratch register, and push it back after pushing 0. |
| p = appendp(p, ASet, regAddr(REG_PC_B)) |
| p = appendp(p, AI32Const, constAddr(0)) |
| p = appendp(p, AGet, regAddr(REG_PC_B)) |
| |
| p = appendp(p, ACallIndirect) |
| |
| default: |
| panic("bad target for JMP") |
| } |
| |
| p = appendp(p, AReturn) |
| |
| case obj.ACALL, ACALLNORESUME: |
| call := *p |
| p.As = obj.ANOP |
| |
| pcAfterCall := call.Link.Pc |
| if call.To.Sym == sigpanic { |
| pcAfterCall-- // sigpanic expects to be called without advancing the pc |
| } |
| |
| // jmpdefer manipulates the return address on the stack so deferreturn gets called repeatedly. |
| // Model this in WebAssembly with a loop. |
| if call.To.Sym == deferreturn { |
| p = appendp(p, ALoop) |
| } |
| |
| // SP -= 8 |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AI32Const, constAddr(8)) |
| p = appendp(p, AI32Sub) |
| p = appendp(p, ASet, regAddr(REG_SP)) |
| |
| // write return address to Go stack |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AI64Const, obj.Addr{ |
| Type: obj.TYPE_ADDR, |
| Name: obj.NAME_EXTERN, |
| Sym: s, // PC_F |
| Offset: pcAfterCall, // PC_B |
| }) |
| p = appendp(p, AI64Store, constAddr(0)) |
| |
| // low-level WebAssembly call to function |
| switch call.To.Type { |
| case obj.TYPE_MEM: |
| if !notUsePC_B[call.To.Sym.Name] { |
| // Set PC_B parameter to function entry. |
| p = appendp(p, AI32Const, constAddr(0)) |
| } |
| p = appendp(p, ACall, call.To) |
| |
| case obj.TYPE_NONE: |
| // (target PC is on stack) |
| p = appendp(p, AI32WrapI64) |
| p = appendp(p, AI32Const, constAddr(16)) // only needs PC_F bits (16-31), PC_B bits (0-15) are zero |
| p = appendp(p, AI32ShrU) |
| |
| // Set PC_B parameter to function entry. |
| // We need to push this before pushing the target PC_F, |
| // so temporarily pop PC_F, using our PC_B as a |
| // scratch register, and push it back after pushing 0. |
| p = appendp(p, ASet, regAddr(REG_PC_B)) |
| p = appendp(p, AI32Const, constAddr(0)) |
| p = appendp(p, AGet, regAddr(REG_PC_B)) |
| |
| p = appendp(p, ACallIndirect) |
| |
| default: |
| panic("bad target for CALL") |
| } |
| |
| // gcWriteBarrier has no return value, it never unwinds the stack |
| if call.To.Sym == gcWriteBarrier { |
| break |
| } |
| |
| // jmpdefer removes the frame of deferreturn from the Go stack. |
| // However, its WebAssembly function still returns normally, |
| // so we need to return from deferreturn without removing its |
| // stack frame (no RET), because the frame is already gone. |
| if call.To.Sym == jmpdefer { |
| p = appendp(p, AReturn) |
| break |
| } |
| |
| // return value of call is on the top of the stack, indicating whether to unwind the WebAssembly stack |
| if call.As == ACALLNORESUME && call.To.Sym != sigpanic && call.To.Sym != sigpanic0 { // sigpanic unwinds the stack, but it never resumes |
| // trying to unwind WebAssembly stack but call has no resume point, terminate with error |
| p = appendp(p, AIf) |
| p = appendp(p, obj.AUNDEF) |
| p = appendp(p, AEnd) |
| } else { |
| // unwinding WebAssembly stack to switch goroutine, return 1 |
| p = appendp(p, ABrIf) |
| unwindExitBranches = append(unwindExitBranches, p) |
| } |
| |
| // jump to before the call if jmpdefer has reset the return address to the call's PC |
| if call.To.Sym == deferreturn { |
| // get PC_B from -8(SP) |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AI32Const, constAddr(8)) |
| p = appendp(p, AI32Sub) |
| p = appendp(p, AI32Load16U, constAddr(0)) |
| p = appendp(p, ATee, regAddr(REG_PC_B)) |
| |
| p = appendp(p, AI32Const, constAddr(call.Pc)) |
| p = appendp(p, AI32Eq) |
| p = appendp(p, ABrIf, constAddr(0)) |
| p = appendp(p, AEnd) // end of Loop |
| } |
| |
| case obj.ARET, ARETUNWIND: |
| ret := *p |
| p.As = obj.ANOP |
| |
| if framesize > 0 { |
| // SP += framesize |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AI32Const, constAddr(framesize)) |
| p = appendp(p, AI32Add) |
| p = appendp(p, ASet, regAddr(REG_SP)) |
| // TODO(neelance): This should theoretically set Spadj, but it only works without. |
| // p.Spadj = int32(-framesize) |
| } |
| |
| if ret.To.Type == obj.TYPE_MEM { |
| // Set PC_B parameter to function entry. |
| p = appendp(p, AI32Const, constAddr(0)) |
| |
| // low-level WebAssembly call to function |
| p = appendp(p, ACall, ret.To) |
| p = appendp(p, AReturn) |
| break |
| } |
| |
| // SP += 8 |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, AI32Const, constAddr(8)) |
| p = appendp(p, AI32Add) |
| p = appendp(p, ASet, regAddr(REG_SP)) |
| |
| if ret.As == ARETUNWIND { |
| // function needs to unwind the WebAssembly stack, return 1 |
| p = appendp(p, AI32Const, constAddr(1)) |
| p = appendp(p, AReturn) |
| break |
| } |
| |
| // not unwinding the WebAssembly stack, return 0 |
| p = appendp(p, AI32Const, constAddr(0)) |
| p = appendp(p, AReturn) |
| } |
| } |
| |
| for p := s.Func.Text; p != nil; p = p.Link { |
| switch p.From.Name { |
| case obj.NAME_AUTO: |
| p.From.Offset += int64(framesize) |
| case obj.NAME_PARAM: |
| p.From.Reg = REG_SP |
| p.From.Offset += int64(framesize) + 8 // parameters are after the frame and the 8-byte return address |
| } |
| |
| switch p.To.Name { |
| case obj.NAME_AUTO: |
| p.To.Offset += int64(framesize) |
| case obj.NAME_PARAM: |
| p.To.Reg = REG_SP |
| p.To.Offset += int64(framesize) + 8 // parameters are after the frame and the 8-byte return address |
| } |
| |
| switch p.As { |
| case AGet: |
| if p.From.Type == obj.TYPE_ADDR { |
| get := *p |
| p.As = obj.ANOP |
| |
| switch get.From.Name { |
| case obj.NAME_EXTERN: |
| p = appendp(p, AI64Const, get.From) |
| case obj.NAME_AUTO, obj.NAME_PARAM: |
| p = appendp(p, AGet, regAddr(get.From.Reg)) |
| if get.From.Reg == REG_SP { |
| p = appendp(p, AI64ExtendI32U) |
| } |
| if get.From.Offset != 0 { |
| p = appendp(p, AI64Const, constAddr(get.From.Offset)) |
| p = appendp(p, AI64Add) |
| } |
| default: |
| panic("bad Get: invalid name") |
| } |
| } |
| |
| case AI32Load, AI64Load, AF32Load, AF64Load, AI32Load8S, AI32Load8U, AI32Load16S, AI32Load16U, AI64Load8S, AI64Load8U, AI64Load16S, AI64Load16U, AI64Load32S, AI64Load32U: |
| if p.From.Type == obj.TYPE_MEM { |
| as := p.As |
| from := p.From |
| |
| p.As = AGet |
| p.From = regAddr(from.Reg) |
| |
| if from.Reg != REG_SP { |
| p = appendp(p, AI32WrapI64) |
| } |
| |
| p = appendp(p, as, constAddr(from.Offset)) |
| } |
| |
| case AMOVB, AMOVH, AMOVW, AMOVD: |
| mov := *p |
| p.As = obj.ANOP |
| |
| var loadAs obj.As |
| var storeAs obj.As |
| switch mov.As { |
| case AMOVB: |
| loadAs = AI64Load8U |
| storeAs = AI64Store8 |
| case AMOVH: |
| loadAs = AI64Load16U |
| storeAs = AI64Store16 |
| case AMOVW: |
| loadAs = AI64Load32U |
| storeAs = AI64Store32 |
| case AMOVD: |
| loadAs = AI64Load |
| storeAs = AI64Store |
| } |
| |
| appendValue := func() { |
| switch mov.From.Type { |
| case obj.TYPE_CONST: |
| p = appendp(p, AI64Const, constAddr(mov.From.Offset)) |
| |
| case obj.TYPE_ADDR: |
| switch mov.From.Name { |
| case obj.NAME_NONE, obj.NAME_PARAM, obj.NAME_AUTO: |
| p = appendp(p, AGet, regAddr(mov.From.Reg)) |
| if mov.From.Reg == REG_SP { |
| p = appendp(p, AI64ExtendI32U) |
| } |
| p = appendp(p, AI64Const, constAddr(mov.From.Offset)) |
| p = appendp(p, AI64Add) |
| case obj.NAME_EXTERN: |
| p = appendp(p, AI64Const, mov.From) |
| default: |
| panic("bad name for MOV") |
| } |
| |
| case obj.TYPE_REG: |
| p = appendp(p, AGet, mov.From) |
| if mov.From.Reg == REG_SP { |
| p = appendp(p, AI64ExtendI32U) |
| } |
| |
| case obj.TYPE_MEM: |
| p = appendp(p, AGet, regAddr(mov.From.Reg)) |
| if mov.From.Reg != REG_SP { |
| p = appendp(p, AI32WrapI64) |
| } |
| p = appendp(p, loadAs, constAddr(mov.From.Offset)) |
| |
| default: |
| panic("bad MOV type") |
| } |
| } |
| |
| switch mov.To.Type { |
| case obj.TYPE_REG: |
| appendValue() |
| if mov.To.Reg == REG_SP { |
| p = appendp(p, AI32WrapI64) |
| } |
| p = appendp(p, ASet, mov.To) |
| |
| case obj.TYPE_MEM: |
| switch mov.To.Name { |
| case obj.NAME_NONE, obj.NAME_PARAM: |
| p = appendp(p, AGet, regAddr(mov.To.Reg)) |
| if mov.To.Reg != REG_SP { |
| p = appendp(p, AI32WrapI64) |
| } |
| case obj.NAME_EXTERN: |
| p = appendp(p, AI32Const, obj.Addr{Type: obj.TYPE_ADDR, Name: obj.NAME_EXTERN, Sym: mov.To.Sym}) |
| default: |
| panic("bad MOV name") |
| } |
| appendValue() |
| p = appendp(p, storeAs, constAddr(mov.To.Offset)) |
| |
| default: |
| panic("bad MOV type") |
| } |
| |
| case ACallImport: |
| p.As = obj.ANOP |
| p = appendp(p, AGet, regAddr(REG_SP)) |
| p = appendp(p, ACall, obj.Addr{Type: obj.TYPE_MEM, Name: obj.NAME_EXTERN, Sym: s}) |
| p.Mark = WasmImport |
| } |
| } |
| |
| { |
| p := s.Func.Text |
| if len(unwindExitBranches) > 0 { |
| p = appendp(p, ABlock) // unwindExit, used to return 1 when unwinding the stack |
| for _, b := range unwindExitBranches { |
| b.To = obj.Addr{Type: obj.TYPE_BRANCH, Val: p} |
| } |
| } |
| if len(entryPointLoopBranches) > 0 { |
| p = appendp(p, ALoop) // entryPointLoop, used to jump between basic blocks |
| for _, b := range entryPointLoopBranches { |
| b.To = obj.Addr{Type: obj.TYPE_BRANCH, Val: p} |
| } |
| } |
| if numResumePoints > 0 { |
| // Add Block instructions for resume points and BrTable to jump to selected resume point. |
| for i := 0; i < numResumePoints+1; i++ { |
| p = appendp(p, ABlock) |
| } |
| p = appendp(p, AGet, regAddr(REG_PC_B)) // read next basic block from PC_B |
| p = appendp(p, ABrTable, obj.Addr{Val: tableIdxs}) |
| p = appendp(p, AEnd) // end of Block |
| } |
| for p.Link != nil { |
| p = p.Link // function instructions |
| } |
| if len(entryPointLoopBranches) > 0 { |
| p = appendp(p, AEnd) // end of entryPointLoop |
| } |
| p = appendp(p, obj.AUNDEF) |
| if len(unwindExitBranches) > 0 { |
| p = appendp(p, AEnd) // end of unwindExit |
| p = appendp(p, AI32Const, constAddr(1)) |
| } |
| } |
| |
| currentDepth = 0 |
| blockDepths := make(map[*obj.Prog]int) |
| for p := s.Func.Text; p != nil; p = p.Link { |
| switch p.As { |
| case ABlock, ALoop, AIf: |
| currentDepth++ |
| blockDepths[p] = currentDepth |
| case AEnd: |
| currentDepth-- |
| } |
| |
| switch p.As { |
| case ABr, ABrIf: |
| if p.To.Type == obj.TYPE_BRANCH { |
| blockDepth, ok := blockDepths[p.To.Val.(*obj.Prog)] |
| if !ok { |
| panic("label not at block") |
| } |
| p.To = constAddr(int64(currentDepth - blockDepth)) |
| } |
| } |
| } |
| } |
| |
| func constAddr(value int64) obj.Addr { |
| return obj.Addr{Type: obj.TYPE_CONST, Offset: value} |
| } |
| |
| func regAddr(reg int16) obj.Addr { |
| return obj.Addr{Type: obj.TYPE_REG, Reg: reg} |
| } |
| |
| // Most of the Go functions has a single parameter (PC_B) in |
| // Wasm ABI. This is a list of exceptions. |
| var notUsePC_B = map[string]bool{ |
| "_rt0_wasm_js": true, |
| "wasm_export_run": true, |
| "wasm_export_resume": true, |
| "wasm_export_getsp": true, |
| "wasm_pc_f_loop": true, |
| "runtime.wasmMove": true, |
| "runtime.wasmZero": true, |
| "runtime.wasmDiv": true, |
| "runtime.wasmTruncS": true, |
| "runtime.wasmTruncU": true, |
| "runtime.gcWriteBarrier": true, |
| "cmpbody": true, |
| "memeqbody": true, |
| "memcmp": true, |
| "memchr": true, |
| } |
| |
| func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { |
| type regVar struct { |
| global bool |
| index uint64 |
| } |
| |
| type varDecl struct { |
| count uint64 |
| typ valueType |
| } |
| |
| hasLocalSP := false |
| regVars := [MAXREG - MINREG]*regVar{ |
| REG_SP - MINREG: {true, 0}, |
| REG_CTXT - MINREG: {true, 1}, |
| REG_g - MINREG: {true, 2}, |
| REG_RET0 - MINREG: {true, 3}, |
| REG_RET1 - MINREG: {true, 4}, |
| REG_RET2 - MINREG: {true, 5}, |
| REG_RET3 - MINREG: {true, 6}, |
| REG_PAUSE - MINREG: {true, 7}, |
| } |
| var varDecls []*varDecl |
| useAssemblyRegMap := func() { |
| for i := int16(0); i < 16; i++ { |
| regVars[REG_R0+i-MINREG] = ®Var{false, uint64(i)} |
| } |
| } |
| |
| // Function starts with declaration of locals: numbers and types. |
| // Some functions use a special calling convention. |
| switch s.Name { |
| case "_rt0_wasm_js", "wasm_export_run", "wasm_export_resume", "wasm_export_getsp", "wasm_pc_f_loop", |
| "runtime.wasmMove", "runtime.wasmZero", "runtime.wasmDiv", "runtime.wasmTruncS", "runtime.wasmTruncU", "memeqbody": |
| varDecls = []*varDecl{} |
| useAssemblyRegMap() |
| case "memchr", "memcmp": |
| varDecls = []*varDecl{{count: 2, typ: i32}} |
| useAssemblyRegMap() |
| case "cmpbody": |
| varDecls = []*varDecl{{count: 2, typ: i64}} |
| useAssemblyRegMap() |
| case "runtime.gcWriteBarrier": |
| varDecls = []*varDecl{{count: 4, typ: i64}} |
| useAssemblyRegMap() |
| default: |
| // Normal calling convention: PC_B as WebAssembly parameter. First local variable is local SP cache. |
| regVars[REG_PC_B-MINREG] = ®Var{false, 0} |
| hasLocalSP = true |
| |
| var regUsed [MAXREG - MINREG]bool |
| for p := s.Func.Text; p != nil; p = p.Link { |
| if p.From.Reg != 0 { |
| regUsed[p.From.Reg-MINREG] = true |
| } |
| if p.To.Reg != 0 { |
| regUsed[p.To.Reg-MINREG] = true |
| } |
| } |
| |
| regs := []int16{REG_SP} |
| for reg := int16(REG_R0); reg <= REG_F31; reg++ { |
| if regUsed[reg-MINREG] { |
| regs = append(regs, reg) |
| } |
| } |
| |
| var lastDecl *varDecl |
| for i, reg := range regs { |
| t := regType(reg) |
| if lastDecl == nil || lastDecl.typ != t { |
| lastDecl = &varDecl{ |
| count: 0, |
| typ: t, |
| } |
| varDecls = append(varDecls, lastDecl) |
| } |
| lastDecl.count++ |
| if reg != REG_SP { |
| regVars[reg-MINREG] = ®Var{false, 1 + uint64(i)} |
| } |
| } |
| } |
| |
| w := new(bytes.Buffer) |
| |
| writeUleb128(w, uint64(len(varDecls))) |
| for _, decl := range varDecls { |
| writeUleb128(w, decl.count) |
| w.WriteByte(byte(decl.typ)) |
| } |
| |
| if hasLocalSP { |
| // Copy SP from its global variable into a local variable. Accessing a local variable is more efficient. |
| updateLocalSP(w) |
| } |
| |
| for p := s.Func.Text; p != nil; p = p.Link { |
| switch p.As { |
| case AGet: |
| if p.From.Type != obj.TYPE_REG { |
| panic("bad Get: argument is not a register") |
| } |
| reg := p.From.Reg |
| v := regVars[reg-MINREG] |
| if v == nil { |
| panic("bad Get: invalid register") |
| } |
| if reg == REG_SP && hasLocalSP { |
| writeOpcode(w, ALocalGet) |
| writeUleb128(w, 1) // local SP |
| continue |
| } |
| if v.global { |
| writeOpcode(w, AGlobalGet) |
| } else { |
| writeOpcode(w, ALocalGet) |
| } |
| writeUleb128(w, v.index) |
| continue |
| |
| case ASet: |
| if p.To.Type != obj.TYPE_REG { |
| panic("bad Set: argument is not a register") |
| } |
| reg := p.To.Reg |
| v := regVars[reg-MINREG] |
| if v == nil { |
| panic("bad Set: invalid register") |
| } |
| if reg == REG_SP && hasLocalSP { |
| writeOpcode(w, ALocalTee) |
| writeUleb128(w, 1) // local SP |
| } |
| if v.global { |
| writeOpcode(w, AGlobalSet) |
| } else { |
| if p.Link.As == AGet && p.Link.From.Reg == reg { |
| writeOpcode(w, ALocalTee) |
| p = p.Link |
| } else { |
| writeOpcode(w, ALocalSet) |
| } |
| } |
| writeUleb128(w, v.index) |
| continue |
| |
| case ATee: |
| if p.To.Type != obj.TYPE_REG { |
| panic("bad Tee: argument is not a register") |
| } |
| reg := p.To.Reg |
| v := regVars[reg-MINREG] |
| if v == nil { |
| panic("bad Tee: invalid register") |
| } |
| writeOpcode(w, ALocalTee) |
| writeUleb128(w, v.index) |
| continue |
| |
| case ANot: |
| writeOpcode(w, AI32Eqz) |
| continue |
| |
| case obj.AUNDEF: |
| writeOpcode(w, AUnreachable) |
| continue |
| |
| case obj.ANOP, obj.ATEXT, obj.AFUNCDATA, obj.APCDATA: |
| // ignore |
| continue |
| } |
| |
| writeOpcode(w, p.As) |
| |
| switch p.As { |
| case ABlock, ALoop, AIf: |
| if p.From.Offset != 0 { |
| // block type, rarely used, e.g. for code compiled with emscripten |
| w.WriteByte(0x80 - byte(p.From.Offset)) |
| continue |
| } |
| w.WriteByte(0x40) |
| |
| case ABr, ABrIf: |
| if p.To.Type != obj.TYPE_CONST { |
| panic("bad Br/BrIf") |
| } |
| writeUleb128(w, uint64(p.To.Offset)) |
| |
| case ABrTable: |
| idxs := p.To.Val.([]uint64) |
| writeUleb128(w, uint64(len(idxs)-1)) |
| for _, idx := range idxs { |
| writeUleb128(w, idx) |
| } |
| |
| case ACall: |
| switch p.To.Type { |
| case obj.TYPE_CONST: |
| writeUleb128(w, uint64(p.To.Offset)) |
| |
| case obj.TYPE_MEM: |
| if p.To.Name != obj.NAME_EXTERN && p.To.Name != obj.NAME_STATIC { |
| fmt.Println(p.To) |
| panic("bad name for Call") |
| } |
| r := obj.Addrel(s) |
| r.Off = int32(w.Len()) |
| r.Type = objabi.R_CALL |
| if p.Mark&WasmImport != 0 { |
| r.Type = objabi.R_WASMIMPORT |
| } |
| r.Sym = p.To.Sym |
| if hasLocalSP { |
| // The stack may have moved, which changes SP. Update the local SP variable. |
| updateLocalSP(w) |
| } |
| |
| default: |
| panic("bad type for Call") |
| } |
| |
| case ACallIndirect: |
| writeUleb128(w, uint64(p.To.Offset)) |
| w.WriteByte(0x00) // reserved value |
| if hasLocalSP { |
| // The stack may have moved, which changes SP. Update the local SP variable. |
| updateLocalSP(w) |
| } |
| |
| case AI32Const, AI64Const: |
| if p.From.Name == obj.NAME_EXTERN { |
| r := obj.Addrel(s) |
| r.Off = int32(w.Len()) |
| r.Type = objabi.R_ADDR |
| r.Sym = p.From.Sym |
| r.Add = p.From.Offset |
| break |
| } |
| writeSleb128(w, p.From.Offset) |
| |
| case AF32Const: |
| b := make([]byte, 4) |
| binary.LittleEndian.PutUint32(b, math.Float32bits(float32(p.From.Val.(float64)))) |
| w.Write(b) |
| |
| case AF64Const: |
| b := make([]byte, 8) |
| binary.LittleEndian.PutUint64(b, math.Float64bits(p.From.Val.(float64))) |
| w.Write(b) |
| |
| case AI32Load, AI64Load, AF32Load, AF64Load, AI32Load8S, AI32Load8U, AI32Load16S, AI32Load16U, AI64Load8S, AI64Load8U, AI64Load16S, AI64Load16U, AI64Load32S, AI64Load32U: |
| if p.From.Offset < 0 { |
| panic("negative offset for *Load") |
| } |
| if p.From.Type != obj.TYPE_CONST { |
| panic("bad type for *Load") |
| } |
| if p.From.Offset > math.MaxUint32 { |
| ctxt.Diag("bad offset in %v", p) |
| } |
| writeUleb128(w, align(p.As)) |
| writeUleb128(w, uint64(p.From.Offset)) |
| |
| case AI32Store, AI64Store, AF32Store, AF64Store, AI32Store8, AI32Store16, AI64Store8, AI64Store16, AI64Store32: |
| if p.To.Offset < 0 { |
| panic("negative offset") |
| } |
| if p.From.Offset > math.MaxUint32 { |
| ctxt.Diag("bad offset in %v", p) |
| } |
| writeUleb128(w, align(p.As)) |
| writeUleb128(w, uint64(p.To.Offset)) |
| |
| case ACurrentMemory, AGrowMemory: |
| w.WriteByte(0x00) |
| |
| } |
| } |
| |
| w.WriteByte(0x0b) // end |
| |
| s.P = w.Bytes() |
| } |
| |
| func updateLocalSP(w *bytes.Buffer) { |
| writeOpcode(w, AGlobalGet) |
| writeUleb128(w, 0) // global SP |
| writeOpcode(w, ALocalSet) |
| writeUleb128(w, 1) // local SP |
| } |
| |
| func writeOpcode(w *bytes.Buffer, as obj.As) { |
| switch { |
| case as < AUnreachable: |
| panic(fmt.Sprintf("unexpected assembler op: %s", as)) |
| case as < AEnd: |
| w.WriteByte(byte(as - AUnreachable + 0x00)) |
| case as < ADrop: |
| w.WriteByte(byte(as - AEnd + 0x0B)) |
| case as < ALocalGet: |
| w.WriteByte(byte(as - ADrop + 0x1A)) |
| case as < AI32Load: |
| w.WriteByte(byte(as - ALocalGet + 0x20)) |
| case as < AI32TruncSatF32S: |
| w.WriteByte(byte(as - AI32Load + 0x28)) |
| case as < ALast: |
| w.WriteByte(0xFC) |
| w.WriteByte(byte(as - AI32TruncSatF32S + 0x00)) |
| default: |
| panic(fmt.Sprintf("unexpected assembler op: %s", as)) |
| } |
| } |
| |
| type valueType byte |
| |
| const ( |
| i32 valueType = 0x7F |
| i64 valueType = 0x7E |
| f32 valueType = 0x7D |
| f64 valueType = 0x7C |
| ) |
| |
| func regType(reg int16) valueType { |
| switch { |
| case reg == REG_SP: |
| return i32 |
| case reg >= REG_R0 && reg <= REG_R15: |
| return i64 |
| case reg >= REG_F0 && reg <= REG_F15: |
| return f32 |
| case reg >= REG_F16 && reg <= REG_F31: |
| return f64 |
| default: |
| panic("invalid register") |
| } |
| } |
| |
| func align(as obj.As) uint64 { |
| switch as { |
| case AI32Load8S, AI32Load8U, AI64Load8S, AI64Load8U, AI32Store8, AI64Store8: |
| return 0 |
| case AI32Load16S, AI32Load16U, AI64Load16S, AI64Load16U, AI32Store16, AI64Store16: |
| return 1 |
| case AI32Load, AF32Load, AI64Load32S, AI64Load32U, AI32Store, AF32Store, AI64Store32: |
| return 2 |
| case AI64Load, AF64Load, AI64Store, AF64Store: |
| return 3 |
| default: |
| panic("align: bad op") |
| } |
| } |
| |
| func writeUleb128(w io.ByteWriter, v uint64) { |
| if v < 128 { |
| w.WriteByte(uint8(v)) |
| return |
| } |
| more := true |
| for more { |
| c := uint8(v & 0x7f) |
| v >>= 7 |
| more = v != 0 |
| if more { |
| c |= 0x80 |
| } |
| w.WriteByte(c) |
| } |
| } |
| |
| func writeSleb128(w io.ByteWriter, v int64) { |
| more := true |
| for more { |
| c := uint8(v & 0x7f) |
| s := uint8(v & 0x40) |
| v >>= 7 |
| more = !((v == 0 && s == 0) || (v == -1 && s != 0)) |
| if more { |
| c |= 0x80 |
| } |
| w.WriteByte(c) |
| } |
| } |