美文网首页
WWDC22-Link fast: Improve build

WWDC22-Link fast: Improve build

作者: 凌云壮志几多愁 | 来源:发表于2022-07-25 15:58 被阅读0次

    一、背景

    首先要知道链接是干什么的。我们可以通过IDE写一部分代码,也需要从其他的Libray或者FrameWork使用第三方提供的代码。为了能够用到这些三方代码,我们需要一个链接器将代码结合起来。关于链接大致上可以分为两种类型:

    • 静态链接:发生在我们构建我们应用程序的时候,也就是说这种链接方式会影响到我们构建程序的时长、以及构建出来的应用程序的大小;
    • 动态链接:发生在我们应用程序启动/运行的时候,它会影响我们应用程序的启动时长;

    这篇Session里面会同时涉及到它们两种,包含的议题如下:

    • 什么静态链接;
    • ld64最近改进的地方;
    • 静态链接最佳实践;
    • 什么是动态链接;
    • 最近关于dyld的改进;
    • 动态链接最佳实践;
    • 最后会介绍一些新的工具;

    在这篇文章中除了会介绍该Session的内容,还会穿插一些我个人对相应知识的理解。

    二、静态链接

    对于只有一个源文件的程序,构建他对于我们来说是很简单的。比如我们有个简单的程序:

    #include <unistd.h>
    int main(int argc, const char *argv[]) {
        char buf[] = "Link fast: Improve build and launch times\n";
        write(STDOUT_FILENO, buf, sizeof(buf));
        return 0;
    }
    

    然后通过clang进行编译构建即可。
    但是如果有多个源文件需要构建呢?当然我们也使用clang去编译这些源文件。但是问题来了,难道我们每次都要全量重新构建所有的源文件吗?为了避免这个问题,我们可以将这些源文件拆分成多个不同部分。这样他们之间相互影响的可能性就降低了:

    /// main.m
    #include "pfb_out.h"
    
    int main(int argc, const char *argv[]) {
        char buf[] = "Link fast: Improve build and launch times\n";
        pfb_std_out(buf, sizeof(buf));
        return 0;
    }
    
    /// pfb_out.c
    #include <unistd.h>
    
    void pfb_std_out(const void * __buf, int len) {
        write(STDOUT_FILENO, __buf, len);
    }
    

    针对多个源文件的情况下,我们就不是一次性将源文件构建为一个可执行的应用程序,而是先将其编译成“可重定位”的目标文件。我们用如下指令来演示上面的过程(如果不清楚ld到底需要哪些参数,我们可以直接clang编译一个完成的程序并加上-v参数,它会详细地展示中途发生了任何事件):

    $ cc -c prog.c -o prog.o
    $ cc -c pfb_out.c -o pfb_out.o
    // syslibroot:指定搜索的library和framework,因为这里我使用了write函数。它需要由系统库提供支持
    // lSystem:寻找libSystem
    $ ld prog.o pfb_out.o -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -o prog -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin/libclang_rt.osx.a
    

    既然都分了模块,那我们就可以使用“ar”指令来构造一个静态库:

    $ ar -rc libPfbout.a pfb_out.o /// 目标文件打包为library
    //$ ar -x libPfbout.a /// library拆分为目标文件
    
    
    // -Ldir 指定要查找library的路径
    // -lPfbout 要链接libPfbout.a
    $ ld prog.o  -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -o prog -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin/libclang_rt.osx.a -L/path/to/libarary/dir -lPfbout
    

    使用这种静态库的方式极大地改善了代码地共享。

    重定位

    用上面这种方式生成的最终产物有可能是十分庞大的,这是因为它可能会包含从Library中拷贝出来成千上万的函数(即便是说我们明确引用并没有多少函数)。所以针对这个问题Apple提供了一个比较巧妙的优化方式,相较于使用静态库中所有的.o(object 目标)文件,我们在构建的时候可以只获取其中的一部分。

    还是以刚刚的例子我们改动一下,他们之间的依赖关系如下:prog.c依赖pfb_string.c(调用了pfb_string里面的newString、str_description、str_free函数),pfb_string依赖libpfbio.a(调用了pfb_std_out)。其中libpfbio.a的pfb_std_in函数并未调用,pfb_string.c中的pfb_str_len也未被调用。



    最终我们使用ld指令生成最后的产物:

    $ ld prog.o pfb_string.o -lpfbio -L/Users/wangwang/WorkStation/iOS/exec/Clang_basic/Clang_basic -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin/libclang_rt.osx.a -o prog
    

    通过对比之后发现原来prog.o中callq指令后面的数字被替换为__text端里面真正的地址了,并且libpfbio.a里面包含的pfb_in.o相关的内容并未在可执行文件的__text中看见其踪影,但是我们在pfb_string.o里面未使用的函数pfb_str_len却出现在了最终产物的__text里面。
    那这个过程是如何执行的呢?这是因为在每个目标文件中存在一个Relocations来保留其所需要重定位的信息,它描述了如何修改相应段里面的内容:

    序号 address pcrel length extern type scattered symbolnum/value
    1 0x00000034 1 2 1 2 0 3
    2 0x0000002b 1 2 1 2 0 2
    3 0x0000001e 1 2 1 2 0 1
    4 0x00000019 1 2 0 1 0 2

    这里需要对重定位表的内容解析一下,重定位的结构定义在relo.h 中。其中extern字段为1则表明symbolnum的值为符号表序号,比如第一个symbolmum为3找到符号表(下表),表示该符号名称为_str_descirption;
    如果extern字段为0则表示section的序号,比如第4项它的symbolnum值表示的是目标文件中的Section64(__TEXT, __cstring)。

    序号 Type SEC_INDEX StringTable Symbol Name
    1 0x0f 01 0x00000012 _main
    2 0x01 00 0x00000018 _newString
    3 0x01 00 0x00000001 _str_description
    4 0x01 00 0x00000023 _str_free

    而重定位表中的address字段表示的是当前要被重定位Section的偏移量。比如prog.o的第一个address值为0x34,通过otool查看prog.o的(__TEXT, __text)原始值:

    ➜ ✗ otool -t prog.o
    prog.o:
    (__TEXT,__text) section
    0000000000000000 55 48 89 e5 48 83 ec 20 c7 45 fc 00 00 00 00 89 
    0000000000000010 7d f8 48 89 75 f0 48 8d 3d 23 00 00 00 e8 00 00 
    0000000000000020 00 00 48 89 45 e8 48 8b 7d e8 e8 00 00 00 00 48 
    0000000000000030 8b 7d e8 e8 00 00 00 00 31 c0 48 83 c4 20 5d c3 
    

    可以看到其记录的值均为e8后面的00(这里e8是操作码,对应call近地址相对位移指令:该指令后面所跟的值是指的与其下一个指令的偏移地址),由于在当前目标文件中,并不能知道真实的函数地址。因此这里是用00来进行代替。针对这个现象简单描述一下最基础的链接过程:

    ld首先加载prog.o查看其符号表:

    ➜  ✗ nm -ax prog.o
    //
    Type SEC_INDEX     StringTable Offset     Symbol Name
    0f      01         00000012               _main
    01      00         00000018               _newString
    01      00         00000001               _str_description
    01      00         00000023               _str_free
    /*
    前面两个用于高4位
    #define N_STAB  0xe0
    #define N_PEXT  0x10
    后面两个用于低4位
    #define N_TYPE  0x0e
    #define N_UNDF  0x0 
    ========================================================================================================
    子Type
    #define N_ABS   0x2 
    #define N_SECT  0xe 
    #define N_PBUD  0xc
    #define N_INDR  0xa
    #define N_EXT   0x01
    */
    

    首先Type上面4个位进行操作。如果其是和N_TYPE进行按位与操作,那将得到的结果还需要和其5个子type进行对比;
    而另外三个只需要和自身相比是否为0即可。比如:

    0f & 0e(N_TYPE) = 0e ==> N_SECT
    0f & 01(N_EXT) = 01 ==> N_EXT
    
    01 &  0e(N_TYPE) = 00 ==> N_UNDF
    01 & 01(N_EXT) = 01 ==> N_EXT
    

    通过分析Type为01的_newString、_str_description、_str_free为未定义的符号。因此需要加载pfb_string.o来处理上一步中出现的三个未定义的符号。同样也引入了新的未定义的符号:

    ➜ ✗ nm -ax pfb_string.o 
    
    0000000000000000 01 00 0000 00000001 _pfb_std_out
    
    0000000000000000 01 00 0000 00000043 _free
    0000000000000000 01 00 0000 00000049 _malloc
    
    0000000000000000 0f 01 0000 0000002c _growth
    00000000000000e0 0f 01 0000 00000034 _newString
    00000000000001f0 0f 01 0000 0000001f _pfb_str_len
    0000000000000190 0f 01 0000 0000000e _str_description
    00000000000001c0 0f 01 0000 0000003f _str_free
    

    在libpfbio.a中发现了一个我们需要的的未定义的符号:

    ➜ ✗ nm -ax libpfbio.a 
    
    libpfbio.a(pfb_in.o):
    0000000000000000 0f 01 0000 00000001 _pfb_std_in
    0000000000000000 01 00 0000 0000000d _read
    
    libpfbio.a(pfb_out.o):
    0000000000000000 0f 01 0000 00000001 _pfb_std_out
    0000000000000000 01 00 0000 0000000e _write
    

    最后我们来看一下最终的可执行程序中符号表的情况(只剩下未定义的符号)上述各个目标文件中出现的未定义的符号,大部分的值都被重新进行了设置,除了几个在下文将会提及的动态链接的符号。

    我们再来对看看链接后的变化:



    静态库中我们不需要的符号(比如这里的pfb_std_in)并未拷贝到最终的产物中,源文件中的未使用的函数pfb_str_len依然被拷贝到了最终的产物里面。

    ld64改进的地方

    我们现在所用的ld实际上都是ld64,Apple说他们在这一年针对ld64进行了改进。按照Apple的说法,对于大部分的工程来说链接速度快了2倍。其能够更好利用我们机器的核心,也就是说大部分的时候可以用多核并行执行链接操作,其中包括有:

    • 从输入文件中拷贝相关内容到输出文件;
    • 并行构建LINKEDIT;
    • 并行hash计算;
    • 优化export-trie构建算法(对于使用C++ string_view来表示每个符号的字符串效果十分明显);
    • 针对二进制文件uuid计算,使用基于硬件加速的加密库(SHA256);

    静态链接最佳实践

    在提升链接器性能时,有些应用程序中的配置问题会影响链接时间。下面我们就来看看哪些事情是我们能进行优化的。
    比如我们将源文件构建到已有静态库的时候,不仅仅是源文件需要编译,而且该静态库也需要重新编译。这是由于文件重新编译后,整个静态库包括相关的table都需要重新构建, 这就引入了很多额外的 I/O。因此静态库只对于相对稳定的代码有意义:

    如果是需要频繁变更的代码,最好是将其从静态库从移除!

    而当我们在构建静态库的时候,有三个不为人熟知选项可以优化链接耗时:

    • 1、-all_load和-force_load;
    • 2、-no_export_symbols;
    • 3、-no_deduplicate;

    这几个选项分别都有什么用处呢?在前面我们提到了可以从静态库中选择部分内容来进行加载,这样做的一个坏处就是会降低链接速度。这是因为要满足这个特性的话,链接器就必须要遵守传统的链接规则以串行的方式处理各个静态库,这意味着说我们不能使用基于ld64的并行化能力。反之我们就可以链接器的相关选项来加速我们的构建工作。
    这时候我们就可以使用all_load选项,它告诉链接器从所有的静态库中加载所有的目标文件。如果我们最后的程序是需要这里面大部分的内容的话,这个选项会有很大的收益,这是因为all_load是让链接器并行地去解析相关内容。

    不过使用这个选项也有坏处:

    • 当我们的应用程序使用某种手段让多个静态库使用了相同的符号,并且是强依赖ld指令中输入的静态库顺序的时候。那我们就没有办法使用all_load选项了;
    • 使用了all_load之后会使得我们的应用程序包体积变大,这是因为很多无用的代码也会被添加到程序中;
      为了弥补这个缺点,我们可以使用 -dead_strip(死代码消除) 选项,该选项会让链接器去移除那些无用的代码和数据。Apple说现在dead strip的算法很很快,足以弥补我们为了优化链接耗时使用all_load而带来包体积的劣化。不过Apple还是建议我们可以用-all_load/-dead_strip和不使用all_load做一下收益对比(不同应用程序有不同的表现)。

    这第二个选项就是 no_export_symbols。这里先插入一点背景知识,由链接器生成的LINKEDIT段包含了export-trie(它是一个前缀树,对所有生成的符号名称、地址和标志进行编码):

    虽然说所有dylib都需要导出符号(export Symbols),但程序二进制文件通常不需要任何导出符号。通常是不需要在程序的非主二进制文件(这里“主”的意思是相对其他静态库而言的,比如包含了main函数的二进制文件)进行符号查找的。这样的话,我们可以对相应的程序使用 -no_exported_symbols 以节省在 LINKEDIT 中创建 trie 数据结构的耗时。

    当然这也是有坏处的:

    • 如果我们主程序要加载可以链接到主线程的插件时,不导出相关符号是没有办法;
    • 如果我们将xctest做为程序的主环境(host environment)来加载xctest bundles时,不导出相关符号也是无法实现的;

    对于这个选项,Apple的建议是只有当export-trie很大时才有必要使用这个选项。我们可以使用dyld_info指令来检查我们程序导出符号的数量:

    ➜ ✗ dyld_info -exports prog
    prog [x86_64]:
        -exports:
            offset      symbol
            0x00000000  __mh_execute_header
            0x00003CC0  _growth
            0x00003DA0  _newString
            0x00003E50  _str_description
            0x00003E80  _str_free
            0x00003EB0  _pfb_str_len
            0x00003ED0  _pfb_std_out
            0x00003F00  _main
    ➜ ✗ dyld_info -exports prog | wc -l
          11
    

    显然我们这个demo的导出符号的数量太少,使用这个选项并没有多大的收益,但对于存在很大导出符号的程序而言,Apple给出的数据是链接器需要花费2到3秒的时间来构建export-trie。

    第三个是 no_deduplicate 选项。Apple介绍在几年前他们为链接器新添加了一个用于合并具有相同指令但是名称不一样函数的pass,这么做代价是比较大的,因为链接器需要对每个函数的指令递归地进行hash操作,以便于能够找到重复的内容。因此Apple限制其只在弱定义的(weak-def)符号上使用该算法。

    这里的弱符号简单延伸一下,强弱符号的定义和引用都是针对于链接器而言的。默认我们在定义一个符号时给了其一个初值,它默认是强符号;反之如果只是简单定义了符号由编译器给一个缺省值,这种就默认是弱符号(发生符号冲突之后,弱符号会被强符号覆盖)。当然我们也可以明确指定弱符号,使用attribute((weak)):

    int pfb_strong_symbol = 1; /// 强符号
    int pfb_weak_symbol; /// 弱符号
    __attribute__((weak)) int pfb_symbol_value = 10;
    void pfb_symbol_function(int a) __attribute((weak));
    

    相对于定义而言,引用也有强弱之分。我们可以使用attribute((weakref))来定义:

    __attribute__ ((weakref)) void foo();
    

    deduplicate主要是用于体积优化。对于debug是为了快速构建而不是包体积,所以在默认情况下Xcode是通过传递no_deduplicate来取消去重能力,如果设置-O0的也是关闭去重能力的。
    这些选项我们在Xcode里面都可以进行设置:

    当我们在使用静态库的时候会出现一些意想不到的事情。比如当我们使用静态库链接到我们程序中时某些代码可能并不会出现在最终的产物里面。比如我们对某些函数添加了attribute((unused)),或者是使用了Objective-C的category,由于链接器会选择性地加载了静态库中用到的符号,而没有用到的将不会出现在产物里面;

    另一个比较神奇的现象是静态库和dead_strip的搭配,dead_strip可以隐藏很多静态库的问题。正常来说缺少符号和重复符号都会使得链接器报错,而当配置了dead_strip之后链接器会从main函数开始对所有的指令和数据进行可达性分析,如果发现丢失/重复的符号来自无法访问的代码,链接器将不会抛出符号缺失/重复的错误信息。

    最神奇的现象是当一个静态库被合并到多个framework中的时候,他们单独运行没有问题。但当他们被同时放到同一个程序中时,由于多个定义而遇到奇怪的运行时问题。

    总的来说静态库很强大,但这取决于我们对其有充分认识以避免会出现各种各样奇怪问题。

    三、动态链接

    由于应用程序可能会引入越来越多的代码或者静态库,这就导致我们的程序包体积会越来越大,这就是为什么我们需要动态链接的原因!
    在90年代的时候,是通过将ar(我们前面讲的将目标文件打包成静态库的工具)改成ld输出一个可执行的二进制文件。
    在Mac相关的生态里面动态库叫做dylib,而在其他平台则是叫做DSOs(Linux一般都叫做dso ,dynamic shared Object)或者DDLs(Windows平台,Dynamic-Link Library)。

    相较于静态链接是将代码拷贝到最终的产物里面,动态链接只是记录了从动态库中使用到的符号、以及运行时的路径。这样做的好处就是我们可以自己控制程序的包大小,最终的产物只是包含相关源代码,动态库相关的内容只有在运行时才需要。并且静态链接的时间只是和我们的代码多少相关,和需要动态库的多少无关;另外一个好处针对内存这块,当同一个动态库被多个进程使用的时候,虚拟内存系统会针对dylib重用相同的物理页。

    为了能够更好地理解这一点,我这边构建了一个简单的动态库。其源代码如下:

    #include <stdio.h>
    
    void pfb_foo(int i) {
        printf("\nLink Fast: %d\n", i);
    }
    

    使用 "clang -shared -fPIC -o libpfbdyc.dylib pfb_dylib.c" 进行构建。这里PIC指的是地址无关代码,这样共享的指令在装载时不会因为装载地址的改变而发生改变,也就是我们常说的la_symbol_ptr。然后我们再修改一下前面用到prog.c:

    #include "pfb_string.h"
    #include <unistd.h>
    
    extern void pfb_foo(int i);
    
    int main(int argc, const char *argv[]) {
        pfb_string *str_ptr = newString("Link fast: Improve build and launch times");
        str_description(str_ptr);
        int len = pfb_str_len(str_ptr);
        str_free(str_ptr);
    
        pfb_foo(len);
        int i = 0;
        while (i < 60)
        {
            sleep(1);
            i++;
        }
        return 0;
    }
    

    然后我们使用" clang prog.c -lpfbio -lpfbdyc -L . pfb_string.o -o prog_dylib "构建出一个可执行文件。为了验证第一点,即动态链接并不会将代码拷贝到最终的产物里面。我们可以通过“otool -tv prog_dylib”来查看其内容:

    prog_dylib:
    (__TEXT,__text) section
    _main:
    0000000100003c70        pushq   %rbp
    0000000100003c71        movq    %rsp, %rbp
    ...
    
    _pfb_std_out:
    0000000100003cf0        pushq   %rbp
    ...
    _growth:
    0000000100003d20        pushq   %rbp
    ...
    _newString:
    0000000100003e00        pushq   %rbp
    ...
    _str_description:
    0000000100003eb0        pushq   %rbp
    ...
    _str_free:
    0000000100003ee0        pushq   %rbp
    ...
    _pfb_str_len:
    0000000100003f10        pushq   %rbp
    ...
    

    在上面的(__TEXT,__text)里面并未看到pfb_foo相关的代码。

    而对于内存使用这块儿,是虚拟内存系统分配这块儿做针对物理内存进行了复用。不过这块儿目前我没有找到可以验证的方法,但是可以通过 vmmap 来查看每个进程的虚拟内存使用情况。

    动态绑定过程

    当我们在进行程序构建的时候,我们会通过ld链接器对所有的符号进行重定位(大致是静态链接)。但是动态链接的操作只是在最终的产物里面添加了stub(也就是通常说的桩),并不是真正意义上的函数或者数据的地址:


    地址0x100003f2e的内容如下:


    针对每一个stub在集合中的下标,根据相同的下标在在__la_symbol_ptr里面找到对应的symbol的指针。这个__la_symbol_ptr是存在于(__DATA, __la_symbol_ptr)。这个数据段是可写的,我们通过lldb的“image dump section”来进行验证:


    查看其内存值:

    (lldb) x 0x0000000100008000
    0x100008000: 8c 3f 00 00 01 00 00 00 
    0x100008008: 96 3f 00 00 01 00 00 00
    0x100008010: a0 3f 00 00 01 00 00 00
    

    地址0x3F8C对应的指令如下,接着会执行push和jmp指令。这个jmp指令就到了__stub_helper的起始地址,真正执行相关的跳转操作。


    当我们动态链接之后再来看该地址所对应的值:

    (lldb) x 0x0000000100008000
    0x100008000: 80 4f 1d 00 01 00 00 00 
    0x100008008: ab ff 1c 12 f8 7f 00 00
    0x100008010: af 76 1b 12 f8 7f 00 00
    

    而对应执行的指令已经被正确地更正了:

    ->  0x100003f6e <+0>: jmpq   *0x4094(%rip)             ; (void *)0x00007ff8121cffab: printf
    

    这个过程相对来说就是一个比较宏观的动态链接过程,这个过程Apple称之为“Fix up”。
    Fishhook实际上就是利用了这个原理,通过下面这一系列的操作找到我们需要替换的符号:

    这样就可以定位我们需要修改的符号,最后修改对应__lazy_symbol_ptr的值(函数指针)从而避免了走stub那一套:

    struct dysymtab_command* dysymtab_cmd = NULL;
    ...
    dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
    ...
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
    ...
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    ...
    /// 更新函数指针
    indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
    

    针对动态链接的优化

    Chain Fixup

    今年Apple提供了一个新的fix up——“Chained Fixups”。它的第一个优势是使得LinkEdit变得更小了,LinkEdit里面包含了很多和链接相关的信息,我们在静态链接部分提到过。由于不是存储所有需要fix的位置,新格式只存储每个 DATA 页面中第一个修正位置的位置以及导入符号的列表,其余的信息都是被编码在DATA段的内部。


    该结果在iOS13.4以后就已经支持。

    加载流程缓存

    第二个优化就是针对和dyld相关的执行流程优化,传统的程序运行过程是先解析Mach-O文件,接着是找到所有依赖的动态库,找到之后通过mmap映射到内存中。接着寻找所需的符号并进行修正。最后执行相关的初始化器。



    在2017年以后,Apple上面的三个绿色的步骤做了缓存。也就是说如果没有修改程序、而且动态库没有发生修改的情况下,绿色的步骤是被缓存起来的。

    Page In Linking

    相对于每次启动的时候都去做相关的修正操作,内核现在则是在DATA Page In的时候进行修正(页装载到内存中的时候)。当我们通过mmap首次使用某些地址的时候会触发内核去读取某些页,现在如果是DATA页的话,内核还会去做修正工作。这么做的话会使得在启动阶段,减少部分Dirty Memory。

    这个feature之前只在MacOS上生效,iOS16也会引入该能力。需要注意的是 dyld 仅在启动期间使用此机制,在此之后的任何时间调用 dlopen 的 dylib 都不支持page-in linking。


    动态链接最佳实践

    上面Apple也做不少的优化工作,我们唯一能做的就是控制动态库的数量了。如果我们的代码每次都会执行的话,可以考虑将动态库迁移到静态库;

    所有在初始化阶段需要耗时超过几毫秒的任务都不要放在初始化阶段,比如IO和网络相关的事情;

    动态链接的两面性

    那这些收益的代价是什么呢?可以肯定的是使用动态编译可以优化我们的构建时间,代价却是在启动程序的时长变得更长了。这是因为加载不仅仅只是装载一个程序,而是需要将各个dylib加载和连接起来;

    其次是使用了动态库的程序会存在更多的脏页,iOS的虚拟内存分为clean memory(不可更改,或者未写的内存)、dirty memory(可更改、或者被写入数据的内存)。这里为什么说dirty memory变多了呢?是因为一些lazy bind操作所需的符号信息,是放在__DATA里面的。而每个dylib都有自己的__DATA;

    最后,由于动态链接机制的原因需要做运行时的链接操作,这会使得运行期间的执行效率将会有所下降。

    五、工具介绍

    第一个工具是dyld_usage,它只在macOS下面生效,不过我们可以在模拟器启动阶段使用:

    sudo dyld_usage imeituan
    

    由于我没有升级最新的系统,所以我就针对WWDC视频截图:


    第二个工具是dyld_info,可以使用它来检查磁盘上和当前 dyld 缓存中的二进制文件。比如我们可以用fixup选项,来看我们在动态链接的时候需要修正的符号:

    ➜ ✗ dyld_info -fixups prog_dylib2
    prog_dylib2 [x86_64]:
        -fixups:
            segment      section          address                 type   target
            __DATA_CONST __got            0x100004000              bind  libSystem.B.dylib/dyld_stub_binder
            __DATA       __la_symbol_ptr  0x100008000            rebase  0x100003F8C
            __DATA       __la_symbol_ptr  0x100008000              bind  libpfbdyc.dylib/_pfb_foo
            __DATA       __la_symbol_ptr  0x100008008            rebase  0x100003F96
            __DATA       __la_symbol_ptr  0x100008008              bind  libSystem.B.dylib/_printf
            __DATA       __la_symbol_ptr  0x100008010            rebase  0x100003FA0
            __DATA       __la_symbol_ptr  0x100008010              bind  libSystem.B.dylib/_sleep
    

    这基本上和我们前面看到的lazy_symbol_ptr一致。

    使用export选项可以查看当前动态库,对外导出了哪些符号:

    ➜ ✗ dyld_info -exports libpfbdyc.dylib 
    libpfbdyc.dylib [x86_64]:
        -exports:
            offset      symbol
            0x00003F80  _pfb_foo
    

    六、其他

    相关文章

      网友评论

          本文标题:WWDC22-Link fast: Improve build

          本文链接:https://www.haomeiwen.com/subject/ukkpirtx.html