Swift进阶(四)—— 指针

7,502 阅读9分钟

Swift指针类型

swift的指针类型分为两类,指定数据类型指针typed pointer和未指定数据类型指针raw pointer,其中未指定数据类型的指针也叫做原生指针。

  • 指定数据类型指针:用unsafePointer<T>表示,T指代泛型。
  • 未指定数据类型指针:用unsafeRawPointer表示。

常用的几种指针类型

我们比较经常使用的swift指针主要有以下几种

  • unsafePointer<T> 在OC中等同于const T* 代表这个指针及所指向的内容都不可变。

  • unsafeMutablePointer<T> 在OC中等同于T * 代表这个指针及其所指向的内存内容均可变。 

  • unsafeRawPointer 在OC中等同于const void * 代表这个指针指向的内存区域未定。

  • unsafeMutableRawPointer 在OC中等同于void * 代表这个指针指向的内存区域未定。

Swift指针的安全性

在Swift指针中,我们发现使用了unsafe做前缀修饰,说明这个指针是不安全的。它为什么是不安全的呢?主要有以下三点:

  • 我们在创建⼀个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的声明周期是有限的,也就意味着如果我们使⽤指针指向这块内容空间,如果当前内存空间的⽣命周期到了(引⽤计数为0),那么我们当前的指针就变成了未定义的⾏为,也就可能会成为一个野指针。
  • 我们创建的内存空间是有边界的,⽐如我们创建⼀个⼤⼩为10的数组,这个时候我们通过指针访问了index = 11的位置,这个时候就会越界,这个指针也就会访问了⼀个未知的内存空间。
  • 指针类型与内存的值类型不⼀致,也是不安全的。 比如我们创建了一个Int类型的指针,如果这个指针指向了Int16类型的指针,就会造成数据精度缺失。

原生指针的使用

原生指针常用API

这里我选择UnsafeMutableRawPointer里面的API做说明

1.创建可变指针

///byteCount:需要多少内存空间
///alignment:内存对齐大小
    allocate(byteCount: Int, alignment: Int) -> UnsafeMutableRawPointer

2.存储数据

//of:数据大小
//as:数据类型
storeBytes(of value:T, as:T.type)

3.读取数据

//fromByteOffset: 读取数据偏移大小
//as:数据类型
load(fromByteOffset: as:)

4.移动指针到下一个存储的位置

//by: 可以移动的步长
advanced(by:)

5.销毁可变指针

deallocate()

原生指针案例

let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
for i in 0..<4 {
    p.storeBytes(of: i, as: Int.self)
    print(p)
}
for i in 0..<4 {
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index \(i), value \(value)")
}

p.deallocate()
//打印结果
index 0, value 3
index 1, value 0
index 2, value 16
index 3, value 0

当我们打印出来结果后,和我们预期想的结果不对。我们在存数据的地方先打印出来p,再使用lldb分析。打印结果如下: 截屏2022-02-19 上午9.12.28.png 我们可以看到,打印出来的p的内存地址是同一个地址,也就是说我们存储数据的时候把数据都存储在同一个地址里面,而读取数据的时候是有偏移的。因此存储数据的时候也要偏移一定的步长才可以。

我们把存储数据的代码修改一下,看一下结果。

for i in 0..<4 {
    p.advanced(by: MemoryLayout<Int>.stride * i).storeBytes(of: i, as: Int.self)
}

//打印结果
index 0, value 0
index 1, value 1
index 2, value 2
index 3, value 3

这次的结果是正确的,我们先通过advanced(by:)来获取到偏移后的指针地址,然后存储数据,这次是要存储4个Int类型的数据,所以我们根据Int类型的步长来进行偏移,然后存储数据。

Swift内存布局

swift提供了一个MemoryLayout枚举类型来帮助我们查看数据类型的内存布局。

  • MemoryLayout<T>.size 提供实际存储该数据类型所需要的字节数。
  • MemoryLayout<T>.stride 提供连续存储多个元素指针间的间隔。
  • MemoryLayout<T>.alignment 需要多少内存(其值的倍数)才能将完全对齐的内容保存在内存缓冲区中,也就是内存对齐的方式。 想看Swift里面具体如何进行内存布局,可以参考这篇文章

泛型指针的使用

对于范型指针,其实就是原生指针制定了指针类型,因为确定了类型,就固定了存储类型的stride 和alignment,我们就不需要通过loadstore的方式存取,而是通过其内置的 pointee变量来存取数据。

获取UnsafePointer的⽅式有两种。⼀种⽅式就是通过已有变量获取

var age = 18
//通过withUnsafePointer直接访问age的地址
withUnsafePointer(to: &age) { ptr in
    print(ptr)
}
//打印结果
0x00000001000081b0

修改已有变量的值

var age = 18
//如果想要修改当前age,使用withUnsafePointer只能返回当前修改的值,无法直接修改age的值
age = withUnsafePointer(to: &age) { ptr in
    return ptr.pointee + 21
}
//打印结果
39
var b = 42
//如果想直接修改变量的值,可以使用withUnsafeMutablePointer
withUnsafeMutablePointer(to: &b) { ptr in
    ptr.pointee += 100
}
print(b)
//打印结果
142

另外一种方式是直接分配内存

var age = 10
//分配一块Int类型的内存空间,这时候还没有开始初始化
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
//初始化分配的内存空间
tPtr.initialize(to: age)
print(tPtr.pointee)
//释放已经初始化的内存空间,和initialize成对出现
tPtr.deinitialize(count: 1)
//销毁这块分配好的内存空间,和allocate成对出现
tPtr.deallocate()

我们可以通过以下这张图片来更直观的了解一下: 869b45e0017c41d09768f2d79e3acec0_tplv-k3u1fbpfcp-watermark.webp

对于结构体和类,我们应该怎么去初始化指针并存储值呢?

struct LGStruct {
    var age: Int
    var height: Double
}

var tPtr = UnsafeMutablePointer<LGStruct>.allocate(capacity: 5)
tPtr[0] = LGStruct(age: 18, height: 1.50)
tPtr[1] = LGStruct(age: 20, height: 1.80)
tPtr.deinitialize(count: 5)
tPtr.deallocate()

我们也可以这样初始化

var tPtr = UnsafeMutablePointer<LGStruct>.allocate(capacity: 5)
tPtr.initialize(to: LGStruct(age: 18, height: 1.50))
tPtr.advanced(by: MemoryLayout<LGStruct>.stride).initialize(to: LGStruct(age: 20, height: 1.80))
tPtr.deinitialize(count: 5)
tPtr.deallocate()

内存绑定

在Swift中提供了三种不同的API来绑定指针,或者重新绑定指针

  • assumingMemoryBound(to:) 有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,否则就会报错,比如以下代码: 截屏2022-02-23 下午8.23.06.png 这时我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。需要注意的是,这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换。 截屏2022-02-23 下午8.27.51.png

  • bindMemory(to: capacity:) ⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。 截屏2022-02-23 下午8.30.24.png

  • withMemoryRebound(to: capacity: body:) 当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来回复制数据;这个时候我们就可以使⽤ withMemoryRebound(to: capacity: body:) 来临时更改内存绑定类型。当离开当前的作用域就会失效,重新绑定为原始类型。这可以将临时类型指针的访问和其他代码的作用域分开。 截屏2022-02-23 下午8.42.15.png

指针案例练习

使用指针读取类的属性名称

我们在上面Swift进阶(三)—— 属性中通过Mach-o文件得到了一个类的属性信息,这次我们使用指针来读取类的属性名称。

首先,我们先定义一个LGTeacher类,里面只有agename两个属性

class LGTeacher {
    var age: Int = 18
    var name: String = "FY"
}

我们先去获取程序运行的随机ASLR地址

var mhHeaderPtr = _dyld_get_image_header(0)

这里我因为使用了MacOS程序,所以用上面代码读取出来的ASLR地址是和通过image lsit 的lldb指定读取的ASLR地址是一致的。如果使用了真机运行这段代码,在iOS15的系统中读取出来的地址,和通过image lsit 的lldb指定读取的第三个是一致的。所以你需要改成代码_dyld_get_image_header(3)。 或者通过以下API去调用。

var excute_header: UnsafePointer<mach_header>?
let count = _dyld_image_count()
print("count (count)")
for i in 0..<count{
    let mhHeaderPtr = _dyld_get_image_header(i)
    if mhHeaderPtr!.pointee.filetype == MH_EXECUTE {
        excute_header = mhHeaderPtr
    }
}
print("excute_header (excute_header)")

再去获取Mach-O文件中__LINKEDIT的信息,然后得到这个文件的内存地址和文件偏移量,通过内存地址和偏移量可以得到这个虚拟内存的基地址。

var segment_command_linkedit = getsegbyname("__LINKEDIT")

var linkBaseAddress:UInt64 = 0

if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileOff = setCommond64Ptr?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileOff
}

接着我们获取Mach-O文件中__swift5_types字段的内存地址,这里存放的是Swift结构体、枚举、类的Descriptor信息。通过代码获取的内存地址是有添加了虚拟内存的基地址的,因此要减去基地址获得这个__swift5_types字段的内存偏移地址。

var size: UInt = 0

var ptr = getsectdata("__TEXT", "__swift5_types", &size)

var offset:UInt64 = 0

if let unwrappedPtr = ptr {
    //把指针类型转成UInt64类型
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))

    offset = intRepresentation - linkBaseAddress
}

再去获取__swift5_types字段的Data LO的内存地址。

//Data LO 内存地址

//把ASLR地址转成UInt64类型
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))

//获取dataLO在内存中存储的地址
var dataLOAddress = mhHeaderPtr_IntRepresentation + offset

var dataLoAddressPtr = withUnsafePointer(to: &dataLOAddress) { return $0 }
//前面代码里面只有一个类,因此dataLO地址的前四个字节就是我们这个类的ClassDescriptor信息的偏移地址。
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLOAddress) ?? 0)?.pointee

通过上面得到的ClassDescriptor信息的偏移地址,我们可以计算出ClassDescriptor信息的真实内存地址。

let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress

var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation

print(typeDescAddress)

通过 Swift进阶(二) —— 方法探究里面我们可以知道ClassDescriptor的数据结构,因此我们现在定义一个这样的结构体。

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

接下来我们尝试去获取这个类的名称。

//创建一个TargetClassDescriptor类型的指针,指向这个类的ClassDescriptor内存地址。
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee

if let name = classDescriptor?.name {
    /*类名属性在classDescriptor中排在flags和parent后面,这两个属性都是UInt32类型,
    因此classDescriptor指针要往后移动8个字节,加上类名属性的偏移地址,就是name属性的真实内存偏移量。*/
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8

//获取name属性的内存地址
    let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)

    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)) {

        print(String(cString: cChar))

    }
}

//打印出来的类名
LGTeacher

Swift进阶(三)—— 属性中,我们知道属性存储在fieldDescriptor这个属性中,因此我们先获取fieldDescriptor的内存地址。ClassDescriptor的内存地址往后移动16个字节。

let fieldDescriptorRelaticveAddress = typeDescOffset + 16 + mhHeaderPtr_IntRepresentation

从上面文章中我们得到了FieldDescriptorFieldRecord的具体数据结构,我们把它们用结构体表示。

struct FieldDescriptor  {

    var mangledTypeName: Int32

    var superclass: Int32

    var Kind: UInt16

    var fieldRecordSize: UInt16

    var numFields: UInt32

    var fieldRecords: [FieldRecord]

}

struct FieldRecord{

   var Flags: UInt32

   var mangledTypeName: Int32

   var fieldName: UInt32

}

最后我们来获取这个类的属性名称

//获取fieldDescriptor的内存地址偏移量
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptorRelaticveAddress) ?? 0)?.pointee

//获取fieldDescriptor的内存地址
let fieldDescriptorAddress = fieldDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)

//创建一个FieldDescriptor类型的指针指向fieldDescriptor
let fieldDercriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

根据fieldDercriptor的numFields属性判断有多少个fieldRecord
for i in 0..<fieldDercriptor!.numFields {

    //每个fieldRecord占用12字节内存,因此每次访问一个fieldRecord,指针步长需要移动12字节
    let stride: UInt64 = UInt64(i * 12)

    //属性列表filedRecords的内存偏移首地址为fieldDescriptor往后偏移16字节
    let fieldRecordAddress = fieldDescriptorAddress + 16 + stride

    //获取每个fieldName属性的偏移地址,为每个fieldRecord首地址往后偏移8个字节
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation

    //获取属性名称存储在内存的地址偏移量
    let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee

    //获取属性名称存储在内存的实际地址
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress

    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)) {
        print(String(cString: cChar))
    }
}

//打印结果
age
name

使用指针获取类的方法名称

我们现在来使用指针获取类的方法名。

首先,我们定义一个方法类

class LGTeacher {

    func test() {

    }
    
    func test1() {

    }

    func test2() {

    }
}

通过 Swift进阶(二) —— 方法探究里面我们可以知道方法名的内存地址存储在TargetClassDescriptor类的size属性后面,所以我们可以按照上面的方法去获取到TargetClassDescriptor类的内存地址。然后根据这个内存地址来获取用来存储方法名称的V_Table属性,V_Table属性可以用以下的结构体来表示:

struct VTable {

    var kind: UInt32

    var offset: UInt32
}
//因为我们上面定义一个类的时候,只有方法名,没有属性,因此可以根据size属性获取这个类里面有多少方法。
let numOfVTable = classDescriptor?.size

for i in 0 ..< numOfVTable! {

    //计算每个VTable的内存地址偏移量。
    let vTableOffset = Int(typeDescOffset) + MemoryLayout<TargetClassDescriptor>.size + Int(i) * MemoryLayout<VTable>.size

    //获取每个VTable的内存地址
    let vTableAddress = UInt64(vTableOffset) + mhHeaderPtr_IntRepresentation

    let vTable = UnsafePointer<VTable>.init(bitPattern: Int(exactly: vTableAddress) ?? 0)?.pointee

    //获取Imp的内存地址,也就是VTable的offset属性的内存地址
    let ImpAddress = vTableAddress + 4 + UInt64(vTable!.offset) - linkBaseAddress

    LGTest.callImp(IMP(bitPattern: UInt(ImpAddress))!)

}
//打印结果
test
test1
teat2

在上面代码中LGTest是一个OC类,我们通过OC的方法调用IMP,来打印方法信息。代码如下:

@interface LGTest : NSObject
+ (void)callImp: (IMP)imp;
@end

@implementation LGTest

+ (void)callImp:(IMP)imp {
    imp();
}

@end