diff --git a/src/gopheros/device/acpi/aml/entity.go b/src/gopheros/device/acpi/aml/entity.go index 0e0658b..6a70839 100644 --- a/src/gopheros/device/acpi/aml/entity.go +++ b/src/gopheros/device/acpi/aml/entity.go @@ -88,10 +88,12 @@ func (ent *namedEntity) setArg(argIndex uint8, arg interface{}) bool { func (ent *namedEntity) TableHandle() uint8 { return ent.tableHandle } func (ent *namedEntity) setTableHandle(h uint8) { ent.tableHandle = h } -// constEntity is an unnamedEntity which always evaluates to a constant value. -// Calls to setArg for argument index 0 will memoize the argument value that is +// constEntity is an optionally-named entity which always +// evaluates to a constant value. Calls to setArg for +// argument index 0 will memoize the argument value that is // stored inside this entity. type constEntity struct { + name string tableHandle uint8 op opcode args []interface{} @@ -114,7 +116,7 @@ func (ent *constEntity) setOpcode(op opcode) { ent.val = uint64(1<<64 - 1) } } -func (ent *constEntity) Name() string { return "" } +func (ent *constEntity) Name() string { return ent.name } func (ent *constEntity) Parent() ScopeEntity { return ent.parent } func (ent *constEntity) setParent(parent ScopeEntity) { ent.parent = parent } func (ent *constEntity) getArgs() []interface{} { return ent.args } @@ -406,21 +408,33 @@ type namedReference struct { func (ref *namedReference) Resolve(errWriter io.Writer, rootNs ScopeEntity) bool { if ref.target == nil { - ref.target = scopeFind(ref.parent, rootNs, ref.targetName) + if ref.target = scopeFind(ref.parent, rootNs, ref.targetName); ref.target == nil { + kfmt.Fprintf(errWriter, "could not resolve referenced symbol: %s (parent: %s)\n", ref.targetName, ref.parent.Name()) + return false + } } - if ref.target == nil { - kfmt.Fprintf(errWriter, "could not resolve referenced symbol: %s (parent: %s)\n", ref.targetName, ref.parent.Name()) - } - - return ref.target != nil + return true } // methodInvocationEntity describes an AML method invocation. type methodInvocationEntity struct { unnamedEntity - methodDef *Method + methodName string + method *Method +} + +func (m *methodInvocationEntity) Resolve(errWriter io.Writer, rootNs ScopeEntity) bool { + if m.method == nil { + var isMethod bool + if m.method, isMethod = scopeFind(m.parent, rootNs, m.methodName).(*Method); !isMethod { + kfmt.Fprintf(errWriter, "could not resolve merenced method: %s (parent: %s)\n", m.methodName, m.parent.Name()) + return false + } + } + + return true } // Method defines an invocable AML method. diff --git a/src/gopheros/device/acpi/aml/entity_test.go b/src/gopheros/device/acpi/aml/entity_test.go index 7e49e97..97ea402 100644 --- a/src/gopheros/device/acpi/aml/entity_test.go +++ b/src/gopheros/device/acpi/aml/entity_test.go @@ -133,6 +133,8 @@ func TestEntityResolveErrors(t *testing.T) { &indexFieldEntity{connectionName: `\`, indexRegName: `\`, dataRegName: "DAT0"}, // Unknown reference &namedReference{unnamedEntity: unnamedEntity{parent: scope}, targetName: "TRG0"}, + // Unknown method name + &methodInvocationEntity{unnamedEntity: unnamedEntity{parent: scope}, methodName: "MTH0"}, } for specIndex, spec := range specs { @@ -141,3 +143,31 @@ func TestEntityResolveErrors(t *testing.T) { } } } + +func TestMethodInvocationResolver(t *testing.T) { + scope := &scopeEntity{name: `\`} + scope.Append(&Method{ + scopeEntity: scopeEntity{ + name: "MTH0", + }, + }) + + validInv := &methodInvocationEntity{ + methodName: "MTH0", + } + + invalidInv := &methodInvocationEntity{ + methodName: "FOO0", + } + + scope.Append(validInv) + scope.Append(invalidInv) + + if !validInv.Resolve(ioutil.Discard, scope) { + t.Fatal("expected method invocation to resolve method", validInv.methodName) + } + + if invalidInv.Resolve(ioutil.Discard, scope) { + t.Fatal("expected method invocation NOT to resolve method", invalidInv.methodName) + } +} diff --git a/src/gopheros/device/acpi/aml/parser.go b/src/gopheros/device/acpi/aml/parser.go index 5a01ddb..2b25bdf 100644 --- a/src/gopheros/device/acpi/aml/parser.go +++ b/src/gopheros/device/acpi/aml/parser.go @@ -21,13 +21,20 @@ type Parser struct { scopeStack []ScopeEntity tableName string tableHandle uint8 + + // methodArgCount is initialized in a pre-parse step with the names and expected + // number of args for each function declaration. This is required as function + // invocations do not employ any mechanism to indicate the number of args that + // need to be parsed. Moreover, the spec allows for forward function declarations. + methodArgCount map[string]uint8 } // NewParser returns a new AML parser instance. func NewParser(errWriter io.Writer, rootEntity ScopeEntity) *Parser { return &Parser{ - errWriter: errWriter, - root: rootEntity, + errWriter: errWriter, + root: rootEntity, + methodArgCount: make(map[string]uint8), } } @@ -43,7 +50,12 @@ func (p *Parser) ParseAML(tableHandle uint8, tableName string, header *table.SDT uint32(unsafe.Sizeof(table.SDTHeader{})), ) - // Pass 1: decode bytecode and build entitites + // Pass 1: scan bytecode and locate all method declarations. This allows us to + // properly parse the arguments to method invocations at pass 2 even if the + // the name of the invoked method is a forward reference. + p.detectMethodDeclarations() + + // Pass 2: decode bytecode and build entitites p.scopeStack = nil p.scopeEnter(p.root) if !p.parseObjList(header.Length) { @@ -53,12 +65,27 @@ func (p *Parser) ParseAML(tableHandle uint8, tableName string, header *table.SDT } p.scopeExit() - // Pass 2: resolve forward references + // Pass 3: check parents and resolve forward references var resolveFailed bool scopeVisit(0, p.root, EntityTypeAny, func(_ int, ent Entity) bool { + // Skip method bodies; their contents will be lazily resolved by the interpreter + if _, isMethod := ent.(*Method); isMethod { + return false + } + + // Populate parents for any entity args that are also entities but are not + // linked to a parent (e.g. a package inside a named entity). + for _, arg := range ent.getArgs() { + if argEnt, isArgEnt := arg.(Entity); isArgEnt && argEnt.Parent() == nil { + argEnt.setParent(ent.Parent()) + } + } + if res, ok := ent.(resolver); ok && !res.Resolve(p.errWriter, p.root) { resolveFailed = true + return false } + return true }) @@ -69,6 +96,57 @@ func (p *Parser) ParseAML(tableHandle uint8, tableName string, header *table.SDT return nil } +// detectMethodDeclarations scans the AML byte-stream looking for function +// declarations. For each discovered function, the method will parse its flags +// and update the methodArgCount map with the number of required arguments. +func (p *Parser) detectMethodDeclarations() { + var ( + next *opcodeInfo + method string + startOffset = p.r.Offset() + curOffset, pkgLen uint32 + flags uint64 + ok bool + ) + + for !p.r.EOF() { + if next, ok = p.nextOpcode(); !ok { + // Skip one byte to the right and try again. Maybe we are stuck inside + // the contents of a string or buffer + _, _ = p.r.ReadByte() + continue + } + + if next.op != opMethod { + continue + } + + // Parse pkg len; if this fails then this is not a method declaration + curOffset = p.r.Offset() + if pkgLen, ok = p.parsePkgLength(); !ok { + continue + } + + // Parse method name + if method, ok = p.parseNameString(); !ok { + continue + } + + // The next byte encodes the method flags which also contains the arg count + // at bits 0:2 + if flags, ok = p.parseNumConstant(1); !ok { + continue + } + + p.methodArgCount[method] = uint8(flags) & 0x7 + + // At this point we can use the pkg length to skip over the term list + p.r.SetOffset(curOffset + pkgLen) + } + + p.r.SetOffset(startOffset) +} + // parseObjList tries to parse an AML object list. Object lists are usually // specified together with a pkgLen block which is used to calculate the max // read offset that the parser may reach. @@ -96,7 +174,7 @@ func (p *Parser) parseObj() bool { curOffset = p.r.Offset() if info, ok = p.nextOpcode(); !ok { p.r.SetOffset(curOffset) - return p.parseMethodInvocationOrNameRef() + return p.parseNamedRef() } hasPkgLen := info.flags.is(opFlagHasPkgLen) || info.argFlags.contains(opArgTermList) || info.argFlags.contains(opArgFieldList) @@ -352,34 +430,29 @@ func (p *Parser) makeObjForOpcode(info *opcodeInfo) Entity { return obj } -// parseMethodInvocationOrNameRef attempts to parse a method invocation and its term -// args. This method first scans the NameString and performs a lookup. If the -// lookup returns a method definition then we consult it to figure out how many -// arguments we need to parse. +// parseNamedRef attempts to parse either a method invocation or a named +// reference. As AML allows for forward references, the actual contents for +// this entity will not be known until the entire AML stream has been parsed. // // Grammar: // MethodInvocation := NameString TermArgList // TermArgList = Nothing | TermArg TermArgList // TermArg = Type2Opcode | DataObject | ArgObj | LocalObj | MethodInvocation -func (p *Parser) parseMethodInvocationOrNameRef() bool { - invocationStartOffset := p.r.Offset() +func (p *Parser) parseNamedRef() bool { name, ok := p.parseNameString() if !ok { return false } - // Lookup Name and try matching it to a function definition - if methodDef, ok := scopeFind(p.scopeCurrent(), p.root, name).(*Method); ok { - var ( - invocation = &methodInvocationEntity{ - methodDef: methodDef, - } - curOffset uint32 - argIndex uint8 - arg Entity - ) + var ( + curOffset uint32 + argIndex uint8 + arg Entity + argList []interface{} + ) - for argIndex < methodDef.argCount && !p.r.EOF() { + if argCount, isMethod := p.methodArgCount[name]; isMethod { + for argIndex < argCount && !p.r.EOF() { // Peek next opcode curOffset = p.r.Offset() nextOpcode, ok := p.nextOpcode() @@ -390,7 +463,7 @@ func (p *Parser) parseMethodInvocationOrNameRef() bool { arg, ok = p.parseArgObj() default: // It may be a nested invocation or named ref - ok = p.parseMethodInvocationOrNameRef() + ok = p.parseNamedRef() if ok { arg = p.scopeCurrent().lastChild() p.scopeCurrent().removeChild(arg) @@ -403,23 +476,24 @@ func (p *Parser) parseMethodInvocationOrNameRef() bool { break } - invocation.setArg(argIndex, arg) + argList = append(argList, arg) argIndex++ } - if argIndex != methodDef.argCount { - kfmt.Fprintf(p.errWriter, "[table: %s, offset: %d] argument mismatch (exp: %d, got %d) for invocation of method: %s\n", p.tableName, invocationStartOffset, methodDef.argCount, argIndex, name) + // Check whether all expected arguments have been parsed + if argIndex != argCount { + kfmt.Fprintf(p.errWriter, "[table: %s, offset: %d] unexpected arglist end for method %s invocation: expected %d; got %d\n", p.tableName, p.r.Offset(), name, argCount, argIndex) return false } - p.scopeCurrent().Append(invocation) - return true + return p.scopeCurrent().Append(&methodInvocationEntity{ + unnamedEntity: unnamedEntity{args: argList}, + methodName: name, + }) } - // This is a name reference; assume it's a forward reference for now - // and delegate its resolution to a post-parse step. - p.scopeCurrent().Append(&namedReference{targetName: name}) - return true + // Otherwise this is a reference to a named entity + return p.scopeCurrent().Append(&namedReference{targetName: name}) } func (p *Parser) nextOpcode() (*opcodeInfo, bool) { @@ -830,7 +904,7 @@ func (p *Parser) parseTarget() (interface{}, bool) { } // In this case, this is either a NameString or a control method invocation. - if ok := p.parseMethodInvocationOrNameRef(); ok { + if ok := p.parseNamedRef(); ok { obj := p.scopeCurrent().lastChild() p.scopeCurrent().removeChild(obj) return obj, ok diff --git a/src/gopheros/device/acpi/aml/parser_test.go b/src/gopheros/device/acpi/aml/parser_test.go index a7db7ec..654df99 100644 --- a/src/gopheros/device/acpi/aml/parser_test.go +++ b/src/gopheros/device/acpi/aml/parser_test.go @@ -3,6 +3,7 @@ package aml import ( "gopheros/device/acpi/table" "io/ioutil" + "os" "path/filepath" "runtime" "strings" @@ -14,6 +15,7 @@ func TestParser(t *testing.T) { specs := [][]string{ []string{"DSDT.aml", "SSDT.aml"}, []string{"parser-testsuite-DSDT.aml"}, + []string{"parser-testsuite-fwd-decls-DSDT.aml"}, } for specIndex, spec := range specs { @@ -29,7 +31,11 @@ func TestParser(t *testing.T) { rootNS.Append(&scopeEntity{op: opScope, name: `_SI_`}) // System indicators rootNS.Append(&scopeEntity{op: opScope, name: `_TZ_`}) // ACPI 1.0 thermal zone namespace - p := NewParser(ioutil.Discard, rootNS) + // Inject pre-defined OSPM objects + rootNS.Append(&constEntity{name: "_OS_", val: "gopheros"}) + rootNS.Append(&constEntity{name: "_REV", val: uint64(2)}) + + p := NewParser(os.Stderr, rootNS) for _, tableName := range spec { tableName = strings.Replace(tableName, ".aml", "", -1) @@ -91,6 +97,30 @@ func TestTableHandleAssignment(t *testing.T) { } } +func TestParserForwardDeclParsing(t *testing.T) { + var resolver = mockResolver{ + tableFiles: []string{"parser-testsuite-fwd-decls-DSDT.aml"}, + } + + // Create default scopes + rootNS := &scopeEntity{op: opScope, name: `\`} + rootNS.Append(&scopeEntity{op: opScope, name: `_GPE`}) // General events in GPE register block + rootNS.Append(&scopeEntity{op: opScope, name: `_PR_`}) // ACPI 1.0 processor namespace + rootNS.Append(&scopeEntity{op: opScope, name: `_SB_`}) // System bus with all device objects + rootNS.Append(&scopeEntity{op: opScope, name: `_SI_`}) // System indicators + rootNS.Append(&scopeEntity{op: opScope, name: `_TZ_`}) // ACPI 1.0 thermal zone namespace + + p := NewParser(ioutil.Discard, rootNS) + + for _, tableName := range resolver.tableFiles { + tableName = strings.Replace(tableName, ".aml", "", -1) + if err := p.ParseAML(0, tableName, resolver.LookupTable(tableName)); err != nil { + t.Errorf("[%s]: %v", tableName, err) + break + } + } +} + func TestParsePkgLength(t *testing.T) { specs := []struct { payload []byte @@ -307,13 +337,12 @@ func TestParserErrorHandling(t *testing.T) { } }) - t.Run("parseMethodInvocationOrNameRef errors", func(t *testing.T) { + t.Run("parseNamedRef errors", func(t *testing.T) { t.Run("missing args", func(t *testing.T) { p.root = &scopeEntity{op: opScope, name: `\`} - p.root.Append(&Method{ - scopeEntity: scopeEntity{name: "MTHD"}, - argCount: 10, - }) + p.methodArgCount = map[string]uint8{ + "MTHD": 10, + } mockParserPayload(p, []byte{ 'M', 'T', 'H', 'D', @@ -321,8 +350,8 @@ func TestParserErrorHandling(t *testing.T) { }) p.scopeEnter(p.root) - if p.parseMethodInvocationOrNameRef() { - t.Fatal("expected parseMethodInvocationOrNameRef to return false") + if p.parseNamedRef() { + t.Fatal("expected parseNamedRef to return false") } }) }) @@ -556,6 +585,77 @@ func TestParserErrorHandling(t *testing.T) { }) } +func TestDetectMethodDeclarations(t *testing.T) { + p := &Parser{ + errWriter: ioutil.Discard, + } + + validMethod := []byte{ + byte(opMethod), + 5, // pkgLen + 'M', 'T', 'H', 'D', + 2, // flags (2 args) + } + + t.Run("success", func(t *testing.T) { + mockParserPayload(p, validMethod) + p.methodArgCount = make(map[string]uint8) + p.detectMethodDeclarations() + + argCount, inMap := p.methodArgCount["MTHD"] + if !inMap { + t.Error(`detectMethodDeclarations failed to parse method "MTHD"`) + } + + if exp := uint8(2); argCount != exp { + t.Errorf(`expected arg count for "MTHD" to be %d; got %d`, exp, argCount) + } + }) + + t.Run("bad pkgLen", func(t *testing.T) { + mockParserPayload(p, []byte{ + byte(opMethod), + // lead byte bits (6:7) indicate 1 extra byte that is missing + byte(1 << 6), + }) + + p.methodArgCount = make(map[string]uint8) + p.detectMethodDeclarations() + }) + + t.Run("error parsing namestring", func(t *testing.T) { + mockParserPayload(p, append([]byte{ + byte(opMethod), + byte(5), // pkgLen + 10, // bogus char, not part of namestring + }, validMethod...)) + + p.methodArgCount = make(map[string]uint8) + p.detectMethodDeclarations() + + argCount, inMap := p.methodArgCount["MTHD"] + if !inMap { + t.Error(`detectMethodDeclarations failed to parse method "MTHD"`) + } + + if exp := uint8(2); argCount != exp { + t.Errorf(`expected arg count for "MTHD" to be %d; got %d`, exp, argCount) + } + }) + + t.Run("error parsing method flags", func(t *testing.T) { + mockParserPayload(p, []byte{ + byte(opMethod), + byte(5), // pkgLen + 'F', 'O', 'O', 'F', + // Missing flag byte + }) + + p.methodArgCount = make(map[string]uint8) + p.detectMethodDeclarations() + }) +} + func mockParserPayload(p *Parser, payload []byte) *table.SDTHeader { resolver := fixedPayloadResolver{payload} header := resolver.LookupTable("DSDT") diff --git a/src/gopheros/device/acpi/aml/scope.go b/src/gopheros/device/acpi/aml/scope.go index 51ed528..4057fea 100644 --- a/src/gopheros/device/acpi/aml/scope.go +++ b/src/gopheros/device/acpi/aml/scope.go @@ -36,12 +36,20 @@ func scopeVisit(depth int, ent Entity, entType EntityType, visitorFn Visitor) bo if !visitorFn(depth, ent) { return false } + + // Visit any args that are also entities + for _, arg := range ent.getArgs() { + if argEnt, isEnt := arg.(Entity); isEnt && !scopeVisit(depth+1, argEnt, entType, visitorFn) { + return false + } + } } - // If the entity defines a scope we need to visit the child entities. - if scopeEnt, ok := ent.(ScopeEntity); ok { - for _, child := range scopeEnt.Children() { - scopeVisit(depth+1, child, entType, visitorFn) + switch typ := ent.(type) { + case ScopeEntity: + // If the entity defines a scope we need to visit the child entities. + for _, child := range typ.Children() { + _ = scopeVisit(depth+1, child, entType, visitorFn) } } diff --git a/src/gopheros/device/acpi/aml/scope_test.go b/src/gopheros/device/acpi/aml/scope_test.go index 7dd8fec..fd2dc85 100644 --- a/src/gopheros/device/acpi/aml/scope_test.go +++ b/src/gopheros/device/acpi/aml/scope_test.go @@ -9,6 +9,9 @@ func TestScopeVisit(t *testing.T) { scopeMap := genTestScopes() root := scopeMap[`\`].(*scopeEntity) + keepRecursing := func(Entity) bool { return true } + stopRecursing := func(Entity) bool { return false } + // Append special entities under IDE0 ide := scopeMap["IDE0"].(*scopeEntity) ide.Append(&Device{}) @@ -26,26 +29,53 @@ func TestScopeVisit(t *testing.T) { ide.Append(&Method{}) ide.Append(&Method{}) ide.Append(&Method{}) + ide.Append(&methodInvocationEntity{ + unnamedEntity: unnamedEntity{ + args: []interface{}{ + &constEntity{val: uint64(1)}, + &constEntity{val: uint64(2)}, + }, + }, + }) specs := []struct { - searchType EntityType - keepRecursing bool - wantHits int + searchType EntityType + keepRecursingFn func(Entity) bool + wantHits int }{ - {EntityTypeAny, true, 21}, - {EntityTypeAny, false, 1}, - {EntityTypeDevice, true, 1}, - {EntityTypeProcessor, true, 2}, - {EntityTypePowerResource, true, 3}, - {EntityTypeThermalZone, true, 4}, - {EntityTypeMethod, true, 5}, + {EntityTypeAny, keepRecursing, 24}, + {EntityTypeAny, stopRecursing, 1}, + { + EntityTypeAny, + func(ent Entity) bool { + // Stop recursing after visiting the methodInvocationEntity + _, isInv := ent.(*methodInvocationEntity) + return !isInv + }, + 22, + }, + + { + EntityTypeAny, + func(ent Entity) bool { + // Stop recursing after visiting the first constEntity + _, isConst := ent.(*constEntity) + return !isConst + }, + 23, + }, + {EntityTypeDevice, keepRecursing, 1}, + {EntityTypeProcessor, keepRecursing, 2}, + {EntityTypePowerResource, keepRecursing, 3}, + {EntityTypeThermalZone, keepRecursing, 4}, + {EntityTypeMethod, keepRecursing, 5}, } for specIndex, spec := range specs { var hits int scopeVisit(0, root, spec.searchType, func(_ int, obj Entity) bool { hits++ - return spec.keepRecursing + return spec.keepRecursingFn(obj) }) if hits != spec.wantHits { diff --git a/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.aml b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.aml index dcf8dbd..5d278a1 100644 Binary files a/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.aml and b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.aml differ diff --git a/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.dsl b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.dsl index 54968ba..a81a1e2 100644 --- a/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.dsl +++ b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-DSDT.dsl @@ -49,6 +49,8 @@ DefinitionBlock ("parser-testsuite-DSDT.aml", "DSDT", 2, "GOPHER", "GOPHEROS", 0 // Other entity types Event(HLO0) + Mutex(MUT0,1) + Signal(HLO0) // Other executable bits Method (EXE0, 1, Serialized) @@ -79,12 +81,10 @@ DefinitionBlock ("parser-testsuite-DSDT.aml", "DSDT", 2, "GOPHER", "GOPHEROS", 0 Reset(HLO0) // Mutex support - Mutex(MUT0, 1) Acquire(MUT0, 0xffff) // no timeout Release(MUT0) // Signal/Wait - Signal(HLO0) Wait(HLO0, 0xffff) // Get monotonic timer value diff --git a/src/gopheros/device/acpi/table/tabletest/parser-testsuite-fwd-decls-DSDT.aml b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-fwd-decls-DSDT.aml new file mode 100644 index 0000000..00d0f4d Binary files /dev/null and b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-fwd-decls-DSDT.aml differ diff --git a/src/gopheros/device/acpi/table/tabletest/parser-testsuite-fwd-decls-DSDT.dsl b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-fwd-decls-DSDT.dsl new file mode 100644 index 0000000..0df4e12 --- /dev/null +++ b/src/gopheros/device/acpi/table/tabletest/parser-testsuite-fwd-decls-DSDT.dsl @@ -0,0 +1,16 @@ +DefinitionBlock ("parser-testsuite-fwd-decls-DSDT.aml", "DSDT", 2, "GOPHER", "GOPHEROS", 0x00000002) +{ + Method(NST0, 1, NotSerialized) + { + // Invoke a method which has not been defined at the time the parser + // reaches this block (forward declaration) + Return(NST1(Arg0)) + } + + // The declaration of NST1 in the AML stream occurs after the declaration + // of NST0 method above. + Method(NST1, 1, NotSerialized) + { + Return(Arg0+42) + } +}