diff --git a/Makefile b/Makefile index ec80919..c9bcaa7 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,7 @@ lint: lint-check-deps --deadline 300s \ --exclude 'return value not checked' \ --exclude 'possible misuse of unsafe.Pointer' \ + --exclude 'x \^ 0 always equals x' \ ./... lint-check-deps: diff --git a/src/gopheros/kernel/kfmt/early/early_fmt.go b/src/gopheros/kernel/kfmt/fmt.go similarity index 51% rename from src/gopheros/kernel/kfmt/early/early_fmt.go rename to src/gopheros/kernel/kfmt/fmt.go index 224152c..7780dcd 100644 --- a/src/gopheros/kernel/kfmt/early/early_fmt.go +++ b/src/gopheros/kernel/kfmt/fmt.go @@ -1,20 +1,48 @@ -package early +package kfmt -import "gopheros/kernel/hal" +import ( + "io" + "unsafe" +) + +// maxBufSize defines the buffer size for formatting numbers. +const maxBufSize = 32 var ( errMissingArg = []byte("(MISSING)") errWrongArgType = []byte("%!(WRONGTYPE)") errNoVerb = []byte("%!(NOVERB)") errExtraArg = []byte("%!(EXTRA)") - padding = byte(' ') trueValue = []byte("true") falseValue = []byte("false") + + numFmtBuf = []byte("012345678901234567890123456789012") + + // singleByte is used as a shared buffer for passing single characters + // to doWrite. + singleByte = []byte(" ") + + // earlyPrintBuffer is a ring buffer that stores Printf output before the + // console and TTYs are initialized. + earlyPrintBuffer ringBuffer + + // outputSink is a io.Writer where Printf will send its output. If set + // to nil, then the output will be redirected to the earlyPrintBuffer. + outputSink io.Writer ) -// Printf provides a minimal Printf implementation that can be used before the -// Go runtime has been properly initialized. This version of printf does not -// allocate any memory and uses hal.ActiveTerminal for its output. +// SetOutputSink sets the default target for calls to Printf to w and copies +// any data accumulated in the earlyPrintBuffer to itt . +func SetOutputSink(w io.Writer) { + outputSink = w + if w != nil { + io.Copy(w, &earlyPrintBuffer) + } +} + +// Printf provides a minimal Printf implementation that can be safely used +// before the Go runtime has been properly initialized. This implementation +// does not allocate any memory. // // Similar to fmt.Printf, this version of printf supports the following subset // of formatting verbs: @@ -46,7 +74,17 @@ var ( // starts generating calls to runtime.convT2E (which calls runtime.newobject) // when assembling the argument slice which obviously will crash the kernel since // memory management is not yet available. +// +// The output of Printf is written to the currently active TTY. If no TTY is +// available, then the output is buffered into a ring-buffer and can be +// retrieved by a call to FlushRingBuffer. func Printf(format string, args ...interface{}) { + Fprintf(outputSink, format, args...) +} + +// Fprintf behaves exactly like Printf but it writes the formatted output to +// the specified io.Writer. +func Fprintf(w io.Writer, format string, args ...interface{}) { var ( nextCh byte nextArgIndex int @@ -62,8 +100,11 @@ func Printf(format string, args ...interface{}) { } if blockStart < blockEnd { + // passing format[blockStart:blockEnd] to doWrite triggers a + // memory allocation so we need to do this one byte at a time. for i := blockStart; i < blockEnd; i++ { - hal.ActiveTerminal.WriteByte(format[i]) + singleByte[0] = format[i] + doWrite(w, singleByte) } } @@ -75,7 +116,8 @@ func Printf(format string, args ...interface{}) { nextCh = format[blockEnd] switch { case nextCh == '%': - hal.ActiveTerminal.Write([]byte{'%'}) + singleByte[0] = '%' + doWrite(w, singleByte) break parseFmt case nextCh >= '0' && nextCh <= '9': padLen = (padLen * 10) + int(nextCh-'0') @@ -83,21 +125,21 @@ func Printf(format string, args ...interface{}) { case nextCh == 'd' || nextCh == 'x' || nextCh == 'o' || nextCh == 's' || nextCh == 't': // Run out of args to print if nextArgIndex >= len(args) { - hal.ActiveTerminal.Write(errMissingArg) + doWrite(w, errMissingArg) break parseFmt } switch nextCh { case 'o': - fmtInt(args[nextArgIndex], 8, padLen) + fmtInt(w, args[nextArgIndex], 8, padLen) case 'd': - fmtInt(args[nextArgIndex], 10, padLen) + fmtInt(w, args[nextArgIndex], 10, padLen) case 'x': - fmtInt(args[nextArgIndex], 16, padLen) + fmtInt(w, args[nextArgIndex], 16, padLen) case 's': - fmtString(args[nextArgIndex], padLen) + fmtString(w, args[nextArgIndex], padLen) case 't': - fmtBool(args[nextArgIndex]) + fmtBool(w, args[nextArgIndex]) } nextArgIndex++ @@ -105,80 +147,87 @@ func Printf(format string, args ...interface{}) { } // reached end of formatting string without finding a verb - hal.ActiveTerminal.Write(errNoVerb) + doWrite(w, errNoVerb) } blockStart, blockEnd = blockEnd+1, blockEnd+1 } if blockStart != blockEnd { + // passing format[blockStart:blockEnd] to doWrite triggers a + // memory allocation so we need to do this one byte at a time. for i := blockStart; i < blockEnd; i++ { - hal.ActiveTerminal.WriteByte(format[i]) + singleByte[0] = format[i] + doWrite(w, singleByte) } } // Check for unused args for ; nextArgIndex < len(args); nextArgIndex++ { - hal.ActiveTerminal.Write(errExtraArg) + doWrite(w, errExtraArg) } } -// fmtBool prints a formatted version of boolean value v using hal.ActiveTerminal -// for its output. -func fmtBool(v interface{}) { +// fmtBool prints a formatted version of boolean value v. +func fmtBool(w io.Writer, v interface{}) { switch bVal := v.(type) { case bool: switch bVal { case true: - hal.ActiveTerminal.Write(trueValue) + doWrite(w, trueValue) case false: - hal.ActiveTerminal.Write(falseValue) + doWrite(w, falseValue) } default: - hal.ActiveTerminal.Write(errWrongArgType) + doWrite(w, errWrongArgType) return } } -// fmtString prints a formatted version of string or []byte value v, applying the -// padding specified by padLen. This function uses hal.ActiveTerminal for its -// output. -func fmtString(v interface{}, padLen int) { +// fmtString prints a formatted version of string or []byte value v, applying +// the padding specified by padLen. +func fmtString(w io.Writer, v interface{}, padLen int) { switch castedVal := v.(type) { case string: - fmtRepeat(padding, padLen-len(castedVal)) + fmtRepeat(w, ' ', padLen-len(castedVal)) + // converting the string to a byte slice triggers a memory allocation + // so we need to do this one byte at a time. for i := 0; i < len(castedVal); i++ { - hal.ActiveTerminal.WriteByte(castedVal[i]) + singleByte[0] = castedVal[i] + doWrite(w, singleByte) } case []byte: - fmtRepeat(padding, padLen-len(castedVal)) - hal.ActiveTerminal.Write(castedVal) + fmtRepeat(w, ' ', padLen-len(castedVal)) + doWrite(w, castedVal) default: - hal.ActiveTerminal.Write(errWrongArgType) + doWrite(w, errWrongArgType) } } -// fmtRepeat writes count bytes with value ch to the hal.ActiveTerminal. -func fmtRepeat(ch byte, count int) { +// fmtRepeat writes count bytes with value ch. +func fmtRepeat(w io.Writer, ch byte, count int) { + singleByte[0] = ch for i := 0; i < count; i++ { - hal.ActiveTerminal.WriteByte(ch) + doWrite(w, singleByte) } } -// fmtInt prints out a formatted version of v in the requested base, applying the -// padding specified by padLen. This function uses hal.ActiveTerminal for its -// output, supports all built-in signed and unsigned integer types and supports -// base 8, 10 and 16 output. -func fmtInt(v interface{}, base, padLen int) { +// fmtInt prints out a formatted version of v in the requested base, applying +// the padding specified by padLen. This function supports all built-in signed +// and unsigned integer types and base 8, 10 and 16 output. +func fmtInt(w io.Writer, v interface{}, base, padLen int) { var ( sval int64 uval uint64 divider uint64 remainder uint64 - buf [20]byte padCh byte left, right, end int ) + if padLen >= maxBufSize { + padLen = maxBufSize - 1 + } + switch base { case 8: divider = 8 @@ -213,7 +262,7 @@ func fmtInt(v interface{}, base, padLen int) { case int: sval = int64(v.(int)) default: - hal.ActiveTerminal.Write(errWrongArgType) + doWrite(w, errWrongArgType) return } @@ -224,13 +273,13 @@ func fmtInt(v interface{}, base, padLen int) { uval = uint64(sval) } - for { + for right < maxBufSize { remainder = uval % divider if remainder < 10 { - buf[right] = byte(remainder) + '0' + numFmtBuf[right] = byte(remainder) + '0' } else { // map values from 10 to 15 -> a-f - buf[right] = byte(remainder-10) + 'a' + numFmtBuf[right] = byte(remainder-10) + 'a' } right++ @@ -243,27 +292,55 @@ func fmtInt(v interface{}, base, padLen int) { // Apply padding if required for ; right-left < padLen; right++ { - buf[right] = padCh + numFmtBuf[right] = padCh } // Apply negative sign to the rightmost blank character (if using enough padding); // otherwise append the sign as a new char if sval < 0 { - for end = right - 1; buf[end] == ' '; end-- { + for end = right - 1; numFmtBuf[end] == ' '; end-- { } if end == right-1 { right++ } - buf[end+1] = '-' + numFmtBuf[end+1] = '-' } // Reverse in place end = right for right = right - 1; left < right; left, right = left+1, right-1 { - buf[left], buf[right] = buf[right], buf[left] + numFmtBuf[left], numFmtBuf[right] = numFmtBuf[right], numFmtBuf[left] } - hal.ActiveTerminal.Write(buf[0:end]) + doWrite(w, numFmtBuf[0:end]) +} + +// doWrite is a proxy that uses the runtime.noescape hack to hide p from the +// compiler's escape analysis. Without this hack, the compiler cannot properly +// detect that p does not escape (due to the call to the yet unknown outputSink +// io.Writer) and plays it safe by flagging it as escaping. This causes all +// calls to Printf to call runtime.convT2E which triggers a memory allocation +// causing the kernel to crash if a call to Printf is made before the Go +// allocator is initialized. +func doWrite(w io.Writer, p []byte) { + doRealWrite(w, noEscape(unsafe.Pointer(&p))) +} + +func doRealWrite(w io.Writer, bufPtr unsafe.Pointer) { + p := *(*[]byte)(bufPtr) + if w != nil { + w.Write(p) + } else { + earlyPrintBuffer.Write(p) + } +} + +// noEscape hides a pointer from escape analysis. This function is copied over +// from runtime/stubs.go +//go:nosplit +func noEscape(p unsafe.Pointer) unsafe.Pointer { + x := uintptr(p) + return unsafe.Pointer(x ^ 0) } diff --git a/src/gopheros/kernel/kfmt/early/early_fmt_test.go b/src/gopheros/kernel/kfmt/fmt_test.go similarity index 79% rename from src/gopheros/kernel/kfmt/early/early_fmt_test.go rename to src/gopheros/kernel/kfmt/fmt_test.go index 32440a6..0a660fd 100644 --- a/src/gopheros/kernel/kfmt/early/early_fmt_test.go +++ b/src/gopheros/kernel/kfmt/fmt_test.go @@ -1,31 +1,20 @@ -package early +package kfmt import ( "bytes" - "gopheros/kernel/driver/tty" - "gopheros/kernel/driver/video/console" - "gopheros/kernel/hal" + "fmt" + "strings" "testing" - "unsafe" ) func TestPrintf(t *testing.T) { - origTerm := hal.ActiveTerminal defer func() { - hal.ActiveTerminal = origTerm + outputSink = nil }() // mute vet warnings about malformed printf formatting strings printfn := Printf - ega := &console.Ega{} - fb := make([]uint8, 160*25) - ega.Init(80, 25, uintptr(unsafe.Pointer(&fb[0]))) - - vt := &tty.Vt{} - vt.AttachTo(ega) - hal.ActiveTerminal = vt - specs := []struct { fn func() expOutput string @@ -124,6 +113,10 @@ func TestPrintf(t *testing.T) { func() { printfn("int arg longer than padding: '%5x'", int(-0xbadf00d)) }, "int arg longer than padding: '-badf00d'", }, + { + func() { printfn("padding longer than maxBufSize '%128x'", int(-0xbadf00d)) }, + fmt.Sprintf("padding longer than maxBufSize '-%sbadf00d'", strings.Repeat("0", maxBufSize-8)), + }, // multiple arguments { func() { printfn("%%%s%d%t", "foo", 123, true) }, @@ -156,25 +149,42 @@ func TestPrintf(t *testing.T) { }, } - for specIndex, spec := range specs { - for index := 0; index < len(fb); index++ { - fb[index] = 0 - } - vt.SetPosition(0, 0) + var buf bytes.Buffer + SetOutputSink(&buf) + for specIndex, spec := range specs { + buf.Reset() spec.fn() - var buf bytes.Buffer - for index := 0; ; index += 2 { - if fb[index] == 0 { - break - } - - buf.WriteByte(fb[index]) - } - if got := buf.String(); got != spec.expOutput { - t.Errorf("[spec %d] expected to get %q; got %q", specIndex, spec.expOutput, got) + t.Errorf("[spec %d] expected to get\n%q\ngot:\n%q", specIndex, spec.expOutput, got) } } } + +func TestPrintfToRingBuffer(t *testing.T) { + defer func() { + outputSink = nil + }() + + exp := "hello world" + Fprintf(&buf, exp) + + var buf bytes.Buffer + SetOutputSink(buf) + + if got := buf.String(); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } +} + +func TestFprintf(t *testing.T) { + var buf bytes.Buffer + + exp := "hello world" + Fprintf(&buf, exp) + + if got := buf.String(); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } +}