Testing ACPI AML code in the Windows interpreter

Summary
I wanted a way to test the behaviour of the Windows NT AML interpreter. Ultimately, this led me to write a driver that could execute custom AML code. I briefly describe some of the interesting things that came out of this.
Before I tried writing the driver, I tried the old-school approach of remote-debugging a kernel and triggering it to run AML. This did not go well, and I mention a couple of learning points at the end of this post.
Background
ACPI defines ASL and its compiled equivalent, AML - these are bytecode provided by the firmware that can be run by the operating system (or OSPM in ACPI-speak). At the time of writing, the latest spec version is here.
Unfortunately, the spec is well-known to have parts that are buggy, ambiguous, or both. There is a reference implementation for how Intel believe AML should be interpreted, and this is used in the Linux kernel. Sadly, the Windows interpreter is different in many respects. Plus, firmware does not always follow the spec in their AML, and interpreters still need to deal with it.
This means that writers of AML interpreters need to understand how other interpreters treat AML code, and make decisions about how they want their interpreter to treat that code.
There are two well-known interpreters where this task is easy: ACPICA and uACPI. For both of these, it's a matter of downloading the code, compiling it, and figuring out how to pass AML to their test executables.
The tricky one - but probably the most important one - is the Windows interpreter.
In principle, it's possible to directly inspect the operation of Windows' acpi.sys using WinDBG. There is a set
of debugging commands prefixed with
!amli that provide common debugging
tools such as breakpoints and single-stepping. Actually using them is a totally different story.
My interest comes from my recent work on the rust-osdev interpreter. There are
issues where I want to see how the NT interpreter behaves, and as noted below, I found it very difficult to get WinDBG
to work.
What else would work?
After trying - and failing - to execute AML code using WinDBG, I realised that hardware drivers are able to call methods that are part of their associated device in the DSDT namespace. This means we could wrap the code under test in a Method within a device, and get a driver to call it.
"How difficult can it be to write a driver?" Spoiler alert: actually not too hard!
Task and Structure
What I want to do is to run arbitrary AML code using the Windows acpi.sys interpreter.
To do that using my custom driver idea, I need the following parts:
- AML code that gets loaded by Windows - it will load
C:\Windows\System32\acpitabl.dat, which is sufficient for my purposes.- Windows reads the
acpitabl.datfile at startup, and treats the contents as additional ACPI tables in the same format they would have when stored in RAM. This allows us to load an extra SSDT with the AML we want to run.
- Windows reads the
- A driver that causes that AML code to be executed on demand.
- A user-mode process that triggers the driver to execute the AML
- The communication between the two is via the function
DeviceIoControl
- The communication between the two is via the function
Loading custom AML
Simplifying the details - partly because I only have a shallow understanding - during startup, Windows parses the DSDT
(and SSDTs). It constructs the namespace, and enumerates the devices it contains. Based on the location in the
namespace, as well as calling the _HID, _CID or _ADR methods, Windows determines which driver to load.
Clearly we want Windows to load our driver for a device in the namespace, and for it to have no conflicts with other
drivers. I simply chose a random-ish _HID value of MRTN1234, which I doubt is used by any genuine device!
I came up with an ASL file that looks like this:
DefinitionBlock("", "SSDT", 1, "INTROP", "TESTTABL", 1) {
Scope (\) {
Device (MRTN) {
Name (_HID, "MRTN1234")
Method (ABCD, 1) {
Return (0)
}
}
}
}
The method name ABCD is arbitrary. Otherwise, this should look familiar to anyone who has written ASL before.
It can be compiled using iasl in the usual way. Then rename the AML file to acpitabl.dat and copy it to
C:\Windows\System32\.
Writing the Driver
To try and mitigate any silly mistakes that might cause crashes if run in Kernel mode, I decided to write a user-mode driver. Microsoft provide a "Getting Started" guide here that I followed.
Since this is not a high-value project I decided to try some AI assisted coding. I asked Google questions like "how do I
send an IOCTL_ACPI_EVAL_METHOD_V1 IOCTL from a driver to the ACPI system?" and merged the results into a
plausible-looking driver by hand.
This was fairly successful - it took only about an hour to get a driver that compiled. I tested it by loading the
acpitabl.dat snippet above onto a test machine, then installing the driver. After a reboot I could see an
"AcpiTestDevice Driver" entry in Device Manager.
Success! Time to move on. (Spoiler alert: partial success...)
The userspace program
Once again I leaned on AI assistance to write the user mode program. I could easily find the docs for
DeviceIoControl, and they
are reasonably clear about how to use it once you have a handle for the relevant device. I struggled to find good
information about opening a handle for a device, but Google again produced some plausible-looking code.
Total time to have a sensible-looking program: about 30 minutes.
Sadly, it immediately failed. It couldn't find the device. Since the documentation is sparse, I found it hard to know how to debug the problem. Almost by chance I stumbled on the solution: the actual device name was wrong.
Google AI had tried to locate ACPI\\MRTN1234, but the correct name was ACPI\\VEN_MRTN&DEV_1234. Not obvious at all,
but in hindsight it makes total sense! I'm not sure there was much to learn here, so I don't feel too bad - the docs for
this specific case are almost non-existent.
That was a good fix. Sadly, I now learned the lovely-looking driver code didn't actually work.
Being annoyed with the driver
So straight up - this generated a good learning point; or a good reminder at least:
AI is very confident, but has a few blind spots:
- If there are two similar contexts, it can get them confused.
- It struggles if the corpus of examples is sparse.
The first manifested itself in that the AI had written a very plausible kernel mode driver, whereas my user mode driver needed a little extra code to make it work.
The second of those blind spots relates to the specific error - it appears on only a handful of websites, so no surprise that LLMs don't really know what to make of it.
In order to make my way through the problem, I first had to figure out how to debug the driver - in hindsight it's probably obvious, and I feel kind of daft that it took me so long to realise. If you need pointers, see the Debugging drivers section later on.
So what error was I actually seeing? The slightly unhelpful
pHostFileObj should not be NULL
OK, what does that mean?
I'll be honest, even after reading the docs, I haven't fully grokked this. But I also don't necessarily feel like it's important - my driver works, and I don't intend to do much more driver work.
Essentially though, in user mode drivers, IO requests to other drivers need some sort of "file context" associated with the request. I assume this is to allow for safe data transfer between privilege levels, but like I say: I'm not sure. (ed: Asking Gemini 3.7 it says this assumption is "spot on", but I haven't investigated it further.)
To get to the bottom of it, I spent a good hour or so trying to understand the documentation - starting from that for
WdfIoTargetSendIoctlSynchronously
Unfortunately, in my notes, I didn't write down the page that had the solution - to use a combination of
WDF_IO_TARGET_OPEN_PARAMS_INIT_OPEN_BY_FILE and WdfIoTargetOpen to add a file context to the request. It took me
quite some time to find though, maybe a couple of hours or so.
If you decide to follow through the docs then you will not see this mentioned often - almost all of the driver API docs assume that you're writing a kernel rather than user mode driver.
I probably spent as long reading docs as I would have if I hadn't used AI to start with. Which is a shame, but no particular loss. Just a reminder of its weaknesses.
Success!
As soon as I worked out this file context issue, the test program started working straight away.
And that was that - my first successful driver!
Very quickly I was able to do what I had wanted to do to start with - run some tests to see the expected behaviour for this issue. Now I just have to actually fix it...
Debugging drivers
As mentioned above, it took me some time to figure out how to debug my user-mode driver.
Initially, all I had was a "program closed unexpectedly" error. What program?
I tried adding debugging statements to the driver, but I couldn't see them in either WinDbg or DebugView.
I'm not sure what triggered me to look, but in the Windows Event Viewer I could see that wudfhost.exe was restarting.
At least I knew my driver was crashing... Then I had an epiphany - I had assumed that I needed to do "kernel debugging",
whereas my driver was actually in user-mode - so I could just attach Visual Studio to it. (That is, to wudfhost.exe)
D'oh. Lesson learned.
Actual kernel debugging
At the beginning of this long article I briefly mentioned trying to use WinDbg to execute AML code. I learned a few beginner lessons about kernel debugging that might save someone some time, so here they are:
-
You really do need two machines! If you are debugging the local kernel and hit a breakpoint, you will see a BSOD. Whereas with two machines, it works as intended.
- These two machines can be virtual machines.
-
If you're using
virt-manager, you can do remote debugging via serial ports. I assumed that if I put the same pipe name into the PTS configuration then the two machines would speak to each other. Not so - the one that booted second just created a new pipe and removed the pipe from the first machine.The solution is to use a different pipe name for each machine. Then run
socat <pipe one> <pipe two>to actually connect them.