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

编译的过程
编译的产物

一.静态链接

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

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


静态链接

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  */
    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 编译与链接三:静态链接和动态链接)