diff --git a/src/gopheros/device/acpi/aml/vm/compiler.go b/src/gopheros/device/acpi/aml/vm/compiler.go new file mode 100644 index 0000000..5bb84f0 --- /dev/null +++ b/src/gopheros/device/acpi/aml/vm/compiler.go @@ -0,0 +1,141 @@ +package vm + +import ( + "bytes" + "gopheros/device/acpi/aml/entity" + "gopheros/kernel" + "gopheros/kernel/kfmt" +) + +type opMapping struct { + vmOp uint8 + compilerFn func(*compilerContext, uint8, entity.Entity) *kernel.Error +} + +type compilerContext struct { + rootNS entity.Container + + // opcodeMap contains the mappings of AML opcodes into a VM opcode plus + // a compiler function to handle the opcode conversion. + opcodeMap map[entity.AMLOpcode]opMapping + + // methodCallMap maps an entity definition to an index in VM.methodCalls. + methodCallMap map[*entity.Method]uint16 + + vmCtx *Context + lastErr *kernel.Error +} + +func newCompilerContext(rootNS entity.Container) *compilerContext { + compCtx := &compilerContext{ + rootNS: rootNS, + methodCallMap: make(map[*entity.Method]uint16), + vmCtx: new(Context), + } + + // the opcodeMap needs to be dynamically populated here instead of + // having a shared, globally initialized map to prevent the Go compiler + // from complaining about potential initialization loops (e.g. + // compileFn -> compileStatement -> compileFn...) + compCtx.opcodeMap = map[entity.AMLOpcode]opMapping{} + + return compCtx +} + +// Compile returns a bytecode representation of the AML tree starting at +// rootNS that can be consumed by the AML VM. +func Compile(rootNS entity.Container) (*Context, *kernel.Error) { + compCtx := newCompilerContext(rootNS) + compileAMLTree(compCtx) + + if compCtx.lastErr != nil { + return nil, compCtx.lastErr + } + + return compCtx.vmCtx, nil +} + +// compileAMLTree converts the AML entity tree inside the supplied +// compilerContext into a bytecode representation that can be consumed by the +// host VM. This function is intentionally separate from the above Compile +// function so tests can easily stub the opcodeMap field of compCtx. +func compileAMLTree(compCtx *compilerContext) { + populateMethodMap(compCtx, compCtx.rootNS) + entity.Visit(0, compCtx.rootNS, entity.TypeMethod, func(_ int, ent entity.Entity) bool { + if compCtx.lastErr != nil { + return false + } + + compCtx.lastErr = compileMethod(compCtx, ent.(*entity.Method)) + return false + }) +} + +// populateMethodMap visits all method entities under root and populates the +// VM.methodCalls list as well as compCtx.methodCallMap which is used by the +// asmContext to efficiently encode method calls to the correct index in the +// methodCalls list. +func populateMethodMap(compCtx *compilerContext, root entity.Container) { + entity.Visit(0, root, entity.TypeMethod, func(_ int, ent entity.Entity) bool { + m := &methodCall{method: ent.(*entity.Method), entrypoint: 0xffffffff} + compCtx.vmCtx.methodCalls = append(compCtx.vmCtx.methodCalls, m) + compCtx.methodCallMap[m.method] = uint16(len(compCtx.vmCtx.methodCalls) - 1) + return false + }) +} + +// compileMethod receives an AML method entity and emits the appropriate +// bytecode for the statements it contains. Calls to compileMethod will also +// populate the method's entrypoint address which is used by the VM to +// implement the "call" opcode. +func compileMethod(compCtx *compilerContext, method *entity.Method) *kernel.Error { + var errBuf bytes.Buffer + + // Setup the entrypoint address for this method. + compCtx.vmCtx.methodCalls[compCtx.methodCallMap[method]].entrypoint = uint32(len(compCtx.vmCtx.bytecode)) + for stmtIndex, stmt := range method.Children() { + if err := compileStatement(compCtx, stmt); err != nil { + kfmt.Fprintf(&errBuf, "[%s:%d] %s", method.Name(), stmtIndex, err.Message) + err.Message = errBuf.String() + return err + } + } + + return nil +} + +// compileStatement receives as input an AML entity that represents a method +// statement and emits the appropriate bytecode for executing it. +func compileStatement(compCtx *compilerContext, ent entity.Entity) *kernel.Error { + mapping, hasMapping := compCtx.opcodeMap[ent.Opcode()] + if !hasMapping { + emit8(compCtx, opNop) + return nil + } + + return mapping.compilerFn(compCtx, mapping.vmOp, ent) +} + +// emit8 appends an opcode that does not require any operands to the generated +// bytecode stream. +func emit8(compCtx *compilerContext, op uint8) { + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, op) +} + +// emit16 appends an opcode followed by an word operand to the generated +// bytecode stream. The word operand will be encoded in big-endian format. +func emit16(compCtx *compilerContext, op uint8, operand uint16) { + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, op) + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, uint8((operand>>8)&0xff)) + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, uint8(operand&0xff)) +} + +// emit32 appends an opcode followed by an dword operand to the generated +// bytecode stream. The word operand will be encoded in big-endian format. +func emit32(compCtx *compilerContext, op uint8, operand uint32) { + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, op) + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, uint8((operand>>24)&0xff)) + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, uint8((operand>>16)&0xff)) + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, uint8((operand>>8)&0xff)) + compCtx.vmCtx.bytecode = append(compCtx.vmCtx.bytecode, uint8(operand&0xff)) +} diff --git a/src/gopheros/device/acpi/aml/vm/opcodes.go b/src/gopheros/device/acpi/aml/vm/opcodes.go new file mode 100644 index 0000000..ce93324 --- /dev/null +++ b/src/gopheros/device/acpi/aml/vm/opcodes.go @@ -0,0 +1,26 @@ +package vm + +// This is the list of opcodes used by the AML virtual machine. The 2 MSB of +// each opcode indicates the number of operands that need to be decoded by the +// VM: +// - 00 : no operands +// - 01 : 2 byte operands (word) +// - 10 : 4 byte operands (dword) +// +// The length of a particular instruction in bytes (including its opcode) +// can be found via the instrEncodingLen function. +// +// | mnemonic | opcode | operands [count]: [operand labels] | stack before | stack after | description | +// |------------|--------|-------------------------------------------------------|------------------|----------------------------------|--------------------------------------------------------------------------------------------------------------| +// | nop | 0x00 | | | | nop | +const ( + opNop uint8 = 0x00 +) + +// instrEncodingLen returns the number of bytes that encode the instruction +// specified by opcode op. +func instrEncodingLen(op uint8) int { + // Instead of extracting the 2 MSB and then multiplying by 2 we extract the 3 + // MSB and clear the right-most bit which yields the same result. + return 1 + int((op>>5)&0x6) +}