First, memory management
Like OC, Swift adopts the ARC memory management scheme (for heap space) based on reference counting. There are three kinds of references in ARC of Swift, which are strong reference, weak reference and undirected reference respectively.
In the first article, “Structures and Classes,” we learned that the Swift class is essentially a pointer to a HeapObject structure. The HeapObject structure has two member variables, metadata and refCounts. Metadata is a pointer to a metadata object that stores class information, such as attribute information and virtual function tables. RefCounts, as they are known by name, are a reference count information thing. Let’s take a look at refCounts.
1. RefCounts – Reference count information
Find refCounts in heapObject. h and refCounts in heapObject. h.
RefCounts is of type InlineRefCounts.
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
Copy the code
RefCounts accepts a generic parameter. Let’s look at the structure of RefCounts:
What is RefCounts? RefCounts are basically a wrapper around a reference count, the type of which depends on the generic arguments passed in. What is the generic parameter InlineRefCountBits? It is defined as follows:
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
Copy the code
RefCountIsInline is also a template class, and it also has a parameter RefCountIsInline. RefCountIsInline is either true or false, so you can find it in the source code. Let’s look at the RefCountBitsT structure:
The RefCountBitsT class has only one attribute, bits, as shown in the figure. Bits are of Type BitsType and are defined by Type in RefCountBitsInt.
// 64-bit inline
// 64-bit out of line
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline8 >,{
typedef uint64_t Type;
typedef int64_t SignedType;
};
Copy the code
As you can see, the Type of Type is a uint64_t bit-field information that stores reference counts related to the running life cycle.
At this point, we still don’t know how to set this up, so let’s look at, what is the current reference count when we create an instance object? We find the definition of HeapObject. In the HeapObject initialization method we see the initialization assignment of refCounts as follows:
We see refCounts passing in an Initialized, and then a global search for Initialized_t finds a value of Initialized for Initialized_t enumeration.
Constexpr RefCounts(Initialized_t) RefCounts (RefCountBits(0, 1)) {} for a new object, the reference count is 1, and the RefCountBitsT parameter is passed. Let’s go back to the RefCountBitsT class and find its initialization method as follows:
As shown, we have RefCountBitsT initialization methods called externally, strongExtraCount passed 0 and unownedCount passed 1. Then Offsets: : StrongExtraRefCountShift = 33, Offsets: : PureSwiftDeallocShift = 0, Offsets: : UnownedRefCountShift = 1, So where do these three come from?
Let’s look at the 64-bit implementation of RefCountBitOffsets:
PureSwiftDeallocShift = 0, no doubt, StrongExtraRefCountShift and UnownedRefCountShift, we see that they both call the same method shiftAfterField, Find its implementation as follows:
# define shiftAfterField(name) (name##Shift + name##BitCount)
Copy the code
This is a macro definition implementation that passes a parameter name and adds internally. Note that the ## operator is used in C++ to glue, for example, a name, or a value, so:
UnownedRefCountShift
Travels isPureSwiftDealloc
, then the internal implementation is:
(PureSwiftDeallocShift + PureSwiftDeallocBitCount)- > (0 + 1)= 1
Copy the code
StrongExtraRefCountShift
Travels isIsDeiniting
, then the internal implementation:
(IsDeinitingShift + IsDeinitingBitCount)
Copy the code
And IsDeinitingShift passes UnownedRefCount, so it should be.
((UnownedRefCountShift + UnownedRefCountBitCount) + IsDeinitingBitCount)- > ((1 + 31 + 1))= 33
Copy the code
Now that we know where these three values come from, we calculate the bits value of RefCountBitsT’s initialization method call:
0 << 33 | 1 << 0 | 1 << 1;
0 | 1 | 2 = 3;
Copy the code
We ended up with 3, so what were we doing the whole process, we were evaluating refCounts.
So the first time you create an object and reference it, refCounts = 3. To verify this, the code looks like this:
class SHPerson {
var age = 18
var name = "Coder_ zhang SAN"
}
let p = SHPerson(a)// Prints the memory pointer address of the current p instance
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())
print("Hello, World!")
Copy the code
In print (” Hello, World!” (x/4gx); (x/4gx); (x/4gx)
Print result:0x0000000100720ce0
(lldb) x/4gx 0x0000000100720ce0
0x100720ce0: 0x0000000100008198 0x0000000000000003
0x100720cf0: 0x0000000000000012 0xbce55f7265646f43
(lldb)
Copy the code
Sure enough, refCounts is 0x0000000000000003, which is 3.
2. Strong reference
By default, references are strong references. RefCounts is a reference count. After creating an object, its initial value is 0x0000000000000003. If I make multiple references to this instance object, will the refCounts change?
The code is as follows:
class SHPerson {
var age = 18
var name = "Coder_ zhang SAN"
}
let p = SHPerson(a)// Prints a memory pointer to the current p instance
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())
let p1 = p
let p2 = p
print("Hello, World!")
Copy the code
Print (“Hello, World!” ), print the memory structure of p pointer in the console through x/4gx, print once through a breakpoint, print three times respectively, corresponding to the result before p1, before P2, after P2 assignment.
As shown, during the reference to P2, the refCounts value changes to 0x0000000000000003 -> 0x0000000200000003 -> 0x0000000400000003. Open up your calculator and see what happens:
Notice that starting from the high 32 bits, the high 33 bits are 1 when 0x0000000200000003 is used. When it is 0x0000000400000003, the 34th bit is 1. So, when referring to an instance object, it’s actually a displacement operation.
The diagram above shows the store of refCounts under 64-bit domain information. To test isDeinitingMask, if there is no strong reference to the instance, the instance will be released, i.e. the 32 bits will become 1. The code is as follows:
class SHPerson {
var age = 18
var name = "Coder_ zhang SAN"
}
var p: SHPerson? = SHPerson(a)// Prints a memory pointer to the current p instance
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())
p = nil
print("Hello, World!")
Copy the code
Print (“Hello, World!” ), format the output memory structure as follows:
After p = nil, refCounts becomes 0x0000000100000003 and we open our calculator like this:
_swift_retain_ = ‘swift_retain_’; heapObject.cpp = ‘swift_retain_’; heapobject.cpp = ‘swift_retain_’;
static HeapObject *_swift_retain_(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
if (isValidPointerForNativeRetain(object)) object->refCounts.increment(1);
return object;
}
Copy the code
When we do a strong reference, we essentially call the increment method of refCounts. Let’s look at the implementation of increment:
void increment(uint32_t inc = 1) {
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// constant propagation will remove this in swift_retain, it should only
// be present in swift_retain_n
if (inc ! = 1 && oldbits.isImmortal(true)) {
return;
}
RefCountBits newbits;
do {
newbits = oldbits;
bool fast = newbits.incrementStrongExtraRefCount(inc);
if (SWIFT_UNLIKELY(!fast)) {
if (oldbits.isImmortal(false)) return;
returnincrementSlow(oldbits, inc); }}while (!refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_relaxed));
}
Copy the code
See key code, call the incrementStrongExtraRefCount in increment, we could look at the incrementStrongExtraRefCount implementation:
// Returns true if the increment is a fast-path result.
// Returns false if the increment should fall back to some slow path
// (for example, because UseSlowRC is set or because the refcount overflowed).
SWIFT_NODISCARD SWIFT_ALWAYS_INLINE bool
incrementStrongExtraRefCount(uint32_t inc) {
// This deliberately overflows into the UseSlowRC field.
bits + = BitsType(inc) << Offsets: :StrongExtraRefCountShift;
return (SignedBitsType(bits) > = 0);
}
Copy the code
Note that we already know that StrongExtraRefCountShift = 33 and externally passed in Inc = 1. If bits = 0 then 1 << 33 = 0x2. If there is a strong reference to it, it is 1 += 1 << 33 -> 2 << 33 = 0x4.
Here, the realization of the incrementStrongExtraRefCount is corresponding to speak in front of the assignment (p1, p2, refCounts high 32-bit began to change.
3. A weak reference
In the actual development process, we mostly use strong references. In some scenarios, using strong references will cause circular references if not used properly. Here’s an example:
A strongly references B, and B strongly references A, causing A circular reference. As A result, A certain type of memory cannot be released independently, resulting in memory leakage. The solution is to change one of the references to A weak reference, as shown in the figure, from A strong reference to B to A weak reference. This similar scenario is often used in the development of the proxy pattern.
Weak references can be defined by weak in Swift, and must be defined by var. When defining a property with weak, the property defaults to an optional value because ARC automatically sets weak references to nil after instance destruction. Note that ARC does not fire the property viewer when it automatically sets weak references to nil.
Let’s look at the nature of the weak modifier as follows:
class SHPerson {
var age = 18
var name = "Coder_ zhang SAN"
}
weak var p = SHPerson(a)print("Hello, World!")
Copy the code
By making a breakpoint at print, we look at the assembly code.
From assembly, we can see that after the weak modifier, p becomes optional, and a swift_weakInit function is called, followed by a swift_release function, which releases the instance of P.
Let’s take a look at swift_weakInit function in the source code is how to achieve, in heapObject. CPP file, swift_weakInit implementation is as follows:
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}
Copy the code
Through the source code, it can be known that after the weak modification, internally generated WeakReference type variable, and called nativeInit function in swift_weakInit. NativeInit is implemented as follows:
void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
Copy the code
Here, it calls refCounts formWeakReference function, forming a weak reference, let’s look at formWeakReference implementation:
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
auto side = allocateSideTable(true);
if (side)
return side->incrementWeak();
else
return nullptr;
}
Copy the code
As you can see, it essentially creates a hash table. Let’s look at creating a hash table:
// Return an object's side table, allocating it if necessary.
// Returns null if the object is deiniting.
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
// 1. Insert refCounts- reference counts
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// Preflight failures before allocating a new side table.
// 2. Check whether the original refCounts have the current reference count
if (oldbits.hasSideTable()) {
// Already have a side table. Return it.
// If there is a direct return
return oldbits.getSideTable();
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
// Return nil if not and destructing
return nullptr;
}
// Preflight passed. Allocate a side table.
// FIXME: custom side table allocator
// create a hash table
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
auto newbits = InlineRefCountBits(side);
// 4. Some processing of the original hash table and the ongoing destruction
do {
if (oldbits.hasSideTable()) {
// Already have a side table. Return it and delete ours.
// Read before delete to streamline barriers.
auto result = oldbits.getSideTable();
delete side;
return result;
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
side->initRefCounts(oldbits);
} while (! refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_release, std::memory_order_relaxed));
return side;
}
Copy the code
Creating a hash table can be divided into four steps:
- Take out the original
refCounts
– Reference count information. - Judge the original
refCounts
Does it have a hash table, returns nil if it does, and returns nil if it doesn’t and is destructing. - Create a hash table.
- Some processing of the original hash table and the ongoing destruction.
HeapObjectSideTableEntry = HeapObjectSideTableEntry = HeapObjectSideTableEntry = HeapObjectSideTableEntry
In fact, here we are officially told the difference between strong references and weak references internally. Let’s look at the structure of the heapObject TableEntry.
As you can see, HeapObjectSideTableEntry holds the pointer to the object, and it also has a refCounts, and refCounts is of type SideTableRefCounts, What is this side table lerefcounts? SideTableRefCounts is a template class that inherits from RefCountBitsT.
Also, it has a weakBits.
At this point, after the weak modifier, the hash will store Pointers to the object and reference count information. Let’s verify that we store a pointer to an object as follows:
class SHPerson {
var age = 18
var name = "Coder_ zhang SAN"
}
var p = SHPerson(a)// Prints a memory pointer to the current p instance
print(Unmanaged.passUnretained(p as AnyObject).toOpaque())
weak var p1 = p
print("Hello, World!")
Copy the code
So let’s see what happens when we use weak.
As shown in the figure, refCounts changes from 0x0000000000000003 to 0xC000000020264920 with weak modifier. Open your calculator as follows:
As shown in the figure, the memory address changed to 0xC000000020264920 after being modified with weak will be changed to 1 in the 62nd and 63rd bits. At this time, it needs to be restored. The memory address changed to 0x20264920 after restoration.
Let’s look at the hash table generation – InlineRefCountBits.
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
Copy the code
As you can see, InlineRefCountBits is also a template class for RefCountBitsT. Its corresponding initialization method is as follows:
RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
| (BitsType(1) << Offsets::UseSlowRCShift)
| (BitsType(1) << Offsets::SideTableMarkShift))
{
assert(refcountIsInline);
}
Copy the code
SideTableUnusedLowBits = 3, so, in this process, the hash table passed in is moved 3 bits to the right, and the next two bits are 62 and 63 bits marked as 1. So, let’s go back to our calculator, and since it moved 3 to the right, let me move 3 to the left to restore it.
0x20264920 moving 3 bits to the left equals 0x101324900. Next in Xcode we format 0x101324900 as follows:
As shown in figure, the verified results are consistent with the analysis. So when you use the weak modifier, you’re essentially creating a hash table.
4. No master reference
In Swift, unowned can be used to define an unhosted reference. unowned does not generate a strong reference, and the memory address of the instance remains after destruction (similar to unsafe_unretained in OC). It is important to note that attempting to access an owner-reference after the instance has been destroyed produces a runtime error (wild pointer).
Weak and unowned can solve the problem of circular reference. unowned has less performance cost than weak. How do we choose weak and unowned?
According to apple’s official documentation. When we know that the life cycles of two objects are not related, we must use weak. In contrast, non-strongly referenced objects that have the same or longer lifetime as strongly referenced objects should use unowned.
To put it simply:
- It may become nil use during the lifetime
weak
. - The use of an initial assignment never becomes nil again
unowned
.
5. Circular references to closures
5.1. Closure loop reference causes and solutions
Closure expressions by default generate an extra strong reference to the used outer object (the outer object is retained).
The following code generates a circular reference that causes the SHPerson object to be unable to be freed (no deinit for SHPerson is called).
class SHPerson {
var closure: (()->())?
func run(a) { print("run")}deinit { print("deinit")}}func test(a) {
let p = SHPerson()
p.closure = {
p.run()
}
p.closure()
}
test()
Copy the code
Circular references can be addressed by declaring weak or unowned references in the capture list of closure expressions. The code is as follows:
p.closure ={[weak p] in
p?.run()
}
Copy the code
p.closure ={[unowned p] in
p.run()
}
Copy the code
We can also define new names in the capture list, and even add other constants like this:
p.closure ={[weak wp = p, unowned up = p, a = 10 + 20] in
wp?.run()
}
Copy the code
If you want to reference self while defining a closure attribute, the closure must be lazy (because self cannot be referenced until the instance is initialized). In the following code, the compiler forces self to be written explicitly within a closure if instance members (properties, methods) are used.
class SHPerson {
lazy var closure: (()->()) ={[weak self] in
self?.run()
}
func run(a) { print("run")}deinit { print("deinit")}}func test(a) {
let p = SHPerson()
p.closure()
}
test()
Copy the code
If lazy is the result of a closure call, then you don’t have to worry about circular references because the closure’s declaration cycle ends when the closure is called. The code is as follows:
class SHPerson {
var age = 18
lazy var getAge: Int = {
self.age
}()
func run(a) { print("run")}deinit { print("deinit")}}Copy the code
5.2. Capture lists
So what is a capture list?
-
By default, closure expressions capture constants and variables from the range around them and strongly reference these values. You can use capture lists to explicitly control how values are captured in closures.
-
Before the argument list, the capture list is written as a comma-enclosed list of expressions, enclosed in square brackets. If a captured column table is used, the in keyword must be used even if the parameter name, parameter type, and return type are omitted.
When you create a closure, you initialize the entries in the capture list. For each entry in the capture list, initialize the constant to the value of a constant or variable with the same name in the surrounding range.
For example, in the code below, the capture list contains age, but the capture list does not contain height, which makes them behave differently.
var age = 0
var height = 0.0
let closure = { [age] in
print("age: ", age)
print("height: ", height)
}
age = 10
height = 180
closure() // Age: 0, height: 180.0
Copy the code
-
When a closure is created, the age in the inner scope is initialized with the value of the age in the outer scope, but their values are not concatenated in any special way.
-
This means that changing the value of a in the outer scope does not affect the value of age in the inner scope, does not change the value of the enclosing inner scope, and does not affect the value of the enclosing outer scope.
-
In contrast, there is only one variable named height – height in the outer scope, so changes made inside or outside the closure are visible in both places.
Second, the pointer
Pointers in Swift are classified into two types: typed Pointer A pointer of a data type. Raw Pointer A pointer that does not specify a data type (a native pointer). Unsafe, these are defined as “Unsafe,” and there are four common types:
-
UnsafePointer
similar to const Pointee *. -
UnsafeMutablePointer
Similar to Pointee *. -
UnsafeRawPointer is similar to const void *.
-
UnsafeMutableRawPointer is similar to void *.
Why the pointer is not safe.
-
For example, when we create an object, we need to allocate memory space in the heap. But this memory space has a limited declaration period. If we use a pointer to point to the content space and the current memory space reaches its lifetime (reference count is 0), then the pointer becomes undefined behavior.
-
The memory space we create is bounded, for example, if we create an array of size 10 and we access index = 11 through a pointer, then we cross the boundary.
-
An unknown memory space was accessed. Pointer types inconsistent with memory value types are also unsafe.
1. Native Pointers
We create a mutable native pointer, 8 bytes in size, 8 bytes aligned. The storeBytes method is used to store values, and the load method is used to value values. The code is as follows:
// Create a mutable native pointer, 8 bytes in size, 8 bytes aligned
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 8, alignment: 8)
/ / value
ptr.storeBytes(of: 10, as: Int.self)
/ / value
let value = ptr.load(as: Int.self)
print(value)
Copy the code
Next we create a 32 byte size, 8 byte aligned mutable native pointer as follows:
// Get the cloth length, that is, the value of the instance size with bytes aligned
let stride = MemoryLayout<Int>.stride
// Create a mutable native pointer, 32 bytes in size, 8 bytes aligned
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 4 * stride, alignment: 8)
/ / value
for i in 0..<4 {
ptr.advanced(by: i * stride).storeBytes(of: i * 2, as: Int.self)}/ / value
for i in 0..<4 {
let value = ptr.load(fromByteOffset: i * 8, as: Int.self)
print("i: \(i) - value: \(value)")}Copy the code
We store I * 2 into a pointer and need to call the Advanced method before calling the storeBytes method. Advanced passes a value that can be offset by a specified distance. Note the value passed by Advanced, which is not based on byte alignment, but on the stride, the size of memory allocated to the structure by the system.
The value must be offset from the specified distance because the value is offset from the specified distance. The fromByteOffset parameter specifies the distance to be offset.
Generic Pointers
In contrast to native Pointers, generic Pointers specify that the current pointer is bound to a specific type.
In the case of generic pointer access, we do not use the load and store methods for storage operations. There are two ways to get UnsafePointer using the variable pointee built into the current generic pointer.
One way to do this is by using existing variables.
We can use generic Pointers to get a pointer to a variable:
var age = 18
let ptr = withUnsafePointer(to: &age) { $0 }
print(ptr)
print(ptr.pointee)
Copy the code
We can modify the stored value of a pointer with mutable generic Pointers:
var age = 18
let ptr = withUnsafeMutablePointer(to: &age) { ptr -> UnsafeMutablePointer<Int> in
ptr.pointee + = 1
return ptr
}
print(ptr)
print(ptr.pointee)
Copy the code
Another option is to allocate memory directly:
var age = 18
// Allocate a block of memory of type Int
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// Initialize the allocated memory space
ptr.initialize(to: age)
print(ptr)
print(ptr.pointee)
Copy the code
To get a clearer picture, let’s take a look:
To understand this diagram, let’s look at the following code:
struct SHPerson {
var age: Int
var height: Double
}
// Allocate a block of SHPerson memory
let ptr = UnsafeMutablePointer<SHPerson>.allocate(capacity: 2)
ptr[0] = SHPerson(age: 18, height: 180)
ptr[1] = SHPerson(age: 20, height: 190)
print(ptr[0])
print(ptr[1])
ptr.deinitialize(count: 2)
ptr.deallocate()
Copy the code
In addition to access by subscript, we can also initialize generic Pointers in this way:
let ptr = UnsafeMutablePointer<SHPerson>.allocate(capacity: 2)
ptr.initialize(to: SHPerson(age: 18, height: 180))
ptr.advanced(by: MemoryLayout<SHPerson>.stride).initialize(to: SHPerson(age: 20, height: 190))
print(ptr.advanced(by: 0).pointee)
print(ptr.advanced(by: MemoryLayout<SHPerson>.stride).pointee)
ptr.deinitialize(count: 2)
ptr.deallocate()
Copy the code
3. Get a pointer to the heapspace instance
class SHPerson {}
var p = SHPerson(a)var ptr = withUnsafePointer(to: &p) { UnsafeRawPointer($0)}var heapPtr = UnsafeRawPointer(bitPattern: ptr.load(as: UInt.self))
print(heapPtr ?? "")
Copy the code
4. Memory binding
Swift provides three different apis to bind/rebind Pointers:
4.1. assumingMemoryBound(to:)
The assumption memorybound (to:) can be used to tell the compiler what type to expect from the assumption memorybound (to:). (Note: this is just a way for the compiler to bypass type checking; no conversion of the actual type occurs.)
func testPointer(p: UnsafePointer<Int>) {
print(p[0])
print(p[1])}// A tuple is a value type that stores 10 and 20, which is essentially an Int
let tuple = (10.20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer< (Int.Int) >)in
The sumingmemorybound (to:) method is used to tell the compiler that the memory is already bound to an Int and the compiler will not check.
testPointer(p: UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))}Copy the code
4.2. BindMemory (to: capacity:)
Used to change the type of memory binding, if the memory does not already have a type binding, will bind to that type for the first time; Otherwise, rebind the class and all the values in memory will change to that type.
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer< (Int.Int) >)in
testPointer(p: UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 2))}Copy the code
4.3. WithMemoryRebound (to: capacity: body:)
When we pass parameters to external functions, there are inevitably some data type gaps. If we cast, we have to copy data back and forth; We can use withMemoryRebound(to: Capacity: body:) to change the memory binding type.
func testPointer(_ p: UnsafePointer<Int8>){
print(p)
}
let uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
testPointer(int8Ptr)
})
Copy the code
5. Conversion between Pointers
The unsafeBitCast function is a cast that ignores data types and does not change the original memory data due to data type changes, similar to reinterpret_cast in C++.
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
ptr.assumingMemoryBound(to: Int.self).pointee = 11
(ptr + 8).assumingMemoryBound(to: Double.self).pointee = 22.0
print(unsafeBitCast(ptr, to: UnsafePointer<Int>.self).pointee) / / 11
print(unsafeBitCast(ptr + 8, to: UnsafePointer<Double>.self).pointee) / / 22.0
ptr.deallocate()
Copy the code
In the above code, I store 11 Int and 22.0 Double into the memory of the PTR pointer, respectively. We can use unsafeBitCast to fetch 11 for Int and 22.0 for Double.
The following code casts SHPerson to UnsafeRawPointer via unsafeBitCast.
class SHPerson {}
var p = SHPerson(a)var ptr = unsafeBitCast(p, to: UnsafeRawPointer.self)
print(ptr)
Copy the code
6. Pointer strengthening exercises
In the methods and properties we learned earlier, we found information about methods and properties in the Mach-O file using source code + assembly analysis. Next we use Pointers to get information about methods and attributes in the Mach-O file.
Preparations:
class SHPerson {
var age: Int = 18
var name: String = "Coder_ zhang SAN"
}
struct TargetClassDescriptor {
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
//V-Table
}
struct FieldDescriptor {
var MangledTypeName: UInt32
var Superclass: UInt32
var Kind: UInt16
var FieldRecordSize: UInt16
var NumFields: UInt32
var FieldRecords: FieldRecord
}
struct FieldRecord {
var Flags: UInt32
var MangledTypeName: UInt32
var FieldName: UInt32
}
// VTable structure
struct TargetMethodDescriptor {
// 4 bytes, Flags which method is used.
var Flags: UInt32
// this is not a real IMP, but a pointer to offset.
var Impl: UInt32
};
Copy the code
TargetMethodDescriptor, TargetClassDescriptor, FieldDescriptor, and FieldRecord are described in previous chapters, so I won’t go into too much detail here. Note that SHPerson must come first, otherwise it will not print the properties or methods of SHPerson.
6.1. Obtain the attribute information of the Mach-O file through Pointers
Get the correct memory address of __swift5_types
var size: UInt = 0
// __swift5_types
let types_ptr = getsectdata("__TEXT"."__swift5_types".&size)
//print(types_ptr)
// Get the __LINKEDIT information from the Mach-o file
var segment_command_linkedit = getsegbyname("__LINKEDIT")
// Get the file memory address of the segment
let vmaddr = segment_command_linkedit?.pointee.vmaddr
// Get the file offset for this segment
let fileoff = segment_command_linkedit?.pointee.fileoff
// Calculate the base address of the link (i.e. the base address of the virtual memory)
var link_base_address = (vmaddr ?? 0) - (fileoff ?? 0)
// The memory address of __swift5_types is obviously the base address of the virtual memory.
// So to get the correct memory address for Swift, subtract the virtual memory base address from the memory address of __swift5_types
var offset: UInt64 = 0
if let unwrapped_ptr = types_ptr {
// Convert types_ptr to an integer type to evaluate
let types_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: unwrapped_ptr)))
offset = types_int_representation - link_base_address
print("offset: ", offset)
}
// 2. Get the four bytes of __swift5_types
// Get the base address of the current program. I have Xcode version 13.2.1 and macOS version 12.1, and I have tested that when I call _dyLD_GET_image_header, I need to pass index 3 to get the base address of the program.
// This depends on the Xcode version and the macOS version. Normally, if you pass 0, you should get it. If not, pass 3 or try something else.
var app_base_address = _dyld_get_image_header(3)
//print(app_base_ptr)
// Convert app_base_address to an integer for calculation
let app_base_address_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: app_base_address)))
// Calculate the address of the four bytes of __swift5_types stored in program memory
var data_load_address = app_base_address_int_representation + offset
// We need to get what these four bytes point to
// Convert data_load_address to a pointer type
let data_load_address_ptr = withUnsafePointer(to: data_load_address) { $0 }
let data_load_content = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: data_load_address) ?? 0)?.pointee
print("data_load_content: ",data_load_content)
// 3. Obtain the address of Description
// Get Description in the mach-o file
let description_offset = offset + UInt64(data_load_content ?? 0) - link_base_address
print("description_offset: ", description_offset)
// Get a pointer to Description in memory
let description_address = description_offset + app_base_address_int_representation
// Point the pointer address of Description to TargetClassDescriptor
let class_description = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: description_address) ?? 0)?.pointee
print("class_description: ", class_description)
// 4. Get attribute information - FieldDescriptor
// 16 is the size of the four member variables in front of the fieldDescriptor, four 4-bytes, so 16
let fieldDescriptor_address_int_representation = description_offset + 16 + app_base_address_int_representation
print("fieldDescriptor_address_int_representation: ", fieldDescriptor_address_int_representation)
// Convert the fieldDescriptor_address_int_representation to the pointer address, where the value of the address is the offset of the fieldDescriptor
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptor_address_int_representation) ?? 0)?.pointee
print("fieldDescriptorOffset: ", fieldDescriptorOffset)
// fieldDescriptor_address_int_representation + Offset information = the real memory address of the fieldDescriptor
let fieldDescriptorAddress = fieldDescriptor_address_int_representation + UInt64(fieldDescriptorOffset!)
print("fieldDescriptorAddress: ", fieldDescriptorAddress)
// Convert memory address to fieldDescriptor
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
print("fieldDescriptor: ", fieldDescriptor)
for i in 0..<fieldDescriptor!.NumFields {
let a = MemoryLayout<FieldRecord>.stride
let stride: UInt64 = UInt64(i * UInt32(a))
let fieldRecordAddress = fieldDescriptorAddress + 16 + stride
// print(fieldRecordRelactiveAddress)
// let fieldRecord = UnsafePointer
.init(bitPattern: Int(exactly: fieldRecordAddress) ?? 0)? .pointee
// print(fieldRecord)
let fieldNameRelactiveAddress = (fieldRecordAddress + 8 - link_base_address) + app_base_address_int_representation
let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
// print(offset)
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - link_base_address
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
print(String(cString: cChar))
}
}
Copy the code
We already know how to find attributes in a Mach-O file based on the attributes in the previous article.
- Step 1: Get
__swift5_types
The correct memory address in - Step 2: Get
__swift5_types
The contents of those four bytes - Step 3: Get the TargetClassDescriptor.
- Step 4: get the FieldDescriptor.
You can follow the comments in the code to understand.
6.2. Method information for obtaining Mach-O files through Pointers
The method information of obtaining Mach-O file is basically the same as the procedure and thought of obtaining attribute information of Mach-O file.
Again, the previous three steps remain the same. After we get the TargetClassDescriptor information, we do some preparation, comment out the SHPerson property, and add two methods like this:
class SHPerson {
func test1(a) {
print("test1")}func test2(a) {
print("test2")}}Copy the code
Next, prepare the VTable structure as follows:
// VTable structure
struct TargetMethodDescriptor {
// 4 bytes, Flags which method is used.
var Flags: UInt32
// this is not a real IMP, but a pointer to offset.
var Impl: UInt32
}
Copy the code
According to the understanding of the “method” article, the code for obtaining method information is as follows:
// 5. Get method information - FieldDescriptor
let VTable_size = class_description?.size
for i in 0..<(VTable_size ?? 0) {
// VTable offset
let VTable_offset = Int(description_offset) + MemoryLayout<TargetClassDescriptor>.size + MemoryLayout<TargetMethodDescriptor>.size * Int(i)
// Get the address of VTable
let VTable_address = Int(app_base_address_int_representation) + VTable_offset
// Convert VTable_address to TargetMethodDescriptor structure
let method_descriptor = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: VTable_address) ?? 0)?.pointee
// Get the function address of the method
let imp_address = VTable_address + 4 + Int((method_descriptor?.Impl ?? 0)) - Int(link_base_address)
/ / to IMP
let imp: IMP = IMP(bitPattern: UInt(imp_address))!
// Invoke IMP from OC's classes and syntax, printing the method name
SHCallFunc.callFunc(imp: imp)
}
Copy the code
SHCallFunc is an OC class, in order to facilitate printing, through the OC method directly call IMP, you can clearly know whether to get the Swift class method information, SHCallFunc code is as follows:
@interface SHCallFunc : NSObject
+ (void)callFuncWithImp:(IMP)imp;
@end
@implementation SHCallFunc
+ (void)callFuncWithImp:(IMP)imp {
imp();
}
@end
Copy the code