Implemented running executables in memory
All checks were successful
/ test (push) Successful in 16s

This commit is contained in:
Eduard Urbach 2025-07-06 20:02:28 +02:00
parent e5aa006623
commit a7fd4172b9
Signed by: akyoto
GPG key ID: 49226B848C78F6C8
10 changed files with 183 additions and 42 deletions

View file

@ -9,5 +9,5 @@ Commands:
--os [os] cross-compile for OS: [linux|mac|windows]
--verbose, -v show everything
run [directory | file] build and run the executable
run [directory | file] build and run the executable in memory
help show this help

View file

@ -3,10 +3,10 @@ package cli
import (
"fmt"
"os"
"os/exec"
"git.urbach.dev/cli/q/src/compiler"
"git.urbach.dev/cli/q/src/linker"
"git.urbach.dev/cli/q/src/memfile"
)
// run builds and runs the executable.
@ -17,23 +17,20 @@ func run(args []string) int {
return exit(err)
}
result, err := compiler.Compile(b)
env, err := compiler.Compile(b)
if err != nil {
return exit(err)
}
err = linker.WriteFile(b.Executable(), b, result)
file, err := memfile.New(b.Executable())
if err != nil {
return exit(err)
}
cmd := exec.Command(b.Executable())
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
err = cmd.Run()
linker.Write(file, b, env)
err = memfile.Exec(file)
if err != nil {
fmt.Fprintln(os.Stderr, err)

41
src/linker/Write.go Normal file
View file

@ -0,0 +1,41 @@
package linker
import (
"io"
"git.urbach.dev/cli/q/src/asm"
"git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/core"
"git.urbach.dev/cli/q/src/data"
"git.urbach.dev/cli/q/src/elf"
"git.urbach.dev/cli/q/src/macho"
"git.urbach.dev/cli/q/src/pe"
)
// Write writes an executable to the given writer.
func Write(writer io.WriteSeeker, b *build.Build, env *core.Environment) {
program := asm.Assembler{
Instructions: make([]asm.Instruction, 0, 32),
Data: make(data.Data, 32),
}
init := env.Functions["run.init"]
traversed := make(map[*core.Function]bool, len(env.Functions))
// This will place the init function immediately after the entry point
// and also add everything the init function calls recursively.
init.EachDependency(traversed, func(f *core.Function) {
program.Merge(&f.Assembler)
})
code, data, libs := program.Compile(b)
switch b.OS {
case build.Linux:
elf.Write(writer, b, code, data)
case build.Mac:
macho.Write(writer, b, code, data)
case build.Windows:
pe.Write(writer, b, code, data, libs)
}
}

View file

@ -3,13 +3,8 @@ package linker
import (
"os"
"git.urbach.dev/cli/q/src/asm"
"git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/core"
"git.urbach.dev/cli/q/src/data"
"git.urbach.dev/cli/q/src/elf"
"git.urbach.dev/cli/q/src/macho"
"git.urbach.dev/cli/q/src/pe"
)
// WriteFile writes an executable file to disk.
@ -20,36 +15,12 @@ func WriteFile(executable string, b *build.Build, env *core.Environment) error {
return err
}
init := env.Functions["run.init"]
traversed := make(map[*core.Function]bool, len(env.Functions))
final := asm.Assembler{
Instructions: make([]asm.Instruction, 0, 32),
Data: make(data.Data, 32),
}
// This will place the main function immediately after the entry point
// and also add everything the main function calls recursively.
init.EachDependency(traversed, func(f *core.Function) {
final.Merge(&f.Assembler)
})
code, data, libs := final.Compile(b)
switch b.OS {
case build.Linux:
elf.Write(file, b, code, data)
case build.Mac:
macho.Write(file, b, code, data)
case build.Windows:
pe.Write(file, b, code, data, libs)
}
err = file.Close()
Write(file, b, env)
err = file.Chmod(0o755)
if err != nil {
return err
}
return os.Chmod(executable, 0o755)
return file.Close()
}

47
src/memfile/Exec_linux.go Normal file
View file

@ -0,0 +1,47 @@
//go:build linux
package memfile
import (
"os"
"strconv"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
// Exec executes an in-memory file.
func Exec(file *os.File) error {
empty, err := syscall.BytePtrFromString("")
if err != nil {
return err
}
argv := []string{"/proc/self/fd/" + strconv.Itoa(int(file.Fd()))}
argvp, err := syscall.SlicePtrFromStrings(argv)
if err != nil {
return err
}
envv := os.Environ()
envvp, err := syscall.SlicePtrFromStrings(envv)
if err != nil {
return err
}
_, _, errno := syscall.Syscall6(
unix.SYS_EXECVEAT,
uintptr(file.Fd()),
uintptr(unsafe.Pointer(empty)),
uintptr(unsafe.Pointer(&argvp[0])),
uintptr(unsafe.Pointer(&envvp[0])),
uintptr(unix.AT_EMPTY_PATH),
0,
)
return errno
}

29
src/memfile/Exec_other.go Normal file
View file

@ -0,0 +1,29 @@
//go:build !linux
package memfile
import (
"os"
"os/exec"
)
// Exec executes an in-memory file.
func Exec(file *os.File) error {
err := file.Chmod(0o700)
if err != nil {
return err
}
err = file.Close()
if err != nil {
return err
}
cmd := exec.Command(file.Name())
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd.Run()
}

21
src/memfile/New_linux.go Normal file
View file

@ -0,0 +1,21 @@
//go:build linux
package memfile
import (
"os"
"golang.org/x/sys/unix"
)
// New creates a new anonymous in-memory file.
func New(name string) (*os.File, error) {
fd, err := unix.MemfdCreate(name, unix.MFD_CLOEXEC)
if err != nil {
return nil, err
}
memFile := os.NewFile(uintptr(fd), name)
return memFile, nil
}

20
src/memfile/New_other.go Normal file
View file

@ -0,0 +1,20 @@
//go:build !linux
package memfile
import (
"os"
"git.urbach.dev/cli/q/src/global"
)
// New creates a new anonymous in-memory file.
func New(name string) (*os.File, error) {
pattern := ""
if global.OS == "windows" {
pattern = "*.exe"
}
return os.CreateTemp("", pattern)
}

View file

@ -0,0 +1,15 @@
package memfile_test
import (
"testing"
"git.urbach.dev/cli/q/src/memfile"
"git.urbach.dev/go/assert"
)
func TestNew(t *testing.T) {
file, err := memfile.New("")
assert.Nil(t, err)
assert.NotNil(t, file)
// memfile.Exec can't be tested because it would replace the test executable
}