前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从Mach-O角度谈谈Swift和OC的存储差异

从Mach-O角度谈谈Swift和OC的存储差异

作者头像
用户6543014
发布2021-01-25 12:06:34
1.5K0
发布2021-01-25 12:06:34
举报
文章被收录于专栏:CU技术社区CU技术社区

导读

本文从二进制的角度初步介绍了Swift与OC的差异性,包括Swift在可执行文件中函数表的存储结构、函数的存储结构等(目前只列出基本结构,泛型等结构描述会陆续补充)。为了方便阅读理解,文末附有Demo地址。OC版本的二进制解析工具已经开源,针对Swift的二进制解析工具目前正在开发中,近期即将发布,敬请关注WBBlades~

背景

经过数年的更新,Swift的ABI终于稳定了。由此引来的就是各大厂对Swift引入的争相尝试。为此58同城APP在集团内发起了引入Swift语言的协同项目—混天项目。混天项目从混编架构、工具链、基础组件、UI组件等多方面着手,旨在提高Swift引入后的开发效率。本文是混天项目工具链组阶段性研究成果。

动态调用

在正文开始之前,我们先来看个与主题无关的例子。

代码语言:javascript
复制
class MyClass {
    var p:Int = 0
    init() {
        print("init")
    }
    func helloSwift() -> Int {
        print("helloSwift")
        return 100
    }
    func helloSwift1() -> Int {
        print("helloSwift1")
        return 100
    }
    func helloSwift2() -> Int {
        print("helloSwift2")
        return 100
    }
}

在运行时,我们能否动态调用上面这个类的函数呢?如果换成OC语言,我相信绝大多数iOSer 都知道如何动态调用。 以下面代码为例:

代码语言:javascript
复制
/**
假设MyClass由OC实现
*/
@interface MyClass : NSObject

@property(nonatomic,assign)int p;

@end

@implementation MyClass

- (instancetype)init{
    if (self = [super init]) {
        NSLog(@"init");
    }
    return self;
}
- (int)helloSwift{
    NSLog(@"helloSwift");
    return  100;
}
- (int)helloSwift1{
    NSLog(@"helloSwift1");
    return  100;
}
- (int)helloSwift2{
    NSLog(@"helloSwift2");
    return  100;
}

@end
代码语言:javascript
复制
/**
那么通过runtime可以获取到任意的方法IMP
*/ 
 Class class = NSClassFromString(@"MyClass");
 unsigned int count = 0;
 Method *list = class_copyMethodList(class,&count);
 for (int i = 0; i < count; i++) {
      Method method = list[i];
      NSLog(@"- [%@ %@]",class,NSStringFromSelector(method_getName(method)));
 }
 NSLog(@"%@ count = %u",class,count);

 //模拟通过IMP调用更直观
 Method method = class_getInstanceMethod(class, @selector(helloSwift));
 IMP imp = method_getImplementation(method);
 imp();
代码语言:javascript
复制
打印结果如下
2020-11-19 17:16:08.885763+0800 SwiftToolDemo[45037:17709798] - [MyClass init]
2020-11-19 17:16:08.886219+0800 SwiftToolDemo[45037:17709798] - [MyClass helloSwift]
2020-11-19 17:16:08.886389+0800 SwiftToolDemo[45037:17709798] - [MyClass helloSwift1]
2020-11-19 17:16:08.886537+0800 SwiftToolDemo[45037:17709798] - [MyClass helloSwift2]
2020-11-19 17:16:08.886680+0800 SwiftToolDemo[45037:17709798] - [MyClass p]
2020-11-19 17:16:08.886823+0800 SwiftToolDemo[45037:17709798] - [MyClass setP:]
2020-11-19 17:16:08.886932+0800 SwiftToolDemo[45037:17709798] MyClass count = 6
2020-11-19 17:16:08.887166+0800 SwiftToolDemo[45037:17709798] helloSwift

但是换成Swift,可能会难倒一部分同学。首先我们来观察下,MyClass没有继承自任何类,它是一个纯Swift类。我们通过runtime获取到类,但是无法获取到相关的函数信息。

代码语言:javascript
复制
 Class class = NSClassFromString(@"SwiftDynamicRun.MyClass");
  unsigned int count = 0;
  Method *list = class_copyMethodList(class,&count);
  for (int i = 0; i < count; i++) {
       Method method = list[i];
       NSLog(@"- [%@ %@]",class,NSStringFromSelector(method_getName(method)));
   }

打印结果如下:
2020-11-11 16:08:30.714057+0800 SwiftDynamic[71869:13232511] SwiftDynamic.MyClass count = 0

OC的存储

为什么OC能够在运行时找到类和方法呢?归根到底还是由于Mach-O文件存储了类和函数的信息。在Mach-O中,所有的类都存储到__objc_classlist这个section中。

通过 __objc_classlist中的地址,我们能找到每个类的详细信息。本文以arm64架构为例,在找到0x11820文件偏移后,我们很容易通过结构体结构套取到类的信息。

代码语言:javascript
复制
struct class64
{
    unsigned long long isa;
    unsigned long long superClass;
    unsigned long long cache;
    unsigned long long vtable;
    unsigned long long data;
};

在本文中,可能有同学对地址和偏移的换算存在困惑。例如8字节中存储的是0x1000011820,为什么我们要去寻找0x11820的文件偏移。在Mach-O需要先判断0x1000011820位于哪个segment中,在Load Commands里会记录每个segment的起始虚拟地址及size。

代码语言:javascript
复制
if (address >= segmentCommand.vmaddr && address <= segmentCommand.vmaddr + segmentCommand.vmsize) {
       return address - (segmentCommand.vmaddr - segmentCommand.fileoff);
 }

在本文中,为了不影响阅读,可以将虚拟地址 - 0x100000000当做文件偏移。 因此class64结构体的isa就位于0x11820的连续8字节。data就位于0x11820随后的第5个8字节。

上文中struct class64 中的data指向了class64Info结构体的地址。根据class64Info结构体我们很容易能找到类名和类的实例方法列表。并且通过方法列表的IMP找到每个函数的起始地址。

上文的简单演示下OC类信息的遍历过程,即如何找到每个类的每个方法及首条指令地址。除了实例方法外还有类方法、分类中的方法等等,详细的过程和代码可以参考58开源的WBBlades(https://github.com/wuba/WBBlades),代码中有详细的过程,在此不再赘述。

Swift

不论是OC类还是Swift类,都会被存储到__objc_classlist中。Swift类完整的保留了OC的存储结构。也就是说上文中的MyClass也是按照OC的查找方式也是能找到对应的结构的。

虽然Swift完整保留了struct class64和struct class64Info的数据结构,但是MyClass并没有将方法列表保存到struct class64Info中。那么在这里就会有2个问题

  • 为什么Swift类要保留OC的类结构?
  • MyClass的方法存在哪里?

Swift类要保留OC的类结构是为了兼容OC,部分Swift类继承自OC,并且需要向OC暴露接口,不可避免地需要借用OC的消息转发机制。

那么MyClass的方法存储在哪里呢?参考Swift5.0的Runtime机制浅析的总结(https://www.jianshu.com/p/158574ab8809),可能一部分方法在编译优化时被内联化。假设先不考虑内联这种场景,如何找到每个MyClass的函数表呢? Swift除了兼容了OC的存储结构外,还具备自己的存储结构,通过MachOView能看到Mach-O文件中存储了很多以swift5命名的section(以swift5示例)。

这些section中,__swift5_types中存储的是Class、Struct、Enum的地址。具体每个section存储Swift的哪些数据,在Swift metadata(https://knight.sc/reverse%20engineering/2019/07/17/swift-metadata.html)一文中有较为详细的描述。 如果此时你打开MachOView,查看__swift5_types的二进制数据后你会发现它与OC的存储有很大的不同。在OC中,存储地址通常都是8字节的直接存储对应的地址。但是types不是8字节地址,而是4字节,并且所存储的数据明显不是直接地址,而是相对地址。那么如何得出MyClass的地址呢?当前文件偏移 + 随后4字节中存储的value即可得到地址。

为什么Swift要采用这种方式来存储数据呢?猜测是为了节省包大小,按照OC的存储习惯存储一个地址需要8字节,而在这里4字节就够了。 经过计算后可发现,MyClass的偏移位于__TEXT,__const中。无论是按 Scott Knight(https://knight.sc/)整理好的结构:

代码语言:javascript
复制
type ClassDescriptor struct {
    Flags                       uint32
    Parent                      int32
    Name                        int32
    AccessFunction              int32
    FieldDescriptor             int32
    SuperclassType              int32
    MetadataNegativeSizeInWords uint32
    MetadataPositiveSizeInWords uint32
    NumImmediateMembers         uint32
    NumFields                   uint32
}

还是按HandyJSON(https://github.com/alibaba/HandyJSON)整理的结构:

代码语言:javascript
复制
struct _ClassContextDescriptor: _ContextDescriptorProtocol {
    var flags: Int32
    var parent: Int32
    var mangledNameOffset: Int32
    var fieldTypesAccessor: Int32
    var reflectionFieldDescriptor: Int32
    var superClsRef: Int32
    var metadataNegativeSizeInWords: Int32
    var metadataPositiveSizeInWords: Int32
    var numImmediateMembers: Int32
    var numberOfFields: Int32
    var fieldOffsetVector: Int32
}

都无法得知Swift函数表的存储位置。不过从2者之间的差异可以推测,ClassContextDescriptor的结构可能双方都没有罗列完全。之所以会这样猜想,是因为在通过MachOView查看二进制时,因为好奇计算了下MyClass的ClassDescriptor后续几个字节的地址,发现确实是指向了汇编代码。

那到底是不是ClassDescriptor这个结构体还有其他的内容呢?这个只能从源码中寻找答案了。 首先查看了 ClassContextDescriptorBuilder 的layout方法,这里似乎能看到我们想要的信息——VTable。

代码语言:javascript
复制
class ClassContextDescriptorBuilder
//重写了addLayoutInfo
void layout() {
      super::layout();
      addVTable();
      addOverrideTable();
      addObjCResilientClassStubInfo();
      maybeAddCanonicalMetadataPrespecializations();
    }
}

‍ClassContextDescriptorBuilder的结构并不是在一个类中完全确定的,而是通过继承关系逐渐添加丰富完成的。

例如,最开始的2个4字节内容是由基类ContextDescriptorBuilderBase确定的,而TypeContextDescriptorBuilderBase又不断的向自己的结构中丰富完善信息。最后,ClassContextDescriptorBuilder除了通过重写layout()方法和addLayoutInfo()方法外,还在此基础上添加了Vtable(其他信息暂不关注)。

代码语言:javascript
复制
ClassContextDescriptorBuilder //重写父类addLayoutInfo方法,从而添加SuperclassType 、MetadataNegativeSizeInWords、MetadataPositiveSizeInWords、NumImmediateMembers 、NumFields、FieldOffsetVectorOffset、VTable、OverrideTable等
               ^
               |
TypeContextDescriptorBuilderBase // 添加Name、AccessFunction、FieldDescriptor、NumFields、FieldOffsetVectorOffset
               ^
               |
ContextDescriptorBuilderBase  //添加Flag 、Parent
代码语言:javascript
复制
class TypeContextDescriptorBuilderBase
void layout() {
      asImpl().computeIdentity();

      super::layout();
      asImpl().addName();
      asImpl().addAccessFunction();
      asImpl().addReflectionFieldDescriptor();
      asImpl().addLayoutInfo();
      asImpl().addGenericSignature();
      asImpl().maybeAddResilientSuperclass();
      asImpl().maybeAddMetadataInitialization();
    }

     void addLayoutInfo() {
      auto properties = getType()->getStoredProperties();

      // uint32_t NumFields;
      B.addInt32(properties.size());

      // uint32_t FieldOffsetVectorOffset;
      B.addInt32(FieldVectorOffset / IGM.getPointerSize());
    }
}
代码语言:javascript
复制
 class ContextDescriptorBuilderBase {
  void layout() {
      asImpl().addFlags();
      asImpl().addParent();
    }
}

那么Vtable是如何添加的呢?按Mach-O的存储习惯,大概率是先约定单个函数的存储长度,再告诉我们函数个数;

代码语言:javascript
复制
 void addVTable() {
       ...     
      B.addInt32(VTableEntries.size()); 
      for (auto fn : VTableEntries)
        emitMethodDescriptor(fn);
  }

在addVTable函数中可以看出,在依次存储函数前,先通过4字节存储函数表的大小。

从上文中的代码描述来看,在某些情况下是不存在VTable的,那么怎么才能知道是否存在VTable呢?如果不存在VTable的情况下,按照存在VTable的结构去解析,会造成错乱。 按照Mach-O的习惯,一般Kind、Flag这样的字节都会有一定的标示性,能够通过一个或几个字节告诉我们后续内容的类别情况。 经过整理,Flag的详细说明如下:

代码语言:javascript
复制
 -------------------------------------------------------------------------------------------------
 |  TypeFlag(16bit)  |  version(8bit) | generic(1bit) | unique(1bit) | unknown (1bit) | Kind(5bit) |
 -------------------------------------------------------------------------------------------------

先来看2个枚举:

代码语言:javascript
复制
// Kinds of context descriptor.
enum class ContextDescriptorKind : uint8_t {
/// This context descriptor represents a module.
Module = 0,

/// This context descriptor represents an extension.
Extension = 1,

/// This context descriptor represents an anonymous possibly-generic context
/// such as a function body.
Anonymous = 2,

/// This context descriptor represents a protocol context.
Protocol = 3,

/// This context descriptor represents an opaque type alias.
OpaqueType = 4,

/// First kind that represents a type of any sort.
Type_First = 16,

/// This context descriptor represents a class.
Class = Type_First,

/// This context descriptor represents a struct.
Struct = Type_First + 1,

/// This context descriptor represents an enum.
Enum = Type_First + 2,

/// Last kind that represents a type of any sort.
Type_Last = 31,
};sVTable = 15,  };
代码语言:javascript
复制
/// Flags for nominal type context descriptors. These values are used as the
/// kindSpecificFlags of the ContextDescriptorFlags for the type.
class TypeContextDescriptorFlags : public FlagSet<uint16_t> {
  enum {
    // All of these values are bit offsets or widths.
    // Generic flags build upwards from 0.
    // Type-specific flags build downwards from 15.

    /// Whether there's something unusual about how the metadata is
    /// initialized.
    ///
    /// Meaningful for all type-descriptor kinds.
    MetadataInitialization = 0,
    MetadataInitialization_width = 2,

    /// Set if the type has extended import information.
    ///
    /// If true, a sequence of strings follow the null terminator in the
    /// descriptor, terminated by an empty string (i.e. by two null
    /// terminators in a row).  See TypeImportInfo for the details of
    /// these strings and the order in which they appear.
    ///
    /// Meaningful for all type-descriptor kinds.
    HasImportInfo = 2,

    /// Set if the type descriptor has a pointer to a list of canonical
    /// prespecializations.
    HasCanonicalMetadataPrespecializations = 3,

    // Type-specific flags:

    /// The kind of reference that this class makes to its resilient superclass
    /// descriptor.  A TypeReferenceKind.
    ///
    /// Only meaningful for class descriptors.
    Class_ResilientSuperclassReferenceKind = 9,
    Class_ResilientSuperclassReferenceKind_width = 3,

    /// Whether the immediate class members in this metadata are allocated
    /// at negative offsets.  For now, we don't use this.
    Class_AreImmediateMembersNegative = 12,

    /// Set if the context descriptor is for a class with resilient ancestry.
    ///
    /// Only meaningful for class descriptors.
    Class_HasResilientSuperclass = 13,

    /// Set if the context descriptor includes metadata for dynamically
    /// installing method overrides at metadata instantiation time.
    Class_HasOverrideTable = 14,

    /// Set if the context descriptor includes metadata for dynamically
    /// constructing a class's vtables at metadata instantiation time.
    ///
    /// Only meaningful for class descriptors.
    Class_HasVTable = 15,
  };

Flag比较有用的低5位和高16位。低5位可以代表32类型,中间位用来表示version、是否唯一、泛型等,暂不关心。其中:

  • 低5位标识当前描述的类型,是Class | Struct | Enum | Protocol等等。
  • 高16位用于标识是否有Class_HasVTable | Class_HasOverrideTable | Class_HasResilientSuperclass 等等。

以MyClass的Falg = 0x80000050为例。低5位为0x50 = 1 0 0 0 0 。其十进制为16,在ContextDescriptorKind中,16标识Class。高16位为0x8000 = 1 0 0 0 0 0 0 0 0 0 0 0 0 0, 在TypeContextDescriptorFlags中,第16位为1标识Class_HasVTable。因此0x80000050的意思为具有VTable的类。

如何实现动态调用

感兴趣的可以下载Demo( https://github.com/pilaf-king/SwiftMachODemo ),在运行时大家可能会有疑问,为什么输出的函数数量与实际写的函数不一致。因为除了自己写的函数外,还有额外自动生成的函数,也被加入到VTable中。

代码语言:javascript
复制
函数的Flag解释如下,感兴趣的可以关注下
/**
 ------------------------------------------------------------------------------------
 |  ExtraDiscriminator(16bit) | .. | isDynamic(1bit) | isInstance(1bit) | Kind(4bit) |
 ------------------------------------------------------------------------------------

enum class Kind {
    Method,
    Init,
    Getter,
    Setter,
    ModifyCoroutine,
    ReadCoroutine,
  };
 */

另外,overrideTable在Demo中没有实现,但是结构和存储位置在代码做了注释标记,感兴趣的可以自己解析下。

代码语言:javascript
复制
//OverrideTable结构如下,紧随VTable后4字节为OverrideTable数量,再其后为此结构数组
struct SwiftOverrideMethod {
    struct SwiftClassType *OverrideClass;
    struct SwiftMethod *OverrideMethod;
    struct SwiftMethod *Method;
};

总结

本文从动态调用开始引入思考,逐渐探索Swift的二进制存储。Swift的函数存储具有很大的局限性,例如:我们只能知道函数的类型及Index,通过Index和类型确定哪个函数,一旦函数发生变化那么VTable的位置就发生了变化。本文并不是推广动态调用,仅仅是从动态调用这个场景将大家吸引到Mach-O的解析过程中。Swift作为一门很先进的语言,有太多的特性值得我们去探索。笔者也只是刚接触Swift,难免带着OC的思维去揣摩和探索Swift,如有疏漏之处,敬请指正。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 SACC开源架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档