Hex-Rays Blog: IDA Pro Tutorials & Reverse Engineering Tips

IDA 9.4: Improved analysis of compiled Swift binaries

Written by Hex-Rays | Jul 2, 2026

IDA 9.4 constitutes the first step towards better handling of Swift binaries. Perhaps unsurprisingly, our focus is on Swift for ARM64 Mach-O files, but we generally aim to improve Swift support also in the more unusual settings across different architectures and file formats like x86-64 and ELF. For 9.4, we want to highlight two different improvements: proper modelling of the Swift ABI, and proper typing of Swift runtime functions.

Proper Modelling of the Swift ABI

With IDA 9.4, we introduce three new keywords for function prototypes: __swiftself, __swiftthrows, and __swiftasync.

Those three keywords all are preconditioned on the function being declared as __swiftcall. __swiftcall has existed for many IDA releases already, but it now acts as a strong discriminator for any Swift-related logic inside IDA. For Objective-C/Swift mixed binaries, __swiftcall is your way to tell IDA that a function should be handled by the Swift machinery.

Let's go over the three new keywords one by one: since each of them is strongly linked to a Swift-specific ABI register, they are best presented by understanding the corresponding part of the Swift ABI.

The Self Register

Probably the most relevant one, because it made Swift reversing terribly annoying. The following very small snippet of ARM64 assembly code highlights the problem rather clearly:

The code just concatenates two Swift strings: the existing string is a temporary placed on the stack (#0x70+stack_str), and the string that is to be appended, the compiler stored in register pair X27 and X28. We'll get to the peculiarities of Swift string internals at a later point in time; the annoyance we are trying to solve here is how the compiler passes both the existing and the to-be-appended strings to the String.append function. We see that the to-be-appended string is passed by value in registers X0 and X1, but the existing string is passed via a pointer in X20. X0 and X1 are standard argument registers under the regular ARM64 calling convention, but X20 is callee-saved under standard AAPCS64. Pre-9.4 IDA knew that a Swift::String occupied two 8-byte registers, but it had no concept of the Swift ABI specialties, so the decompiler would (wrongly) render this as follows:

The attentive reverser will immediately spot that String.append wrongly receives just one argument: the string that is to be appended. Crucially, we have no clue about the target of the string append operation. This might be recoverable/guessable from context in this isolated example, but since the Swift compiler actually relies a lot on passing pointers to objects in X20, the overall decompiler output becomes almost unintelligible, because the relationship between entities is difficult to track.

For this purpose, IDA 9.4 introduces the __swiftself annotation for arguments in function prototypes. With this new keyword, the String.append function looks as follows:

With the updated function signature, the call site in the decompiler renders exactly as we would expect, with an explicit pointer to the object being manipulated passed as the first argument.

Only functions carrying the __swiftcall keyword can contain exactly one __swiftself-tagged argument. On x86-64 Swift binaries, the same logic is applied to r13.

The Async Context Register

A similar problem arises if we study the following snippet:

Despite the PAC-aware prologue, what draws our attention here are the X22-based memory accesses without an obvious preceding sequence that would make X22 point to anything sensible. Again, that would be fair confusion on compiler-generated AAPCS64 code, but the Swift compiler reserves X22 (and its x86-64 counterpart r14) as the async context register.

The async context stores state needed to suspend and resume an async function, such as continuation/resume information, but most notably async frame data, which can often span entire Swift objects. Keeping it in a dedicated callee-saved register lets Swift async code access that state cheaply across calls without repeatedly passing it as a normal argument.

This deviation from the regular ABI used to confuse pre-9.4 IDAs once again. Here's the corresponding pseudocode in IDA 9.3 (ignore the terrible whitespacing induced by the Swift demangler for a second):

The key is the line saying // 100003ED4: variable 'v0' is possibly undefined: in GUI IDA, v0 would render in red, indicating that the underlying register was read from before it was written to. (Such red markers serve as a great indicator that something is wrong with the current pseudocode.)

In IDA 9.4, we can mark this function as __swiftasync to tell the decompiler that X22 carries a pointer to a meaningful async context:

As you'll notice, we decided against modelling the async context as an explicit parameter, and rather fall back to a custom intrinsic, __swift_get_async_context. This intrinsic is internally marked as non-propagating, so that it always results in an explicit local variable that can be explicitly typed by the user. This way, we can define a new struct type my_async_ctxt, then incrementally build up information about the currently executing async context and edit the underlying type as we go.

The Error Return Register

Which brings us to our last contender for the top spot in the bizarre ABI zoo: the error return register (ARM64 X21 / x86-64 r12). Here is the third snippet for you to study:

The most interesting part is how this function treats X21: the MOV at 0x100000ED0 initializes it to zero, followed by a call at 0x100000ED4, which is in turn followed by a check for zero at 0x100000ED8.

Maybe a bit surprisingly, pre-9.4 IDAs would decompile this function as follows:

Why? Again because of Swift-specific ABI trickery: in vanilla ARM64, X21 is callee-saved. That means the decompiler sees the sequence "initialize X21; call something [that will preserve X21, because it's callee-saved]; check if X21 is still zero", and consequently concludes that the CMP at 0x100000ED8 will always set the EQ flag and hence the CSEL will always take the X0 input reg (which still holds the return value of the callee in standard AAPCS64) — it hence completely optimizes away the check, leading to the flawed pseudocode output.

In IDA 9.4, however, the __swiftthrows function prototype annotation is here to save the day: unlike the other two new keywords presented in this blog post, __swiftthrows alters the behaviour of all callers, rather than the function that carries the annotation. In our example that means the sole callee of this function is typed

and hence signals to the caller that X21 is precisely not callee-saved, but rather carries the "exception state" (~zero = no exception, ~non-zero = exception; we'll spare you an explanation of Swift's typed exceptions today). As a consequence, the decompiler surfaces any subsequent X21 accesses in terms of the new __swift_get_error() custom intrinsic, and we get the desired result:

This is tremendously helpful for analyzing complex control flow. The __swiftthrows keyword is either deduced from the demangled function prototype (if available) or inferred from the callee's structure for unsymbolicated but known-Swift callees. We had to make this auto-detection check rather strict to prevent false positives because, sure enough, in other parts of the code — in non-excepting contexts — the Swift compiler uses X21 as a perfectly fine callee-saved local scratch register. This new keyword and mechanic in the caller supersedes the old way of handling the Swift error return register, where IDA would work with spoils lists when it detected a thrower via the prototype (resulting in incantations like Swift::Int __swiftcall __spoils<CF,ZF,NF,VF,X0,X1,...,X17,X21,Q0,...,Q31> Thrower(Swift::Int x, Swift::Int y, Swift::Int z);).

Runtime Symbol Typing

Modelling the ABI fixes how Swift functions talk to each other. The other half of the readability problem is the functions a Swift binary talks to: the runtime and standard library. A typical app calls hundreds of swift_* entry points, and historically IDA had nothing to say about any of them. They are not described by the __swift5 metadata IDA walks (that metadata describes the types the binary defines, not the runtime it links against), so the best IDA could do was the usual __int64 __fastcall foo(_QWORD, _QWORD, ...) guess. Multiply that by every call site, and a Swift pseudocode listing turns into a sea of untyped quadwords.

IDA 9.4 ships a curated set of prototypes for the runtime surface and applies them by name, so these functions arrive fully typed: with __swiftcall, the right argument types, and meaningful enums. Take swift_beginAccess, the exclusivity-checking primitive the compiler emits around inout and stored-property access. Before:

and after:

The prototype is now the actual one — __swiftcall, with typed arguments rather than a guess. Notice how the flags argument is typed as Swift::ExclusivityFlags, so 33 prints as Tracking|Modify (0x20|0x1) and 1 as Modify.

The Future

Those of you closely following IDA’s progress over the last releases already know that what was presented above is just the beginning. Upcoming IDA versions will address many more shortcomings (Strings, Metadata, Typing, Mangling, …) of current analysis and continue closing the gap between Swift binaries and the analysis they deserve.