背景描述:
在cocpoads管理下自研库和第三方的开源库可以使用frameWork的方式集成到主工程中,在我们编译过程中,只要不改动pod下集成的库代码,第一次编译后,第三方库会编译成frameWork,通过Xcode 的 buildSetting 的 framework search Path 记录所有生成的framework地址,在项目的编译后期去link这些framework 。在没有修改pod子工程的库文件前提下,之后的每一次build都直接link缓存中编译过的framework文件。但是当文件编译成framework,成为了可执行的二进制文件,我们却任然可以借助Xcode做源码调试。那么Xcode工具是如何通过一个二进制文件执行时找到它对应的源码文件的?
鉴于上个问题,我们可以先看看Xcode的编译过程
Xcode作为一个GUI工具,实际上是通过调用一系列的命令行工具,将命令行工具处理的结果汇总输出。Xcode 使用clang编译器进行编译 会使用一系列 xcrun clang 的命令来编译文件,xcrun是用来定位clang工具位置的
接着看编译过程中都经历了些什么?
大致过程如下:
1.文件预处理
符号化 (Tokenization)
宏定义的展开
include 的展开
2.语法语义分析
将符号化后的内容转化为一棵解析树 (parse tree)
解析树做语义分析
输出一棵抽象语法树(Abstract Syntax Tree* (AST))
3.代码生成和优化
将 AST 转换为更低级的中间码 (LLVM IR)
对生成的中间码做优化
生成特定目标代码
输出汇编代码
4.汇编器
将汇编代码转换为目标对象文件。
5.link文件
将多个目标对象文件合并为一个可执行文件 (或者一个动态库)
整个过程比较复杂,特别是语义分析,和代码优化过程,可以先简单的看一下与处理过程。clang 有很多命令,可以通过
`$xcrun clang —help `
查看提示信息
其中关于预编译的:
-c Only run preprocess, compile, and assemble steps
-E Only run the preprocessor
我尝试使用上面的命令预编译了main.c文件
main.c文件:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
在main.c当前目录下执行
xcrun clang -E main.c | open -f
Open -f 是将预编译结果在文件中打开,预编译部分结果截图如下:
1.png
2.png预编译过程因为要展开所有的宏定义,inclue , import。 输出的文件中行前面的 “#”号 (hash) 接着的数字 代表的插入的源文件中的行号, 后面的文件路径代表的是需要引入的文件的路径 后面的数字代表的是引入的内容在新生成的文件所在的行数,hash下面跟的内容是指定文件 指定行数 所引入的内容,例如第二张图中
将 /Application/Xcode.app/Contents/Developer/platforms/MacOSX10.12.sdk/usr/include/machine/_types.h 文件中的第33行,34行,55行的代码段
插入新生成的文件中。
Xcode也支持查看文件的预编译结果 ,选中文件点击product—>perform Action —>preprocess,之后便能看到预编译的结果,可以对比所引入的文件所在行数和引入的内容是否对应。
Xcode基于预编译的结果去构建语法树,生成中间字节码,下面是hello world的LLVM中间字节码
3.png
可以简单了解一下上面llvm中间码的意思,@符号是LLVM中间字节码(IR)的变量前缀,@代表全局变量标志,%代表局部变量标志。@.str是全局变量名,%cast210 是局部变量的名称 。i8, i32,i64代表的存储字节类型,char是i8,一个字节,int是i32,4个字节。@main,@put,@puts是方法名,在IR中function和全局变量统称全局值(global values)都是@前缀标志。
根据中间字节码(LLVM的字节码)做代码优化,优化后输出汇编代码。
可以使用下面命令来查看生成的汇编代码:
xcrun clang -S -o - main.c | open -f
结果如下图:
按照上面说的步骤,就进入了调用汇编器编译汇编代码了,汇编器将汇编代码转换成机器码,称为目标对象文件,在MacOSX, ios下为 Mach-O文件格式下图是苹果官方文档对mach-o文件格式的介绍:
Mach-O 的组成结构如下图所示包括了Header、Load commands、Data(包含Segement的具体数据)
我们可以在工程的编译路径下找到编译后生成的可执行文件APP,我本地路径为~/Library/Developer/Xcode/DerivedData/App-ackwqnrbjrdvslercxzskjictqru/Build/Intermediates/App.build/Release-iphonesimulator/App.build/Objects-normal/i386
在此路径下使用size工具查看文件结构
xcrun size -x -l -m App
输出
6.png
刚好与苹果给出的结构图相匹配,可以暂时先简单的了解下,__TEXT segment 包含了被执行的代码,它被以只读和可执行的方式映射。__DATA segment 中包含了可读写数据, 以可读写和不可执行的方式映射,它包含了将会被更改的数据。__LINKEDIT segment 包含了动态链接器的原始数据,如符号,字符串和重定位的表的入口.
7.png最后进入连接器连接的阶段
链接器解决了目标文件和库之间的链接,链接器需要将所需的lib函数(可以理解为系统提供的函数),库函文件的内存地址编码进最后的可执行文件中,接着链接器会输出可以运行的执行文件:a.out,得到a.out 命令为:
xcrun clang man.c
用size 工具查看a.out的文件结构
可以看到 main.c文件生成的可执行文件a.out 的虚拟地址空间从0x10000000 开始的,之前的地址是不可访问
此时在借助mac下可执行文件的阅读器。machOView工具,看生成的a.out的文件结构和内容,发现了一张叫symbols的表
9.png
这里面就很清楚的记录了,编译前的字符串,在编译成二进制可执行文件后的虚拟地址,和app打包生成的dsym基本类似,这就可以猜想,苹果是如何通过crash的二进制栈信息解析出可读性的程序代码调用栈的。借助symbols表给我的希望,我用machOView打开了我编译生成的frameWork
原谅我把图片糊得像翔。。。。因为是公司的项目的frameWork
首先第一张里表示了在不同平台下的可执行数据,当前frameWork可在ARM,X86,Arm64,X86_64平台下执行
一.png
第二张图是所有frameWork内的类名对应的编译生成的虚拟地址
二.png
第三张是挑了第二张图中的一个类的symbols表,记录的是当前文件编译后的字符对照表。
三.png
那么有了以上三张表的对比后我们很容易的了解到Xcode工具如何通过运行期间的二进制地址对应到具体文件的具体代码调用了。
网友评论