美文网首页IOS知识积累iOS逆向
iOS 编译与链接三:静态链接和动态链接

iOS 编译与链接三:静态链接和动态链接

作者: Trigger_o | 来源:发表于2022-07-21 17:31 被阅读0次

    编译的过程
    编译的产物

    一.静态链接

    随着计算机的发展,代码早就不会只写在一个文件里了,不同的文件互相关联,但却需要分开编译,在编译的时候,每个.m文件都会分别编译并生成目标文件,也就是.o的文件,而.o就是mach-o类型文件.
    每个mach-o都可能有导入符号,这些符号的地址在编译的时候是不知道的.打个比方就是要解决A.o如何访问B.o的函数或者变量的问题.
    编译出来的文件可能是.o,静态库(.o的集合)等.LLVM的连接器会对符号的地址引用进行修正,因为在编译的时候,这些地址都是假的占位,在链接的时候才会替换成真实的,把各个模块间相互的引用能够正确的链接好,最终将这些mach-o合并成一个mach-o.

    而这个过程叫做静态链接.完成这项工作的是链接器,从编译到静态链接,叫做构建(bulid).


    静态链接

    1.链接器

    code文件经过编译生成.o, 接下来.o和.a以及.dylib一起经过链接器合并成可执行文件.
    生成的可执行文件有两种去处,一个是运行时被loader执行,开启进程,也就是主程序;另一个是服务于dynamic linker,也就是动态链接库.

    苹果使用的ld叫做ld64,位置在Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld;
    并且开放了源码

    可以在终端查看ld的信息

    man ld
    
    image.png

    2.dead code striping

    image.png
    静态库里未被引用的符号会被剥离,而主target里的只要在compile source里添加的,就会被链接.

    3.链接策略

    这里主要讲的是build setting -> linking -> other link flag的配置,主要有-l -framework -Objc -all_load -force_load等

    -l指主动链接静态库 如 -l"sqlite3.0"
    -framework指主动链接framework 如-framework"AVFoundation"
    对于这两个其实并不是必要的,ld64具有自动链接的特性,编译.o时,解析import,把依赖的framework写入最后 Mach-O 里的LC_LINKER_OPTION.

    -ObjC 强制加载所有包含ObjC class和category的.o (symbol name 包含 OBJC_CLASS 或者.objc_c)
    想知道它是如何工作的,需要先了解oc符号的生成逻辑.
    前文说到mach-o的符号有三种可见度,本地符号,全局符号和未定义符号.

    对于一个.o:
    OC定义的类,是全局符号;
    OC定义的方法,是本地符号;
    OC引用的外部类,是未定义符号;
    而OC引用方法,却不会生成符号.

    也就是说A.m引用了B.m一个方法,在A.o的符号表只有OBJC_CLASS$_ClassB(undefined),而没有-[ClassB method].
    如果现在有一个分类文件C.m,它是B的分类,编译之后,C.o中确实有一个方法符号-[ClassB categoryMethod];
    当要链接的时候,如果C在主Target中,lb64会直接解析它,维护一个objc-cat-list,会保存所有的分类.
    如果C在一个静态库中,lb64就没有理由去链接它,因为A.o并没有一个-[ClassB categoryMethod]的未定义符号需要重定位.
    而-ObjC就是为了解决这个问题,可以强制把静态库中的objc class和category都链接进来.

    现在我们知道了:
    1.在静态库单独定义的category默认不会被链接;
    2.为被引用的符号会被剥离.
    因此我们也可以手动实现-ObjC的效果,那就是在分类的.m文件中实现一个别的东西,可以是c方法,可以是oc类等等,然后引用他们,这样分类也可以被链接,不过这个操作意义不是很大, 总归要使用第三方静态库的,别人不一定会这么做.

    -all_load会链接所有的.o,代价很大不建议使用
    -force_load $(SRCROOT)/... 需要跟上路径,指定链接某个静态库的全部.o

    *4.静态链接

    分别创建A.c和B.c

    //A.c
    extern int global_var;
    void func(int a);
    int main() {
        int a = 100;
        func(a+global_var);
        return 0;
    }
    
    //B.c
    int global_var = 1;
    void func(int a) {
        global_var = a;
    }
    

    分别编译出A.o和B.o

    xcrun clang -c A.c
    xcrun clang -c B.c
    

    然后连接A.o和B.o生成可执行文件AB

    xcrun clang A.o B.o -o AB
    
    MachOView打开A.o,B.o,AB

    查看A.o的符号

    objdump --macho --syms A.o
    objdump --macho --syms B.o
    

    输出

    A.o:
    
    SYMBOL TABLE:
    0000000000000000 l     F __TEXT,__text ltmp0
    0000000000000048 l     O __LD,__compact_unwind ltmp1
    0000000000000000 g     F __TEXT,__text _main
    0000000000000000         *UND* _func
    0000000000000000         *UND* _global_var
    
    B.o:
    
    SYMBOL TABLE:
    0000000000000000 l     F __TEXT,__text ltmp0
    000000000000001c l     O __DATA,__data ltmp1
    0000000000000020 l     O __LD,__compact_unwind ltmp2
    0000000000000000 g     F __TEXT,__text _func
    000000000000001c g     O __DATA,__data _global_var
    

    A中未初始化的fun和global_var是未定义符号
    B中实现了func和global_var,是全局符号

    再看看AB

    AB:
    
    SYMBOL TABLE:
    0000000100000000 g     F __TEXT,__text __mh_execute_header
    0000000100003f94 g     F __TEXT,__text _func
    0000000100004000 g     O __DATA,__data _global_var
    0000000100003f4c g     F __TEXT,__text _main
    

    都是全局符号

    image.png

    在MachOView中也能区分,白色是本地符号,土黄色是全局符号,绿色是未定义符号.

    2.符号解析

    也叫做符号决议.
    1.根据预定规则来检查符号,比如不允许存在相同的强符号,如果存在报错dumplicate symbols,相同的符号有强有弱则保留强符号,多个相同的弱符号只保留一个.
    2.处理未定义的符号,所有的已定义符号和未定义符号分别存在两个集合中,然后遍历未定义集合,去已定义集合中找,匹配成功就移除,如果最后未定义符号集合有没能成功匹配的,也就是非空,则会报错Undefined symbols.
    3.如果链接了一个静态库,那么链接器会放弃静态库中没有被引用的符号.比如引入了一个A.a,但是没有一个目标文件(或者说项目)引用这个A.a里的符号(类,方法,变量),最终可执行文件里就不会包含A.a里的符号.此时可执行文件的大小和没引入A.a编译的可执行文件大小相同.

    这个过程是做一个检查,放到代码上说,就相当于检查引用的类,变量,方法等是否真的定义了.如果这一步成功了,基本上就build succeeded了.

    3.符号重定位

    经过检查之后,知道了未定义的符号其实都在别的目标文件中定义了,那么下面要做的就是确定这些未定义符号的地址.
    在上一篇提到过符号的地址,到目前为止程序还没有运行起来,自然和内存没关系,这个指的是虚拟地址.
    这个地址是从0x0开始的,当程序运行的时候,分配一个偏移量,这偏移就是程序在内存的物理地址的开始,在这时偏移加上符号的地址就是物理地址了.

    链接器在合并A和B的时候,首先两个mach-o的段会进行合并,代码段和数据段.
    然后处理段的信息,合并mach header和load command.
    最后重定位符号,要把那些未定义的符号都解决掉.

    符号表中描述符号结构体nlist定义如下,下载源码
    位置在EXTERNAL_HEADERS/mach-o/nlist.h

    struct nlist {
        union {
    #ifndef __LP64__
            char *n_name;   /* for use when in-core */
    #endif
            uint32_t n_strx;    /* index into the string table */
        } n_un;
        uint8_t n_type;     /* type flag, see below */
        uint8_t n_sect;     /* section number or NO_SECT */
        int16_t n_desc;     /* see <mach-o/stab.h> */
        uint32_t n_value;   /* value of this symbol (or stab offset) */
    };
    
    #define N_STAB  0xe0  /* if any of these bits set, a symbolic debugging entry */
    #define N_PEXT  0x10  /* private external symbol bit */
    #define N_TYPE  0x0e  /* mask for the type bits */
    #define N_EXT   0x01  /* external symbol bit, set for external symbols */
    
    #define N_UNDF  0x0     /* undefined, n_sect == NO_SECT */
    #define N_ABS   0x2     /* absolute, n_sect == NO_SECT */
    #define N_SECT  0xe     /* defined in section number n_sect */
    #define N_PBUD  0xc     /* prebound undefined (defined in a dylib) */
    #define    N_INDR   0xa     /* indirect */
    
    #define NO_SECT     0   /* symbol is not in any section */
    #define MAX_SECT    255 /* 1 thru 255 inclusive */
    
    符号描述

    N_SECT表示明确位置,N_EXT表示外部符号,N_UNDF表示位置不明确.
    所以只要N_SECT表示本地,N_SECT+N_EXT表示全局,有N_UNDF表示未定义.

    编译器无法在编译期确定所有符号的地址,会在mach-o中生成一条对应的Relocation信息,这样连接器就知道section中哪些位置需要被重定位,如何重定位.
    在进行重定位的时候,首先会检查重定位表Relocations


    重定位表

    重定位表中元素的结构体定义如下
    位置在EXTERNAL_HEADERS/mach-o/reloc.h

    struct relocation_info {
       int32_t  r_address;  /* offset in the section to what is being
                       relocated */
       uint32_t     r_symbolnum:24, /* symbol index if r_extern == 1 or section
                       ordinal if r_extern == 0 */
            r_pcrel:1,  /* was relocated pc relative already */
            r_length:2, /* 0=byte, 1=word, 2=long, 3=quad */
            r_extern:1, /* does not include value of sym referenced */
            r_type:4;   /* if not 0, machine specific relocation type */
    };
    

    链接时,首先段进行合并,符号表也会合并,然后在重定位表取一个符号,对应到合并后的符号表,将地址等补充完整.


    可执行文件AB的符号表

    三.动态链接

    上面说到,为了不把代码都写在同一个文件,产生了静态链接.
    而动态链接:
    cocoa的各种库,比如每个app都需要UIKit,每个app在编译的时候都拷贝一份,这会占用很多硬盘空间,运行的时候,又会都加载到内存中,增加内存占用,当这些库需要更新的时候,所有的app都需要更新一次.
    因此为了解决这些问题,在硬盘和内存中共用文件,所以产生了动态链接.
    也因此,和静态连接是在编译的时候,目标文件和静态库会被链接打包成一个mach-o不同,动态链接是在运行时进行的.

    1.dyld
    需要加载动态链接库的mach-o,其load command会有一个dyld加载命令.指定了dyld的位置

    LC_LOAD_DYLINKER
    这个命令是这么定义的
    struct dylinker_command {
        uint32_t    cmd;        /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
                           LC_DYLD_ENVIRONMENT */
        uint32_t    cmdsize;    /* includes pathname string */
        union lc_str    name;       /* dynamic linker's path name */
    };
    

    这个命令还指定了dyld的路径,如果有这个命令,dlyd就会开始工作.

    dyld (the dynamic link editor),动态连接器,也是一个mach-o,loader中定义这个filetype

    #define MH_DYLINKER 0x7     /* dynamic link editor */
    

    启动了dyld,之后就要加载具体的dylib(dynamically linked shared library)动态链接库.
    因此还有dylib加载命令,和dylib结构体的定义

    struct dylib {
        union lc_str  name;         /* library's path name */
        uint32_t timestamp;         /* library's build time stamp */
        uint32_t current_version;       /* library's current version number */
        uint32_t compatibility_version; /* library's compatibility vers number*/
    };
    
    /*
     * A dynamically linked shared library (filetype == MH_DYLIB in the mach header)
     * contains a dylib_command (cmd == LC_ID_DYLIB) to identify the library.
     * An object that uses a dynamically linked shared library also contains a
     * dylib_command (cmd == LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, or
     * LC_REEXPORT_DYLIB) for each library it uses.
     */
    struct dylib_command {
        uint32_t    cmd;        /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
                           LC_REEXPORT_DYLIB */
        uint32_t    cmdsize;    /* includes pathname string */
        struct dylib    dylib;      /* the library identification */
    };
    
    dylib

    和静态链接类似,只不过这一步被推迟到程序加载的时候.编译的时候,引用自动态链接库的符号会被标记上dylib的名称,并且只有占位地址.


    dylib

    制作一个dylib查看一下内容结构

    xcrun clang -fPIC -shared B.c -o dyB.dylib
    
    dylib

    2.动态链接的符号重定向

    每次启动程序时,系统ASLR安全机制在都会分配一个随机偏移值,符号在内存的地址等于符号的偏移地址+随机偏移值
    举个例子
    新建一个iOS app,在viewDidLoad断点


    image.png

    然后编译,成功后在DerivedData里找到可执行文件,用MachOView打开,查看符号表


    image.png
    然后看到一个偏移值,1f80
    接下来运行,在断点时,选择xcode->Debug-> Debug WorkFlow -> Always show disassembly查看汇编
    image.png
    然后我们看到viewDidLoad的地址

    接下来使用lldb命令 image list,找到最上面程序起始地址


    image.png
    首地址本应该是0x0,现在是0x000000010428b000,这个就是随机偏移量
    然后计算一下,正好是1f80.
    (lldb) p/x 0x10428cf80-0x000000010428b000
    (long) $0 = 0x0000000000001f80
    

    一个动态链接库比如libsystem.B.dylib,里面有巨量的符号,但是这个main只使用了一个NSLog,因此动态链接不会在程序一启动的时候就去连接,而是在使用到某个符号的时候才会去做符号重定位,再之后使用就不需要重定位了.
    当程序首次访问外部符号时,先执行Symbol Stubs桩代码,然后跳转到Lazy Symbol Pointers对应符号的地址,首次访问会根据这个地址在Assembly文件中找到相应的代码执行,最后调用dyld_stub_binder函数进行符号绑定,绑定完成之后就会更新Lazy Symbol Pointers表中的值,将符号地址直接写入到表中,再次访问的时候就可以直接访问这个地址而不需要在执行Assembly中的代码.

    添加一句NSLog,这个函数来自Foundation.


    image.png

    然后运行,


    image.png
    可以看到Symbol Stub

    和__TEXT__text的汇编对比


    image.png
    image.png

    相关文章

      网友评论

        本文标题:iOS 编译与链接三:静态链接和动态链接

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