Apple Kernelcache
This page walks through how IDA 9.5 lets you analyse an Apple kernelcache: opening one, navigating its contents from the KC Index, loading the KEXTs you actually care about, locating things across the cache without loading anything first, and what IDA reconstructs of the IOKit class hierarchy.
If you already know the Dyld Shared Cache workflow, this one will feel familiar: same index, same on-demand loading, same locators. The differences are the ones the format forces, and they are called out below.
Opening a kernelcache
Apple ships the XNU kernel and its drivers as one container rather than as files. IDA recognises three shapes of it, and the loader offers a single Apple XNU kernelcache entry for all of them:
Fileset (iOS 16+, and recent macOS) — the modern layout. The kernel and every KEXT are
LC_FILESET_ENTRYsubfiles of one Mach-O, sharing a single__LINKEDIT.Prelinked (iOS 11 and older) — the KEXTs live inside
__PRELINK_TEXT, described by a plist in__PRELINK_INFO.Split KernelCollections (recent macOS) —
BootKernelExtensions.kcandSystemKernelExtensions.kc. The system collection links against the boot one; see Linked collections.
As with a shared cache, the loader's job is only to map what is needed to navigate: by default the kernel itself, leaving the hundreds of KEXTs for you to load on demand.

The KC Index
The KC Index is the entry point to everything else. It is a tree of every region of the cache, grouped by the KEXT that owns it, with sizes, addresses and whether the KEXT is currently loaded.

If you close the panel, reopen it through the KernelCache (KC) index action under View > Open subviews.
KEXTs are named by their bundle identifier, and the tree nests on the dots, so com.apple.iokit.IONetworkingFamily sits under com → apple → iokit. That groups Apple's families the way you would expect to browse them.

LoadedEach KEXT also carries what its bundle plist declares: its version, the .kext it was built from, the classes its IOKitPersonalities name, and what it was linked against.
On-demand loading
As in the shared cache, any address may or may not be backed by loaded bytes at any moment. Cross-references to unmapped addresses do not break, decompilation does not abort, and you can load more whenever you want.
A load is triggered by double-clicking a KEXT in the KC Index, or by navigating to an unmapped address in the listing. Either way the KEXT's segments are created, its symbols applied, and the view follows.

Loading a KEXT is not only a mapping step. Everything IDA derives from the runtime — the class hierarchy in particular — is recomputed for the KEXTs that just arrived, and added to what earlier passes found.
The Functions list
A kernelcache maps hundreds of drivers into one address space, so without grouping their functions would be a single flat heap. They are not: each KEXT's functions go into a folder of their own, named after its bundle identifier and nested on the dots, the same way the KC Index nests them.

IONetworkingFamily's functions, under the folders its bundle identifier spells outThe folder name is a configuration option; set it to group drivers differently, or to the empty string for a flat list.
Linked collections
A macOS System KC is only half the picture: what it references lives in the Boot KC beside it. When IDA opens one and finds its sibling next to it on disk, it harvests it, and the KC Index shows both collections.
This matters most when searching. A symbol or string that the system collection does not have may well be in the boot one, which is what the Search linked collections option on the two locators is for.
Locating important information
You will often need to look up a symbol or a string coming from a panic log, a stack trace or an external tool. Three locator actions sit in the KC Index context menu, and all of them work across the whole cache without loading anything first — they read the cache file directly, so a KEXT nobody has mapped is searched like any other:
Symbol — substring search across the KEXTs' symbol tables.
String — substring search across the KEXTs' string sections.
Address — given an address, report which region, KEXT and collection it belongs to.
The two substring locators work like their DSC counterparts: the action first opens a query prompt with a history and a collapsible Options group; confirming opens the KC symbols / KC strings results panel, which keeps the query field so further searches can be refined in place.


The options differ slightly from the shared cache's:
Ignore case — as elsewhere.
Search linked collections — widen the search to the Boot KC, as described in Linked collections.
Scan every section (strings only) — by default only the sections that hold strings are read. This scans the rest of each KEXT too, which is slower and finds the same text wherever else it is embedded.
In the results, each row marks where in its text the match landed — useful for strings, where the row is a window onto the surrounding bytes, and for symbols, where the match is buried in a mangled name.

allocator; each row marks the match inside the bytes around itScan every section is worth reaching for when a string is not where strings usually live. Here the needle is found nowhere by the default scan, and once in a KEXT's __DATA.__data with the option on:

__DATA.__dataSearch linked collections reaches the other half of a split pair. This needle is in no KEXT of the system collection; with the option on it comes back once, at an address belonging to the boot one:

Stripped caches
Modern iOS kernelcaches carry no symbol table at all. A symbol search there cannot succeed however you spell the needle, so IDA says so up front rather than returning an empty result that reads like a missing needle:

The string locator is unaffected: the symbol tables are gone, the string sections are not. And the class names recovered below come from the runtime rather than from any symbol table, so they survive stripping too.
The OSMetaClass class hierarchy
IOKit drivers are C++, and XNU carries its own runtime for them: every class registers itself at load time by constructing a static gMetaClass object. IDA reads that runtime back.
Each class's registration runs from its KEXT's __mod_init_func as:
which names the class, locates its descriptor, names its superclass and gives its instance size — all without a single symbol, which is what makes this work on a stripped cache.
Starting from the module-initialisation functions, IDA:
recovers every class: its name, its
gMetaClassdescriptor, its superclass and its instance size, by decoding the arguments of each registration call;names and types the descriptors, so
X::gMetaClassreads as itself in the listing and in cross-references;rebuilds the hierarchy, base before derived;
synthesizes an instance struct per class, inheriting from its superclass's;
finds and names both vtables of each class — the object vtable and the one belonging to its
X::MetaClass— typing them from the platform's own type library, which IDA picks by kernel version, so each slot carries the name and signature that kernel gives it;creates the methods the vtable slots point at, and names them;
types the result of
OSDynamicCast/metaCastat the call site, from the metaclass the cast refers to;re-runs as KEXTs are loaded, adding to what earlier passes found. The pass is idempotent.
In the listing, a registration reads as the C++ above: the class name, its superclass's descriptor and its instance size, all named.

IOWatchdogEmbedded registering itself, with its superclass's descriptor and the vtable its constructor installsThe decompiler makes the same call plainer still:

sizeof the instanceWhat that buys a reader shows up away from the registration. Here a method IDA named from a vtable calls one it resolved through another, and the OSDynamicCast between them names the metaclass it tests against:

IOWatchdogUserClient::start dispatching to IOService::start, and a cast against IOWatchdog::gMetaClassAnd the types land in the database, where you can use them: an instance struct that derives from its superclass's, and a vtable struct whose inherited slots carry the names and prototypes the kernel gives them.

IOWatchdogEmbedded : IOWatchdog, and its vtable structConfiguration
cfg/kcu.cfg holds the kernelcache options. Each is documented in the file itself, at more length than here; this is what there is to look for.
SEGNAME_FORMAT
How a KEXT's segments are named. Default "%m:%S.%s", e.g. com.apple.iokit.IONetworkingFamily:__TEXT.__text.
FUNC_FOLDER_FORMAT
How the Functions list folder of a KEXT is named. Default "%M", one folder per KEXT under its bundle identifier; empty means no folders.
BOOT_KC_PATH, SYS_KC_PATH
Where the collections this one links against are, when they do not sit next to the input file.
Both format options are built from the same pieces — %m the bundle identifier, %M the same as a path, %b its last component, %c the collection, %S and %s the Mach-O segment and section. A piece that does not exist takes its neighbouring punctuation with it. cfg/dscu.cfg spells the two the same way for the shared cache.
Some behaviour is driven by environment variables rather than the config file, for scripted and batch loads: IDA_KCACHE_KERNEL, IDA_KCACHE_ALL and IDA_KCACHE_KEXT choose what is mapped at load time, and the IDA_KCU_NO_* switches turn off the plugin's own passes. They are listed under environment variables, and kcu.cfg documents them too.
Scripting (kcu_svc_t API)
The whole workflow is driven by a public C++ service, kcu_svc_t, exposed through the <kcu.h> SDK header and the ida_kcu Python module. The same calls the UI makes are available to scripts and plugins: enumerate collections, KEXTs and regions; read a bundle's plist; find symbols and strings without loading anything; resolve an address; load KEXTs.
It mirrors dscu_svc_t closely enough that a script written against one is easy to port to the other.
Extracting a KEXT as a standalone Mach-O
A KEXT inside a kernelcache is not a file you can hand to another tool: the builder splits its segments across the container, merges everybody's __LINKEDIT into one blob, and shares a single string pool between all of them. IDA ships an IDAPython example that reassembles one — see Extracting cache images, which covers kernelcaches and shared caches alike.
For the shared-cache service itself, see dscu_query.py.
See also
Dyld Shared Cache — the same workflow, for Apple's userland cache.
Extracting cache images — writing a KEXT out as a standalone Mach-O.
Last updated
Was this helpful?
