LLDB
Swift debugging
type casting
memory management
raw address

LLDB Swift Casting Raw Address into Usable Type

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

When you debug Swift code in LLDB, sooner or later you end up with a raw memory address and need to turn it into something meaningful. That usually happens while investigating memory corruption, checking object layout, or inspecting a value that is no longer easy to reach from source-level variables. The safe way to do it is to build a typed pointer in LLDB's Swift expression mode, then read or print the value with the correct type.

Reading Plain Values From A Raw Address

For simple value types, start with UnsafeRawPointer(bitPattern:) and load the target type. Suppose you know that an Int lives at a given address:

lldb
(lldb) expr -l Swift -- let raw = UnsafeRawPointer(bitPattern: 0x0000000101234000)!
(lldb) expr -l Swift -- raw.load(as: Int.self)

If the address is valid and properly aligned, LLDB prints the integer stored there. The same pattern works for your own structs:

swift
1struct Header {
2    let count: Int32
3    let flag: UInt8
4}
lldb
(lldb) expr -l Swift -- let raw = UnsafeRawPointer(bitPattern: 0x0000000101235000)!
(lldb) expr -l Swift -- raw.load(as: Header.self)

This is the cleanest approach when the memory really contains the bytes of a value type. You are telling LLDB, "interpret the bytes at this address as this Swift type."

Using Typed Pointers For Repeated Inspection

If you need to inspect several fields or read multiple values, convert the address to a typed pointer once. That gives you .pointee and makes repeated reads easier.

lldb
(lldb) expr -l Swift -- let ptr = UnsafePointer<Header>(bitPattern: 0x0000000101235000)!
(lldb) expr -l Swift -- ptr.pointee.count
(lldb) expr -l Swift -- ptr.pointee.flag

This pattern is useful when the target memory is stable and you want better readability. It also matches the mental model you would use in regular Swift code.

One practical rule: use UnsafeRawPointer when you are still figuring out what the memory holds, and switch to UnsafePointer<T> once you are confident about the concrete type.

Casting An Object Address Back To A Swift Class

Class instances are different from value types because the address points to a heap object managed by the Swift runtime. If you already have the object's address, use Unmanaged to turn it back into a reference without changing retain counts:

swift
1final class User {
2    let id: Int
3    let name: String
4
5    init(id: Int, name: String) {
6        self.id = id
7        self.name = name
8    }
9}
lldb
(lldb) expr -l Swift -- let raw = UnsafeMutableRawPointer(bitPattern: 0x0000000102C04F30)!
(lldb) expr -l Swift -- let user = Unmanaged<User>.fromOpaque(raw).takeUnretainedValue()
(lldb) expr -l Swift -- user.name

takeUnretainedValue() is usually the right choice during debugging because you are observing an existing object, not claiming ownership of it. Once you have the reference, po user or expr -l Swift -- user will show the object normally.

Verifying The Address Before You Cast

A raw cast is only as good as the address you started with. If you are not sure what is there, inspect the memory bytes first:

lldb
(lldb) memory read --format x --size 8 --count 4 0x0000000101235000

This helps you confirm alignment and detect obvious mistakes, such as reading from freed memory or using the address of a pointer variable instead of the address stored inside it.

It also helps to stop in a debug build. Optimized Swift binaries can inline, move, or eliminate values in ways that make raw memory inspection much less reliable.

Common Pitfalls

The biggest error is using the wrong type. If the bytes in memory do not match the layout of the type you request, LLDB may print nonsense or trigger another fault. Always verify whether you are dealing with a struct payload, a pointer to a value, or a heap object.

Another common issue is confusing an object's address with the address of a variable that stores a reference to that object. Those are not the same thing. For class instances, you usually want the heap object's address, then Unmanaged to recover the reference.

Alignment is another trap. load(as:) expects the target address to satisfy the alignment requirements of the type. If alignment is questionable, inspect bytes first instead of forcing a typed load.

Finally, debugging optimized code can produce misleading results. Variables may be moved or not exist at all in the way source code suggests.

Summary

  • Use UnsafeRawPointer(bitPattern:) with load(as:) for value types at known addresses.
  • Use UnsafePointer<T> when repeated typed inspection is helpful.
  • Recover class instances with Unmanaged<T>.fromOpaque(...).takeUnretainedValue().
  • Verify raw memory before casting if the address is uncertain.
  • Prefer debug builds because optimized Swift code is harder to inspect accurately.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms