Swift底层探索4 - 指针

1、不安全性

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

2、基础认知

在使用Swift指针时,内存管理需要手动管理,也就是创建的指针在最后需要调用 deallocate

内存布局方式的三个重要概念:

  • size 实际需要的大小(MemoryLayout<T>.size)
  • stride 步长,连续存储多个元素指针间的间隔,也可以理解为系统最终分配的大小(MemoryLayout<T>.stride)
  • alignment 内存对齐的基数,最终分配的内存空间大小是它的倍数(MemoryLayout<T>.alignment)

3 、类型

Swift中的指针分为两大类, typed pointer 指定数据类型指针, raw pointer 未指定数据类型的指针(原⽣指针)。根据可不可变,内存连续与否等情况可以细分8种:

Swift Objective-C 说明
unsafePointer<T> const T * 指针及所指向的内容都不可变
unsafeMutablePointer<T> T * 指针及其所指向的内存内容均可变
unsafeRawPointer const void * 指针指向的内存区域未定
unsafeMutableRawPointer void * 指针指向的内存区域未定,指向的内存可变
unsafeBufferPointer<T> 指针指向连续的内存空间,指针及所指向的内容都不可变
unsafeMutableBufferPointer< T> 指针指向连续的内存空间,指针及其所指向的内存内容均可变
unsafeRawBufferPointer 指针指向一段连续的内存区域未定
unsafeMutableRawBufferPointer 指针指向一段连续的内存区域未定,指向的内存可变

3.1 原始指针

/*
 案例:
    用原生指针存储 4 个整形的数据,使用UnsafeMutableRawPointer
 步骤:
    1、开辟内存空间
    2、调用storeBytes方法存储当前的整形数值
    3、调用load方法加载当前内存当中
*/

//实际大小
let intSize = MemoryLayout<Int>.size;
//内存对齐基数
let intAlignment = MemoryLayout<Int>.alignment;
//内存对齐后的大小
let intStride = MemoryLayout<Int>.stride;


/*
 allocate(byteCount:,alignment:):创建一个指针
    - byteCount: 需要多少内存空间,即总的字节大小,应该使用内存对齐后的大小
    - alignment: 内存对齐的基数,按照它的倍数对齐
*/
let p = UnsafeMutableRawPointer.allocate(byteCount: intStride * 4, alignment: intAlignment);


/*
 advanced(by:) : 移动指针到下一个存储的位置
    - by: 移动的步长
 storeBytes(of: , as: ) : 存储数据
    - of: 数据值
    - as: 数据类型
*/
for i in 0..<4{
    p.advanced(by: i * intStride).storeBytes(of: i, as: Int.self)
}

/*
 load(fromByteOffset: , as: ) : 存储数据
  - fromByteOffset: 读取数据移动的步长
  - as: 数据类型
*/
for i in 0..<4{
    let value = p.load(fromByteOffset: i * intStride, as: Int.self)
    print("Int at \(i):\(value)")
}

print(p)

//销毁p指向的空间
p.deallocate();

/*
 打印结果:
    Int at 0:0
    Int at 1:1
    Int at 2:2
    Int at 3:3
*/

通过上面的例子,可以发现Int的stride为8,同时在内存上,p的存储为连续的Int的stride(8字节)存放。

  • 8字节打印说明:因为x/8g是16进制打印8段内存,16进制是4位,一个字节占8位,所以两个16进制就可以表示一个字节,打印的内存结构可知是8字节。

3.2 泛型指针

对于泛型指针,其实就是原生指针绑定了具体的类型,因为确定了类型,就固定了存储类型的 stride 和 alignment,我们就不需要通过 load 和 store 的方式存取,而是通过其内置的 pointee变量来存取。

获取的方式有两种:

  • 通过已有变量获取
var age = 18

/*
 通过返回当前的修改来修改age
 withUnsafePointer<T, Result>(to value: T, _ body: (UnsafePointer<T>)
    - value: 初始化的值.
    - body: 可以操作指针的闭包表达式,withUnsafePointer<T>,无法直接在闭包中修改
*/

age = withUnsafePointer(to: age, { ptr in
    return ptr.pointee + 12
})

print("first age = \(age)")

/*
 直接操作pointee变量来修改age的值
 withUnsafeMutablePointer<T, Result>(to value: inout T, _ body: (UnsafeMutablePointer<T>)
    - value: 初始化的值.
    - body: 可以操作指针的闭包表达式,UnsafeMutablePointer<T>直接在闭包中修改
*/
withUnsafeMutablePointer(to: &age) { ptr in
    ptr.pointee += 12
}

print("second age = \(age)")


/*
 打印结果
    first age = 30
    second age = 42
*/
  • 直接分配内存
var age = 18

/*
 allocate(capacity: ) : 分配一块 类型 的内存空间,此时还没有被初始化
    - capacity: 分配多少个 类型 大小的连续空间
 */
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)

//初始化分配内存空间,把age拷贝到内存中
ptr.initialize(to: age)
//直接通过访问pointee属性来访问
ptr.pointee += 12

print(ptr.pointee)

//数据清零
ptr.deinitialize(count: 1)
//销毁内存空间
ptr.deallocate()

/*
 打印结果:
    30
*/

注意:allocate 和 deallocate 、initialize 和 deinitialize 都是成对出现的,如图:

/*
  案例:两种不同的方式初始化泛型指针
*/
struct Person {
    var age: Int
    let name: String
}


let ptr = UnsafeMutablePointer<Person>.allocate(capacity: 2)

//方式一:下标的模式直接访问
ptr[0] = Person(age: 18, name: "张三")
ptr[1] = Person(age: 20, name: "李四")

print(ptr[0])
print(ptr[1])

ptr.deinitialize(count: 2)
ptr.deallocate()


let ptr2 = UnsafeMutablePointer<Person>.allocate(capacity: 4)

//方式二:initialize
ptr2.initialize(to: Person(age: 18, name:"张三"))
//advanced 偏移的方式
ptr2.advanced(by: 1).initialize(to: Person(age: 20, name: "李四"))
//successor 返回下一个连续值的方式(也就是下一个容量的起始地址)
ptr2.successor().successor().initialize(to: Person(age: 22, name: "赵五"))
//直接内存平移的方式(移动一个ptr2 的类型大小)
(ptr2 + 3).initialize(to: Person(age: 24, name: "钱六"))

print(ptr2.advanced(by: 0).pointee)
print(ptr2.advanced(by: 1).pointee)
print(ptr2.advanced(by: 2).pointee)
print(ptr2.advanced(by: 3).pointee)

ptr2.deinitialize(count: 4)
ptr2.deallocate()

/* 
 打印结果
  Person(age: 18, name: "张三")
  Person(age: 20, name: "李四")
  Person(age: 22, name: "赵五")
  Person(age: 24, name: "钱六")
*/
/*
 pt2的内存打印结果:
  x/12g 0x0000000109b06070
  0x109b06070: 0x0000000000000012 0x000089b8e4a0bce5
  0x109b06080: 0xa600000000000000 0x0000000000000014
  0x109b06090: 0x00009b9be58e9de6 0xa600000000000000
  0x109b060a0: 0x0000000000000016 0x000094bae4b5b5e8
  0x109b060b0: 0xa600000000000000 0x0000000000000018
  0x109b060c0: 0x0000ad85e5b192e9 0xa600000000000000
*/

这里有一个需要注意的点:
为什么ptr2.advanced(by: 1)可以?为什么不用 ptr2.advanced(by: MemoryLayout<Person>.stride)?
原因:
关键在于这句代码 --> let ptr2 = UnsafeMutablePointer<Person>.allocate(capacity: 4),此时我们是知道ptr2的具体类型的,就是指向Person的指针。
在确定指针的类型后,通过步长的移动+1,就表示移动了那个类的实例大小空间+1。

3.3 内存指针

直接使⽤指针需要我们去管理内存,这很繁琐,并且很危险。于是,Unmanaged 出现了。 Unmanaged 能够将由 C API 传递过来的指针进⾏托管,我们可以通过Unmanaged标定它是否接受引⽤计数的分配,以便实现类似⾃动释放的效果;同时,如果不是使⽤引⽤计数,也可以使⽤Unmanaged 提供的 release 函数来⼿动释放,这⽐在指针中进⾏这些操作要简单很多

//案例:将对象转换成结构体指针
struct HeapObject{
    var metadata : UnsafeRawPointer
    var refCounted1 : UInt32
    var refCounted2 : UInt32
}

class Person {
    var age: Int = 18
    let name: String = "小明"
}

var p = Person()

//使用Unmanaged拿到当前对象的内存指针
let objRawPtr = Unmanaged.passUnretained(p as AnyObject).toOpaque()

//绑定具体类型
let objPtr = objRawPtr.bindMemory(to: HeapObject.self, capacity: 1)

print(objPtr.pointee)

//打印结果
//HeapObject(metadata: 0x0000000100008228, refCounted1: 3, refCounted2: 0)

3.4 内存绑定

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

  • assumingMemoryBound(to:)
    在代码中我们可能只有原始指针(没有保留指针类型), 但是可以确定指针的类型,可以使用assumingMemoryBound(to:)来告诉编译器预期的类型(注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)
    不同类型直接报错

    绕过检查后
  • bindMemory(to: capacity:)
    用于更改内存绑定的类型,如果当前内存还没有绑定类型,则将首次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。
func testPoint(_ p: UnsafePointer<Int>){
    print(p[0])
    print(p[1])

}
let tuple = (10,20)

withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    //1为一个tupleptr大小的空间容量
    testPoint(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}
  • withMemoryRebound(to: capacity: body:)
    当我们在给外部函数传递参数时,不免会有一些数据类型的差距。如果我们进行类型转换,必然要来回复制数据,这个时候我们就可以使用withMemoryRebound(to: capacity: body:)来临时更改内存绑定类型。当离开当前的作用域就会失效,重新绑定为原始类型。这可以将临时类型指针的访问和其他代码的作用域分开。
func testPoint(_ p: UnsafePointer<Int8>){
    print(p)
}

let uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)

uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
    testPoint(int8Ptr)
})

4、案例 - 获取 Mach-O 文件的类名,属性名称和方法信息

/*
 案例:
    获取 Mach-O 文件的类名,属性和方法信息
 步骤:
    1.读取类的 Description的地址偏移,根据虚拟基地址算出它在Mach-O的位置
    2.然后根据Description的结构来获取 FieldDescriptor在Mach—O的偏移地址,找到FieldDescriptor在Mach—O的位置
    3.通过FieldDescriptor 和其结构找到 FieldRecords数组即可获得属性
    4.根据Description的结构获取 v-table
*/

class Person  {
    let age : Int = 18
    let tall : Double = 1.75
    func getName() {}
}

//类的信息
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 //属性名称
}

//方法结构
struct TargetMethodDescriptor {
    var Flags: UInt32
    var Impl: UInt32
}

var size : UInt = 0;

//获取 __swift5_types 中正确的内存地址
var swift5_types_ptr = getsectdata("__TEXT", "__swift5_types", &size)
//0x0000000100007e24
//print(swift5_types_ptr)

// 获取 Mach-O 文件中 __LINKEDIT 的信息
var segment_command_linkedit = getsegbyname("__LINKEDIT")
// 获取该段的文件内存的虚拟地址
let vmaddr = segment_command_linkedit?.pointee.vmaddr
// 获取该段的文件偏移量
let fileoff = segment_command_linkedit?.pointee.fileoff
// 计算出链接的基地址(也就是虚拟内存的基地址)
var link_base_address = (vmaddr ?? 0) - (fileoff ?? 0)
//十进制4294967296,即16进制0x100000000
//print(link_base_address)

//swift5_types_ptr是虚拟地址,那么此时,需要计算出实际在Mach-O的偏移量
var offset : UInt64 = 0
if let unwrappedPtr = swift5_types_ptr {
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    offset = intRepresentation - link_base_address
    //十进制32292,即16进制的7e24
//    print(offset)
}


//获取当前程序运行基地址,注意,_dyld_get_image_header获取哪个要根据xcode和macos版本,目前发现可能为0,也可能是3,可用lldb命令 - image list 查看
var app_base_address = _dyld_get_image_header(0);

//把 app_base_address 转换成整型,进行计算
let app_base_address_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: app_base_address)))

//获取DataLo的内存地址,即 __swift5_types 中四个字节在程序内存中存放的地址
var data_load_address = app_base_address_int_representation + offset

// 将 data_load_address 转成指针类型
let data_load_address_ptr = withUnsafePointer(to: data_load_address) { $0 }

//获取 dataLo 四字节存放内容
let data_load_content = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: data_load_address) ?? 0)?.pointee

// 获取 Description 在 Mach-O 文件的信息
let description_offset = offset + UInt64(data_load_content!) - link_base_address

// 获取 Description 在内存中的指针地址
let description_address = description_offset + app_base_address_int_representation

// 将 Description 的指针地址指向 TargetClassDescriptor
let class_description = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: description_address) ?? 0)?.pointee

// 第一目标:获取类名
if let name = class_description?.name{
    //name为类名相对于 classDescriptor 在内存中的偏移量(即结构体内的偏移量),前面两个成员变量均32位,占2个四字节,所以为8
    let nameAddress = Int64(name) + Int64(description_address) + 8
    
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
        print(String(cString: cChar))
    }
}

// 因为 fieldDescriptor 在结构体中排第五个, 前面有四个成员变量的大小每个都是32位 = 4 个 4 字节,所以为 16
let fieldDescriptor_address_int_representation = description_address + 16

// 将 fieldDescriptor_address_int_representation 转成指针地址,这里拿到的地址的值为 fieldDescriptor 的偏移信息
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptor_address_int_representation) ?? 0)?.pointee

// fieldDescriptor_address_int_representation + 偏移信息 = fieldDescriptor 的真正的内存地址
let fieldDescriptorAddress = fieldDescriptor_address_int_representation + UInt64(fieldDescriptorOffset!)

//将 fieldDescriptor 内存地址转成 FieldDescriptor
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

//第二目标:获取属性
for i in 0..<fieldDescriptor!.NumFields {
    //根据步长来循环读取
    let a = MemoryLayout<FieldRecord>.stride
    let stride: UInt64 = UInt64(i * UInt32(a))
    
    //获取属性信息地址,FieldRecord 前面五个字段,共16字节
    let fieldRecordAddress = fieldDescriptorAddress + 16 + stride
    
    //FieldName 前面两个字段,共8字节
    let fieldNameRelactiveAddress = fieldRecordAddress + 8
    
    //获取属性名称的偏移量
    let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
    
    //获取属性名称的实际运行地址
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - link_base_address
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
        print(String(cString: cChar))
    }
}

//目标三:获取方法信息
for i in 0..<class_description!.size {
    // VTable offset
    let VTable_offset = Int(description_offset) + MemoryLayout<TargetClassDescriptor>.size + MemoryLayout<TargetMethodDescriptor>.size * Int(i)
    
    // 获取 VTable 的地址
    let VTable_address = Int(app_base_address_int_representation) + VTable_offset
    
    // 将 VTable_address 转成 TargetMethodDescriptor 结构
    let method_descriptor = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: VTable_address) ?? 0)?.pointee
    
    // 拿到方法的函数地址
    let imp_address = VTable_address + 4 + Int((method_descriptor?.Impl ?? 0)) - Int(link_base_address)
    
    // 转成 IMP
    let imp: IMP = IMP(bitPattern: UInt(imp_address))!

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容