From fbd4bd3f07852527af3800eb7b90537e4272e942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Clerget?= Date: Wed, 4 Feb 2026 13:46:04 -0600 Subject: [PATCH] Add EFI support for amd64 and arm64 platforms. Some decisions points: - no external libc dependency, builtin functions replaced by Go equivalent (runtime_minimal_libc.go) - assembly efi_main entrypoint as assembly stub is required for sbat and few symbols anyway - a default sbat section (required for Linux shim secure boot) - dedicated rand reader in crypto/rand for EFI as it doesn't fit well with the generic machine interface - the timer calibration is deliberately simple but hopefully good enough for EFI purpose - no heap grow support, just get the biggest memory region available --- GNUmakefile | 12 + builder/build.go | 3 +- builder/builder_test.go | 5 +- emulators/qemu-uefi-aarch64.sh | 15 + emulators/qemu-uefi-x86_64.sh | 14 + src/crypto/rand/rand_uefi.go | 39 +++ src/internal/task/task_stack_amd64.go | 2 +- ..._windows.go => task_stack_amd64_winabi.go} | 2 +- src/machine/uefi/api.go | 17 ++ src/machine/uefi/api_amd64.S | 80 ++++++ src/machine/uefi/api_arm64.S | 98 +++++++ src/machine/uefi/clock.go | 21 ++ src/machine/uefi/console.go | 32 +++ src/machine/uefi/cpu.go | 45 +++ src/machine/uefi/cpu_amd64.S | 75 +++++ src/machine/uefi/cpu_arm64.S | 60 ++++ src/machine/uefi/keyboard.go | 258 ++++++++++++++++++ src/machine/uefi/memory.go | 116 ++++++++ src/machine/uefi/system.go | 86 ++++++ src/machine/uefi/tables.go | 140 ++++++++++ src/machine/uefi/time.go | 84 ++++++ src/machine/uefi/types.go | 43 +++ src/runtime/interrupt/interrupt_none.go | 2 +- src/runtime/os_winabi.go | 74 +++++ src/runtime/os_windows.go | 72 +---- src/runtime/rand_norng.go | 2 +- src/runtime/runtime_minimal_libc.go | 135 +++++++++ src/runtime/runtime_uefi.go | 228 ++++++++++++++++ src/runtime/runtime_uefi_amd64.S | 80 ++++++ src/runtime/runtime_uefi_arm64.S | 83 ++++++ targets/uefi-amd64.json | 42 +++ targets/uefi-arm64.json | 41 +++ 32 files changed, 1936 insertions(+), 70 deletions(-) create mode 100755 emulators/qemu-uefi-aarch64.sh create mode 100755 emulators/qemu-uefi-x86_64.sh create mode 100644 src/crypto/rand/rand_uefi.go rename src/internal/task/{task_stack_amd64_windows.go => task_stack_amd64_winabi.go} (97%) create mode 100644 src/machine/uefi/api.go create mode 100644 src/machine/uefi/api_amd64.S create mode 100644 src/machine/uefi/api_arm64.S create mode 100644 src/machine/uefi/clock.go create mode 100644 src/machine/uefi/console.go create mode 100644 src/machine/uefi/cpu.go create mode 100644 src/machine/uefi/cpu_amd64.S create mode 100644 src/machine/uefi/cpu_arm64.S create mode 100644 src/machine/uefi/keyboard.go create mode 100644 src/machine/uefi/memory.go create mode 100644 src/machine/uefi/system.go create mode 100644 src/machine/uefi/tables.go create mode 100644 src/machine/uefi/time.go create mode 100644 src/machine/uefi/types.go create mode 100644 src/runtime/os_winabi.go create mode 100644 src/runtime/runtime_minimal_libc.go create mode 100644 src/runtime/runtime_uefi.go create mode 100644 src/runtime/runtime_uefi_amd64.S create mode 100644 src/runtime/runtime_uefi_arm64.S create mode 100644 targets/uefi-amd64.json create mode 100644 targets/uefi-arm64.json diff --git a/GNUmakefile b/GNUmakefile index 99a654ca7f..f9bafd5f04 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -814,6 +814,18 @@ endif @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=waveshare-rp2040-tiny examples/echo @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.exe -target=uefi-amd64 examples/echo2 + @$(MD5SUM) test.exe + $(TINYGO) build -size short -o test.exe -target=uefi-amd64 examples/empty + @$(MD5SUM) test.exe + $(TINYGO) build -size short -o test.exe -target=uefi-amd64 examples/time-offset + @$(MD5SUM) test.exe + $(TINYGO) build -size short -o test.exe -target=uefi-arm64 examples/echo2 + @$(MD5SUM) test.exe + $(TINYGO) build -size short -o test.exe -target=uefi-arm64 examples/empty + @$(MD5SUM) test.exe + $(TINYGO) build -size short -o test.exe -target=uefi-arm64 examples/time-offset + @$(MD5SUM) test.exe # test pwm $(TINYGO) build -size short -o test.hex -target=itsybitsy-m0 examples/pwm @$(MD5SUM) test.hex diff --git a/builder/build.go b/builder/build.go index a598f01965..cc633eda6c 100644 --- a/builder/build.go +++ b/builder/build.go @@ -19,6 +19,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "sort" "strconv" "strings" @@ -808,7 +809,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe } ldflags = append(ldflags, "-mllvm", "-mcpu="+config.CPU()) ldflags = append(ldflags, "-mllvm", "-mattr="+config.Features()) // needed for MIPS softfloat - if config.GOOS() == "windows" { + if config.GOOS() == "windows" || slices.Contains(config.Target.BuildTags, "uefi") { // Options for the MinGW wrapper for the lld COFF linker. ldflags = append(ldflags, "-Xlink=/opt:lldlto="+strconv.Itoa(speedLevel), diff --git a/builder/builder_test.go b/builder/builder_test.go index 8e62d9183c..0aa6a9e3c0 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" "github.com/tinygo-org/tinygo/compileopts" @@ -35,6 +36,8 @@ func TestClangAttributes(t *testing.T) { "nintendoswitch", "riscv-qemu", "tkey", + "uefi-amd64", + "uefi-arm64", "wasip1", "wasip2", "wasm", @@ -128,7 +131,7 @@ func testClangAttributes(t *testing.T, options *compileopts.Options) { defer mod.Dispose() // Check whether the LLVM target matches. - if mod.Target() != config.Triple() { + if mod.Target() != config.Triple() && !strings.HasPrefix(mod.Target(), config.Triple()) { t.Errorf("target has LLVM triple %#v but Clang makes it LLVM triple %#v", config.Triple(), mod.Target()) } diff --git a/emulators/qemu-uefi-aarch64.sh b/emulators/qemu-uefi-aarch64.sh new file mode 100755 index 0000000000..231ec44a6e --- /dev/null +++ b/emulators/qemu-uefi-aarch64.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +binary=$1 +dir=$(dirname $binary) + +mkdir -p ${dir}/qemu-disk/efi/boot +mv $binary ${dir}/qemu-disk/efi/boot/bootaa64.efi + +ovmf_path=${OVMF_CODE:-/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd} + +exec qemu-system-aarch64 -M virt -bios $ovmf_path -device virtio-rng-pci \ + -cpu max,pauth-impdef=on -drive format=raw,file=fat:rw:${dir}/qemu-disk \ + -net none -nographic -no-reboot diff --git a/emulators/qemu-uefi-x86_64.sh b/emulators/qemu-uefi-x86_64.sh new file mode 100755 index 0000000000..df82c3ec84 --- /dev/null +++ b/emulators/qemu-uefi-x86_64.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +binary=$1 +dir=$(dirname $binary) + +mkdir -p ${dir}/qemu-disk/efi/boot +mv $binary ${dir}/qemu-disk/efi/boot/bootx64.efi + +ovmf_path=${OVMF_CODE:-/usr/share/qemu/OVMF.fd} + +exec qemu-system-x86_64 -bios $ovmf_path -cpu qemu64,+rdrand -device virtio-rng-pci \ + -drive format=raw,file=fat:rw:${dir}/qemu-disk -net none -nographic -no-reboot diff --git a/src/crypto/rand/rand_uefi.go b/src/crypto/rand/rand_uefi.go new file mode 100644 index 0000000000..a85036fdde --- /dev/null +++ b/src/crypto/rand/rand_uefi.go @@ -0,0 +1,39 @@ +//go:build uefi + +package rand + +import ( + "errors" + "machine/uefi" +) + +func init() { + Reader = &reader{} +} + +type reader struct { +} + +func (r *reader) Read(b []byte) (n int, err error) { + if !uefi.HasRNGSupport() { + return 0, errors.New("no hardware rng available") + } else if len(b) == 0 { + return 0, nil + } + + var randomByte uint64 + for i := range b { + if i%8 == 0 { + var ok bool + randomByte, ok = uefi.ReadRandom() + if !ok { + return n, errors.New("no random seed available") + } + } else { + randomByte >>= 8 + } + b[i] = byte(randomByte) + } + + return len(b), nil +} diff --git a/src/internal/task/task_stack_amd64.go b/src/internal/task/task_stack_amd64.go index d252b1c50d..bfd18b5758 100644 --- a/src/internal/task/task_stack_amd64.go +++ b/src/internal/task/task_stack_amd64.go @@ -1,4 +1,4 @@ -//go:build scheduler.tasks && amd64 && !windows +//go:build scheduler.tasks && amd64 && !windows && !uefi package task diff --git a/src/internal/task/task_stack_amd64_windows.go b/src/internal/task/task_stack_amd64_winabi.go similarity index 97% rename from src/internal/task/task_stack_amd64_windows.go rename to src/internal/task/task_stack_amd64_winabi.go index f174196f35..8d5e8f62c3 100644 --- a/src/internal/task/task_stack_amd64_windows.go +++ b/src/internal/task/task_stack_amd64_winabi.go @@ -1,4 +1,4 @@ -//go:build scheduler.tasks && amd64 && windows +//go:build scheduler.tasks && amd64 && (windows || uefi) package task diff --git a/src/machine/uefi/api.go b/src/machine/uefi/api.go new file mode 100644 index 0000000000..b46cd1e750 --- /dev/null +++ b/src/machine/uefi/api.go @@ -0,0 +1,17 @@ +//go:build uefi + +package uefi + +// callAsm is the single assembly stub for all UEFI calls. +// It takes a function pointer, a pointer to an argument array, and the count. +// +//export uefiCall +func callAsm(fn uintptr, args *uintptr, nargs uintptr) Status + +// Call invokes a UEFI function with the given arguments via the MS x64 ABI. +func Call(fn uintptr, args ...uintptr) Status { + if len(args) == 0 { + return callAsm(fn, nil, 0) + } + return callAsm(fn, &args[0], uintptr(len(args))) +} diff --git a/src/machine/uefi/api_amd64.S b/src/machine/uefi/api_amd64.S new file mode 100644 index 0000000000..8fd768e534 --- /dev/null +++ b/src/machine/uefi/api_amd64.S @@ -0,0 +1,80 @@ +// UEFI Call Stub for x86_64 +// +// Generic stub for calling any UEFI function via the Microsoft x64 ABI. +// +// Go signature: func callAsm(fn uintptr, args *uintptr, nargs uintptr) Status +// MS x64 entry: +// RCX = fn (function pointer to call) +// RDX = args (pointer to uintptr array of arguments) +// R8 = nargs (number of arguments, 0-N) +// +// MS x64 ABI register mapping for the callee: +// Args 1-4: RCX, RDX, R8, R9 +// Args 5+: stack (after 32-byte shadow space) +// Return: RAX +// Stack: 16-byte aligned before CALL +// + +.section .text.uefiCall,"ax" +.global uefiCall +uefiCall: + pushq %rbp + movq %rsp, %rbp + pushq %rsi + pushq %rdi + + movq %rcx, %r10 // Save fn + movq %rdx, %rsi // Save args pointer + movq %r8, %rdi // Save nargs + + // Calculate stack space: align16(32 + max(0, nargs-4) * 8) + // After 3 pushes, RSP is 16-byte aligned. Subtracting an aligned + // value keeps it aligned for the CALL instruction. + xorq %rax, %rax + cmpq $4, %rdi + jle 1f + movq %rdi, %rax + subq $4, %rax +1: + shlq $3, %rax // nStackArgs * 8 + addq $47, %rax // + 32 (shadow) + 15 (round-up) + andq $-16, %rax // align to 16 + subq %rax, %rsp + + // Copy stack args: args[4..nargs-1] -> RSP+32+j*8 + movq %rdi, %rcx + subq $4, %rcx + jle 2f + xorq %rax, %rax +3: movq 32(%rsi,%rax,8), %r11 + movq %r11, 32(%rsp,%rax,8) + incq %rax + cmpq %rcx, %rax + jl 3b +2: + // Load register args + xorq %rcx, %rcx + xorq %rdx, %rdx + xorq %r8, %r8 + xorq %r9, %r9 + + testq %rdi, %rdi + jz 4f + movq 0(%rsi), %rcx // args[0] -> RCX + cmpq $2, %rdi + jl 4f + movq 8(%rsi), %rdx // args[1] -> RDX + cmpq $3, %rdi + jl 4f + movq 16(%rsi), %r8 // args[2] -> R8 + cmpq $4, %rdi + jl 4f + movq 24(%rsi), %r9 // args[3] -> R9 +4: + callq *%r10 + + leaq -16(%rbp), %rsp + popq %rdi + popq %rsi + popq %rbp + retq diff --git a/src/machine/uefi/api_arm64.S b/src/machine/uefi/api_arm64.S new file mode 100644 index 0000000000..26fa5541bd --- /dev/null +++ b/src/machine/uefi/api_arm64.S @@ -0,0 +1,98 @@ +// UEFI Call Stub for ARM64 +// +// Generic stub for calling any UEFI function via the AAPCS64 calling convention. +// +// Go signature: func callAsm(fn uintptr, args *uintptr, nargs uintptr) Status +// AAPCS64 entry: +// x0 = fn (function pointer to call) +// x1 = args (pointer to uintptr array of arguments) +// x2 = nargs (number of arguments, 0-N) +// +// AAPCS64 register mapping for the callee: +// Args 1-8: x0-x7 +// Args 9+: stack (no shadow space) +// Return: x0 +// Stack: 16-byte aligned at all times + +.section .text.uefiCall,"ax" +.global uefiCall +uefiCall: + // Save callee-saved registers and link register. + stp x29, x30, [sp, #-64]! + mov x29, sp + stp x19, x20, [sp, #16] + stp x21, x22, [sp, #32] + + // Move parameters to callee-saved registers so they survive + // the stack setup and register loading below. + mov x19, x0 // x19 = fn + mov x20, x1 // x20 = args pointer + mov x21, x2 // x21 = nargs + + // Calculate and reserve stack space for args 9+. + // nStackArgs = max(0, nargs - 8) + // Stack space = align16(nStackArgs * 8) + subs x22, x21, #8 + b.le .Lno_stack_args + + // Round up to 16-byte alignment: ((nStackArgs * 8) + 15) & ~15 + lsl x3, x22, #3 // nStackArgs * 8 + add x3, x3, #15 + and x3, x3, #~0xF + sub sp, sp, x3 + + // Copy stack args: args[8..nargs-1] -> sp[0..] + add x6, x20, #64 // x6 = &args[8] + mov x4, #0 // index = 0 +.Lcopy_stack: + ldr x5, [x6, x4, lsl #3] + str x5, [sp, x4, lsl #3] + add x4, x4, #1 + cmp x4, x22 + b.lt .Lcopy_stack + +.Lno_stack_args: + // Load register arguments x0-x7 from the args array. + // Zero all registers first, then load as many as nargs specifies. + mov x0, #0 + mov x1, #0 + mov x2, #0 + mov x3, #0 + mov x4, #0 + mov x5, #0 + mov x6, #0 + mov x7, #0 + + cbz x21, .Lcall // nargs == 0, skip loading + ldr x0, [x20, #0] // args[0] + cmp x21, #2 + b.lt .Lcall + ldr x1, [x20, #8] // args[1] + cmp x21, #3 + b.lt .Lcall + ldr x2, [x20, #16] // args[2] + cmp x21, #4 + b.lt .Lcall + ldr x3, [x20, #24] // args[3] + cmp x21, #5 + b.lt .Lcall + ldr x4, [x20, #32] // args[4] + cmp x21, #6 + b.lt .Lcall + ldr x5, [x20, #40] // args[5] + cmp x21, #7 + b.lt .Lcall + ldr x6, [x20, #48] // args[6] + cmp x21, #8 + b.lt .Lcall + ldr x7, [x20, #56] // args[7] + +.Lcall: + blr x19 // Call UEFI function, result in x0 + + // Restore stack and callee-saved registers. + mov sp, x29 + ldp x19, x20, [sp, #16] + ldp x21, x22, [sp, #32] + ldp x29, x30, [sp], #64 + ret diff --git a/src/machine/uefi/clock.go b/src/machine/uefi/clock.go new file mode 100644 index 0000000000..1057482a70 --- /dev/null +++ b/src/machine/uefi/clock.go @@ -0,0 +1,21 @@ +//go:build uefi + +package uefi + +const microsecondsCalibration = 10000 // 10 milliseconds + +// CalibrateTimerFrequency calibrates the timer frequency by measuring +// ticks over a known interval. Returns the timer frequency in ticks +// per microsecond. +func CalibrateTimerFrequency() uint64 { + // Not the most accurate method, but should be good enough for EFI. + start := Ticks() + Stall(microsecondsCalibration) + end := Ticks() + + frequency := (end - start) / microsecondsCalibration // ticks per microsecond + if frequency == 0 { + frequency = 1000 // fallback + } + return frequency +} diff --git a/src/machine/uefi/console.go b/src/machine/uefi/console.go new file mode 100644 index 0000000000..51e5e6fe1d --- /dev/null +++ b/src/machine/uefi/console.go @@ -0,0 +1,32 @@ +//go:build uefi + +package uefi + +import "unsafe" + +// SimpleTextOutputProtocol provides text output services. +type SimpleTextOutputProtocol struct { + Reset uintptr + OutputString uintptr + TestString uintptr + QueryMode uintptr + SetMode uintptr + SetAttribute uintptr + ClearScreen uintptr + SetCursorPosition uintptr + EnableCursor uintptr + Mode uintptr +} + +// OutputString prints a UCS-2 string to the console. +func OutputString(str *uint16) { + st := GetSystemTable() + if st == nil || st.ConOut == nil || st.ConOut.OutputString == 0 { + return + } + Call( + st.ConOut.OutputString, + uintptr(unsafe.Pointer(st.ConOut)), + uintptr(unsafe.Pointer(str)), + ) +} diff --git a/src/machine/uefi/cpu.go b/src/machine/uefi/cpu.go new file mode 100644 index 0000000000..da186fe602 --- /dev/null +++ b/src/machine/uefi/cpu.go @@ -0,0 +1,45 @@ +//go:build uefi + +package uefi + +// Ticks returns a high-resolution monotonic counter. +// - amd64: RDTSC (Time Stamp Counter) +// - arm64: CNTVCT_EL0 (generic timer virtual count) +// +//export uefiTicks +func Ticks() uint64 + +// CpuPause hints to the CPU that we are in a spin-wait loop. +// - amd64: PAUSE instruction +// - arm64: YIELD instruction +// +//export uefiCpuPause +func CpuPause() + +// ReadRandom reads a 64-bit hardware random number. +// Returns the random value and true if successful, or 0 and false if +// the hardware RNG is not ready (entropy exhausted). +// Check HasRNGSupport() before calling this function. +// - amd64: RDRAND instruction +// - arm64: RNDR register (ARMv8.5-A) +// +//export uefiReadRandom +func ReadRandom() (value uint64, ok bool) + +// hasRNG returns true if the CPU has a hardware RNG. +// - amd64: checks CPUID leaf 1 ECX bit 30 (RDRAND) +// - arm64: checks ID_AA64ISAR0_EL1 bits [63:60] (RNDR) +// +//export uefiHasRNG +func hasRNG() bool + +var hasRNGSupport *bool + +// HasRNGSupport returns true if the CPU has a hardware RNG. +func HasRNGSupport() bool { + if hasRNGSupport == nil { + supported := hasRNG() + hasRNGSupport = &supported + } + return *hasRNGSupport +} diff --git a/src/machine/uefi/cpu_amd64.S b/src/machine/uefi/cpu_amd64.S new file mode 100644 index 0000000000..1ac41fdaef --- /dev/null +++ b/src/machine/uefi/cpu_amd64.S @@ -0,0 +1,75 @@ +// uefiTicks returns the x86 Time Stamp Counter value using RDTSC instruction +// Go: func uefiTicks() uint64 +// Returns: RAX = full 64-bit TSC value +// +.section .text.uefiTicks,"ax" +.global uefiTicks +uefiTicks: + rdtsc + // RDTSC returns TSC in EDX:EAX (high 32 bits in EDX, low 32 bits in EAX) + // Combine into 64-bit value: RAX = (RDX << 32) | EAX + shlq $32, %rdx + orq %rdx, %rax + retq + +// uefiCpuPause executes the PAUSE instruction for spin-wait loops +// Go: func uefiCpuPause() +// +// The PAUSE instruction is a hint to the CPU that we're in a spin-wait loop. +// It improves performance on hyperthreaded CPUs by: +// - Reducing power consumption +// - Avoiding memory order violations +// - Allowing the other hyperthread to use resources +// +.section .text.uefiCpuPause,"ax" +.global uefiCpuPause +uefiCpuPause: + pause + retq + +// uefiReadRandom generates a 64-bit random number using the RDRAND instruction +// Go: func uefiReadRandom() (value uint64, ok bool) +// +// Returns: +// RAX = random value (undefined if CF=0) +// RDX = 1 if success (CF=1), 0 if failed (CF=0) +// +// The RDRAND instruction uses the CPU's hardware random number generator. +// It may fail if the entropy pool is exhausted, so callers should retry. +// +.section .text.uefiReadRandom,"ax" +.global uefiReadRandom +uefiReadRandom: + xorq %rax, %rax // Clear RAX (return 0 on failure) + xorq %rdx, %rdx // Clear RDX (ok = false) + rdrand %rax // Try to get random number into RAX + jnc 1f // Jump if carry flag not set (failure) + movq $1, %rdx // ok = true +1: + retq + +// uefiHasRNG checks if RDRAND instruction is supported +// Go: func uefiHasRNG() bool +// +// Returns: +// RAX = 1 if RDRAND is supported, 0 otherwise +// +// Uses CPUID leaf 1 to check ECX bit 30 (RDRAND feature flag) +// +.section .text.uefiHasRNG,"ax" +.global uefiHasRNG +uefiHasRNG: + pushq %rbx // Save RBX (CPUID clobbers it) + + movl $1, %eax // CPUID leaf 1 + cpuid + + xorq %rax, %rax // Clear result + testl $(1 << 30), %ecx // Check RDRAND bit (bit 30 of ECX) + jz 1f // Jump if not set + movq $1, %rax // RDRAND supported +1: + popq %rbx // Restore RBX + retq + + diff --git a/src/machine/uefi/cpu_arm64.S b/src/machine/uefi/cpu_arm64.S new file mode 100644 index 0000000000..bd90e045b4 --- /dev/null +++ b/src/machine/uefi/cpu_arm64.S @@ -0,0 +1,60 @@ +// uefiTicks returns the ARM64 generic timer counter value (CNTVCT_EL0). +// This is the ARM64 equivalent of x86 RDTSC. +// Go: func uefiTicks() uint64 +// Returns: x0 = 64-bit counter value +// +.section .text.uefiTicks,"ax" +.global uefiTicks +uefiTicks: + mrs x0, CNTVCT_EL0 + ret + +// uefiCpuPause executes the YIELD instruction for spin-wait loops. +// This is the ARM64 equivalent of x86 PAUSE. +// Go: func uefiCpuPause() +// +.section .text.uefiCpuPause,"ax" +.global uefiCpuPause +uefiCpuPause: + yield + ret + +// uefiReadRandom generates a 64-bit random number using the RNDR register. +// Requires ARMv8.5-A RNG extension. Check HasRNGSupport() first. +// Go: func uefiReadRandom() (value uint64, ok bool) +// +// Returns: +// x0 = random value (0 if failed) +// x1 = 1 if success, 0 if failed +// +// The RNDR instruction sets NZCV flags: if ZF=1, the read failed +// (entropy exhausted). +// +.section .text.uefiReadRandom,"ax" +.global uefiReadRandom +uefiReadRandom: + mov x0, #0 // Default: value = 0 + mov x1, #0 // Default: ok = false + mrs x0, S3_3_C2_C4_0 // RNDR register (encoded as S3_3_C2_C4_0) + cset x1, ne // ok = (ZF == 0), i.e. success + // If ZF was set (failure), x0 is undefined; clear it. + csel x0, x0, xzr, ne // x0 = (success) ? x0 : 0 + ret + +// uefiHasRNG checks if the RNDR instruction is supported. +// Go: func uefiHasRNG() bool +// +// Returns: +// x0 = 1 if RNDR is supported, 0 otherwise +// +// Reads ID_AA64ISAR0_EL1 and checks bits [63:60] for the RNDR field. +// Value >= 1 means RNDR is supported. +// +.section .text.uefiHasRNG,"ax" +.global uefiHasRNG +uefiHasRNG: + mrs x0, ID_AA64ISAR0_EL1 + ubfx x0, x0, #60, #4 // Extract bits [63:60] + cmp x0, #0 + cset x0, ne // x0 = (field != 0) ? 1 : 0 + ret diff --git a/src/machine/uefi/keyboard.go b/src/machine/uefi/keyboard.go new file mode 100644 index 0000000000..a5057fef85 --- /dev/null +++ b/src/machine/uefi/keyboard.go @@ -0,0 +1,258 @@ +//go:build uefi + +package uefi + +import ( + "unsafe" +) + +// SimpleTextInputProtocol provides text input services. +// Kept for SystemTable layout compatibility. +type SimpleTextInputProtocol struct { + Reset uintptr + ReadKeyStroke uintptr + WaitForKey uintptr +} + +// SimpleTextInputExProtocol provides extended text input services. +// Located via BootServices->LocateProtocol using its GUID. +type SimpleTextInputExProtocol struct { + Reset uintptr + ReadKeyStrokeEx uintptr + WaitForKeyEx uintptr + SetState uintptr + RegisterKeyNotify uintptr + UnregisterKeyNotify uintptr +} + +// InputKey represents a keystroke from UEFI console input. +type InputKey struct { + ScanCode uint16 + UnicodeChar uint16 +} + +// KeyState contains the shift and toggle state for a key press. +type KeyState struct { + KeyShiftState uint32 + KeyToggleState uint8 + _ [3]byte +} + +// KeyData is the extended key data returned by ReadKeyStrokeEx. +type KeyData struct { + Key InputKey + KeyState KeyState +} + +// GUID for EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL +var simpleTextInputExGUID = GUID{ + Data1: 0xdd9e7534, + Data2: 0x7762, + Data3: 0x4698, + Data4: [8]uint8{0x8c, 0x14, 0xf5, 0x85, 0x17, 0xa6, 0x25, 0xaa}, +} + +// Cached pointer to the Ex protocol +var conInEx *SimpleTextInputExProtocol + +// getConInEx locates and caches the SimpleTextInputExProtocol. +func getConInEx() *SimpleTextInputExProtocol { + if conInEx != nil { + return conInEx + } + + st := GetSystemTable() + if st == nil || st.BootServices == nil || st.BootServices.LocateProtocol == 0 { + return nil + } + + var iface uintptr + status := Call( + st.BootServices.LocateProtocol, + uintptr(unsafe.Pointer(&simpleTextInputExGUID)), + 0, // Registration = NULL + uintptr(unsafe.Pointer(&iface)), + ) + if status != Success || iface == 0 { + return nil + } + + conInEx = (*SimpleTextInputExProtocol)(unsafe.Pointer(iface)) + return conInEx +} + +// Scan codes for special keys +const ( + ScanNull = 0x00 + ScanUp = 0x01 + ScanDown = 0x02 + ScanRight = 0x03 + ScanLeft = 0x04 + ScanHome = 0x05 + ScanEnd = 0x06 + ScanIns = 0x07 + ScanDel = 0x08 + ScanPgUp = 0x09 + ScanPgDn = 0x0A + ScanF1 = 0x0B + ScanF2 = 0x0C + ScanF3 = 0x0D + ScanF4 = 0x0E + ScanF5 = 0x0F + ScanF6 = 0x10 + ScanF7 = 0x11 + ScanF8 = 0x12 + ScanF9 = 0x13 + ScanF10 = 0x14 + ScanEsc = 0x17 +) + +// Key input buffer (stores bytes not InputKey) +var ( + keyBuffer [256]byte + keyBufferHead int + keyBufferTail int +) + +// KeyBufferAvailable returns the number of bytes in the buffer. +func KeyBufferAvailable() int { + if keyBufferHead >= keyBufferTail { + return keyBufferHead - keyBufferTail + } + return len(keyBuffer) - keyBufferTail + keyBufferHead +} + +// keyBufferPushByte adds a byte to the buffer (internal use). +func keyBufferPushByte(b byte) bool { + nextHead := (keyBufferHead + 1) % len(keyBuffer) + if nextHead == keyBufferTail { + return false // Buffer full + } + keyBuffer[keyBufferHead] = b + keyBufferHead = nextHead + return true +} + +// keyBufferPushBytes adds multiple bytes to the buffer (internal use). +func keyBufferPushBytes(bytes []byte) bool { + for _, b := range bytes { + if !keyBufferPushByte(b) { + return false + } + } + return true +} + +// KeyBufferPop removes and returns a byte from the buffer. +func KeyBufferPop() (byte, bool) { + if keyBufferHead == keyBufferTail { + return 0, false // Buffer empty + } + b := keyBuffer[keyBufferTail] + keyBufferTail = (keyBufferTail + 1) % len(keyBuffer) + return b, true +} + +// convertKeyToBytes converts an InputKey to bytes (handling special keys). +func convertKeyToBytes(key InputKey) { + // Handle special scan codes (arrow keys, function keys, etc.) + if key.ScanCode != ScanNull { + var seq []byte + switch key.ScanCode { + case ScanUp: + seq = []byte{0x1b, '[', 'A'} + case ScanDown: + seq = []byte{0x1b, '[', 'B'} + case ScanRight: + seq = []byte{0x1b, '[', 'C'} + case ScanLeft: + seq = []byte{0x1b, '[', 'D'} + case ScanHome: + seq = []byte{0x1b, '[', 'H'} + case ScanEnd: + seq = []byte{0x1b, '[', 'F'} + case ScanIns: + seq = []byte{0x1b, '[', '2', '~'} + case ScanDel: + seq = []byte{0x1b, '[', '3', '~'} + case ScanPgUp: + seq = []byte{0x1b, '[', '5', '~'} + case ScanPgDn: + seq = []byte{0x1b, '[', '6', '~'} + case ScanF1: + seq = []byte{0x1b, 'O', 'P'} + case ScanF2: + seq = []byte{0x1b, 'O', 'Q'} + case ScanF3: + seq = []byte{0x1b, 'O', 'R'} + case ScanF4: + seq = []byte{0x1b, 'O', 'S'} + case ScanF5: + seq = []byte{0x1b, '[', '1', '5', '~'} + case ScanF6: + seq = []byte{0x1b, '[', '1', '7', '~'} + case ScanF7: + seq = []byte{0x1b, '[', '1', '8', '~'} + case ScanF8: + seq = []byte{0x1b, '[', '1', '9', '~'} + case ScanF9: + seq = []byte{0x1b, '[', '2', '0', '~'} + case ScanF10: + seq = []byte{0x1b, '[', '2', '1', '~'} + case ScanEsc: + seq = []byte{0x1b} + default: + return // Unknown scan code, ignore + } + keyBufferPushBytes(seq) + return + } + + // Handle regular characters + if c := byte(key.UnicodeChar); c < 128 { + if c != 0 { + keyBufferPushByte(c) + } + return + } + + // Non-ASCII character, output '?' + keyBufferPushByte('?') +} + +// ReadKey reads all available keys using the Ex protocol and buffers them. +func ReadKey() { + ex := getConInEx() + if ex == nil { + return + } + + for { + var keyData KeyData + status := Call( + ex.ReadKeyStrokeEx, + uintptr(unsafe.Pointer(ex)), + uintptr(unsafe.Pointer(&keyData)), + ) + if status != Success { + break + } + convertKeyToBytes(keyData.Key) + } +} + +// IsKeyPressed triggers UEFI's console driver to check for pending input +// using the Ex protocol's WaitForKeyEx event. +func IsKeyPressed() bool { + ex := getConInEx() + if ex == nil { + return false + } + + st := GetSystemTable() + if st == nil || st.BootServices == nil || st.BootServices.CheckEvent == 0 { + return false + } + + return Call(st.BootServices.CheckEvent, ex.WaitForKeyEx) == Success +} diff --git a/src/machine/uefi/memory.go b/src/machine/uefi/memory.go new file mode 100644 index 0000000000..4f43408a9a --- /dev/null +++ b/src/machine/uefi/memory.go @@ -0,0 +1,116 @@ +//go:build uefi + +package uefi + +import "unsafe" + +// Memory allocation types for AllocatePages +type AllocateType uint32 + +const ( + AllocateAnyPages AllocateType = iota + AllocateMaxAddress + AllocateAddress +) + +// Memory types for AllocatePages +type MemoryType uint32 + +const ( + EfiReservedMemoryType MemoryType = iota + EfiLoaderCode + EfiLoaderData + EfiBootServicesCode + EfiBootServicesData + EfiRuntimeServicesCode + EfiRuntimeServicesData + EfiConventionalMemory + EfiUnusableMemory + EfiACPIReclaimMemory + EfiACPIMemoryNVS + EfiMemoryMappedIO + EfiMemoryMappedIOPortSpace + EfiPalCode + EfiPersistentMemory + EfiMaxMemoryType +) + +// PageSize is the UEFI page size (4KB) +const PageSize = 4096 + +// AllocatePages allocates memory pages from UEFI. +// Returns the physical address of the allocated memory, or 0 on failure. +// For AllocateAddress, pass the desired address as addr. +func AllocatePages(allocType AllocateType, memType MemoryType, pages uintptr, addr *uintptr) uintptr { + st := GetSystemTable() + if st == nil || st.BootServices == nil || st.BootServices.AllocatePages == 0 { + return 0 + } + + status := Call( + st.BootServices.AllocatePages, + uintptr(allocType), + uintptr(memType), + pages, + uintptr(unsafe.Pointer(addr)), + ) + if status != Success { + return 0 + } + + return *addr +} + +// FreePages frees memory pages previously allocated with AllocatePages. +func FreePages(memory uintptr, pages uintptr) Status { + st := GetSystemTable() + if st == nil || st.BootServices == nil || st.BootServices.FreePages == 0 { + return ErrUnsupported + } + return Call(st.BootServices.FreePages, memory, pages) +} + +// MemoryDescriptor describes a region in the UEFI memory map. +// Note: the actual descriptor size returned by GetMemoryMap may be larger +// than this struct due to firmware extensions. Always use the returned +// descriptorSize to iterate. +type MemoryDescriptor struct { + Type uint32 + _ uint32 // padding + PhysicalStart uint64 + VirtualStart uint64 + NumberOfPages uint64 + Attribute uint64 + _ uint64 // padding +} + +// GetMemoryMap retrieves the current UEFI memory map. +// Returns the number of entries for iteration. +// Use MemMapEntry(buf, i, descSize) to access entries. +func GetMemoryMap(memMapBuffer []byte, memMapSize *uintptr, memDescSize *uintptr) int { + st := GetSystemTable() + if st == nil || st.BootServices == nil || st.BootServices.GetMemoryMap == 0 { + return 0 + } + + status := Call(st.BootServices.GetMemoryMap, + uintptr(unsafe.Pointer(memMapSize)), + uintptr(unsafe.Pointer(&memMapBuffer[0])), + uintptr(0), + uintptr(unsafe.Pointer(memDescSize)), + uintptr(0), + ) + if status != Success { + return 0 + } else if *memDescSize < unsafe.Sizeof(MemoryDescriptor{}) { + return 0 + } + + return int(*memMapSize) / int(*memDescSize) +} + +// MemMapEntry returns the i-th memory descriptor from the last GetMemoryMap call. +func MemMapEntry(memMapBuffer []byte, i int, descSize uintptr) *MemoryDescriptor { + base := unsafe.Pointer(&memMapBuffer[0]) + return (*MemoryDescriptor)(unsafe.Add(base, i*int(descSize))) +} diff --git a/src/machine/uefi/system.go b/src/machine/uefi/system.go new file mode 100644 index 0000000000..51de49f617 --- /dev/null +++ b/src/machine/uefi/system.go @@ -0,0 +1,86 @@ +//go:build uefi + +package uefi + +// ResetType specifies the type of system reset. +type ResetType uint32 + +const ( + // ResetCold causes a system-wide reset that resets all processors and devices. + ResetCold ResetType = iota + // ResetWarm causes a system-wide initialization without resetting processors. + ResetWarm + // ResetShutdown causes the system to enter a power state equivalent to ACPI G2/S5 or G3. + ResetShutdown + // ResetPlatformSpecific causes a platform-specific reset type. + ResetPlatformSpecific +) + +// Reset performs a system reset. +// resetType specifies the type of reset (ResetCold, ResetWarm, ResetShutdown, ResetPlatformSpecific). +// status is the status code for the reset (typically 0 for Success). +// This function does not return on success. +func Reset(resetType ResetType, status Status) { + st := GetSystemTable() + if st == nil || st.RuntimeServices == nil || st.RuntimeServices.ResetSystem == 0 { + return + } + Call( + st.RuntimeServices.ResetSystem, + uintptr(resetType), + uintptr(status), + 0, // DataSize + 0, // ResetData (NULL) + ) + // Should not return +} + +// Reboot performs a cold reset of the system. +// This function does not return on success. +func Reboot() { + Reset(ResetCold, Success) +} + +// Shutdown powers off the system. +// This function does not return on success. +func Shutdown() { + Reset(ResetShutdown, Success) +} + +// SetWatchdogTimer sets, resets, or disables the watchdog timer. +// timeout is the number of seconds to wait before the watchdog fires (0 disables it). +// By default UEFI sets a 5-minute watchdog; call SetWatchdogTimer(0) to disable it. +func SetWatchdogTimer(timeout uintptr) Status { + st := GetSystemTable() + if st == nil || st.BootServices == nil || st.BootServices.SetWatchdogTimer == 0 { + return ErrUnsupported + } + // SetWatchdogTimer(Timeout, WatchdogCode, DataSize, WatchdogData) + return Call( + st.BootServices.SetWatchdogTimer, + timeout, + 0, // WatchdogCode + 0, // DataSize + 0, // WatchdogData (NULL) + ) +} + +// DisableWatchdog disables the UEFI watchdog timer. +func DisableWatchdog() Status { + return SetWatchdogTimer(0) +} + +// Exit terminates the UEFI application with the specified exit code. +func Exit(code int) { + st := GetSystemTable() + if st == nil || st.BootServices == nil { + return + } + Call( + st.BootServices.Exit, + uintptr(ImageHandle()), + uintptr(code), + 0, + 0, + ) +} diff --git a/src/machine/uefi/tables.go b/src/machine/uefi/tables.go new file mode 100644 index 0000000000..0fa44b46e7 --- /dev/null +++ b/src/machine/uefi/tables.go @@ -0,0 +1,140 @@ +//go:build uefi + +package uefi + +// SystemTable is the main UEFI system table. +type SystemTable struct { + Hdr TableHeader + FirmwareVendor *uint16 + FirmwareRevision uint32 + ConsoleInHandle Handle + ConIn *SimpleTextInputProtocol + ConsoleOutHandle Handle + ConOut *SimpleTextOutputProtocol + StandardErrorHandle Handle + StdErr *SimpleTextOutputProtocol + RuntimeServices *RuntimeServices + BootServices *BootServices + NumberOfTableEntries uintptr + ConfigurationTable uintptr +} + +// BootServices provides boot-time services. +type BootServices struct { + Hdr TableHeader + + // Task Priority Services + RaiseTPL uintptr + RestoreTPL uintptr + + // Memory Services + AllocatePages uintptr + FreePages uintptr + GetMemoryMap uintptr + AllocatePool uintptr + FreePool uintptr + + // Event & Timer Services + CreateEvent uintptr + SetTimer uintptr + WaitForEvent uintptr + SignalEvent uintptr + CloseEvent uintptr + CheckEvent uintptr + + // Protocol Handler Services + InstallProtocolInterface uintptr + ReinstallProtocolInterface uintptr + UninstallProtocolInterface uintptr + HandleProtocol uintptr + Reserved uintptr + RegisterProtocolNotify uintptr + LocateHandle uintptr + LocateDevicePath uintptr + InstallConfigurationTable uintptr + + // Image Services + LoadImage uintptr + StartImage uintptr + Exit uintptr + UnloadImage uintptr + ExitBootServices uintptr + + // Miscellaneous Services + GetNextMonotonicCount uintptr + Stall uintptr + SetWatchdogTimer uintptr + + // Driver Support Services + ConnectController uintptr + DisconnectController uintptr + + // Open and Close Protocol Services + OpenProtocol uintptr + CloseProtocol uintptr + OpenProtocolInformation uintptr + + // Library Services + ProtocolsPerHandle uintptr + LocateHandleBuffer uintptr + LocateProtocol uintptr + InstallMultipleProtocolInterfaces uintptr + UninstallMultipleProtocolInterfaces uintptr + + // 32-bit CRC Services + CalculateCrc32 uintptr + + // Miscellaneous Services (cont.) + CopyMem uintptr + SetMem uintptr + CreateEventEx uintptr +} + +// RuntimeServices provides runtime services. +type RuntimeServices struct { + Hdr TableHeader + + // Time Services + GetTime uintptr + SetTime uintptr + GetWakeupTime uintptr + SetWakeupTime uintptr + + // Virtual Memory Services + SetVirtualAddressMap uintptr + ConvertPointer uintptr + + // Variable Services + GetVariable uintptr + GetNextVariableName uintptr + SetVariable uintptr + + // Miscellaneous Services + GetNextHighMonotonicCount uintptr + ResetSystem uintptr + + // UEFI 2.0 Capsule Services + UpdateCapsule uintptr + QueryCapsuleCapabilities uintptr + + // Miscellaneous UEFI 2.0 Services + QueryVariableInfo uintptr +} + +// Global pointers set by assembly entry point +// +//go:extern efi_image_handle +var imageHandle Handle + +//go:extern efi_system_table +var systemTablePtr *SystemTable + +// ImageHandle returns the UEFI image handle for this application. +func ImageHandle() Handle { + return imageHandle +} + +// GetSystemTable returns the UEFI system table pointer. +func GetSystemTable() *SystemTable { + return systemTablePtr +} diff --git a/src/machine/uefi/time.go b/src/machine/uefi/time.go new file mode 100644 index 0000000000..03e0b8d60f --- /dev/null +++ b/src/machine/uefi/time.go @@ -0,0 +1,84 @@ +//go:build uefi + +package uefi + +import "unsafe" + +// Time represents a UEFI time value. +type Time struct { + Year uint16 + Month uint8 + Day uint8 + Hour uint8 + Minute uint8 + Second uint8 + Pad1 uint8 + Nanosecond uint32 + TimeZone int16 + Daylight uint8 + Pad2 uint8 +} + +// Timestamp converts a UEFI Time to Unix timestamp (seconds since 1970). +func (t Time) Timestamp() int64 { + daysInMonth := [12]int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + + year := int(t.Year) + month := int(t.Month) + day := int(t.Day) + + days := int64(0) + + for y := 1970; y < year; y++ { + if isLeapYear(y) { + days += 366 + } else { + days += 365 + } + } + + for m := 1; m < month; m++ { + days += int64(daysInMonth[m-1]) + if m == 2 && isLeapYear(year) { + days++ + } + } + + days += int64(day - 1) + + sec := days * 24 * 60 * 60 + sec += int64(t.Hour) * 60 * 60 + sec += int64(t.Minute) * 60 + sec += int64(t.Second) + + if t.TimeZone != 2047 && t.TimeZone >= -1440 && t.TimeZone <= 1440 { + sec -= int64(t.TimeZone) * 60 + } + + return sec +} + +func isLeapYear(year int) bool { + return year%4 == 0 && (year%100 != 0 || year%400 == 0) +} + +// GetTime retrieves the current time from UEFI runtime services. +// Returns the status code (0 = success, non-zero = error). +func GetTime(time *Time) Status { + st := GetSystemTable() + if st == nil || st.RuntimeServices == nil || st.RuntimeServices.GetTime == 0 { + return ErrUnsupported + } + return Call(st.RuntimeServices.GetTime, uintptr(unsafe.Pointer(time)), 0) + +} + +// Stall delays execution for the specified number of microseconds. +// Note: This blocks all execution including goroutines. +func Stall(microseconds uint64) { + st := GetSystemTable() + if st == nil || st.BootServices == nil || st.BootServices.Stall == 0 { + return + } + Call(st.BootServices.Stall, uintptr(microseconds)) +} diff --git a/src/machine/uefi/types.go b/src/machine/uefi/types.go new file mode 100644 index 0000000000..5483d10851 --- /dev/null +++ b/src/machine/uefi/types.go @@ -0,0 +1,43 @@ +//go:build uefi + +package uefi + +// Handle is an opaque UEFI handle type. +type Handle uintptr + +// Status represents a UEFI status code. +type Status uintptr + +// UEFI status codes +const ( + Success Status = 0 + + // Error codes (high bit set) + ErrLoadError Status = 0x8000000000000001 + ErrInvalidParameter Status = 0x8000000000000002 + ErrUnsupported Status = 0x8000000000000003 + ErrBadBufferSize Status = 0x8000000000000004 + ErrBufferTooSmall Status = 0x8000000000000005 + ErrNotReady Status = 0x8000000000000006 + ErrDeviceError Status = 0x8000000000000007 + ErrWriteProtected Status = 0x8000000000000008 + ErrOutOfResources Status = 0x8000000000000009 + ErrNotFound Status = 0x800000000000000E +) + +// TableHeader is the standard UEFI table header. +type TableHeader struct { + Signature uint64 + Revision uint32 + HeaderSize uint32 + CRC32 uint32 + Reserved uint32 +} + +// GUID represents a UEFI Globally Unique Identifier. +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]uint8 +} diff --git a/src/runtime/interrupt/interrupt_none.go b/src/runtime/interrupt/interrupt_none.go index ea8bdb68c6..22f1644f0f 100644 --- a/src/runtime/interrupt/interrupt_none.go +++ b/src/runtime/interrupt/interrupt_none.go @@ -1,4 +1,4 @@ -//go:build !baremetal || tkey +//go:build !baremetal || tkey || uefi package interrupt diff --git a/src/runtime/os_winabi.go b/src/runtime/os_winabi.go new file mode 100644 index 0000000000..20a9dd1f70 --- /dev/null +++ b/src/runtime/os_winabi.go @@ -0,0 +1,74 @@ +//go:build windows || uefi + +package runtime + +import "unsafe" + +// MS-DOS stub with PE header offset: +// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only +type dosHeader struct { + signature uint16 + _ [58]byte // skip DOS header + peHeader uint32 // at offset 0x3C +} + +// COFF file header: +// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#file-headers +type peHeader struct { + magic uint32 + machine uint16 + numberOfSections uint16 + timeDateStamp uint32 + pointerToSymbolTable uint32 + numberOfSymbols uint32 + sizeOfOptionalHeader uint16 + characteristics uint16 +} + +// COFF section header: +// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers +type peSection struct { + name [8]byte + virtualSize uint32 + virtualAddress uint32 + sizeOfRawData uint32 + pointerToRawData uint32 + pointerToRelocations uint32 + pointerToLinenumbers uint32 + numberOfRelocations uint16 + numberOfLinenumbers uint16 + characteristics uint32 +} + +// Mark global variables. +// Unfortunately, the linker doesn't provide symbols for the start and end of +// the data/bss sections. Therefore these addresses need to be determined at +// runtime. This might seem complex and it kind of is, but it only compiles to +// around 160 bytes of amd64 instructions. +// Most of this function is based on the documentation in +// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format. +func findGlobalsFromPE(dosHeader *dosHeader, found func(start, end uintptr)) { + // Constants used in this function. + const ( + // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format + IMAGE_SCN_MEM_WRITE = 0x80000000 + ) + + // Find the PE header at offset 0x3C. + pe := (*peHeader)(unsafe.Add(unsafe.Pointer(dosHeader), uintptr(dosHeader.peHeader))) + if gcAsserts && pe.magic != 0x00004550 { // 0x4550 is "PE" + runtimePanic("cannot find PE header") + } + + // Iterate through sections. + section := (*peSection)(unsafe.Pointer(uintptr(unsafe.Pointer(pe)) + uintptr(pe.sizeOfOptionalHeader) + unsafe.Sizeof(peHeader{}))) + for i := 0; i < int(pe.numberOfSections); i++ { + if section.characteristics&IMAGE_SCN_MEM_WRITE != 0 { + // Found a writable section. Scan the entire section for roots. + start := uintptr(unsafe.Pointer(dosHeader)) + uintptr(section.virtualAddress) + end := uintptr(unsafe.Pointer(dosHeader)) + uintptr(section.virtualAddress) + uintptr(section.virtualSize) + found(start, end) + } + section = (*peSection)(unsafe.Add(unsafe.Pointer(section), unsafe.Sizeof(peSection{}))) + } +} diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go index a124e7ab14..2df2d788a6 100644 --- a/src/runtime/os_windows.go +++ b/src/runtime/os_windows.go @@ -4,46 +4,15 @@ import "unsafe" const GOOS = "windows" -//export GetModuleHandleExA -func _GetModuleHandleExA(dwFlags uint32, lpModuleName unsafe.Pointer, phModule **exeHeader) bool - -// MS-DOS stub with PE header offset: -// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only -type exeHeader struct { - signature uint16 - _ [58]byte // skip DOS header - peHeader uint32 // at offset 0x3C -} +const ( + // https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandleexa + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002 +) -// COFF file header: -// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#file-headers -type peHeader struct { - magic uint32 - machine uint16 - numberOfSections uint16 - timeDateStamp uint32 - pointerToSymbolTable uint32 - numberOfSymbols uint32 - sizeOfOptionalHeader uint16 - characteristics uint16 -} - -// COFF section header: -// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers -type peSection struct { - name [8]byte - virtualSize uint32 - virtualAddress uint32 - sizeOfRawData uint32 - pointerToRawData uint32 - pointerToRelocations uint32 - pointerToLinenumbers uint32 - numberOfRelocations uint16 - numberOfLinenumbers uint16 - characteristics uint32 -} +//export GetModuleHandleExA +func _GetModuleHandleExA(dwFlags uint32, lpModuleName unsafe.Pointer, phModule **dosHeader) bool -var module *exeHeader +var module *dosHeader // Mark global variables. // Unfortunately, the linker doesn't provide symbols for the start and end of @@ -53,15 +22,6 @@ var module *exeHeader // Most of this function is based on the documentation in // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format. func findGlobals(found func(start, end uintptr)) { - // Constants used in this function. - const ( - // https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandleexa - GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002 - - // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format - IMAGE_SCN_MEM_WRITE = 0x80000000 - ) - if module == nil { // Obtain a handle to the currently executing image. What we're getting // here is really just __ImageBase, but it's probably better to obtain @@ -72,23 +32,7 @@ func findGlobals(found func(start, end uintptr)) { } } - // Find the PE header at offset 0x3C. - pe := (*peHeader)(unsafe.Add(unsafe.Pointer(module), module.peHeader)) - if gcAsserts && pe.magic != 0x00004550 { // 0x4550 is "PE" - runtimePanic("cannot find PE header") - } - - // Iterate through sections. - section := (*peSection)(unsafe.Pointer(uintptr(unsafe.Pointer(pe)) + uintptr(pe.sizeOfOptionalHeader) + unsafe.Sizeof(peHeader{}))) - for i := 0; i < int(pe.numberOfSections); i++ { - if section.characteristics&IMAGE_SCN_MEM_WRITE != 0 { - // Found a writable section. Scan the entire section for roots. - start := uintptr(unsafe.Pointer(module)) + uintptr(section.virtualAddress) - end := uintptr(unsafe.Pointer(module)) + uintptr(section.virtualAddress) + uintptr(section.virtualSize) - found(start, end) - } - section = (*peSection)(unsafe.Add(unsafe.Pointer(section), unsafe.Sizeof(peSection{}))) - } + findGlobalsFromPE(module, found) } type systeminfo struct { diff --git a/src/runtime/rand_norng.go b/src/runtime/rand_norng.go index ebeba91cb8..d6d0947900 100644 --- a/src/runtime/rand_norng.go +++ b/src/runtime/rand_norng.go @@ -1,4 +1,4 @@ -//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350) +//go:build baremetal && !(nrf || (stm32 && !(stm32f103 || stm32l0x1)) || (sam && atsamd51) || (sam && atsame5x) || esp32c3 || tkey || (tinygo.riscv32 && virt) || rp2040 || rp2350 || uefi) package runtime diff --git a/src/runtime/runtime_minimal_libc.go b/src/runtime/runtime_minimal_libc.go new file mode 100644 index 0000000000..c9a8f9e112 --- /dev/null +++ b/src/runtime/runtime_minimal_libc.go @@ -0,0 +1,135 @@ +//go:build uefi + +package runtime + +import "unsafe" + +// ============================================================================= +// Minimal libc replacements +// ============================================================================= +// +// These are required because LLVM lowers operations like struct zeroing and +// slice copying into calls to memset/memcpy/memmove. fmt package needs log() +// for float formatting. + +// maxArraySize is the maximum array size for pointer-to-array casts. +// On 64-bit: 1<<48-1 (256 TiB - 1), on 32-bit: 1<<31-1 (2 GiB - 1). +const maxArraySize = (1 << (31 + 17*(^uintptr(0)>>63))) - 1 + +//export memset +func libc_memset(dest unsafe.Pointer, c int, n uintptr) unsafe.Pointer { + d := (*[maxArraySize]byte)(dest) + val := byte(c) + for i := uintptr(0); i < n; i++ { + d[i] = val + } + return dest +} + +//export memcpy +func libc_memcpy(dest, src unsafe.Pointer, n uintptr) unsafe.Pointer { + d := (*[maxArraySize]byte)(dest) + s := (*[maxArraySize]byte)(src) + for i := uintptr(0); i < n; i++ { + d[i] = s[i] + } + return dest +} + +//export memmove +func libc_memmove(dest, src unsafe.Pointer, n uintptr) unsafe.Pointer { + if uintptr(dest) < uintptr(src) { + // Copy forward. + d := (*[maxArraySize]byte)(dest) + s := (*[maxArraySize]byte)(src) + for i := uintptr(0); i < n; i++ { + d[i] = s[i] + } + } else { + // Copy backward (handles overlapping regions where dest > src). + d := (*[maxArraySize]byte)(dest) + s := (*[maxArraySize]byte)(src) + for i := n; i > 0; i-- { + d[i-1] = s[i-1] + } + } + return dest +} + +//export memcmp +func libc_memcmp(s1, s2 unsafe.Pointer, n uintptr) int { + p1 := (*[maxArraySize]byte)(s1) + p2 := (*[maxArraySize]byte)(s2) + for i := uintptr(0); i < n; i++ { + if p1[i] != p2[i] { + return int(p1[i] - p2[i]) + } + } + return 0 +} + +//export strlen +func libc_strlen(s unsafe.Pointer) uintptr { + p := (*[maxArraySize]byte)(s) + var n uintptr + for p[n] != 0 { + n++ + } + return n +} + +// log implements the natural logarithm needed by fmt for float formatting. +// Ported from musl libc. +// +//export log +func libc_log(x float64) float64 { + const ( + ln2Hi = 6.93147180369123816490e-01 + ln2Lo = 1.90821492927058770002e-10 + lg1 = 6.666666666666735130e-01 + lg2 = 3.999999999940941908e-01 + lg3 = 2.857142874366239149e-01 + lg4 = 2.222219843214978396e-01 + lg5 = 1.818357216161805012e-01 + lg6 = 1.531383769920937332e-01 + lg7 = 1.479819860511658591e-01 + ) + + u := *(*uint64)(unsafe.Pointer(&x)) + hx := uint32(u >> 32) + k := 0 + + if hx < 0x00100000 || hx>>31 != 0 { + if u<<1 == 0 { + return -1 / (x * x) // log(+-0) = -inf + } + if hx>>31 != 0 { + return (x - x) / 0.0 // log(-x) = NaN + } + k -= 54 + x *= 0x1p54 + u = *(*uint64)(unsafe.Pointer(&x)) + hx = uint32(u >> 32) + } else if hx >= 0x7ff00000 { + return x + } else if hx == 0x3ff00000 && u<<32 == 0 { + return 0 + } + + hx += 0x3ff00000 - 0x3fe6a09e + k += int(hx>>20) - 0x3ff + hx = (hx & 0x000fffff) + 0x3fe6a09e + u = uint64(hx)<<32 | (u & 0xffffffff) + x = *(*float64)(unsafe.Pointer(&u)) + + f := x - 1.0 + hfsq := 0.5 * f * f + s := f / (2.0 + f) + z := s * s + w := z * z + t1 := w * (lg2 + w*(lg4+w*lg6)) + t2 := z * (lg1 + w*(lg3+w*(lg5+w*lg7))) + R := t2 + t1 + dk := float64(k) + return s*(hfsq+R) + dk*ln2Lo - hfsq + f + dk*ln2Hi +} diff --git a/src/runtime/runtime_uefi.go b/src/runtime/runtime_uefi.go new file mode 100644 index 0000000000..ccbf6cdb43 --- /dev/null +++ b/src/runtime/runtime_uefi.go @@ -0,0 +1,228 @@ +//go:build uefi + +package runtime + +import ( + "machine/uefi" + "unsafe" +) + +var timerFrequency uint64 // Timer frequency in ticks per microsecond + +// peImageBase is the base address of the loaded PE image in memory. +// __ImageBase is a synthetic symbol provided by LLD (the PE/COFF linker) +// that points to the start of the PE image headers (the DOS "MZ" stub). +// It is always available in PE/COFF binaries and requires no linker script. +// +//go:extern __ImageBase +var peImageBase [0]byte + +func preinit() { + // Fix stackTop: baremetal.go sets it to the ADDRESS of _stack_top, + // but _stack_top is a variable storing the initial RSP value. + stackTop = *(*uintptr)(unsafe.Pointer(&stackTopSymbol)) + + base := uintptr(unsafe.Pointer(&peImageBase)) + + // Set globalsStart/globalsEnd by parsing PE/COFF headers. + findGlobalsFromPE((*dosHeader)(unsafe.Pointer(base)), func(start, end uintptr) { + if globalsStart == 0 { + globalsStart = start + globalsEnd = end + } + }) + + // Allocate heap dynamically via UEFI Boot Services + allocateUEFIHeap() + // Calibrate timer frequency + timerFrequency = uefi.CalibrateTimerFrequency() +} + +// variables defined as globals and used before the heap is allocated +var ( + // memMapBuffer is a static byte buffer for GetMemoryMap (used before heap). + // 256 * 48 = 12288 bytes, enough for ~256 descriptors at typical OVMF descSize=48. + memMapBuffer [256 * 48]byte + memDescSize uintptr + memMapSize uintptr = uintptr(len(memMapBuffer)) + allocatedMemory uintptr +) + +// allocateUEFIHeap allocates heap memory using UEFI AllocatePages. +// It queries the memory map to determine available memory, allocates the +// largest region found. +func allocateUEFIHeap() { + count := uefi.GetMemoryMap(memMapBuffer[:], &memMapSize, &memDescSize) + if count == 0 { + runtimePanic("failed to get UEFI memory map") + } + + var pages uintptr + var heapAllocatedPages uintptr + + // Find the largest contiguous EfiConventionalMemory region + for i := 0; i < count; i++ { + desc := uefi.MemMapEntry(memMapBuffer[:], i, memDescSize) + if uefi.MemoryType(desc.Type) == uefi.EfiConventionalMemory { + n := uintptr(desc.NumberOfPages) + if n > pages { + pages = n + allocatedMemory = uintptr(desc.PhysicalStart) + } + } + } + + if pages == 0 || allocatedMemory == 0 { + runtimePanic("no conventional memory region found for heap allocation") + } else if pages > 0 { + heapAllocatedPages = pages + } + + // Try to allocate at the known region address first + if allocatedMemory != 0 { + heapStart = uefi.AllocatePages( + uefi.AllocateAddress, + uefi.EfiLoaderData, + heapAllocatedPages, + &allocatedMemory, + ) + } + + // Fall back to any-pages allocation, halving on failure + for heapStart == 0 { + heapStart = uefi.AllocatePages( + uefi.AllocateAnyPages, + uefi.EfiLoaderData, + heapAllocatedPages, + &allocatedMemory, + ) + if heapStart == 0 { + heapAllocatedPages /= 2 + } + } + + if heapStart == 0 { + runtimePanic("failed to allocate heap via UEFI AllocatePages") + } + + heapEnd = heapStart + (heapAllocatedPages * uefi.PageSize) +} + +// called from scheduler by initAll +func init() { + // Disable UEFI watchdog timer + uefi.DisableWatchdog() + // Initialize clock time + initClockTime() +} + +//export main +func main() { + preinit() + run() + exit(0) +} + +// putchar outputs a single character via UEFI ConOut. +func putchar(c byte) { + var buf [2]uint16 + + buf[0] = uint16(c) + buf[1] = 0 + + uefi.OutputString(&buf[0]) +} + +// getchar reads a single byte from the keyboard. +func getchar() byte { + for { + if b, ok := uefi.KeyBufferPop(); ok { + return b + } + if uefi.IsKeyPressed() { + uefi.ReadKey() + } + Gosched() + } +} + +func buffered() int { + if uefi.IsKeyPressed() { + uefi.ReadKey() + } + return uefi.KeyBufferAvailable() +} + +func ticksToNanoseconds(ticks timeUnit) int64 { + if timerFrequency == 0 { + return int64(ticks) + } + return int64(ticks) * 1000 / int64(timerFrequency) +} + +func nanosecondsToTicks(ns int64) timeUnit { + if timerFrequency == 0 { + return timeUnit(ns) + } + return timeUnit(ns / 1000 * int64(timerFrequency)) +} + +func initClockTime() { + mono := nanotime() + + var t uefi.Time + var sec int64 + var nsec int64 + + status := uefi.GetTime(&t) + if status != 0 { + sec = mono / (1000 * 1000 * 1000) + nsec = mono - sec*(1000*1000*1000) + } else { + sec = t.Timestamp() + nsec = int64(t.Nanosecond) + } + + timeOffset = sec*1000000000 + nsec - mono +} + +func ticks() timeUnit { + return timeUnit(uefi.Ticks()) +} + +func sleepTicks(d timeUnit) { + if d <= 0 { + return + } + + target := ticks() + d + for ticks() < target { + uefi.CpuPause() + } +} + +func exit(code int) { + uefi.Exit(code) +} + +func abort() { + exit(1) +} + +func hardwareRand() (uint64, bool) { + if !uefi.HasRNGSupport() { + return 0, false + } + return uefi.ReadRandom() +} + +// TinyGo does not support any form of parallelism on UEFI, so these can +// be left empty. + +//go:linkname procPin sync/atomic.runtime_procPin +func procPin() { +} + +//go:linkname procUnpin sync/atomic.runtime_procUnpin +func procUnpin() { +} diff --git a/src/runtime/runtime_uefi_amd64.S b/src/runtime/runtime_uefi_amd64.S new file mode 100644 index 0000000000..ed134fd8c1 --- /dev/null +++ b/src/runtime/runtime_uefi_amd64.S @@ -0,0 +1,80 @@ +// UEFI Entry Point for x86_64 +// +// efi_main receives (per MS x64 ABI): +// RCX = EFI_HANDLE ImageHandle +// RDX = EFI_SYSTEM_TABLE *SystemTable +// +// We store these for Go code access, then call main. + +.section .text.efi_main,"ax" +.global efi_main +efi_main: + // Save UEFI parameters to global storage + // These will be accessible from Go code via extern vars + leaq efi_image_handle(%rip), %rax + movq %rcx, (%rax) + + leaq efi_system_table(%rip), %rax + movq %rdx, (%rax) + + // Save original stack pointer to __stack_top variable + leaq __stack_top(%rip), %rax + movq %rsp, (%rax) + + // Align stack to 16 bytes (MS ABI requirement) + // This is critical for XMM register operations in task switching + andq $-16, %rsp + + // Reserve shadow space (32 bytes) required by MS x64 ABI + subq $32, %rsp + + // Call TinyGo main (exported as 'main' from Go and never returns) + callq main + +// ============================================================================= +// Data Section +// ============================================================================= + +.section .data,"aw" +.align 8 + +// UEFI handles +.global efi_image_handle +efi_image_handle: + .quad 0 + +.global efi_system_table +efi_system_table: + .quad 0 + +// _fltused is required by the PE/COFF linker when floating-point code is present. +.global _fltused +_fltused: + .long 0 + +// Dummy symbols required by baremetal.go. +// _globals_start/_globals_end: determined at runtime by parsing PE/COFF headers. +// _heap_start/_heap_end: heap is allocated via UEFI AllocatePages. +.global _globals_start +.global _globals_end +.global _heap_start +.global _heap_end +_globals_start: +_globals_end: +_heap_start: +_heap_end: + +// Stack top - written by efi_main with the initial RSP value. +// preinit() reads the value stored here to fix runtime.stackTop. +.global __stack_top +.global _stack_top +__stack_top: +_stack_top: + .quad 0 + +// SBAT (Secure Boot Advanced Targeting) metadata section. +// Required by modern shim for second-stage binary verification. + +.section .sbat,"dr" +.ascii "sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md\n" +.ascii "node,1,TinyGo,app.efi,1.0,https://tinygo.org\n" \ No newline at end of file diff --git a/src/runtime/runtime_uefi_arm64.S b/src/runtime/runtime_uefi_arm64.S new file mode 100644 index 0000000000..253fbad909 --- /dev/null +++ b/src/runtime/runtime_uefi_arm64.S @@ -0,0 +1,83 @@ +// UEFI Entry Point for ARM64 +// +// efi_main receives (per AAPCS64): +// x0 = EFI_HANDLE ImageHandle +// x1 = EFI_SYSTEM_TABLE *SystemTable +// +// We store these for Go code access, then call main. + +.section .text.efi_main,"ax" +.global efi_main +efi_main: + // Save UEFI parameters to global storage. + // ARM64 uses adrp+add for PC-relative addressing. + adrp x2, efi_image_handle + add x2, x2, :lo12:efi_image_handle + str x0, [x2] + + adrp x2, efi_system_table + add x2, x2, :lo12:efi_system_table + str x1, [x2] + + // Save original stack pointer to __stack_top variable. + adrp x2, __stack_top + add x2, x2, :lo12:__stack_top + mov x3, sp + str x3, [x2] + + // Ensure stack is 16-byte aligned (AAPCS64 requirement). + // SP should already be aligned by UEFI firmware, but be safe. + mov x3, sp + and x3, x3, #~0xF + mov sp, x3 + + // Call TinyGo main (exported as 'main' from Go and never returns). + bl main + +// ============================================================================= +// Data Section +// ============================================================================= + +.section .data,"aw" +.align 3 + +// UEFI handles +.global efi_image_handle +efi_image_handle: + .quad 0 + +.global efi_system_table +efi_system_table: + .quad 0 + +// _fltused is required by the PE/COFF linker when floating-point code is present. +.global _fltused +_fltused: + .long 0 + +// Dummy symbols required by baremetal.go. +// _globals_start/_globals_end: determined at runtime by parsing PE/COFF headers. +// _heap_start/_heap_end: heap is allocated via UEFI AllocatePages. +.global _globals_start +.global _globals_end +.global _heap_start +.global _heap_end +_globals_start: +_globals_end: +_heap_start: +_heap_end: + +// Stack top - written by efi_main with the initial SP value. +// preinit() reads the value stored here to fix runtime.stackTop. +.global __stack_top +.global _stack_top +__stack_top: +_stack_top: + .quad 0 + +// SBAT (Secure Boot Advanced Targeting) metadata section. +// Required by modern shim for second-stage binary verification. + +.section .sbat,"dr" +.ascii "sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md\n" +.ascii "node,1,TinyGo,app.efi,1.0,https://tinygo.org\n" \ No newline at end of file diff --git a/targets/uefi-amd64.json b/targets/uefi-amd64.json new file mode 100644 index 0000000000..cb2bdc39c6 --- /dev/null +++ b/targets/uefi-amd64.json @@ -0,0 +1,42 @@ +{ + "llvm-target": "x86_64-unknown-windows", + "cpu": "x86-64", + "features": "+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87", + "build-tags": ["uefi", "baremetal", "amd64"], + "goos": "linux", + "goarch": "amd64", + "gc": "precise", + "scheduler": "tasks", + "linker": "ld.lld", + "rtlib": "compiler-rt", + "default-stack-size": 65536, + "cflags": [ + "-target", "x86_64-unknown-windows-gnu", + "-Werror", + "-fshort-enums", + "-fomit-frame-pointer", + "-fno-exceptions", + "-fno-unwind-tables", + "-fno-asynchronous-unwind-tables", + "-ffunction-sections", + "-fdata-sections", + "-mno-red-zone" + ], + "ldflags": [ + "-m", "i386pep", + "--subsystem", "efi_application", + "--gc-sections", + "--entry", "efi_main", + "--no-dynamicbase", + "-Bstatic" + ], + "extra-files": [ + "src/runtime/runtime_uefi_amd64.S", + "src/runtime/asm_amd64_windows.S", + "src/machine/uefi/cpu_amd64.S", + "src/machine/uefi/api_amd64.S", + "src/internal/task/task_stack_amd64_windows.S" + ], + "emulator": "./emulators/qemu-uefi-x86_64.sh {}", + "gdb": ["gdb", "gdb-multiarch"] +} diff --git a/targets/uefi-arm64.json b/targets/uefi-arm64.json new file mode 100644 index 0000000000..354f36609e --- /dev/null +++ b/targets/uefi-arm64.json @@ -0,0 +1,41 @@ +{ + "llvm-target": "aarch64-unknown-windows", + "cpu": "generic", + "features": "+ete,+fp-armv8,+neon,+trbe,+v8a,-fmv", + "build-tags": ["uefi", "baremetal", "arm64"], + "goos": "linux", + "goarch": "arm64", + "gc": "precise", + "scheduler": "tasks", + "linker": "ld.lld", + "rtlib": "compiler-rt", + "default-stack-size": 65536, + "cflags": [ + "-target", "aarch64-unknown-windows-gnu", + "-Werror", + "-fshort-enums", + "-fomit-frame-pointer", + "-fno-exceptions", + "-fno-unwind-tables", + "-fno-asynchronous-unwind-tables", + "-ffunction-sections", + "-fdata-sections" + ], + "ldflags": [ + "-m", "arm64pe", + "--subsystem", "efi_application", + "--gc-sections", + "--entry", "efi_main", + "--no-dynamicbase", + "-Bstatic" + ], + "extra-files": [ + "src/runtime/runtime_uefi_arm64.S", + "src/runtime/asm_arm64.S", + "src/machine/uefi/cpu_arm64.S", + "src/machine/uefi/api_arm64.S", + "src/internal/task/task_stack_arm64.S" + ], + "emulator": "./emulators/qemu-uefi-aarch64.sh {}", + "gdb": ["gdb-multiarch"] +}