LLVM的概念 使用CLang命令
LLVM是构架编译器的框架系统,C++编写,支持多种源语言或多种硬件架构。苹果提出LLVM代替了GCC。LLVM最重要的方面是通用的代码形式IR代码形式,做到了链接多种前端语言和生成多种后端语言的能力
编译流程
Clang是LLVM项目中的子项目,深入研究Clang对于研发人员带来很多好处。
clang -ccc-print-phases main.m
0: input, "main.m", objective-c\
1: preprocessor, {0}, objective-c-cpp-output 2: compiler, {1}, ir\
3: backend, {2}, assembler\
4: assembler, {3}, object\
5: linker, {4}, image\
6: bind-arch, "x86_64", {5}, image
输入文件:找到源文件
预处理阶段:这个过程处理包括宏的替换,头文件的导入
编译阶段:进行词法分析、语法分析、检测语法是否正确,最终生成`IR`
后端:这里`LLVM`会通过一个一个的`Pass(节点)`去优化,每个`Pass`做一些事情,最终生成汇编代码
生成目标文件
链接:链接需要的动态库和静态库,生成可执行文件
通过不同的架构,生成对应的可行文件
1.预处理阶段,通过预处理将头文件导入和宏进行了替换。
clang -E main.m >> proprocessor.txt
2.词法分析
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
词法分析,是写得每个代码切成一个个token,标注出横向纵向的位置。我的理解就是从像一次格式化一样,将代码规整好,并与下一步操作
#define C 30
typedef int HK_INT_64;
int test(int a,int b){
' Loc=<main.m:7:1>
typedef 'typedef' [StartOfLine] Loc=<main.m:11:1>
int 'int' [LeadingSpace] Loc=<main.m:11:9>
identifier 'HK_INT_64' [LeadingSpace] Loc=<main.m:11:13>
semi ';' Loc=<main.m:11:22>
int 'int' [StartOfLine] Loc=<main.m:13:1>
identifier 'test' [LeadingSpace] Loc=<main.m:13:5>
l_paren '(' Loc=<main.m:13:9>
int 'int' Loc=<main.m:13:10>
identifier 'a' [LeadingSpace] Loc=<main.m:13:14>
comma ',' Loc=<main.m:13:15>
int 'int' Loc=<main.m:13:16>
identifier 'b' [LeadingSpace] Loc=<main.m:13:20>
r_paren ')' Loc=<main.m:13:21>
l_brace '{' Loc=<main.m:13:22>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:14:5>
identifier 'a' [LeadingSpace] Loc=<main.m:14:12>
plus '+' [LeadingSpace] Loc=<main.m:14:14>
identifier 'b' [LeadingSpace] Loc=<main.m:14:16>
plus '+' [LeadingSpace] Loc=<main.m:14:18>
numeric_constant '3' [LeadingSpace] Loc=<main.m:14:20>
semi ';' Loc=<main.m:14:21>
r_brace '}' [StartOfLine] Loc=<main.m:15:1>
int 'int' [StartOfLine] Loc=<main.m:18:1>
identifier 'main' [LeadingSpace] Loc=<main.m:18:5>
l_paren '(' Loc=<main.m:18:9>
int 'int' Loc=<main.m:18:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:18:14>
comma ',' Loc=<main.m:18:18>
const 'const' [LeadingSpace] Loc=<main.m:18:20>
char 'char' [LeadingSpace] Loc=<main.m:18:26>
star '*' [LeadingSpace] Loc=<main.m:18:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:18:33>
l_square '[' Loc=<main.m:18:37>
r_square ']' Loc=<main.m:18:38>
r_paren ')' Loc=<main.m:18:39>
l_brace '{' [LeadingSpace] Loc=<main.m:18:41>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:19:5>
identifier 'a' [LeadingSpace] Loc=<main.m:19:9>
equal '=' [LeadingSpace] Loc=<main.m:19:11>
identifier 'test' [LeadingSpace] Loc=<main.m:19:13>
l_paren '(' Loc=<main.m:19:17>
numeric_constant '1' Loc=<main.m:19:18>
comma ',' Loc=<main.m:19:19>
numeric_constant '30' Loc=<main.m:19:20 <Spelling=main.m:9:11>>
r_paren ')' Loc=<main.m:19:21>
semi ';' Loc=<main.m:19:22>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:20:5>
l_paren '(' Loc=<main.m:20:11>
string_literal '"%d"' Loc=<main.m:20:12>
comma ',' Loc=<main.m:20:16>
identifier 'a' Loc=<main.m:20:17>
r_paren ')' Loc=<main.m:20:18>
semi ';' Loc=<main.m:20:19>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:21:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:21:12>
semi ';' Loc=<main.m:21:13>
r_brace '}' [StartOfLine] Loc=<main.m:22:1>
eof '' Loc=<main.m:22:2>
3.语法分析
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
//UIKit找不到的时候,导入sdk进行分析
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk -fmodules -fsyntax-only -Xclang -ast-dump main.m
语法分析是生成了抽象语法书(AST),判断语法结构是否正确。
4.生成中间代码IR
clang -S -fobjc-arc -emit-llvm main.m
//优化 -O0...-Os优化级别从小到大
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
优化的IR文件代码行数更节俭,同时在xcode中也是有相应的配置
xCode7后苹果开启了Bitcode进一步优化,可以将IR文件.ll生成.bc文件,然后再生成汇编文件。对应不同的苹果架构。
clang -emit-llvm -c main.ll -o main.bc
有了.ll or .bc 就可以生成汇编文件
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
clang -S -fobjc-arc main.m -o main.s
我们可以通过clang对ll/bc/m文件直接生成汇编文件,从汇编文件的行数比较,通过ll/bc生成的汇编文件比m文件生成的文件更加简洁。
下一步生成目标文件
clang -fmodules -c main.s -o main.o
nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main
解释:
最后一步就是生成可执行文件(链接),连接器把编译产生的o文件和.dylib .a文件生成一个mach-o文件
clang main.o -o main
xcrun nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f20 (__TEXT,__text) external _test
0000000100003f40 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
可以看到,printf来自libSystem。找到了相应的库,这就是完成的clang流程。
启动优化和虚拟内存
启动优化,以main函数之前pre-main和main函数之后。通过DYLD反馈pre-main的启动时间,以自己的项目为例,我们看一下分析:思路:不要有资源的浪费(动态库,类) 启动的时候 开启多线程
Total pre-main time: 1.1 seconds (100.0%)
dylib loading time: 177.26 milliseconds (15.5%) 动态库载入 苹果建议不要大于6个动态库。苹果自己的动态库在共享缓存中,加载速度很快。涉及到动态库绑定。
rebase/binding time: 107.88 milliseconds (9.4%) 重定位 绑定 虚拟内存
ObjC setup time: 158.31 milliseconds (13.9%) OC类注册 读取二进制,映射表,全剧表。优化角度:优化废弃的类
initializer time: 694.16 milliseconds (61.0%) load 构造函数 初始化函数 优化角度:不要做延迟加载的东西
slowest intializers :
libSystem.B.dylib : 7.18 milliseconds (0.6%)
libMainThreadChecker.dylib : 26.72 milliseconds (2.3%)
libglInterpose.dylib : 326.65 milliseconds (28.7%)
BaiduTraceSDK : 95.95 milliseconds (8.4%)
NemoSDK : 73.64 milliseconds (6.4%)
easydoctor_appstore : 321.61 milliseconds (28.2%)
含义解释:
编译时期 链接外部文件,告诉dyld我需要一个外部动态库,留一个位置等待启动运行时刻进行绑定。
虚拟内存:程序运行起来访问的内存空间是虚拟内存,虚拟内存和物理内存之前有一张映射关系,是有MMCU内存管理单元进行分配。
rebase 虚拟内存出现 ASLR(操作系统)让每次生产的虚拟的列表,生成一个随机值作为起始位置。生产mach-o的时候每块代码的地址都是生产好得,偏移地址。offset+ALSR 就是rebase的操作。 binging 内部符号要找到外部函数的时候(懒加载绑定)
二进制重排
当我们代码访问到没有载入物理内存的空间时,会出现pagefault。缺页中断。毫秒级别。如果出现大量的pagefault就会对用户有感知。 看一下项目中pagefault的情况,项目出现了1494次pagefault,用掉了299ms,个人感觉可能不太需要优化。 通过link map设置,我们可以看到函数实现的排列编译顺序,那些函数rebase到page中。
通过xcode link map来观察方法加载编译的方法。思路,我们把启动时候需要调用的方法都放到前面来。就可以在启动时刻减少pagefault的次数。、
按照order文件里面的符号顺序,对二机制进行排列.在项目的根目录中创建一个xxx.order的文件。然后在xcode中进行配置。可以看到link map文件发生了变化,main方法的内存地址是第一。
clang插桩
我们遇到的问题是我们不知道自己的项目启动时候函数调用的顺序,通过clang插桩的方式获取到函数调用的顺序,然后写入到order文件中LLVM文档地址
第一步在xcode中配置 other c flags中 -fsanitize-coverage=trace-pc-guard参数
第二步,将两个回调函数放在任意一个m文件中
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
SYNode * node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//结构体入栈
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
char PcDescr[1024];
__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
第三部我们通过一些方法生成order文件
- (void)writeOrderFile
{
//定义数组
NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
while (YES) {//循环体内!进行了拦截!!
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode,next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);//转字符串
//给函数名称添加 _
// if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["]) {//OC方法 直接存
// [symbleNames addObject:name];
// continue;
// }
// [symbleNames addObject:[@"_" stringByAppendingString:name]];
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbolName];
}
//反向遍历数组
// symbleNames = (NSMutableArray<NSString *> *)[[symbleNames reverseObjectEnumerator] allObjects];
// NSLog(@"%@",symbleNames);
NSEnumerator * em = [symbleNames reverseObjectEnumerator];
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
NSString * name;
while (name = [em nextObject]) {
if (![funcs containsObject:name]) {//数组没有name
[funcs addObject:name];
}
}
//去掉自己!
[funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
//写入文件
//1.编程字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ngaridoctor.order"];
NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"%@",funcStr);
}
然后再将order文件放到项目中,然后再看link map文件。然后通过system trace再看一下效果,发现好像作用不大,但是通过这种方式,有些函数是需要进行优化处理的。
完结!