mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-01 09:37:48 +00:00
105fe9b25d
This required a few compiler and runtime tricks to work, but I ran a bunch of tests and it seems fine. (CI will of course do more exhaustive testing). The main benefit here is that we don't need to maintain the darwin version of the syscall package, and reduce extra risks for bugs (because we reuse the well-tested syscall package). For example, Go 1.23 needed a bunch of new constants in the syscall package. That would have been avoided if we had used the native syscall package on MacOS.
394 lines
15 KiB
Go
394 lines
15 KiB
Go
package compiler
|
|
|
|
// This file implements the syscall.Syscall and syscall.Syscall6 instructions as
|
|
// compiler builtins.
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/ssa"
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
// createRawSyscall creates a system call with the provided system call number
|
|
// and returns the result as a single integer (the system call result). The
|
|
// result is not further interpreted (with the exception of MIPS to use the same
|
|
// return value everywhere).
|
|
func (b *builder) createRawSyscall(call *ssa.CallCommon) (llvm.Value, error) {
|
|
num := b.getValue(call.Args[0], getPos(call))
|
|
switch {
|
|
case b.GOARCH == "amd64" && b.GOOS == "linux":
|
|
// Sources:
|
|
// https://stackoverflow.com/a/2538212
|
|
// https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux#syscall
|
|
args := []llvm.Value{num}
|
|
argTypes := []llvm.Type{b.uintptrType}
|
|
// Constraints will look something like:
|
|
// "={rax},0,{rdi},{rsi},{rdx},{r10},{r8},{r9},~{rcx},~{r11}"
|
|
constraints := "={rax},0"
|
|
for i, arg := range call.Args[1:] {
|
|
constraints += "," + [...]string{
|
|
"{rdi}",
|
|
"{rsi}",
|
|
"{rdx}",
|
|
"{r10}",
|
|
"{r8}",
|
|
"{r9}",
|
|
}[i]
|
|
llvmValue := b.getValue(arg, getPos(call))
|
|
args = append(args, llvmValue)
|
|
argTypes = append(argTypes, llvmValue.Type())
|
|
}
|
|
// rcx and r11 are clobbered by the syscall, so make sure they are not used
|
|
constraints += ",~{rcx},~{r11}"
|
|
fnType := llvm.FunctionType(b.uintptrType, argTypes, false)
|
|
target := llvm.InlineAsm(fnType, "syscall", constraints, true, false, llvm.InlineAsmDialectIntel, false)
|
|
return b.CreateCall(fnType, target, args, ""), nil
|
|
|
|
case b.GOARCH == "386" && b.GOOS == "linux":
|
|
// Sources:
|
|
// syscall(2) man page
|
|
// https://stackoverflow.com/a/2538212
|
|
// https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux#int_0x80
|
|
args := []llvm.Value{num}
|
|
argTypes := []llvm.Type{b.uintptrType}
|
|
// Constraints will look something like:
|
|
// "={eax},0,{ebx},{ecx},{edx},{esi},{edi},{ebp}"
|
|
constraints := "={eax},0"
|
|
for i, arg := range call.Args[1:] {
|
|
constraints += "," + [...]string{
|
|
"{ebx}",
|
|
"{ecx}",
|
|
"{edx}",
|
|
"{esi}",
|
|
"{edi}",
|
|
"{ebp}",
|
|
}[i]
|
|
llvmValue := b.getValue(arg, getPos(call))
|
|
args = append(args, llvmValue)
|
|
argTypes = append(argTypes, llvmValue.Type())
|
|
}
|
|
fnType := llvm.FunctionType(b.uintptrType, argTypes, false)
|
|
target := llvm.InlineAsm(fnType, "int 0x80", constraints, true, false, llvm.InlineAsmDialectIntel, false)
|
|
return b.CreateCall(fnType, target, args, ""), nil
|
|
|
|
case b.GOARCH == "arm" && b.GOOS == "linux":
|
|
// Implement the EABI system call convention for Linux.
|
|
// Source: syscall(2) man page.
|
|
args := []llvm.Value{}
|
|
argTypes := []llvm.Type{}
|
|
// Constraints will look something like:
|
|
// ={r0},0,{r1},{r2},{r7},~{r3}
|
|
constraints := "={r0}"
|
|
for i, arg := range call.Args[1:] {
|
|
constraints += "," + [...]string{
|
|
"0", // tie to output
|
|
"{r1}",
|
|
"{r2}",
|
|
"{r3}",
|
|
"{r4}",
|
|
"{r5}",
|
|
"{r6}",
|
|
}[i]
|
|
llvmValue := b.getValue(arg, getPos(call))
|
|
args = append(args, llvmValue)
|
|
argTypes = append(argTypes, llvmValue.Type())
|
|
}
|
|
args = append(args, num)
|
|
argTypes = append(argTypes, b.uintptrType)
|
|
constraints += ",{r7}" // syscall number
|
|
for i := len(call.Args) - 1; i < 4; i++ {
|
|
// r0-r3 get clobbered after the syscall returns
|
|
constraints += ",~{r" + strconv.Itoa(i) + "}"
|
|
}
|
|
fnType := llvm.FunctionType(b.uintptrType, argTypes, false)
|
|
target := llvm.InlineAsm(fnType, "svc #0", constraints, true, false, 0, false)
|
|
return b.CreateCall(fnType, target, args, ""), nil
|
|
|
|
case b.GOARCH == "arm64" && b.GOOS == "linux":
|
|
// Source: syscall(2) man page.
|
|
args := []llvm.Value{}
|
|
argTypes := []llvm.Type{}
|
|
// Constraints will look something like:
|
|
// ={x0},0,{x1},{x2},{x8},~{x3},~{x4},~{x5},~{x6},~{x7},~{x16},~{x17}
|
|
constraints := "={x0}"
|
|
for i, arg := range call.Args[1:] {
|
|
constraints += "," + [...]string{
|
|
"0", // tie to output
|
|
"{x1}",
|
|
"{x2}",
|
|
"{x3}",
|
|
"{x4}",
|
|
"{x5}",
|
|
}[i]
|
|
llvmValue := b.getValue(arg, getPos(call))
|
|
args = append(args, llvmValue)
|
|
argTypes = append(argTypes, llvmValue.Type())
|
|
}
|
|
args = append(args, num)
|
|
argTypes = append(argTypes, b.uintptrType)
|
|
constraints += ",{x8}" // syscall number
|
|
for i := len(call.Args) - 1; i < 8; i++ {
|
|
// x0-x7 may get clobbered during the syscall following the aarch64
|
|
// calling convention.
|
|
constraints += ",~{x" + strconv.Itoa(i) + "}"
|
|
}
|
|
constraints += ",~{x16},~{x17}" // scratch registers
|
|
fnType := llvm.FunctionType(b.uintptrType, argTypes, false)
|
|
target := llvm.InlineAsm(fnType, "svc #0", constraints, true, false, 0, false)
|
|
return b.CreateCall(fnType, target, args, ""), nil
|
|
|
|
case (b.GOARCH == "mips" || b.GOARCH == "mipsle") && b.GOOS == "linux":
|
|
// Implement the system call convention for Linux.
|
|
// Source: syscall(2) man page and musl:
|
|
// https://git.musl-libc.org/cgit/musl/tree/arch/mips/syscall_arch.h
|
|
// Also useful:
|
|
// https://web.archive.org/web/20220529105937/https://www.linux-mips.org/wiki/Syscall
|
|
// The syscall number goes in r2, the result also in r2.
|
|
// Register r7 is both an input parameter and an output parameter: if it
|
|
// is non-zero, the system call failed and r2 is the error code.
|
|
// The code below implements the O32 syscall ABI, not the N32 ABI. It
|
|
// could implement both at the same time if needed (like what appears to
|
|
// be done in musl) by forcing arg5-arg7 into the right registers but
|
|
// letting the compiler decide the registers should result in _slightly_
|
|
// faster and smaller code.
|
|
args := []llvm.Value{num}
|
|
argTypes := []llvm.Type{b.uintptrType}
|
|
constraints := "={$2},={$7},0"
|
|
syscallParams := call.Args[1:]
|
|
if len(syscallParams) > 7 {
|
|
// There is one syscall that uses 7 parameters: sync_file_range.
|
|
// But only 7, not more. Go however only has Syscall6 and Syscall9.
|
|
// Therefore, we can ignore the remaining parameters.
|
|
syscallParams = syscallParams[:7]
|
|
}
|
|
for i, arg := range syscallParams {
|
|
constraints += "," + [...]string{
|
|
"{$4}", // arg1
|
|
"{$5}", // arg2
|
|
"{$6}", // arg3
|
|
"1", // arg4, error return
|
|
"r", // arg5 on the stack
|
|
"r", // arg6 on the stack
|
|
"r", // arg7 on the stack
|
|
}[i]
|
|
llvmValue := b.getValue(arg, getPos(call))
|
|
args = append(args, llvmValue)
|
|
argTypes = append(argTypes, llvmValue.Type())
|
|
}
|
|
// Create assembly code.
|
|
// Parameters beyond the first 4 are passed on the stack instead of in
|
|
// registers in the O32 syscall ABI.
|
|
// We need ".set noat" because LLVM might pick register $1 ($at) as the
|
|
// register for a parameter and apparently this is not allowed on MIPS
|
|
// unless you use this specific pragma.
|
|
asm := "syscall"
|
|
switch len(syscallParams) {
|
|
case 5:
|
|
asm = "" +
|
|
".set noat\n" +
|
|
"subu $$sp, $$sp, 32\n" +
|
|
"sw $7, 16($$sp)\n" + // arg5
|
|
"syscall\n" +
|
|
"addu $$sp, $$sp, 32\n" +
|
|
".set at\n"
|
|
case 6:
|
|
asm = "" +
|
|
".set noat\n" +
|
|
"subu $$sp, $$sp, 32\n" +
|
|
"sw $7, 16($$sp)\n" + // arg5
|
|
"sw $8, 20($$sp)\n" + // arg6
|
|
"syscall\n" +
|
|
"addu $$sp, $$sp, 32\n" +
|
|
".set at\n"
|
|
case 7:
|
|
asm = "" +
|
|
".set noat\n" +
|
|
"subu $$sp, $$sp, 32\n" +
|
|
"sw $7, 16($$sp)\n" + // arg5
|
|
"sw $8, 20($$sp)\n" + // arg6
|
|
"sw $9, 24($$sp)\n" + // arg7
|
|
"syscall\n" +
|
|
"addu $$sp, $$sp, 32\n" +
|
|
".set at\n"
|
|
}
|
|
constraints += ",~{$3},~{$4},~{$5},~{$6},~{$8},~{$9},~{$10},~{$11},~{$12},~{$13},~{$14},~{$15},~{$24},~{$25},~{hi},~{lo},~{memory}"
|
|
returnType := b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType}, false)
|
|
fnType := llvm.FunctionType(returnType, argTypes, false)
|
|
target := llvm.InlineAsm(fnType, asm, constraints, true, true, 0, false)
|
|
call := b.CreateCall(fnType, target, args, "")
|
|
resultCode := b.CreateExtractValue(call, 0, "") // r2
|
|
errorFlag := b.CreateExtractValue(call, 1, "") // r7
|
|
// Pseudocode to return the result with the same convention as other
|
|
// archs:
|
|
// return (errorFlag != 0) ? -resultCode : resultCode;
|
|
// At least on QEMU with the O32 ABI, the error code is always positive.
|
|
zero := llvm.ConstInt(b.uintptrType, 0, false)
|
|
isError := b.CreateICmp(llvm.IntNE, errorFlag, zero, "")
|
|
negativeResult := b.CreateSub(zero, resultCode, "")
|
|
result := b.CreateSelect(isError, negativeResult, resultCode, "")
|
|
return result, nil
|
|
|
|
default:
|
|
return llvm.Value{}, b.makeError(call.Pos(), "unknown GOOS/GOARCH for syscall: "+b.GOOS+"/"+b.GOARCH)
|
|
}
|
|
}
|
|
|
|
// createSyscall emits instructions for the syscall.Syscall* family of
|
|
// functions, depending on the target OS/arch.
|
|
func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) {
|
|
switch b.GOOS {
|
|
case "linux":
|
|
syscallResult, err := b.createRawSyscall(call)
|
|
if err != nil {
|
|
return syscallResult, err
|
|
}
|
|
// Return values: r0, r1 uintptr, err Errno
|
|
// Pseudocode:
|
|
// var err uintptr
|
|
// if syscallResult < 0 && syscallResult > -4096 {
|
|
// err = -syscallResult
|
|
// }
|
|
// return syscallResult, 0, err
|
|
zero := llvm.ConstInt(b.uintptrType, 0, false)
|
|
inrange1 := b.CreateICmp(llvm.IntSLT, syscallResult, llvm.ConstInt(b.uintptrType, 0, false), "")
|
|
inrange2 := b.CreateICmp(llvm.IntSGT, syscallResult, llvm.ConstInt(b.uintptrType, 0xfffffffffffff000, true), "") // -4096
|
|
hasError := b.CreateAnd(inrange1, inrange2, "")
|
|
errResult := b.CreateSelect(hasError, b.CreateSub(zero, syscallResult, ""), zero, "syscallError")
|
|
retval := llvm.Undef(b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType, b.uintptrType}, false))
|
|
retval = b.CreateInsertValue(retval, syscallResult, 0, "")
|
|
retval = b.CreateInsertValue(retval, zero, 1, "")
|
|
retval = b.CreateInsertValue(retval, errResult, 2, "")
|
|
return retval, nil
|
|
case "windows":
|
|
// On Windows, syscall.Syscall* is basically just a function pointer
|
|
// call. This is complicated in gc because of stack switching and the
|
|
// different ABI, but easy in TinyGo: just call the function pointer.
|
|
// The signature looks like this:
|
|
// func Syscall(trap, nargs, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
|
|
|
|
// Prepare input values.
|
|
var paramTypes []llvm.Type
|
|
var params []llvm.Value
|
|
for _, val := range call.Args[2:] {
|
|
param := b.getValue(val, getPos(call))
|
|
params = append(params, param)
|
|
paramTypes = append(paramTypes, param.Type())
|
|
}
|
|
llvmType := llvm.FunctionType(b.uintptrType, paramTypes, false)
|
|
fn := b.getValue(call.Args[0], getPos(call))
|
|
fnPtr := b.CreateIntToPtr(fn, b.dataPtrType, "")
|
|
|
|
// Prepare some functions that will be called later.
|
|
setLastError := b.mod.NamedFunction("SetLastError")
|
|
if setLastError.IsNil() {
|
|
llvmType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.ctx.Int32Type()}, false)
|
|
setLastError = llvm.AddFunction(b.mod, "SetLastError", llvmType)
|
|
}
|
|
getLastError := b.mod.NamedFunction("GetLastError")
|
|
if getLastError.IsNil() {
|
|
llvmType := llvm.FunctionType(b.ctx.Int32Type(), nil, false)
|
|
getLastError = llvm.AddFunction(b.mod, "GetLastError", llvmType)
|
|
}
|
|
|
|
// Now do the actual call. Pseudocode:
|
|
// SetLastError(0)
|
|
// r1 = trap(a1, a2, a3, ...)
|
|
// err = uintptr(GetLastError())
|
|
// return r1, 0, err
|
|
// Note that SetLastError/GetLastError could be replaced with direct
|
|
// access to the thread control block, which is probably smaller and
|
|
// faster. The Go runtime does this in assembly.
|
|
b.CreateCall(setLastError.GlobalValueType(), setLastError, []llvm.Value{llvm.ConstNull(b.ctx.Int32Type())}, "")
|
|
syscallResult := b.CreateCall(llvmType, fnPtr, params, "")
|
|
errResult := b.CreateCall(getLastError.GlobalValueType(), getLastError, nil, "err")
|
|
if b.uintptrType != b.ctx.Int32Type() {
|
|
errResult = b.CreateZExt(errResult, b.uintptrType, "err.uintptr")
|
|
}
|
|
|
|
// Return r1, 0, err
|
|
retval := llvm.ConstNull(b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType, b.uintptrType}, false))
|
|
retval = b.CreateInsertValue(retval, syscallResult, 0, "")
|
|
retval = b.CreateInsertValue(retval, errResult, 2, "")
|
|
return retval, nil
|
|
|
|
default:
|
|
return llvm.Value{}, b.makeError(call.Pos(), "unknown GOOS/GOARCH for syscall: "+b.GOOS+"/"+b.GOARCH)
|
|
}
|
|
}
|
|
|
|
// createRawSyscallNoError emits instructions for the Linux-specific
|
|
// syscall.rawSyscallNoError function.
|
|
func (b *builder) createRawSyscallNoError(call *ssa.CallCommon) (llvm.Value, error) {
|
|
syscallResult, err := b.createRawSyscall(call)
|
|
if err != nil {
|
|
return syscallResult, err
|
|
}
|
|
retval := llvm.ConstNull(b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType}, false))
|
|
retval = b.CreateInsertValue(retval, syscallResult, 0, "")
|
|
retval = b.CreateInsertValue(retval, llvm.ConstInt(b.uintptrType, 0, false), 1, "")
|
|
return retval, nil
|
|
}
|
|
|
|
// Lower a call to internal/abi.FuncPCABI0 on MacOS.
|
|
// This function is called like this:
|
|
//
|
|
// syscall(abi.FuncPCABI0(libc_mkdir_trampoline), uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
|
|
//
|
|
// So we'll want to return a function pointer (as uintptr) that points to the
|
|
// libc function. Specifically, we _don't_ want to point to the trampoline
|
|
// function (which is implemented in Go assembly which we can't read), but
|
|
// rather to the actually intended function. For this we're going to assume that
|
|
// all the functions follow a specific pattern: libc_<functionname>_trampoline.
|
|
//
|
|
// The return value is the function pointer as an uintptr, or a nil value if
|
|
// this isn't possible (and a regular call should be made as fallback).
|
|
func (b *builder) createDarwinFuncPCABI0Call(instr *ssa.CallCommon) llvm.Value {
|
|
if b.GOOS != "darwin" {
|
|
// This has only been tested on MacOS (and only seems to be used there).
|
|
return llvm.Value{}
|
|
}
|
|
|
|
// Check that it uses a function call like syscall.libc_*_trampoline
|
|
itf := instr.Args[0].(*ssa.MakeInterface)
|
|
calledFn := itf.X.(*ssa.Function)
|
|
if pkgName := calledFn.Pkg.Pkg.Path(); pkgName != "syscall" && pkgName != "internal/syscall/unix" {
|
|
return llvm.Value{}
|
|
}
|
|
if !strings.HasPrefix(calledFn.Name(), "libc_") || !strings.HasSuffix(calledFn.Name(), "_trampoline") {
|
|
|
|
return llvm.Value{}
|
|
}
|
|
|
|
// Extract the libc function name.
|
|
name := strings.TrimPrefix(strings.TrimSuffix(calledFn.Name(), "_trampoline"), "libc_")
|
|
if name == "open" {
|
|
// Special case: open() is a variadic function and can't be called like
|
|
// a regular function. Therefore, we need to use a wrapper implemented
|
|
// in C.
|
|
name = "syscall_libc_open"
|
|
}
|
|
if b.GOARCH == "amd64" {
|
|
if name == "fdopendir" || name == "readdir_r" {
|
|
// Hack to support amd64, which needs the $INODE64 suffix.
|
|
// This is also done in upstream Go:
|
|
// https://github.com/golang/go/commit/096ab3c21b88ccc7d411379d09fe6274e3159467
|
|
name += "$INODE64"
|
|
}
|
|
}
|
|
|
|
// Obtain the C function.
|
|
// Use a simple function (no parameters or return value) because all we need
|
|
// is the address of the function.
|
|
llvmFn := b.mod.NamedFunction(name)
|
|
if llvmFn.IsNil() {
|
|
llvmFnType := llvm.FunctionType(b.ctx.VoidType(), nil, false)
|
|
llvmFn = llvm.AddFunction(b.mod, name, llvmFnType)
|
|
}
|
|
|
|
// Cast the function pointer to a uintptr (because that's what
|
|
// abi.FuncPCABI0 returns).
|
|
return b.CreatePtrToInt(llvmFn, b.uintptrType, "")
|
|
}
|