55

iOS 编译知识小结

 3 years ago
source link: http://cocoa-chen.github.io/2020/07/13/iosCompile/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

拖更很久了,今天水文一篇。简单介绍下iOS底层编译的相关知识,帮助我们充分理解了iOS编译的过程,相信会对我们后续的开发有一定帮助。

源码到可执行文件流程

首先看一下iOS代码是如何从源码变成可执行文件的,有助于我们了解程序从编译到运行的全流程

  1. 编译器Clang会将源码XXX.m编译为目标文件XXX.o
  2. 链接器会将目标文件链接打包进最终的可执行文件Mach-O中
  3. 点击App ICON时,动态链接器dyld会加载可执行文件以及依赖的动态库,并最终执行到main.m里,至此App启动完成

QvaAfm.jpg!mobile

编译器

编译器是将编程语言转换为目标语言的程序,大多数编译器由两部分组成:前端和后端。

  • 前端负责词法分析,语法分析,生成中间代码;
  • 后端以中间代码作为输入,进行行架构无关的代码优化,接着针对不同架构生成不同的机器码。

前后端依赖统一格式的中间代码(IR),使得前后端可以独立的变化。新增一门语言只需要修改前端,而新增一个CPU架构只需要修改后端即可。

Objective C/C/C++使用的编译器前端是clang,swift是swift,后端都是LLVM。

zqaUzqf.jpg!mobile

LLVM是一个模块化和可重用的编译器和工具链技术的集合,Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍,

LLVM 还可以提供一种代码编写良好的中间表示 IR,这意味着它可以作为多种语言的后端,这样就能够提供语言无关的优化同时还能够方便的针对多种 CPU 的代码生成。

编译流程

Objective-C的编译器前端是Clang,诞生之初是为了替代GCC,提供更快的编译速度。我们可以通过下面这张图来了解Clang编译的大致流程:

iIB7reY.jpg!mobile

下面我们通过clang命令来具体分析下源码编译的流程:

首先在命令行里输入

clang -ccc-print-phases main.m

可以看到源文件编译需要的几个不同的阶段

➜  clang -ccc-print-phases main.m
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output  //预编译
2: compiler, {1}, ir  //编译成中间代码ir
3: backend, {2}, assembler  //生成汇编
4: assembler, {3}, object  //生成目标文件.O
5: linker, {4}, image  //链接成可执行文件
6: bind-arch, "x86_64", {5}, image

接下来我们新建一个main.m并详细来看下每个步骤分别做了什么

main.m
#include <stdio.h>

int main() {
  printf("hello world\n");
  return 0;
}

预处理(preprocessor)

我们用下面的命令来查看clang预处理的结果:

clang -E main.m

注:如果main.m中用到了UIKit等类,可以在命令后添加-sysroot参数,记得将sdk换成你本机的版本,后续命令解决方法相同。如下所示:

clang -E main.m -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk

可以看到预处理后的文件行数有很多,在最后可以找到main函数

# 13 "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk/System/Library/Frameworks/UIKit.framework/Headers/ShareSheet.h" 2 3
# 17 "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk/System/Library/Frameworks/UIKit.framework/Headers/UIKit.h" 2 3
# 10 "main.m" 2
# 1 "./AppDelegate.h" 1
# 11 "./AppDelegate.h"
@interface AppDelegate : UIResponder <UIApplicationDelegate>
 
@property (strong, nonatomic) UIWindow *window;
 
@end
# 11 "main.m" 2
 
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, ((void *)0), NSStringFromClass([AppDelegate class]));
    }
}

预处理会替进行头文件引入(递归操作),宏替换#define,注释处理,条件编译(#ifdef),#pargma处理等操作。比如#include “stdio.h”就是告诉预处理器将这一行替换成头文件stdio.h中的内容,这个过程是递归的:因为stdio.h也有可能包含其头文件。

词法分析(lexical anaysis)

预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等。

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

语法分析(semantic analysis)

语法分析会校验语法的正确性,然后将所有的节点组成抽象语法树AST。有了抽象语法树,clang就可以对这个树进行分析,找出代码中的错误。比如类型不匹配,亦或Objective C中向target发送了一个未实现的消息。

业内对Clang自定义插件或者开发静态检测插件都是基于AST语法树来分析。相关知识后续会学到。AST是开发者编写clang插件主要交互的数据结构,clang也提供很多API去读取AST。更多细节: Introduction to the Clang AST

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

在输出里可以看到相关的AST结果,如下图:

my6RVfe.jpg!mobile

CodeGen

CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,也是后端的输入。

Objective C代码也在这一步会进行runtime的桥接:property合成,ARC处理等。

clang -S -fobjc-arc -emit-llvm main.m -o main.ll

查看main.ll的内容如下:

...
; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  %6 = call i8* @llvm.objc.autoreleasePoolPush() #1
  %7 = load i32, i32* %4, align 4
  %8 = load i8**, i8*** %5, align 8
  %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
  %10 = bitcast %struct._class_t* %9 to i8*
  %11 = call i8* @objc_opt_class(i8* %10)
  %12 = call %0* @NSStringFromClass(i8* %11)
  %13 = bitcast %0* %12 to i8*
  %14 = notail call i8* @llvm.objc.retainAutoreleasedReturnValue(i8* %13) #1
  %15 = bitcast i8* %14 to %0*
  %16 = call i32 @UIApplicationMain(i32 %7, i8** %8, %0* null, %0* %15)
  store i32 %16, i32* %3, align 4
  %17 = bitcast %0* %15 to i8*
  call void @llvm.objc.release(i8* %17) #1, !clang.imprecise_release !10
  call void @llvm.objc.autoreleasePoolPop(i8* %6)
  %18 = load i32, i32* %3, align 4
  ret i32 %18
}
 
; Function Attrs: nounwind
...

如果在项目配置中开启了 bitcode, 苹果还会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。

clang -emit-llvm -c main.m -o main.bc

生成汇编代码

clang -S -fobjc-arc main.m -o main.s

生成目标文件

汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)

clang -fmodules -c main.m -o main.o

接下来我们用nm命令,查看下main.o中的符号

➜  BuildTest nm -nm main.o
                 (undefined) external _printf
0000000000000000 (__TEXT,__text) external _main

这里可以看到_printf是一个是undefined external的。undefined表示在当前文件暂时找不到符号_printf,而external表示这个符号是外部可以访问的,对应表示文件私有的符号是non-external。

生成可执行文件

链接器可以把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件

clang main.o -o main

接着在命令行执行./main,可以看到输出了结果:hello world。

最后我们用nm命令来分析下可执行文件的符号表:

➜  BuildTest nm -nm main
                 (undefined) external _printf (from libSystem)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f60 (__TEXT,__text) external _main
0000000100002008 (__DATA,__data) non-external __dyld_private

可以看到_printf仍然是undefined,但是后面多了一些信息:from libSystem,表示这个符号来自于libSystem,会在运行时动态绑定。

以上就是Clang编译源文件的完整流程了。

Xcode中查看Clang编译.m文件信息

如果你想在 Xcode 中查看,可以通过 Show the report navigator 里对应 target 的 build 中查看每个 .m 文件的 clang 编译信息,如下图:

UFjIry.png!mobile

随便找一个.m文件编译信息,可以看到Xcode会首先对任务进行描述:

CompileC /Users/chenaibin/Library/Developer/Xcode/DerivedData/PodIntegrationDemo-achbuytjuwbatqbzvlwflifarxwa/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/podLibB.build/Objects-normal/x86_64/podClsB.o /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/podLibB/Classes/podClsB.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler (in target 'podLibB' from project 'Pods')

接下来对会更新工作路径,同时设置 PATH

cd /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/PodIntegrationDemo/Pods
export LANG=en_US.US-ASCII
export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

接下来就是实际的编译命令

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -target x86_64-apple-ios9.0-simulator -fmessage-length=0 -fobjc-arc… -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk -iquote ... -I... -F...-c /.../podClsB.m -o /.../podClsB.o

clang 用到的命令参数如下:

-x 编译语言比如objective-c
-arch 编译的架构,比如arm64
-f 以-f开头的。
-W 以-W开头的,可以通过这些定制编译警告
-D 以-D开头的,指的是预编译宏,通过这些宏可以实现条件编译
-iPhoneSimulator13.0.sdk 编译采用的iOS SDK版本
-I 把编译信息写入指定的辅助文件
-F 需要的Framework
-c 标识符指明需要运行预处理器,语法分析,类型检查,LLVM生成优化以及汇编代码生成.o文件
-o 编译结果

Xcode常见编译报错分析

1. duplicate symbols报错

第一个常见的编译报错原因就是duplicate symbols,如下图就是因为我们链接后的可执行文件存在了重复的类导致的。

emIjMjb.jpg!mobile

注:由于我们工程是由CocoaPods构建的,在xcconfig中OTHER_LINK_FLAG都会被默认设置成$(inherited) -ObjC ……,这会导致工程配置里Other Linker Flags会带上 -ObjC标记,如果我们手动删除了-ObjC,就会发现在编译时不会有duplicate symbols的错误了。但是运行的时候可能会出现unrecognized selector sent to class XXX的错误,这是由于静态库中的分类并没被链接器链接进可执行文件中。

-ObjC会把静态库中所有的类和分类都链接进可执行文件,所以会出现duplicate symbols的错误。下面是官方描述:

This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes.

2. symbol(s) not found for architecture x86_64/arm64

第二个常见报错是在某个架构下找不到相关符号,这是因为引用的某个静态库并没有包含当前工程制式下的架构类型,解决方案是将静态库.a文件合并x86_64/arm64等架构为fat file,再集成到工程里使用。

报错原因如下图:

BneUJjr.jpg!mobile

提示:遇到这种情况时,有时候多次pod update也不能解决报错原因。这是因为你本地缓存了有问题的静态库文件,可在以下目录下找到相关类库并删除,再执行pod install下载fix后的静态库文件。

CocoaPods官方缓存目录:~/Library/Caches/CocoaPods/Pods

这个错误还有另外一种情况,当同一个pod在多个不同的端集成时可能会遇到。报错信息大致如下:

EzeqUb2.jpg!mobile

问题原因:在ProjectA中集成了podA和podB,podA使用了#if __has_include(“podB中的cls.h”)集成了podB中的类;当切换到ProjectB时,只会依赖podA一个库,这个时候编译就会上图中的错误。

解决方案:在ProjectB中将podA以源码重新编译一遍即可。

应用场景

Clang Attributes

在平时开发中,我们经常会遇到头文件里有 attribute 的用法,它是一个高级的的编译器指令,它允许开发者指定更更多的编译检查和一些高级的编译期优化。

__attribute__ 语法格式为: attribute ((attribute-list)) 放在声明分号“;”前面。

比如,在三方库中最常见的,声明一个属性或者方法在当前版本弃用了

@property (strong,nonatomic)CLASSNAME * property __deprecated;

下面是 iOS开发中常见的几个 __attribute__ 用法:

//弃用API,用作API更新
#define __deprecated    __attribute__((deprecated))
 
//带描述信息的弃用
#define __deprecated_msg(_msg) __attribute__((deprecated(_msg)))
 
//遇到__unavailable的变量/方法,编译器直接抛出Error
#define __unavailable   __attribute__((unavailable))
 
//告诉编译器,即使这个变量/方法 没被使用,也不要抛出警告
#define __unused    __attribute__((unused))
 
//和__unused相反
#define __used      __attribute__((used))
 
//如果不使用方法的返回值,进行警告
#define __result_use_check __attribute__((__warn_unused_result__))
 
//OC方法在Swift中不可用
#define __swift_unavailable(_msg)   __attribute__((__availability__(swift, unavailable, message=_msg)))

Clang警告处理

当我们在XCode中屏蔽部分Warning信息时,可以使用下面的内容来解决。通过clang diagnostic push/pop来控制代码块的编译选项。

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
///代码
#pragma clang diagnostic pop

预处理

预处理可以让我们让我们自定义编译器变量,实现条件编译。 比如我们常用的DEBUG宏:

#ifdef DEBUG
//...
#else
//...
#endif

我们可以在XCode的Target中选中Build Setting选项,搜索proprecess,即可看到定义好的预处理宏。

6NvANrq.png!mobile

目前iOS基本都是用CocoaPods来管理工程,我们也可以在每个Pod的podspec文件中配置预编译宏,CocoaPods会在构建工程时将这些信息写到Pod的xcconfig文件里。

# Pod.podspec示例
s.subspec 'YourSubSpec' do | ss |
  ss.source_files = 'Pod/Classes/**/*'
  ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) YOUR_CUSTOM_DEFINE=1' }
end

注意:podA定义的GCC_PREPROCESSOR_DEFINITIONS内容在podB中是不生效的!!!

如果想解决这个问题,推荐podB中单独定义一个subspec来配置预编译宏的值,在外层工程里通过区分是否引入podB的subspec来实现该预编译宏值的控制。

Clang插件开发

上面介绍到语法分析之后我们可以拿到抽象语法树AST,接着就可以对这个树进行分析,做静态代码分析或者无用代码分析都可以,网上也有很多资料介绍这块的研究。感兴趣的可以搜索下或者看下 Introduction to the Clang AST


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK