linux深入

编译器与Clang编译过程

2021-06-01  本文已影响0人  QiShare

前言

编译的主要任务是将源代码文件作为输入,最终输出目标文件,这期间发生了什么?便是我们本篇文章要介绍的。在开始之前我们先了解一下编译器。

编译器

编译器compiler)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。引自维基百科

传统编译器的架构,一般分三部分:

传统编译器的架构

这种架构的优势在于解耦合,实现一种编程语言,只需要实现它的前端,对于优化器与后端部分是可以复用的;支持新的目标架构,只需要实现它的后端即可;如果编译器不是这种架构,三部分未分开,那么实现N个编程语言,去支持M个目标架构,就需要实现N*M个编译器。

编译器架构分析

这种传统编译器的架构有三个成功的案例:

  1. Java.Net虚拟机;它们都提供了对JIT编译器和运行时的支持,并且还定义了字节码的格式(bytecode),这意味着任何可以编译为字节码的语言,都可以复用优化器和JIT(动态编译)和运行时能力。
  2. 将输入源转换为C代码(或其他某种语言)并通过现有的C编译器编译
  3. 这种模式的最终成功实施是GCCGCC支持许多前端和后端,并拥有活跃而广泛的贡献者社区。

GCC

GCC的概述

Xcode5之前的版本中使用的是GCC编译器,由于GCC,历史悠久,体系结构相对复杂,功能模块化复用难度大且不受苹果公司的约束,很难满足苹果系统的发展需求。因此在Xcode5中抛弃了GCC,采用Clang/LLVM进行编译。

GCC:是GNU Compiler Collection的缩写,指GNU编译器套装。Linux系统的核心组成部分就有GNU工具链,GCC也是GNU工具链的重要组成部分,因此GCC也是作为Linux系统的标准编译器。GCC可处理的语言有CC++Objective-CJavaGo等。

GCC编译流程

使用GCC命令gcc -ccc-print-phases main.m查看编译OC的步骤:

*deMacBook-Pro:Mach-O *$ gcc -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

GCC的架构

GCC架构

前端读取源文件将其转化为AST,由于每种语言生成的AST是有差异的,所以需要需要转换为通用的与语言无关的统一形式GENERIC

中端将GENERIC,利用gimplifier技术,简化GENERIC的复杂结构,将其转换为一种中间表示形式称为:GIMPLE,再转换为另一种SSAstatic single assignment)的表示形式也是用于优化的,GCCSSA树执行20多种不同的优化。经过SSA优化后,该树将转换回GIMPLE形式,用来生成一个RTL树,RTL寄存器转换语言,全称(register-transfer language);RTL是基于硬件的表示形式,与抽象的目标架构相对应,处理寄存器分配、指令调度等。RTL优化过程以RTL形式对树进行优化。

后端使用RTL表示形式生成目标架构的汇编代码。如:x86后端。

LLVM

LLVM的概述

LLVM项目是模块化和可重用的编译器及工具链技术的集合。名称LLVMLow Level Virtual Machine的缩写,尽管名称如此,但是LLVM与传统虚拟机关系不大,它是LLVM项目的全名。

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name "LLVM" itself is not an acronym; it is the full name of the project. 引自LLVM官网

LLVM有许多的子项目,比如Clang,LLDB,MLIR等。

LLVM的历史

LLVM的架构

LLVM最重要的设计是中间表示LLVM Intermediate Representation(IR),它是在编译器中表示代码的一种形式。优化器使用LLVM IR作中间的转换与分析处理。LLVM IR本身就是具有良好语义定义的一流语言。

在基于LLVM的编译器中,Frontend负责对输入的代码进行解析,校验和分析错误,然后将解析后的代码转换为LLVM IR(通常情况,是将构建的抽象语法树AST转换为LLVM IR,但不总是这样的)。可以选择通过一系列分析和优化过程来传递LLVM IR,以改进代码,然后将其发送到代码生成器(Backend)中,生成原始的机器码。

LLVM流程

LLVM IR不仅是完整的代码表示,而且也是优化器optimizer的唯一接口。这意味着写一个LLVM的前端只需要知道LLVM IR即可,这是LLVM的一个新颖的特性,也是LLVM成功地被广泛应用的一个主要原因。反观GCC编译器,写一个前端需要知道生成的GCC树的数据结构以及使用GIMPLE去写GCC的前端,GCC后端需要知道RTL是如何工作的。

LLVM IR是前端输出,后端的输入:

LLVM架构

LLVM广义是指LLVM整个架构,狭义指Clang编译器的后端。

Clang

ClangLLVM的子项目,是CC++Objective C语言的编译器的前端。Clang编译Objective-C代码时速度为GCC3倍。详见维基百科

Clang编译过程

下面是一个基于简单的OC工程,不依赖Xcode,而是使用终端编译的例子。

编译前工程源代码主要分为main.mPerson.m类,代码如下:

///main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#define SomeDefine @"你好,世界"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 注释
        NSLog(@"Hello, World!");
#pragma mark 我是注释
        NSLog(@"%@",SomeDefine);
        /// MARK: 我也是注释
        Person *instance = [[Person alloc]init];
        [instance share];
    }
    return 0;
}
///Person.m
#import "Person.h"
@implementation Person
- (void)share {
    NSLog(@"持之以恒");
}
@end

首先我们运行clang -ccc-print-phases main.m查看整体的编译过程:

*deMacBook-Pro:Mach-O *$ 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

接下来,基于这个例子,我们使用终端逐步编译,生成我们的可执行文件,并最终控制台打印我们的信息。

预处理

基于输入,通过预处理器执行一系列的文本转换与文本处理。预处理器是在真正的编译开始之前由编译器调用的独立程序。

终端命令

# 编译阶段选择参数: -E 运行预处理这一步
clang -E main.m 
# 预处理结果输出到main.mi文件中
clang -E main.m -o main.mi

输出结果

# 193 "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 9 "main.m" 2
# 1 "./Person.h" 1
# 10 "./Person.h"
#pragma clang assume_nonnull begin

@interface Person : NSObject
- (void)share;
@end
#pragma clang assume_nonnull end
# 10 "main.m" 2 

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSLog(@"Hello, World!");

        NSLog(@"%@",@"你好,世界");

        Person *instance = [[Person alloc]init];
        [instance share];
    }
    return 0;
}

最终C输出.i文件,C++输出.ii文件,Objective-C输出.mi文件,Objective-C ++输出.mii文件。

预处理的任务:

在预处理的输出中,源文件名和行号信息会以# linenum filename flags形式传递,这被称为行标记,代表着接下来的内容开始于源文件filename的第linenum行,而flags则会有0或者多个,有1234;如果有多个flags时,彼此使用分号隔开。详见此处

每个标识的表示内容如下:

比如# 10 "main.m" 2,表示导入Person.h文件后回到main.m文件的第10行。

词法分析

词法分析属于预处理部分,词法分析的整个过程,主要是按照:标识符、 数字、字符串文字、 标点符号,将我们的代码分割成许多字符串序列,其中每个元素我们称之为Token,整个过程称为Tokenization

终端输入:

# -fmodules: Enable the 'modules' language feature
# -fsyntax-only, Run the preprocessor, parser and type checking stages
#-Xclang <arg>: Pass <arg> to the clang compiler
# -dump-tokens: Run preprocessor, dump internal rep of tokens

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

-fmodules:启用“模块”语言功能。关于Modules特性,详见此处,大意为使用import代替include,编译速度快。

-fsyntax-only:运行预处理器,解析器和类型检查阶段。

-Xclang <arg>:传递参数到clang的编译器。

dump-tokens:运行预处理器,转储Token的内部表示。

更多关于Clang参数的描述,请前往此处

输出结果:

....
int 'int'    [StartOfLine]  Loc=<main.m:11:1>
identifier 'main'    [LeadingSpace] Loc=<main.m:11:5>
l_paren '('     Loc=<main.m:11:9>
int 'int'       Loc=<main.m:11:10>
identifier 'argc'    [LeadingSpace] Loc=<main.m:11:14>
comma ','       Loc=<main.m:11:18>
const 'const'    [LeadingSpace] Loc=<main.m:11:20>
char 'char'  [LeadingSpace] Loc=<main.m:11:26>
star '*'     [LeadingSpace] Loc=<main.m:11:31>
identifier 'argv'    [LeadingSpace] Loc=<main.m:11:33>
l_square '['        Loc=<main.m:11:37>
r_square ']'        Loc=<main.m:11:38>
r_paren ')'     Loc=<main.m:11:39>
l_brace '{'  [LeadingSpace] Loc=<main.m:11:41>
at '@'   [StartOfLine] [LeadingSpace]   Loc=<main.m:12:5>
identifier 'autoreleasepool'        Loc=<main.m:12:6>
l_brace '{'  [LeadingSpace] Loc=<main.m:12:22>
identifier 'NSLog'   [StartOfLine] [LeadingSpace]   Loc=<main.m:14:9>
l_paren '('     Loc=<main.m:14:14>
at '@'      Loc=<main.m:14:15>
string_literal '"Hello, World!"'        Loc=<main.m:14:16>
r_paren ')'     Loc=<main.m:14:31>
semi ';'        Loc=<main.m:14:32>
identifier 'NSLog'   [StartOfLine] [LeadingSpace]   Loc=<main.m:16:9>
l_paren '('     Loc=<main.m:16:14>
at '@'      Loc=<main.m:16:15>
string_literal '"%@"'       Loc=<main.m:16:16>
comma ','       Loc=<main.m:16:20>
at '@'      Loc=<main.m:16:21 <Spelling=main.m:10:20>>
string_literal '"你好,世界"'        Loc=<main.m:16:21 <Spelling=main.m:10:21>>
r_paren ')'     Loc=<main.m:16:31>
semi ';'        Loc=<main.m:16:32>
identifier 'Person'  [StartOfLine] [LeadingSpace]   Loc=<main.m:18:9>
star '*'     [LeadingSpace] Loc=<main.m:18:16>
identifier 'instance'       Loc=<main.m:18:17>
equal '='    [LeadingSpace] Loc=<main.m:18:26>
l_square '['     [LeadingSpace] Loc=<main.m:18:28>
l_square '['        Loc=<main.m:18:29>
identifier 'Person'     Loc=<main.m:18:30>
identifier 'alloc'   [LeadingSpace] Loc=<main.m:18:37>
r_square ']'        Loc=<main.m:18:42>
identifier 'init'       Loc=<main.m:18:43>
r_square ']'        Loc=<main.m:18:47>
semi ';'        Loc=<main.m:18:48>
l_square '['     [StartOfLine] [LeadingSpace]   Loc=<main.m:19:9>
identifier 'instance'       Loc=<main.m:19:10>
identifier 'share'   [LeadingSpace] Loc=<main.m:19:19>
r_square ']'        Loc=<main.m:19:24>
semi ';'        Loc=<main.m:19:25>
r_brace '}'  [StartOfLine] [LeadingSpace]   Loc=<main.m:20:5>
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>

词法分析中Token包含信息(详请见此处):

  1. StartOfLine:表示这是每行开始的第一个Token

  2. LeadingSpace:当通过宏扩展Token时,在Token之前有一个空格字符。该标志的定义是依据预处理器的字符串化要求而进行的非常严格地定义。

  3. DisableExpand:该标志在预处理器内部使用,用来表示identifier令牌禁用宏扩展。

  4. NeedsCleaning:如果令牌的原始拼写包含三字符组或转义的换行符,则设置此标志。

语法分析(Parsing)与语义分析

此阶段对输入文件进行语法分析,将预处理器生成的Tokens转换为语法分析树;一旦生成语法分析树后,将会进行语义分析,执行类型检查和代码格式检查。这个阶段负责生成大多数编译器警告以及语法分析过程的错误。最终输出AST(抽象语法树)。

Parser的意义与作用

所谓 parser,一般是指把某种格式的文本(字符串)转换成某种数据结构的过程。最常见的 parser,是把程序文本转换成编译器内部的一种叫做“抽象语法树”(AST)的数据结构。摘自对 Parser 的误解-王垠

AST的示意图(来源):

AST示意图

终端输入:

# -fmodules: Enable the 'modules' language feature
# -fsyntax-only, Run the preprocessor, parser and type checking stages
#-Xclang <arg>: Pass <arg> to the clang compiler
# -ast-dump: Build ASTs and then debug dump them

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

输出结果:

TranslationUnitDecl 0x7f80ea01c408 <<invalid sloc>> <invalid sloc> <undeserialized declarations>
|-TypedefDecl 0x7f80ea01cca0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7f80ea01c9a0 '__int128'
#...
# cutting out internal declarations of clang
#...
|-ImportDecl 0x7f80ea27d9d8 <main.m:8:1> col:1 implicit Foundation
|-ImportDecl 0x7f80ea27da18 <./Person.h:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f80ea294ff8 <line:12:1, line:14:2> line:12:12 Person
| |-super ObjCInterface 0x7f80ea27db18 'NSObject'
| `-ObjCMethodDecl 0x7f80ea2951f0 <line:13:1, col:14> col:1 - share 'void'
`-FunctionDecl 0x7f80ea295620 <main.m:11:1, line:22:1> line:11:5 main 'int (int, const char **)'
  |-ParmVarDecl 0x7f80ea2953b0 <col:10, col:14> col:14 argc 'int'
  |-ParmVarDecl 0x7f80ea2954d0 <col:20, col:38> col:33 argv 'const char **':'const char **'
  `-CompoundStmt 0x7f80ea29e5b8 <col:41, line:22:1>
    |-ObjCAutoreleasePoolStmt 0x7f80ea29e570 <line:12:5, line:20:5>
    | `-CompoundStmt 0x7f80ea29e540 <line:12:22, line:20:5>
    |   |-CallExpr 0x7f80ea2a26f0 <line:14:9, col:31> 'void'
    |   | |-ImplicitCastExpr 0x7f80ea2a26d8 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
    |   | | `-DeclRefExpr 0x7f80ea2a25e0 <col:9> 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)'
    |   | `-ImplicitCastExpr 0x7f80ea2a2718 <col:15, col:16> 'id':'id' <BitCast>
    |   |   `-ObjCStringLiteral 0x7f80ea2a2660 <col:15, col:16> 'NSString *'
    |   |     `-StringLiteral 0x7f80ea2a2638 <col:16> 'char [14]' lvalue "Hello, World!"
    |   |-CallExpr 0x7f80ea298298 <line:16:9, col:31> 'void'
    |   | |-ImplicitCastExpr 0x7f80ea298280 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
    |   | | `-DeclRefExpr 0x7f80ea2a2730 <col:9> 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)'
    |   | |-ImplicitCastExpr 0x7f80ea2982c8 <col:15, col:16> 'id':'id' <BitCast>
    |   | | `-ObjCStringLiteral 0x7f80ea2a27a8 <col:15, col:16> 'NSString *'
    |   | |   `-StringLiteral 0x7f80ea2a2788 <col:16> 'char [3]' lvalue "%@"
    |   | `-ObjCStringLiteral 0x7f80ea298260 <line:10:20, col:21> 'NSString *'
    |   |   `-StringLiteral 0x7f80ea298238 <col:21> 'char [16]' lvalue "\344\275\240\345\245\275\357\274\214\344\270\226\347\225\214"
    |   |-DeclStmt 0x7f80ea29e4a8 <line:18:9, col:48>
    |   | `-VarDecl 0x7f80ea298320 <col:9, col:47> col:17 used instance 'Person *' cinit
    |   |   |-ObjCMessageExpr 0x7f80ea2988d0 <col:28, col:47> 'Person *' selector=init
    |   |   | `-ObjCMessageExpr 0x7f80ea298658 <col:29, col:42> 'Person *' selector=alloc class='Person'
    |   |   `-FullComment 0x7f80ea2a3900 <line:17:12, col:33>
    |   |     `-ParagraphComment 0x7f80ea2a38d0 <col:12, col:33>
    |   |       `-TextComment 0x7f80ea2a38a0 <col:12, col:33> Text=" MARK: 我也是注释"
    |   `-ObjCMessageExpr 0x7f80ea29e510 <line:19:9, col:24> 'void' selector=share
    |     `-ImplicitCastExpr 0x7f80ea29e4f8 <col:10> 'Person *' <LValueToRValue>
    |       `-DeclRefExpr 0x7f80ea29e4c0 <col:10> 'Person *' lvalue Var 0x7f80ea298320 'instance' 'Person *'
    `-ReturnStmt 0x7f80ea29e5a8 <line:21:5, col:12>
      `-IntegerLiteral 0x7f80ea29e588 <col:12> 'int' 0

ClangAST是从TranslationUnitDecl节点开始进行递归遍历的;AST中许多重要的Node,继承自TypeDeclDeclContextStmt

代码优化和生成

这个阶段主要任务是将AST转换为底层中间的代码LLVM IR,并且最终生成机器码;期间负责生成目标架构的代码以及优化生成的代码。最终输出.s文件(汇编文件)。

LLVM IR有三种格式:

LLVM提供了.ll.bc相互转换的工具:

终端输入:

# -S : Run LLVM generation and optimization stages and target-specific code generation,producing an assembly file
# -fobjc-arc : Synthesize retain and release calls for Objective-C pointers
# -emit-llvm : Use the LLVM representation for assembler and object files
# -o <file> : Write output to <file>

# 汇编表示成.ll文件 -fobjc-arc 可忽略,不作代码优化
clang -S -fobjc-arc -emit-llvm main.m -o main.ll 

# 目标文件表示成 .bc 文件
#  -c : Only run preprocess, compile, and assemble steps
clang -emit-llvm -c main.m -o main.bc
#.ll与.bc的相互转换
llvm-as main.ll -o main.bc
llvm-dis main.bc -o main.ll

此处使用了参数-emit-llvm,来查看LLVM IR

输出结果:

# 此处只贴main函数部分
define i32 @main(i32 %0, i8** %1) #1 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca %0*, align 8
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  %7 = call i8* @llvm.objc.autoreleasePoolPush() #2
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %1*))
  %8 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
  %9 = bitcast %struct._class_t* %8 to i8*
  %10 = call i8* @objc_alloc_init(i8* %9)
  %11 = bitcast i8* %10 to %0*
  store %0* %11, %0** %6, align 8
  %12 = load %0*, %0** %6, align 8
  %13 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
  %14 = bitcast %0* %12 to i8*
  call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %14, i8* %13)
  %15 = bitcast %0** %6 to i8**
  call void @llvm.objc.storeStrong(i8** %15, i8* null) #2
  call void @llvm.objc.autoreleasePoolPop(i8* %7)
  ret i32 0
}

代码优化

Clang代码优化参数有-O0-O1-O2-O3-Ofast-Os-Oz-Og-O-O4

终端输入:

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

输出结果:

#LLVM IR文件头信息
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
#结构体的定义
%0 = type opaque
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
%struct._class_t = type { %struct._class_t*, %struct._class_t*, %struct._objc_cache*, i8* (i8*, i8*)**, %struct._class_ro_t* }
%struct._objc_cache = type opaque
%struct._class_ro_t = type { i32, i32, i32, i8*, i8*, %struct.__method_list_t*, %struct._objc_protocol_list*, %struct._ivar_list_t*, i8*, %struct._prop_list_t* }
%struct.__method_list_t = type { i32, i32, [0 x %struct._objc_method] }
%struct._objc_method = type { i8*, i8*, i8* }
%struct._objc_protocol_list = type { i64, [0 x %struct._protocol_t*] }
%struct._protocol_t = type { i8*, i8*, %struct._objc_protocol_list*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct._prop_list_t*, i32, i32, i8**, i8*, %struct._prop_list_t* }
%struct._ivar_list_t = type { i32, i32, [0 x %struct._ivar_t] }
%struct._ivar_t = type { i64*, i8*, i8*, i32, i32 }
%struct._prop_list_t = type { i32, i32, [0 x %struct._prop_t] }
%struct._prop_t = type { i8*, i8* }
# 全局变量、私有/外部/内部常量的定义或声明
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [14 x i8] c"Hello, World!\00", section "__TEXT,__cstring,cstring_literals", align 1
 # --全局结构体定义与初始化
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i64 13 }, section "__DATA,__cfstring", align 8 #0
@.str.1 = private unnamed_addr constant [3 x i8] c"%@\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_.2 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8 #0
@.str.3 = private unnamed_addr constant [6 x i16] [i16 20320, i16 22909, i16 -244, i16 19990, i16 30028, i16 0], section "__TEXT,__ustring", align 2
@_unnamed_cfstring_.4 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([6 x i16]* @.str.3 to i8*), i64 5 }, section "__DATA,__cfstring", align 8 #0
@"OBJC_CLASS_$_Person" = external global %struct._class_t
@"OBJC_CLASSLIST_REFERENCES_$_" = internal global %struct._class_t* @"OBJC_CLASS_$_Person", section "__DATA,__objc_classrefs,regular,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_ = private unnamed_addr constant [6 x i8] c"share\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_ = internal externally_initialized global i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i64 0, i64 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@llvm.compiler.used = appending global [3 x i8*] [i8* bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8*), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_ to i8*)], section "llvm.metadata"
# main函数的入口:`dso_local`:main函数解析为统一链接单元的符号,而非外部替换的符号
; Function Attrs: ssp uwtable
define dso_local i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #1 {
  %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #2
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)), !clang.arc.no_objc_arc_exceptions !8
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %0* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %0*)), !clang.arc.no_objc_arc_exceptions !8
  %4 = load i8*, i8** bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8**), align 8
  %5 = tail call i8* @objc_alloc_init(i8* %4), !clang.arc.no_objc_arc_exceptions !8
  %6 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
  tail call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %5, i8* %6), !clang.arc.no_objc_arc_exceptions !8
  tail call void @llvm.objc.release(i8* %5) #2, !clang.imprecise_release !8
  tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #2
  ret i32 0
}
#函数声明
; Function Attrs: nounwind
declare i8* @llvm.objc.autoreleasePoolPush() #2

declare void @NSLog(i8*, ...) local_unnamed_addr #3

declare i8* @objc_alloc_init(i8*) local_unnamed_addr

; Function Attrs: nonlazybind
declare i8* @objc_msgSend(i8*, i8*, ...) local_unnamed_addr #4

; Function Attrs: nounwind
declare void @llvm.objc.release(i8*) #2

; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(i8*) #2

#属性组
attributes #0 = { "objc_arc_inert" }
attributes #1 = { ssp uwtable "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nounwind }
attributes #3 = { "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #4 = { nonlazybind }

#该`module`的元数据
##命名元数据
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}
##未命名的元数据
!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"clang version 12.0.0"}
!8 = !{}

浅析 LLVM IR

代码生成

生成目标架构的汇编代码。

终端输入:

#生成目标架构的汇编代码
clang -S -fobjc-arc main.m -o main.s

输出结果:

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 15    sdk_version 10, 15, 6
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## %bb.0:
    pushq   %rbp #将%rbp的内容压栈,保存栈帧到%rsp中
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp # 将栈指针传送至%rbp中,设置当前栈帧
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp # 栈指针 - 32 (申请32个字节的空间)
    movl    $0, -4(%rbp)# 将 0 传送至存储器中,存储器位置为: M[-4 + %rbp] 
    movl    %edi, -8(%rbp) # 将%edi的内容 传送至存储器中,存储器位置为: M[-8 + %rbp] 
    movq    %rsi, -16(%rbp)# 将%rsi的内容 传送至存储器中,存储器位置为: M[-16 + %rbp] 
    callq   _objc_autoreleasePoolPush #调用_objc_autoreleasePoolPush
    leaq    L__unnamed_cfstring_(%rip), %rcx #将`L__unnamed_cfstring_(%rip)`的有效地址写入`%rcx`中
    movq    %rcx, %rdi # 将%rcx的内容 传送至寄存器%rdi
    movq    %rax, -32(%rbp)         ## 8-byte Spill # 将%rax的内容 传送至存储器中,存储器位置为: M[-32 + %rbp] 
    movb    $0, %al # 将立即数0 传送至寄存器的低八位的单字节寄存器`%al`中
    callq   _NSLog #调用 _NSLog
    leaq    L__unnamed_cfstring_.2(%rip), %rcx 
    leaq    L__unnamed_cfstring_.4(%rip), %rdx
    movq    %rcx, %rdi # 将%rcx的内容 传送至寄存器%rdi
    movq    %rdx, %rsi #将%rdx的内容 传送至寄存器%rsi
    movb    $0, %al # 将立即数0 传送至寄存器的低八位的单字节寄存器`%al`中
    callq   _NSLog #调用 _NSLog
    movq    _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx
    movq    %rcx, %rdi
    callq   _objc_alloc_init
    movq    %rax, -24(%rbp)
    movq    -24(%rbp), %rax
    movq    _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
    movq    %rax, %rdi
    callq   *_objc_msgSend@GOTPCREL(%rip)
    xorl    %r8d, %r8d # 使用异或对寄存器`%r8d`清0
    movl    %r8d, %esi
    leaq    -24(%rbp), %rax
    movq    %rax, %rdi
    callq   _objc_storeStrong
    movq    -32(%rbp), %rdi         ## 8-byte Reload
    callq   _objc_autoreleasePoolPop
    xorl    %eax, %eax # 使用异或对寄存器`%eax`清0
    addq    $32, %rsp
    popq    %rbp #将%rbp的内容弹出栈
    retq
    .cfi_endproc
                                        ## -- End function
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  "Hello, World!"

    .section    __DATA,__cfstring
    .p2align    3               ## @_unnamed_cfstring_
L__unnamed_cfstring_:
    .quad   ___CFConstantStringClassReference
    .long   1992                    ## 0x7c8
    .space  4
    .quad   L_.str
    .quad   13                      ## 0xd

    .section    __TEXT,__cstring,cstring_literals
L_.str.1:                               ## @.str.1
    .asciz  "%@"

    .section    __DATA,__cfstring
    .p2align    3               ## @_unnamed_cfstring_.2
L__unnamed_cfstring_.2:
    .quad   ___CFConstantStringClassReference
    .long   1992                    ## 0x7c8
    .space  4
    .quad   L_.str.1
    .quad   2                       ## 0x2

    .section    __TEXT,__ustring
    .p2align    1               ## @.str.3
l_.str.3:
    .short  20320                   ## 0x4f60
    .short  22909                   ## 0x597d
    .short  65292                   ## 0xff0c
    .short  19990                   ## 0x4e16
    .short  30028                   ## 0x754c
    .short  0                       ## 0x0

    .section    __DATA,__cfstring
    .p2align    3               ## @_unnamed_cfstring_.4
L__unnamed_cfstring_.4:
    .quad   ___CFConstantStringClassReference
    .long   2000                    ## 0x7d0
    .space  4
    .quad   l_.str.3
    .quad   5                       ## 0x5

    .section    __DATA,__objc_classrefs,regular,no_dead_strip
    .p2align    3               ## @"OBJC_CLASSLIST_REFERENCES_$_"
_OBJC_CLASSLIST_REFERENCES_$_:
    .quad   _OBJC_CLASS_$_Person

    .section    __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_:                  ## @OBJC_METH_VAR_NAME_
    .asciz  "share"

    .section    __DATA,__objc_selrefs,literal_pointers,no_dead_strip
    .p2align    3               ## @OBJC_SELECTOR_REFERENCES_
_OBJC_SELECTOR_REFERENCES_:
    .quad   L_OBJC_METH_VAR_NAME_

    .section    __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
    .long   0
    .long   64

.subsections_via_symbols

汇编指令

所有以.开头的行,都是指导编译器与链接器的命令。

更多细节请查看苹果官网

汇编器

这个阶段主要任务是运行目标架构的汇编程序(汇编器),将编译器的输出转换为目标架构的目标(object)文件,即:.o文件。

终端输入:

# -c : Run all of the above, plus the assembler, generating a target ".o" object  file.
# -o : write to file
clang -c main.m -o main.o
clang -c Person.m -o person.o

输出结果:

#使用命令查看生成文件
#file main.o person.o
#输出
main.o:   Mach-O 64-bit object x86_64
person.o: Mach-O 64-bit object x86_64

通过汇编器将可读的汇编代码,转换为目标架构的目标文件,最终输出.o文件,也称机器码。

链接器

这个阶段会运行目标架构的链接器,将多个object文件合并成一个可执行文件或动态库。最终的输出a.out.dylib.so

在上述OC代码示例中,Main函数中引用了Person类,因此若要生成可执行的文件,需要将main.operson.o进行链接

终端输入:

#  no stage selection option 
#  If  no  stage  selection  option is specified, all stages above are run, and the
#  linker is run to combine the results into an executable or shared library.
clang main.o person.o -o main

输出结果:

"_NSLog", referenced from:
      _main in main.o
      -[Person share] in person.o
  "_OBJC_CLASS_$_NSObject", referenced from:
      _OBJC_CLASS_$_Person in person.o
  "_OBJC_METACLASS_$_NSObject", referenced from:
      _OBJC_METACLASS_$_Person in person.o
  "___CFConstantStringClassReference", referenced from:
      CFString in main.o
      CFString in main.o
      CFString in main.o
      CFString in person.o
  "__objc_empty_cache", referenced from:
      _OBJC_METACLASS_$_Person in person.o
      _OBJC_CLASS_$_Person in person.o
  "_objc_alloc_init", referenced from:
      _main in main.o
  "_objc_autoreleasePoolPop", referenced from:
      _main in main.o
  "_objc_autoreleasePoolPush", referenced from:
      _main in main.o
  "_objc_msgSend", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture x86_64
clang-12: error: linker command failed with exit code 1 (

链接器未找到上述的符号,原因是我们代码引入了Foundation库,在生成可执行文件时,未进行链接。

在解决这个问题之前先介绍一下工具xcrun,使用xcrun可以从命令行定位和调用开发者工具

#--show-sdk-path : show selected SDK install path
xcrun --show-sdk-path
# 输出`MacOSX.sdk`的路径
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

基于此路径链接我们的Foundation库:

#  -Wl,<arg>   Pass the comma separated arguments in <arg> to the linker #传参给链接器
# `xcrun --show-sdk-path` 等同 $(xcrun --show-sdk-path) 视为命令替换
clang main.o person.o  -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation -o main

最终输出如下图:

输出的可执行文件

执行这个可执行文件:

#执行
./main
#输出
2021-05-08 17:40:45.134 main[30561:1257231] Hello, World!
2021-05-08 17:40:45.135 main[30561:1257231] 你好,世界
2021-05-08 17:40:45.135 main[30561:1257231] 持之以恒

main文件查看:

file main
#输出
main: Mach-O 64-bit executable x86_64

符号对比

符号表查看工具nm,允许我们查看Object文件的符号表内容。

  1. 使用nm终端工具,先观察一下mian.operson.o

    #输入
    nm -nm  main.o person.o
    #输出
                     (undefined) external _NSLog
                     (undefined) external _OBJC_CLASS_$_Person 
                     (undefined) external ___CFConstantStringClassReference
                     (undefined) external _objc_alloc_init
                     (undefined) external _objc_autoreleasePoolPop
                     (undefined) external _objc_autoreleasePoolPush
                     (undefined) external _objc_msgSend
    0000000000000000 (__TEXT,__text) external _main
    00000000000000e8 (__TEXT,__ustring) non-external l_.str.3
    00000000000000f8 (__DATA,__objc_classrefs) non-external _OBJC_CLASSLIST_REFERENCES_$_
    0000000000000108 (__DATA,__objc_selrefs) non-external _OBJC_SELECTOR_REFERENCES_
                     (undefined) external _NSLog
                     (undefined) external _OBJC_CLASS_$_NSObject
                     (undefined) external _OBJC_METACLASS_$_NSObject
                     (undefined) external ___CFConstantStringClassReference
                     (undefined) external __objc_empty_cache
    0000000000000000 (__TEXT,__text) non-external -[Person share]
    0000000000000024 (__TEXT,__ustring) non-external l_.str
    0000000000000058 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person
    00000000000000a0 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person
    00000000000000c0 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person
    0000000000000108 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person
    0000000000000130 (__DATA,__objc_data) external _OBJC_CLASS_$_Person
    

    external表示该符号针对当前目标文件不是私有的,与non-external相反。undefined表示该符号未找到。

  2. 使用nm观察一下可执行文件main的符号表

    #输入
    nm -nm main
    #输出
                     (undefined) external _NSLog (from Foundation)
                     (undefined) external _OBJC_CLASS_$_NSObject (from libobjc)
                     (undefined) external _OBJC_METACLASS_$_NSObject (from libobjc)
                     (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                     (undefined) external __objc_empty_cache (from libobjc)
                     (undefined) external _objc_alloc_init (from libobjc)
                     (undefined) external _objc_autoreleasePoolPop (from libobjc)
                     (undefined) external _objc_autoreleasePoolPush (from libobjc)
                     (undefined) external _objc_msgSend (from libobjc)
                     (undefined) external dyld_stub_binder (from libSystem)
    0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
    0000000100003e80 (__TEXT,__text) external _main 
      #私有符号
    0000000100003f00 (__TEXT,__text) non-external -[Person share] 
    0000000100008020 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person
    0000000100008068 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person
    0000000100008088 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person
      #非私有
    00000001000080e0 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person
    0000000100008108 (__DATA,__objc_data) external _OBJC_CLASS_$_Person
      #私有符号
    0000000100008130 (__DATA,__data) non-external __dyld_private
    

可以发现在经过链接器处理后,为每个符号增加了来源。当我们运行可执行文件时,会由动态链接器dyld通过这些来源对处于undifined的符号进行解析,比如_NSLog,来自Foundation,在运行时会在Foundation中找到指向它的函数地址,并最终调用执行。

系统符号

目标文件的显示工具otool,可以查看Mach-O文件特定SectionSegment的内容。

  1. 可执行文件是知道它需要链接那些库的

    # -L :display the names and version  numbers of the shared libraries that the object file uses
    otool -L main
    # 输出
    main:
     /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1677.104.0)
     /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
     /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1677.104.0)
     /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    

    上述输出我们发现在链接器生成可执行文件时,我们通过-Wl传递给链接器的Foundation的路径与可执行文件最终链接的Foundation路径不一致。参数路径下的文件内容:

    image.png
  2. .tbd文件

    the .tbd files are new "text-based stub libraries", that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size. 引自stackoverflow

    .tbd是个文本文件,提供的是SDK的更简洁版本,明显的降低Xcode的下载大小,具体内容:

    .tbd文件内容

    .tbd文件包含了与文件本身相关的元数据,与架构相关的信息,还有Foundation库针对特定架构的symbols,以及该库所依赖的库。并指定了Foundation库的最终安装路径。

    Foundation
  3. 查看系统符号

    #输入
    nm -nm /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep '_NSLog'
    #输出NSlog的调用地址
    000000000004ce6e (__TEXT,__text) external _NSLog
    

总结

OC代码编译时,首先会经过预处理,接着进行词法分析将文本字符串Token化, 再通过语法与语义分析检查代码的类型与格式,最终生成AST,并在代码优化与生成阶段,将AST转换为底层的中间代码LLVM IR,并最终生成目标架构的汇编代码,交给汇编器进行处理后,将可读的汇编代码转换为目标架构的机器码,即:.O文件,通过链接器,解决.O文件与库的链接问题,最终根据特定的机器架构生成可执行文件。

参考资料

http://www.aosabook.org/en/llvm.html

https://en.m.wikibooks.org/wiki/GNU_C_Compiler_Internals/GNU_C_Compiler_Architecture

http://www.yinwang.org/blog-cn/2015/09/19/parser

https://objccn.io/issue-6-3/

https://llvm.org/docs/LangRef.html

上一篇下一篇

猜你喜欢

热点阅读