How to Write Custom SSDTs for Legacy Hackintosh
Learn to write custom SSDTs for Hackintosh: disassemble DSDT, compile without errors, inject missing devices, and avoid common pitfalls like duplicate objects and buffer confusion.
Opening MaciASL and seeing your DSDT's AML for the first time is tempting—but editing it directly is the fastest way to brick your system. Custom SSDTs (Secondary System Description Tables) let you overlay definitions, fix errors, and inject missing devices without ever touching the original DSDT. The real challenge is writing an SSDT that compiles cleanly and actually does what you expect at runtime.
Why Write an SSDT Instead of Patching the DSDT?
Every motherboard firmware update can overwrite your patched DSDT. An SSDT loaded by the bootloader (OpenCore, Clover) survives firmware updates because it lives on the EFI partition. More importantly, an SSDT can be scoped to a specific ACPI path, making it easy to enable or disable individual fixes. On legacy hardware (Ivy Bridge, Haswell, X79, X99) you often need to inject missing devices such as an EC (Embedded Controller) for USB power, a fake Ethernet device for iMessage, or correct _PRW methods for sleep. All of these are safer as SSDTs.
Required Tools and Setup
- MaciASL (preferably the acidanthera fork) — for disassembly, editing, and compiling. The built-in iasl version matters; use at least 2020-12-17.
- iasl standalone (optional) — for batch compilation and scripting.
- IORegistryExplorer — to verify that your SSDT actually injects the expected properties.
- Clover or OpenCore config.plist — to add the SSDT to the ACPI load list.
- Backup of your current EFI folder — test on a USB stick first.
Step 1: Disassemble Your DSDT and Identify the Target Scope
Boot your Hackintosh, dump the DSDT using sudo cat /sys/firmware/acpi/tables/DSDT > DSDT.aml (Linux) or use Clover’s F4 key at the boot menu. Open the resulting DSDT.aml in MaciASL and press File → New from ACPI → DSDT. MaciASL will disassemble it into a .dsl file. Search for the device or method you want to fix. For example, if your Ethernet controller (e.g., Realtek RTL8111) is not recognised, look for Device (PEX0) or Device (RP01) under the PCI root. Note the exact ACPI path — you will need it for the SSDT.
Step 2: Write the SSDT Source (.dsl)
Create a new file in MaciASL. The header must define the table signature, OEM ID, and table length. A minimal template:
DefinitionBlock ("", "SSDT", 2, "ACID", "custom", 0x00000000)
{
External (_SB_.PCI0, DeviceObj)
Scope (_SB_.PCI0)
{
Device (GIGE)
{
Name (_ADR, 0x00020000) // PCI address of Ethernet
Method (_DSM, 4, NotSerialized)
{
If (!Arg2) { Return (Buffer() { 0x03 }) }
Return (Package()
{
"built-in", Buffer() { 0x00 },
"device-id", Buffer() { 0x81, 0x10, 0x00, 0x00 },
})
}
}
}
}
Replace the device path, _ADR, and properties with your actual values. Use External() declarations for any objects referenced from the DSDT to avoid compilation errors. A common mistake is forgetting to declare External (_SB_.PCI0, DeviceObj) — the compiler will then treat it as a new definition and produce a duplicate-object error.
Step 3: Compile and Fix Warnings
Press Compile (or use iasl from terminal: iasl -vw 2095 ssdt.dsl). The -vw 2095 flag suppresses the “Not all arguments are used” warning from _DSM methods — safe to ignore. Fix any errors: most are missing External declarations, mismatched parentheses, or incorrect buffer sizes. The compiler output tells you the exact line and expected token. Do not proceed until you see “Compilation successful. 0 Errors, 0 Warnings” (or only the suppressed ones).
Step 4: Save as .aml and Place in EFI
Save the compiled AML file (e.g., SSDT-GIGE.aml) into EFI/OC/ACPI/ (OpenCore) or /EFI/CLOVER/ACPI/patched/ (Clover). In OpenCore, add an entry in config.plist under ACPI → Add with the filename, set Enabled to True, and leave the table length and checksum fields empty — they will be calculated at boot. In Clover, the file is automatically loaded from the patched folder; no additional config is needed if you use the correct naming convention.
Step 5: Verify Injection with IORegistryExplorer
Reboot and open IORegistryExplorer. Search for the device name you injected (e.g., “GIGE”). You should see the device node with your custom properties under IODeviceTree:/efi/platform or IOService:/AppleACPIPlatformExpert. If the device does not appear, check the ACPI path — a single misplaced underscore will break the whole table. Also verify that the SSDT is actually loaded: in OpenCore’s boot log (press Space at the picker) look for “Loading SSDT-GIGE.aml” and a success message. In Clover, press F2 at the boot menu to dump preboot.log and search for your SSDT filename.
Common Pitfalls and How to Avoid Them
Duplicate Object Errors
If your SSDT defines a device that already exists in the DSDT, the ACPI subsystem will reject the table. Always use External() to reference existing objects, and only define new ones. To inject properties into an existing device, use Scope (_SB.PCI0.GIGE) and add a Method (_DSM, 4, NotSerialized) inside that scope — the device itself is not redefined, only the method is overridden.
Buffer vs. Integer Confusion
ACPI properties expect data in specific formats. A device-id is typically a 4-byte buffer (little-endian), while built-in is a single byte. Check the property documentation for the kext you are targeting (e.g., AppleIntelE1000 for Intel Ethernet). Wrong data types cause the kext to ignore the injection.
ACPI Path Case Sensitivity
ACPI names are case-insensitive in practice, but the path separators and backslash for root are critical. Use _SB.PCI0 (backslash-root) when the scope is at the top level. If you are unsure, dump the DSDT and copy the exact path from there.
Debugging a Non-Loading SSDT
If the system boots but the SSDT is ignored, first check the file permissions: the .aml must be readable. Next, verify the table signature in the file header: it must be “SSDT” (bytes 0-3). Open the .aml in a hex editor to confirm. A common mistake is saving a .dsl file as .aml — the bootloader expects AML bytecode, not ASCII source. Finally, enable ACPI debugging in OpenCore (Misc → Debug → Target = 0x13) to see why the table was rejected. The log will show “Invalid table” if the checksum is wrong or the table length does not match the file size.
When to Use SSDTTime or Prebuilt Tables
Tools like SSDTTime can generate SSDTs for common fixes (EC, USBX, PLNF, etc.) by parsing your DSDT. They are useful starting points, but the generated code is often generic and may inject unnecessary properties. For legacy hardware with non-standard ACPI implementations (e.g., X79 boards with ASMedia controllers), hand-written SSDTs give you full control. If you use a prebuilt table from a repository, always disassemble it first and verify that the paths match your DSDT — otherwise you risk a kernel panic at boot.
For more detailed guidance on identifying PCI device addresses and injecting properties, refer to the blog’s earlier breakdown on How to Read and Inject PCI Device IDs on Legacy Hackintosh. That article walks through reading the PCI configuration space and mapping it to ACPI device paths — a skill essential for writing SSDTs that target the correct hardware.
Testing Sleep and Wake After Injection
After adding an SSDT that modifies power management (e.g., a fake EC or _PRW fix), test sleep by closing the lid or using the Apple menu. If the system wakes immediately or fails to sleep, the SSDT is likely causing a power state conflict. Use pmset -g log to see wake reasons. A common fix is to add a Method (_PTS, 1, NotSerialized) in the SSDT that stores the wake reason to a debug variable, but that is an advanced topic. For now, ensure that any device you inject has proper _PRW methods pointing to the correct GPE number — you can find the GPE numbers in the DSDT’s _GPE scope.
Before you reboot, double-check that your SSDT’s ACPI path matches the DSDT exactly—one misplaced underscore and the table won’t load. I’ve seen people spend hours debugging only to find they used _SB.PCI0 instead of _SB.PCI0 (case doesn’t matter, but the backslash does). Keep a USB recovery stick handy; if you get a boot loop, boot from it, delete the offending .aml, and you’re back in business.
