Read-only Python tools for the x86_64 kernel collections of macOS 11 and later (BootKernelExtensions.kc, a Mach-O MH_FILESET). They answer the questions you hit when you write a kext for a Hackintosh against private Apple classes: will this kext link, what are the vtable slots and instance sizes of an Apple class on this build, and what changed in them after a macOS update.
Nothing here writes to the system. The collection is opened read-only with mmap; the only files written are the outputs you name and a cache of carved Mach-O copies.
| File | What it does |
|---|---|
linkcheck.py |
Checks a built kext's undefined symbols against the collection: each one must be exported by the kernel or by a bundle the kext lists in OSBundleLibraries. |
kcfile.py |
The reader the others are built on: fileset entries, symbols, vtables, chained pointers, MetaClass instance sizes, carving one entry out for llvm-objdump. |
disasm.py |
Intel-syntax disassembly of one function through llvm-objdump, with symbol names and C strings added as notes, plus a normaliser so two builds can be diffed. |
dump.py |
Writes the ABI facts of IO80211Family, IOSkywalkFamily, corecapture and the Broadcom Wi-Fi kexts for one build: vtables, method lists, instance sizes, the apple80211 GET/SET request tables, the WCL event subscriber tables and the disassembly of a list of functions. |
compare.py |
Diffs two dumps: size changes, vtable slots that moved, request IDs whose handler changed, and which functions changed (with the field offsets, virtual-call slots and callees that differ). |
genhdr.py |
Turns a dump into slot-exact C++ headers for the private IO80211 / IOSkywalk classes a native Wi-Fi driver subclasses, and checks them by compiling a probe subclass with clang and comparing its vtable layout slot by slot with Apple's. |
refresh.py |
The after-update command: dump the new build, compare it with the build your kext was verified on, print a verdict (unchanged, relayout, review), optionally run your build script. |
gfxdump.py |
The graphics-side ABI of one build (IOAcceleratorFamily2, IOGraphicsFamily, IOSurface): vtables, method lists and instance sizes from both kernel collections, in dump.py's layout so compare.py diffs builds. Baselines in abi/graphics/<build>. |
functions-extra.txt |
Extra functions dump.py disassembles on every build (mangled names, a trailing * is a prefix). |
linkcheck.py, kcfile.py and disasm.py are general. dump.py, compare.py, genhdr.py and refresh.py were written for a native IO80211 front end (the MT7925 driver in mt7925-hackintosh-wifi-native); their class lists are at the top of each file and are easy to change for other families.
python3 linkcheck.py out/MyDriver.kext
python3 linkcheck.py out/MyPlugin.kext --own-lib as.vit9696.Lilu
Output for a Lilu plugin on macOS 26.7.1:
LegionCpuPower.kext: 327 undefined symbols, collection BootKernelExtensions.kc
com.apple.iokit.IOACPIFamily 1 ok
com.apple.iokit.IOPCIFamily 2 ok
com.apple.kernel 316 ok
not exported by the collection (8):
__ZN13KernelPatcher10clearErrorEv
...
_lilu_os_log
(expected from as.vit9696.Lilu, which is not in the collection)
link check ok
Exit status 1 when a symbol is exported by nobody, or by a bundle the kext does not declare (that line says NOT IN OSBundleLibraries). Lilu and other kexts OpenCore injects next to yours are not in the collection, so name them with --own-lib: their symbols are listed instead of failing the check. --baseline OTHER.kext takes a kext you know loads; symbols it lacks too are reported as expected. --kc PATH checks against another collection, for example one copied from a different macOS build.
import sys; sys.path.insert(0, '/path/to/kcabi')
from kcfile import KC
from disasm import Disassembler
kc = KC('/System/Library/KernelCollections/BootKernelExtensions.kc')
hex(kc.metaclass_size('IO80211Controller')) # '0x128' on 25G241
for slot, raw, target, name in kc.vtable('IO80211Controller')[:12]:
print(slot, name)
dis = Disassembler(kc, '/tmp/kcabi-cache')
print('\n'.join(dis.function(kc.find('__ZN17IO80211Controller5startEP9IOService'))))python3 dump.py # -> ./dumps/<build>
python3 dump.py --ref ~/src/ga403wm-hackintosh/abi/native-wifi/24H32 --out dumps/25G241
python3 compare.py dumps/24H32 dumps/25G241 # -> dumps/25G241/compare/summary.txt
A dump of 25G241 takes about 8 seconds and is 4 MB. --ref points at an older dump whose disassembled functions are dumped again under the same names (renamed ones are matched by their qualified name); without it only functions-extra.txt and the MetaClass constructors are disassembled.
python3 genhdr.py gen dumps/25G241 include/abi --iface-class MyWlanIf --impl src/MyWlanIf.cpp
python3 genhdr.py check dumps/25G241 include/abi
python3 genhdr.py check-native dumps/25G241 MyWlanIf AppleBCMWLANSkywalkInterface src/MyWlanIf.cpp -- <your clang flags>
python3 refresh.py --old 25G241 -- ./build.sh # after a macOS update, on the updated system
check prints one line per class (slots apple=... clang=... mismatches=0) and ends with headers verified. refresh.py exits 0 when the ABI is unchanged, 3 when slots or sizes moved, 4 when the request tables or a watched function changed. The stub file genhdr.py gen writes (iface_stubs.inc) uses the MTK_IFACE_DECLS / MTK_IFACE_DEFS macros and the nativeCall hook of the MT7925 driver; change render_stubs() if your driver routes requests differently.
- Python 3.9 or later (the one in the Xcode Command Line Tools is fine). Standard library only.
- The Command Line Tools for
nm(link check),clang++,c++filtand the Kernel.framework headers (genhdr). llvm-objdumpfordisasm.py,dump.pyandrefresh.py. It is looked up in$LLVM_OBJDUMP, then the MacPorts llvm-20 paths/opt/local/libexec/llvm-20/bin/llvm-objdumpand/opt/local/bin/llvm-objdump-mp-20. Other LLVM builds should work if you pointLLVM_OBJDUMPat them; only MacPorts llvm 20 has been tested.- x86_64 collections only. Apple Silicon kernel caches use a different pointer format and are not handled.
Environment variables, all optional:
| Variable | Used by | Default |
|---|---|---|
KCABI_KC |
linkcheck.py | /System/Library/KernelCollections/BootKernelExtensions.kc |
LLVM_OBJDUMP |
disasm.py | the MacPorts paths above |
KCABI_CACHE |
dump.py | ~/.cache/kcabi (carved copies, about 320 MB for one build) |
KCABI_REF |
dump.py | none |
KCABI_DUMPS |
refresh.py | ./dumps |
KCABI_ROOT |
genhdr.py | the current directory (generated headers name their dump relative to it) |
Tested on macOS 26.7.1 (25G241), x86_64, on a Hackintosh (Lenovo Legion 5 15IAX10, OpenCore 1.0.8), Python 3.9.6, MacPorts llvm 20:
linkcheck.pyon a Lilu plugin: passes with--own-lib as.vit9696.Lilu, fails without it on the 8 Lilu symbols, as it should.dump.pyof 25G241: 48 vtables, 329 instance sizes, 417 functions. Comparing it with an earlier dump of the same build gives 418 identical functions and no differences.genhdr.py genandcheckon that dump: the 9 headers verify, and they match the headers the MT7925 native driver builds against.refresh.pyfrom a macOS 15 (24H32) dump to 25G241: verdict review (request tables grew from 561 to 586 IDs, Skywalk classes grew by 0x10 bytes).
Only one machine and two builds have been through it. Reports from other builds are welcome (see CONTRIBUTING.md).
How the collection is parsed, how vtables and sizes are read, and how the link check decides: docs/HOW-IT-WORKS.md.
- ga403wm-hackintosh (thanhnndev and contributors, BSD-3-Clause-Clear): the method of reading a kernel collection (
tools/kc.py,docs/IO80211-ABI-2.md), the link check idea (tools/check_kext_links.py), and the header generation method and return type tables ingenhdr.py(tools/io80211_abi.py,gen_io80211_header.py,io80211_check.py,gen_native_iface.py,check_native_slots.py). See NOTICE.md.
ISC, see LICENSE. genhdr.py adapts code from ga403wm-hackintosh, which is BSD-3-Clause-Clear; that licence is in LICENSES/ and applies to the adapted parts as well.