符号链接
SymbolTable:用来保存符号的,存放地址。StringTable:用来保存符号的名称。
Indirect Symbol Table:间接符号表。保存使用的外部符号。更准确一点就是使用外部动态库的符号,是Symbol Table的子集,不可进行strip。例如使用NSLog函数后,这个符号就会被放进此表。
间接符号表保存了当前可执行文件使用的其他动态库的符号。NSLog默认是未定义的符号,符号表的标识是UND。
Common Symbol:定义时未初始化的全局符号,可以重复定义,一旦找到有值的,将把无值的移除。
链接器会默认把未定义的符号的强制初始化一个值例如0。
static是本地符号,标志符号:l。global是全局符号,标志符号:g。
链接过程就是处理目标文件符号的过程
源代码生成.o的时候,会对符号进行归类,生成重定位符号表(.m或.o文件用到的API), .o文件链接,把多个文件合并到一起中后,重定位符号表、符号表也会合并到一张表中。
OC默认的都是导出符号,所以体积比较大;可以配置OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker ##符号## 屏蔽指定的符号。
符号占的空间比较大,符号名称是字符串。因为App链接的静态库除了间接符号表都可以进行strip,动态库的全局符号会变成导出符号,在对动态库进行符号strip的时候,只能剥离非全局符号的符号。app使用的所有的动态库的符号都存放在间接符号表里。所以相对而言,动态库占用的体积大。
对于包体积的优化,App的本地符号全局符号都可以剥离,只留下间接符号表的符号。
在源码编译成.o目标文件的时候,没有生成真实的内存地址,使用0填充占位,把需要重定位的地方放到重定位表里面。静态库的重定位符号表不能删除,可以剥离调试符号。顺便带一笔,使用XCFramework的方式,可以将调式符号给集成者。
对于全局符号,可以跨macho文件使用,例如在一个framework的.m中定义一个方法foo,而在主工程文件的一个.m文件,声明了foo方法后,可以直接使用,代码会执行到上面的foo。
对于静态库的strip方式,将MachO文件解析成模型Object,遍历LoadCommands找到segment=="__DWARF"的LoadCommand,移除section,从符号表移除Symbol,将修改后的模型Object重新写入MachO文件。动态库n_type的判断。
DWARF是一种被众多编译器和调试器使用的用于支持源码级别调试的调试文件格式。dsym文件就是按DWARF格式保存调试信息的文件。dsym是在编译链接中生成的,保存的是真实的虚拟内存地址。ASLR是在Rebase中添加的。
终端输入"man ld",可查找
dead_strip:Remove functions and data that are unreachable by the entry point or exported symbols.
剥离原则:(1)没有被入口函数使用的;(2)没有被导出符号使用的。
dead strip在链接的过程中生效,也就是在没有对OtherFlags配置的情况下,会对分类的代码进行死代码剥离。因分类的代码是运行时调用生效,而在链接时根据dead strip的原则而被干掉。
OTHER_LDFLAGS=-Xlinker -all_load:通过clang编译器,传给ld链接器all_load参数。
-all_load:加载找到的所有目标文件。
-Objc:是告诉链接器除了Objc代码,其他代码正常剥离。
-force_load ##库路径##:告诉ld哪些静态库不要dead strip。
默认是-noall_load,
[Link-Time Optimization](开启Link Time Optimization(LTO)后到底有什么优化?)的优化是在dead code stripping之后的。
deadCodeStripping 死代码剥离是在链接的时候,strip Style是已经生成machO文件的时候。
执行命令objdump --macho --exports-tire ##macho文件## 可查看导出符号。
通过nm的方式还会打印出源代码中 __attribute__((weak, visibility("hidden")))
修饰的符号。
__mh_execute_header符号延申:
"对于标准的UNIX链接编辑器符号,程序可以使用符号__mh_execute_header并遍历它的程序的加载命令来确定程序中任何节或段的结束(或开始)"
"The value of the link editor defined symbol [__mh_execute_header] is the address of the mach header in a Mach-O executable file type. It does not appear in any file type other than a MH_EXECUTE file type. The type of the symbol is absolute as the header is not part of any section."
翻译:"链接编辑器定义的符号[__mh_execute_header]的值是mach - o可执行文件类型中mach头的地址。它不会出现在MH_EXECUTE文件类型以外的任何文件类型中。符号的类型是绝对的,因为头不属于任何节。"
`dyld_stub_binder`就是`dyld`库中进行延迟绑定的方法,要告诉函数我们要寻找哪个方法的调用地址。
通过MachO工具可以查看到可执行文件的Symbol Table详细内容。
otool是Xcode自带的常用工具,可以打印出目标文件的相应细节,终端输入"otool help"可查看具体命令,简单列举:
-f print the fat headers
-a print the archive header
-h print the mach header
-l print the load commands
-L print shared libraries used
-D print shared library id name
-t print the text section (disassemble with -v)
-x print all text sections (disassemble with -v)
弱引用+弱符号
Weak Reference Symbol: 表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置为弱链接标志。可以把整个动态库声明为弱引用。对于library或者framwork,当配置OTHER_LDFLAGS = -Xlinker -weak_framework -Xlinker ##库名## 时,会将这个库声明弱引用,在运用时如果不存在并不会报错,而是输出nil。
Weak defintion Symbol: 表示此符号为弱定义符号。如果链接器找到此符号的一个(非弱)定义,则弱定义将被忽略。只能将合并中部分的符号标记为弱定义。例如一个类文件定义一个弱符号方法,而在另一个文件也定义此符号方法,编译不会报错。在调用此符号对应的方法时,会走强符号的方法逻辑。
man ld:查看更多Xlinker配置信息
//弱符号
void weak_function(void) __attribute__((weak))
//弱引用
void weak_import_function(void) __attribute__((weak_import))
—————————————————————man objdump———————————————————————————————————————
//全部符号
objdump --macho --syms ##可执行文件路径##
//导出符号
objdump --macho --exports-trie ##可执行文件路径##
//间接符号表
objdump --macho --indirect-symbols ##可执行文件路径##
DYLD链接库将会check是否命中启动缓存,如果没有,则加载所有的手动插入动态库(通过配置环境参数DYLD_INSERT_LIBRARIES,动态库注入),然后链接程序需要的动态库,然后链接前面插入的库,然后应用插入函数(例如自定义sectionData),绑定符号,读取LC_MAIN,找到入口函数,在此步之前,调用当前程序与动态库的initializers/constructors构造方法,设置胶水地址,dyld配置完成,然后执行main函数。
ps: Swift是一门编译型的语言,不是动态运行的,编译的时候就可以确定一个符号的类型。
动态库+静态库
通过file命令和注释use_frameworkd的方式查看AFNetworking库:
(1)动态库 => "Mach-O 64-bit dynamically linked shared library arm64"。
(2)静态库 => "current ar archive"
通过ar -t的命令查看动态库构成。显示库文件中有哪些目标文件,只显示名称。
image not found的原因=>生成动态库的时候,路径不对,需要配置install_name”;
通过命令otool -l ##动态库名称## | grep 'LC_ID_DYLIB' -A 5 可查看动态库的路径name值。然后可通过命令“install_name_tool -id ##库路径## ##所要添加的动态库名,即哪一个动态库##”;也可在生成动态库的时候,通过配置install_name ##库路径##链接器参数解决。
关于库路径的解决方式,引出了@rpath相对路径,谁链接这个库,谁提供@rpath.。而这个谁,我们称为链接者。对于库的rpath配置时,@rpath/库的上层级直到和链接者同级所在的文件名...直到库名要在自己的macho中也保存一个@rpath对应。添加rpath到macho文件中的命令"install_name_tool -add_rpath ##链接者所在的上一级目录##"
@executable_path表示可执行程序所在的目录,解析为可执行文件的绝对路径。
@load_path表示被加载的macho文件所在的目录,每次加载都可能被设置为不同的路径,由上层指定。
这些是loadCommand的信息,可通过命令查看。
静态库是.o文件的合集,动态库是.o文件链接后的产物
clang链接一个libray库或framework的参数配置与Xcode build-setting的对应key项:
-I ##directoryPath## : 在指定目录寻找头文件 相当于xcode的header search path
-L ##directoryPath## : 指定库文件路径(.a/.dyld) => library search path
-l ##library_name## : 指定链接的库文件名称 => other link flags。例如-lAFNetworking。
-F ##directoryPath## 在指定目录寻找framework => framework serach path
-framework ##framework_name## 指定链接的framework名称 =>other link flags 例如 -framework AFNetworking
当链接两个同名静态库,可配置OTHER_LDFLAGS 一个“-force_load” 一个“-load_hidden”;
xcframework:1,自动处理头文件;2,调试符号的保存;3,相同架构的处理
动态库内部导入*库
1,当A动态库导入B动态库的时候,主工程在安装A动态库,可以通过sh脚本的方式将B动态库安装至指定的目录frameworks文件夹中,也可直接主工程poddile配置“pod B”。
2, A动态库导入B静态库时,A将B整个库链接进去。 也就是B静态库中的导出符号,在A中将会全部导出。因此,工程可以直接使用B静态库的符号功能。当A并不想暴露B静态库的符号时,可以使用-hidden-l ##库名##
配置链接器参数隐藏B的符号。
静态库导入*库
1,A静态库导入B静态库,当主工程使用A静态库时,也要配置B静态库的header search path + library search path + library name(Other Linker Flags中配置-lB);
2,A静态库导入B动态库, 主工程需要配置B动态库信息,然后同动态库导入动态库的逻辑。
拓展: podfile中关键字“workspace ##指定路径的xcworkspace路径##” 可以对不同的target安装pod,库;对于App项目可以需要project路径。
Module
Module:通过编译单个源文件生成的目标文件,例如一个.m被编译成目标文件.o,此.o就代表一个module。 mudole避免了一个.h文件被多个.m文件引入产生的多次编译。是clang专门处理头文件的解析格式,可以将.h头文件编译成一个二进制,然后偷偷缓存到系统的目录中。
一个swift库调用OC文件,需要通过配置mapmodule的方式解决。对于具有同名符号的两个静态库,在合并后,可以通过配置xcconfig来解决同名问题。
//OTHER_CFLAGS:传递给用来编译C或者OC的编译器,当前就是clang
OTHER_CFLAGS = "-fmodule-map-file=${SRCROOT}/.../module.modulemap" "-fmodule-map-file=${SRCROOT}/.../module.modulemap"
//SWIFT_INCLUDE_PATHS: 传递给SwiftC编译器,告诉他去下面的路径中查找module
SWIFT_INCLUDE_PATHS = "${SRCROOT}/.../module.modulemap" "${SRCROOT}/.../module.modulemap"