8. 指针

169 阅读12分钟

一、不安全的指针

为什么说指针是不安全的?

  • 比如我们在创建一个对象的时候,需要在堆中分配内存空间,但是这个内存空间的生命周期是有限的,这就意味着如果我们使用指针指向这块内存空间,当该内存空间的生命周期到了(引用计数为0),那么我们当前的指针是不是就变成了未定义的行为了。

  • 我们创建的内存空间是有边界的,比如我们创建一个大小为10的数组,这个时候我们通过指针访问到了index = 11的位置,这个时候是不是就越界了,访问了一个未知的内存空间。

  • 指针类型与内存的值类型不一致,也是不安全的。

Swift中其实已经明确告诉开发者指针的不安全性,比如UnsafePointer UnsafeMutableRawPointer等均以Unsafe为前缀。那么作为类型安全的Swift为什么要开放不安全的指针给开发者使用?我们可以用它做什么?

二、指针类型

Swift中的指针分为两类, typed pointer指定数据类型指针,raw pointer未指定数据类型的指针(原始指针)。基本上我们接触到的指针类型有以下几种:

SwiftObject-C说明
UnsafePointerconst T *指针不可变,指向的内容可变
UnsafeMutablePointerT *指针及指向内容均可变
UnsafeRawPointerconst void *指向未知类型的常量指针
UnsafeMutableRawPointervoid *指向未知类型的指针
UnsafeBufferPointer指针不可变,指向的内容可变
UnsafeMutableBufferPointer指针及指向内容均可变
UnsafeRawBufferPointer指向未知类型的常量指针
UnsafeMutableRawBufferPointer指向未知类型的指针

这里需要注意:

  • PointerBufferPointerPointer 表示指向内存中的单个值,如:IntBufferPointer表示指向内存中相同类型的多个值(集合)称为缓冲区指针,如[Int]
  • MutableImmutable:不可变指针,只读。可变指针,可读可写。
1. 原始指针(raw pointer)的使用

我们先添加一段代码:

/// 申请32字节内存,按照8字节对齐
let p = UnsafeMutableRawPointer.allocate(byteCount: 32, alignment: 8)
defer {
  	/// 函数结束时,释放内存(deallocate 与 allocate成对出现)
    p.deallocate()
}

for i in 0..<4 {
  	/// 存数据
    p.storeBytes(of: i, as: Int.self)
}

for i in 0..<4 {
  	/// 读数据
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index\(i), value\(value)")
}

执行结果:

index0, value3
index1, value1152921504606846976
index2, value4437311520
index3, value-1

这里我们发现问题,从内存中读出来的结果好像不是我们期望的。为什么会出现这种情况?原因是在存数据这里,Int类型的数据占8字节,对齐方式也是8字节。申请32字节内存来存4个Int类型的值没有问题,这32字节在内存中是连续的,所以我们应该每8个字节存一个Int类型的值,但存值时我们并没有考虑每个值之间的间距 ,也就是存值的时候并没有做偏移操作。这里p只是这段内存空间的首地址,所以我们在存数据时需要针对p做地址偏移,这里我们对存数据的方式稍作修改:

p.advanced(by: i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)

这里advanced是在做地址偏移,MemoryLayout<Int>.strideInt类型的步长,其实就是8字节。

所以,这行代码也可以写成:

(p + i * 8).storeBytes(of: i, as: Int.self)

此时的执行结果:

index0, value0
index1, value1
index2, value2
index3, value3

很明显,这才是我们想要的结果。原始指针因为不用指定存储的数据类型,所以我们在使用时需要对于存的数据有清晰的认知,数据类型的大小,步长,对齐方式都是我们应该考虑的。那么这些概念是什么意思呢?这里我们可以研究一下,添加如下代码:

struct Person {
    var age: Int
    var gender: Bool
}

/// 类型大小
print(MemoryLayout<Person>.size)
/// 步长
print(MemoryLayout<Person>.stride)
/// 对齐大小
print(MemoryLayout<Person>.alignment)

/// 执行结果
9
16
8

这里alignment就是结构体中最大的元素的内存对齐大小来作为整个结构体的内存对齐大小,在Person中最大的是Int,也就是8。

同样stride也不难理解,虽然当前结构体的大小为9,但它并不是8的倍数,这也就意味着,如果我们要在一段连续的内存中存储下一个数据,比如Person,下一个数据的起始位置应该是16。

这里需要注意一个问题,如果我们调整Person中属性的顺序,比如这样:

struct Person {
    var gender: Bool
    var age: Int
}

print(MemoryLayout<Person>.size)
print(MemoryLayout<Person>.stride)
print(MemoryLayout<Person>.alignment)

/// 执行结果
16
16
8

为什么size会发生变化?我们需要了解一下Swift中结构体的内存布局算法:

  1. 一开始设置size为0,alignment为1
  2. 遍历字段,对于每个字段:
  • 先根据这个字段的alignment来更新size,让这个字段能够对齐
  • size增加这个字段的大小
  • 更新alignment Max(alignment,这个字段的alignment)
  1. 最终拿到总的sizealignment,然后size根据alignment对齐,得到stride

分析案例中的Person

  1. 遇到gender: Bool时,size为1,alignment为1。
  2. 遇到age: Int时,agealignment为8,size先调整为8,然后加上age的大小8,则size为16,更新alignmentMax(1, 8),也就是8。
  3. 得到总size为16,alignment为8,size根据alignment对齐,得到stride为16。

这样才得到最终的执行结果!

2. 类型指针(typed pointer)的使用

这里我们同样添加一段代码:

let p = UnsafeMutablePointer<Int>.allocate(capacity: 4)
defer {
    p.deallocate()
}
print(p)
/// 初始化操作
p.initialize(repeating: 0, count: 4)
defer {
    /// 反初始化,与 initialize 成对出现
    p.deinitialize(count: 4)
}

/// 几种存储数据的方式
p.pointee = 0
p.successor().pointee = 1
(p + 2).pointee = 2
p.advanced(by: 3).pointee = 3

/// Pointer 转成同类型的 BufferPointer
let buffer = UnsafeBufferPointer.init(start: p, count: 4)
for (index, value) in buffer.enumerated() {
    print("index\(index), value\(value)")
}

执行结果:

0x00007fe0c8604670
index0, value0
index1, value1
index2, value2
index3, value3

到这里不难发现,与原生指针相比,类型指针在确定类型之后,每一次赋值不需要像原生指针那样做偏移步长的操作。这里的每次偏移只需要加1,指针会自动偏移一个类型的步长。对比原生指针的赋值,这会方便很多,开发者并不需要知道某个类型的步长。这里我们需要注意的是initialize方法,这里我们可以断点看一下initialize的作用:

image-20220107140822506

2次断点可以看出,initialize将这段内存中的值初始化为0了。没有初始化前,这段内存中是有值的,所以一般在使用前调用initialize方法。需要注意的是如果我们用了initialize,那么在defer中我们需要deinitialize

苹果开发者文档

Return Value

A pointer to a newly allocated region of memory. The memory is allocated, but not initialized.

三、内存访问

Swift中任意类型的值,Swift提供了全局函数直接访问它们的指针或内存中的字节。

1. 访问指针

我们可以通过以下方法,访问任意类型的指针:

withUnsafePointer(to:_:)///只访问,不修改
withUnsafeMutablePointer(to:_:)///可访问,可修改

添加代码:

/// 只访问
let a = 1
withUnsafePointer(to: a) { point in
    print(point.pointee)
}

/// 访问&修改
 var b = 1
 withUnsafeMutablePointer(to: &b) { mPointer in
    mPointer.pointee = 2
 }
 print(b)
 
 /// 执行结果
 1
 2

这里需要注意,在通过指针改变某个属性的值时,该属性必须为变量,调用withUnsafeMutablePointer时需要将该属性标记为inout,变量左侧加上&(其实就是取地址)。

2. 访问字节

我们可以通过以下方法,访问任意类型的指针:

withUnsafeBytes(of:_:)///只访问,不修改
withUnsafeMutableBytes(of:_:)///可访问,可修改

添加代码:

/// 只访问,不修改
var a = UInt32.max
withUnsafeBytes(of: a) { rawBufferPointer in
    for (index, value) in rawBufferPointer.enumerated() {
        print("index:\(index), value:\(value)")
    }
}

// 输出
// index:0, value:255
// index:1, value:255
// index:2, value:255
// index:3, value:255

/// 访问&修改
var b = UInt32.max // 4294967295
withUnsafeMutableBytes(of: &b) { mutableRawBuffer in
    mutableRawBuffer[0] = 0
    mutableRawBuffer[1] = 0
    mutableRawBuffer[2] = 0
    mutableRawBuffer[3] = 0
}

print(b)
// 输出 0

四、内存绑定

Swift 提供了三种不同的 API 来绑定/重新绑定指针:

  • assumingMemoryBound(to:)
  • withMemoryRebound(to: capacity: body:)
  • bindMemory(to: capacity:)
1. assumingMemoryBound(to:)

有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来说明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:)来告诉编译器预期的类型。

我们看一下案例:

image-20220108140341558

这里编译时会报错Cannot convert value of type 'UnsafePointer<(Int, Int)>' to expected argument type 'UnsafePointer<Int>',很明显是类型不匹配的原因。那么我们怎么解决这个问题?此时assumingMemoryBound(to:)的作用就出来了:

image-20220108140836515

这里我们将元组类型的指针p转换成原生指针再绑定成Int类型的指针就可以执行成功。从testPoint方法实现可以看出,取值还是按元组的方式去取(元组是值类型,本质上这块连续的内存空间中存的就是Int类型数据),也就是说这里只是让编译器绕过了类型检查,并没有发生实际类型的转换。

2. withMemoryRebound(to: capacity: body:)

当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来回复制数据,这个时候我们可以使用withMemoryRebound(to: capacity: body:)来临时更改内存绑定类型。

func testPoint(_ p: UnsafePointer<Int8>) {
    print(p)
}

let p = UnsafePointer<UInt8>(bitPattern: 10)
p?.withMemoryRebound(to: Int8.self, capacity: 1) { (p: UnsafePointer<Int8>) in
    testPoint(p)
}

这里我们临时将UInt8类型指针转换成了Int8类型指针,testPoint方法即可正常执行。

3. bindMemory(to: capacity:)
struct HeapObject {
    var metaData: UnsafeRawPointer
    var refCounted1: UInt32
    var refCounted2: UInt32
}

class Person {
    var name = "Tom"
    var age = 3
}

let p = Person()
let objRawPtr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
let objPtr = objRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objPtr.pointee)

这里可以使用bindMemory很方便的将Person的前16个字节绑定到HeapObject

五、利用指针读取MachO中的属性名称

这里为了更方便理解指针的作用,我们利用它来读取MachO文件中的属性名称,当然这里还需要一个工具MachOView来查看MachO文件的具体信息。

1. 获取types内存地址

首先添加一段代码:

import Foundation
import MachO

class Person {
    var name = "Tom"
    var age = 3
}

var size: UInt = 0
/// 获取 __swift5_types section 的地址
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
print(ptr)
/// 执行结果 Optional(0x0000000100003ec0)

通过MachOView查看一下该section的地址:

image-20220108201036552

这和我们代码执行结果匹配,很明显我们取到的地址没错。

2. 获取程序链接的基地址
/// 链接基地址
var linkBaseAddress: UInt64 = 0
/// macho header地址
let headerPtr = _dyld_get_image_header(0)
/// segment_command地址
let segmentCmd = getsegbyname("__LINKEDIT")
/// 虚拟地址 - 文件偏移量 = 链接基地址
if let vmaddr = segmentCmd?.pointee.vmaddr, let fileOff = segmentCmd?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileOff
}
print(linkBaseAddress)

这里需要注意一下getsegbyname("__LINKEDIT")方法,因为我们获取链接基地址可以通过虚拟内存地址减去文件偏移量的方式获取,而它们都在Command信息中,通过MachOView查看__LINKEDIT中可以发现:

image-20220108203319968

我们再进一步查看getsegbyname方法,它的返回值是UnsafePointer<segment_command_64>!,我们再查看segment_command_64

image-20220108203915592

这里可以发现segment_command_64中包含很多信息,当然我们需要虚拟内存地址和文件偏移量也在其中。

3. 获取DataLO内容
/// type偏移量
var offset: UInt64 = 0
if let ptr = ptr {
    let typeAddr = UInt64(bitPattern: Int64(Int(bitPattern: ptr)))
    offset = typeAddr - linkBaseAddress
}
/// 程序运行首地址
let headerPtrInt = UInt64(bitPattern: Int64(Int(bitPattern: headerPtr)))
/// DataLO地址
var dataLoAddress = headerPtrInt + offset
/// DataLO内容
let dataLoContent = UnsafePointer<UInt32>(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
print(dataLoContent)

这里我们先获取type偏移量,然后通过程序运行首地址加上type偏移量得到DataLO地址,然后获取DataLO内容。验证一下:

4. 获取typeDescriptor地址
/// typeDescriptor偏移量
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
/// typeDescriptor地址
let typeDescAddress = typeDescOffset + headerPtrInt
print(typeDescAddress)

这里首先根据DataLO内容加上type偏移量再减去链接基地址,得到的就是typeDescriptor偏移量。然后通过typeDescriptor偏移量加上程序运行首地址得到typeDescriptor的内存地址

5. 获取属性
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
}

/// typeDescriptor地址转TargetClassDescriptor
let classDescriptor = UnsafePointer<TargetClassDescriptor>(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee

if let name = classDescriptor?.name {
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
    print(nameOffset)
    let nameAddress = nameOffset + Int64(headerPtrInt)
    print(nameAddress)
    if let cChar = UnsafePointer<CChar>(bitPattern: Int(nameAddress)) {
        print(String(cString: cChar))
    }
}

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
}

let filedDescriptorRelaticveAddress = typeDescOffset + 16 + headerPtrInt
let fieldDescriptorOffset = UnsafePointer<UInt32>(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
let fieldDescriptor = UnsafePointer<FieldDescriptor>(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

for i in 0 ..< fieldDescriptor!.numFields {
    let stride: UInt64 = UInt64(i * 12)
    let fieldRecordAddress = fieldDescriptorAddress + stride + 16
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + headerPtrInt
    let offset = UnsafePointer<UInt32>(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
    if let cChar = UnsafePointer<CChar>(bitPattern: Int(fieldNameAddress)) {
        print(String(cString: cChar))
    }
}

/// 执行结果
/// Person
/// name
/// age

在找到typeDescriptor的内存地址后,后面打印数据相对要简单一些。我们之前分析过方法的调度,知道typeDescriptor实际类型其实是TargetClassDescriptor。这里我们将typeDescriptor地址转成TargetClassDescriptor类型指针,然后根据其内部属性偏移量的不同获取对应的值打印即可。后面获取FieldDescriptor也是如此。

附:完整代码
import Foundation
import MachO

class Person {
    var name = "Tom"
    var age = 3
}

var size: UInt = 0
/// 获取 __swift5_types section 的地址
var ptr = getsectdata("__TEXT", "__swift5_types", &size)
print(ptr)

/// 链接基地址
var linkBaseAddress: UInt64 = 0
/// macho header地址
let headerPtr = _dyld_get_image_header(0)
/// segment_command地址
let segmentCmd = getsegbyname("__LINKEDIT")
/// 虚拟地址 - 文件偏移量 = 链接基地址
if let vmaddr = segmentCmd?.pointee.vmaddr, let fileOff = segmentCmd?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileOff
}

print(linkBaseAddress)

/// type偏移量
var offset: UInt64 = 0
if let ptr = ptr {
    let typeAddr = UInt64(bitPattern: Int64(Int(bitPattern: ptr)))
    offset = typeAddr - linkBaseAddress
}

/// 程序运行首地址
let headerPtrInt = UInt64(bitPattern: Int64(Int(bitPattern: headerPtr)))
/// DataLO地址
var dataLoAddress = headerPtrInt + offset
/// DataLO内容
let dataLoContent = UnsafePointer<UInt32>(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
print(dataLoContent)

/// typeDescriptor偏移量
let typeDescOffset = UInt64(dataLoContent!) + offset - linkBaseAddress
/// typeDescriptor地址
let typeDescAddress = typeDescOffset + headerPtrInt
print(typeDescAddress)

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
}

/// typeDescriptor地址转TargetClassDescriptor
let classDescriptor = UnsafePointer<TargetClassDescriptor>(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee

if let name = classDescriptor?.name {
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
    print(nameOffset)
    let nameAddress = nameOffset + Int64(headerPtrInt)
    print(nameAddress)
    if let cChar = UnsafePointer<CChar>(bitPattern: Int(nameAddress)) {
        print(String(cString: cChar))
    }
}

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
}

let filedDescriptorRelaticveAddress = typeDescOffset + 16 + headerPtrInt
let fieldDescriptorOffset = UnsafePointer<UInt32>(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
let fieldDescriptor = UnsafePointer<FieldDescriptor>(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

for i in 0 ..< fieldDescriptor!.numFields {
    let stride: UInt64 = UInt64(i * 12)
    let fieldRecordAddress = fieldDescriptorAddress + stride + 16
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + headerPtrInt
    let offset = UnsafePointer<UInt32>(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
    if let cChar = UnsafePointer<CChar>(bitPattern: Int(fieldNameAddress)) {
        print(String(cString: cChar))
    }
}

六、总结

指针是把双刃剑!运用得当,它能帮助我们做很多事情,比如解析MachO文件,参考SwiftDump。反之,它的破坏性也很强,错误的内存操作会直接让程序崩溃!