了解-clang编译过程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了了解-clang编译过程相关的知识,希望对你有一定的参考价值。

参考技术A

第1步 : 创建源码文件 hello.c 如下:

第2步 : 对其进行预编译, 得到 .i 输出文件, 使用命令:

从中可以看到预处理做的一些工作

第3步 :编译,得到 .s 文件, 使用命令

第4步 : 汇编,得到 .o 文件, 使用命令

编译器与Clang编译过程

前言

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

编译器

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

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

  • 前端( Frontend):解析源代码,检查源代码是否有错误,并构建特定语言的抽象语法树( Abstract Syntax Tree缩写: AST)来表示输入的代码。也负责选择性的地将 AST转换为新的表示形式以进行优化。
  • 优化器( Optimizer):负责进行各种转换,以尝试改善代码的运行时间,例如消除冗余计算,并且通常或多或少地独立于编程语言和目标代码。
  • 后端( Backend):也称代码生成器,将代码映射到目标架构的指令集上;其常见部分有:指令选择,寄存器分配,指定调度。
传统编译器的架构

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

编译器与Clang编译过程
编译器架构分析

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

  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等。

编译器与Clang编译过程
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的架构

编译器与Clang编译过程
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起源于 2000Vikram AdveChris Lattner的研究,目的:为所有的静态语言( C/ C++)和动态语言(运行时改变其结构的语言,如: OC/ JavaScript )创造出动态编译技术。
  • 苹果公司 2005雇佣 Chris Lattner与他的团队为苹果电脑开发应用程序系统, LLVM为现今 macOSiOS开发工具的一部分。
  • LLVM对产业的贡献, 2012年获得了ACM软件系统奖。获得该奖项的有 UnixJavaTCP/IPDNSMach
  • 201910月开始, LLVM项目的代码托管正式迁移到了 GitHub

LLVM的架构

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

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

编译器与Clang编译过程
LLVM流程

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

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

编译器与Clang编译过程
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文件。

预处理的任务:

  • 将输入文件读到内存,并断行;

  • 替换注释为单个空格;

  • Tokenization将输入转换为一系列预处理Tokens

  • 处理#import#include将所引的库,以递归的方式,插入到#import#include所在的位置;

  • 替换宏定义;

  • 条件编译,根据条件包括或排除程序代码的某些部分;

  • 插入行标记;

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

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

  • 1表示一个新文件的开始
  • 2表示返回文件(包含另一个文件后)
  • 3表示以下文本来自系统头文件,因此应禁止某些警告
  • 4表示应将以下文本视为包装在隐式 extern "C" 块中。

比如# 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包含信息(详请见此处):

  • Sourece Location:表示Token开始的位置,比如:Loc=<main.m:11:5>

  • Token Kind:表示Token的类型,比如:identifiernumeric_constantstring_literal

  • Flags:词法分析器和处理器跟踪每个Token的基础,目前有四个Flag分别是:

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

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

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

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

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

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

Parser的意义与作用

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

AST的示意图(来源):

编译器与Clang编译过程
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

  • Type :表示类型,比如 BuiltinType
  • Decl :表示一个声明 declaration或者一个定义 definition,比如:变量,函数,结构体, typedef
  • DeclContext :用来声明表示上下文的特定 decl类型的基类;
  • Stmt :表示一条陈述 statement;
  • Expr:在 Clang的语法树中也表示一条陈述 statements;

代码优化和生成

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

LLVM IR有三种格式:

  • 文本格式: .ll文件
  • 内存中用以优化自身时,执行检查和修改的数据结构(编译过程中载入内存的形式)
  • 磁盘二进制( BitCode)格式: .bc文件

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

  • llvm-as:可将 .ll转为 .bc
  • llvm-dis:可将 .bc转为 .ll

终端输入:

# -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

  • -O0:表示没有优化;编译速度最快并生成最可调试的代码
  • -O1:优化程度介于 -O0~ -O2之间。
  • -O2:适度的优化水平,可实现最优化
  • -O3:与 -O2相似,不同之处在于它优化的时间比较长,可能会生成更大的代码
  • -O4:当前等效于 -O3
  • -Ofast:启用 -O3中的所有优化并且可能启用一些激进优化
  • -Os:与 -O2一样,具有额外的优化功能以减少代码大小
  • -Oz:类似于 -Os,进一步减小了代码大小
  • -Og:类似 -O1
  • -O:相当于 -O2

终端输入:

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

  • Module:LLVM程序是由Module组成的,每个Module是输入程序的翻译单元。每个Module都是由functionsglobal variablessymbol table entries组成。Module会通过LLVM链接器组合到一起,链接器会合并函数以及全局变量的定义,解决前置声明以及合并符号表。

  • Target Datalayout:Module需要以字符串的形式指定特定于目标架构的数据布局方式,该字符串指定如何在内存中布局数据。如:target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"

  • 元数据:LLVM IR允许元数据被附加到能够传递代码额外信息给优化器和代码生成器的程序指令上。所有元数据在语法上均由!标识。元数据的两个原语:元数据字符串和元数据节点。

    1. 元数据字符串:用 ""引起来的字符串,以 !作为前缀。如: !"clang version 12.0.0"
    2. 元数据节点:用 {}括起来,使用 ,隔开多个元素,以 !作为前缀。如: !{i32 7, !"PIC Level", i32 2}
  • 命名元数据:是元数据节点的集合

    ; Some unnamed metadata nodes, which are referenced by the named metadata.
    !0 = !{!"zero"}
    !1 = !{!"one"}
    !2 = !{!"two"}
    ; A named metadata.
    !name = !{!0, !1, !2}
  • Linkage Types:所有全局变量和函数都具有链接类型,如上述IR中的:

    1. external: module外部可用
    2. private: module内部可用
    3. appending:仅应用于数组类型的全局变量的指针。当两个使用了 appending的全局变量链接到一起的时候,这两个全局的数组会被拼接到一起。
    4. internal:与 private相似,但该值在目标文件中显示为本地符号。与 C语言中 static关键字的概念相对应。
  • 属性组:属性组是IR中的对象(函数、全局变量)引用的属性组合。它们对于保持.ll文件的可读性很重要,因为许多函数将使用相同的属性集。如上述IR中的#0~#4

  • 函数属性:被用来传递一个函数附加信息。函数属性被认为是函数的一部分,而不是函数类型,所以不同的函数属性可以有相同的函数类型。上述IR中用到最多的函数属性:

    1. nounwind:表示函数不会抛出异常
    2. nonlazybind:阻止函数中某些符号的延迟绑定。
  • 参数属性:函数的返回类型以及每个参数都有与之关联的参数属性集合。被用来传递一个函数的返回值与参数的附加信息。参数属性是函数的一部分,而不是函数类型,所以有不同参数属性的函数可以有相同的参数类型。上述IR中用到最多的参数属性:

    1. nocapture:表示函数调用不会捕获参数的指针,这个属性对于返回值是无效的,仅适用于参数。
    2. readnone:应用于参数表示函数不会取消对此参数指针的引用。
  • 标识符:

    1. 命名值,表示为以上述标识符为前缀的字符串,如: %struct._ivar_t@.str
    2. 未命名值,表示为以上述标识为前缀的无符号的数值,如: %0%1
    1. @为全局标识符。以其开头标识函数,全局变量;
    2. %为本地标识符。以其开头标识寄存器名称,类型;
    3. 标识符的不同的格式:
  • 结构体的定义:

    #语法
    %T1 = type { <type list> }     ; Identified normal struct type
    %T2 = type <{ <type list> }>   ; Identified packed struct type #表示结构体的对齐方式为1字节
    #示例
    {i32, i32}
    %mytype = type { %mytype*, i32 }
  • 数组的定义:

    #语法
    [<# elements> x <elementtype>]
    #语义
    `elements`是个`integer`的值;`elementtype`是任意有大小的类型
    #示例
    [40 x i32] Array of 40 32-bit integer values
  • 全局变量的定义:

    #语法
    @<GlobalVarName> = [Linkage] [PreemptionSpecifier] [Visibility]
                       [DLLStorageClass] [ThreadLocal]
                       [(unnamed_addr|local_unnamed_addr)] [AddrSpace]
                       [ExternallyInitialized]
                       <global | constant> <Type> [<InitializerConstant>]
                       [, section "name"] [, comdat [($name)]]
                       [, align <Alignment>] (, !name !N)*
    #示例
    @G = external global i32 #just declare
    @G = external global i32 8 #InitializerConstant
    1. global constant:表示该变量的内容将 永远不会被修改。
    2. unnamed_addr:表示该变量的地址并不重要,仅指示内容。
    3. local_unnamed_addr:表示变量的地址在 module内并不重要。
  • Runtime Preemption Specifiers:运行时抢占说明符。全局变量,函数和别名可以具有一个可选的运行时抢占说明符。如果未明确指定抢占说明符,则假定该符号为dso_preemptable

    1. dso_preemptable:表示函数或者变量在运行时会被外部的链接单元替换
    2. dso_local:表示函数或变量将解析为同一链接单元中的符号。即使定义不在此编译单元内,也将生成直接访问
  • call:代表一个简单的函数调用;

    #语法
    <result> = [tail | musttail | notail ] call [fast-math flags] [cconv] [ret attrs] [addrspace(<num>)]
               <ty>|<fnty> <fnptrval>(<function args>) [fn attrs] [ operand bundles ]
    1. 可选的tailmusttail标记优化器应执行尾部调用优化

    2. notail标记用于防止执行尾部调用优化

  • ret:该指令表示函数返回

    #语法
    ret <type> <value>       ; Return a value from a non-void function
    ret void                 ; Return from void function
    #示例
    ret i32 5                       ; Return an integer value of 5
    ret void                        ; Return from a void function
    ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2
  • bitcast...to:bitcastvalue 的类型转换为类型 ty2 而不改变它的任何位bits

    #语法
    <result> = bitcast <ty> <value> to <ty2>             ; yields ty2
  • 其他:i32:代表32-bit的整数,i8:代表8-bit的整数;

代码生成

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

终端输入:

#生成目标架构的汇编代码
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

汇编指令

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

  • .section指定汇编器将生成的汇编代码,写入对应的区section

    语法:

    .section  segname , sectname [[[ , type ] , attribute ] , sizeof_stub ]

    示例:

    #`regular`类型 表示该区存放程序指令或初始化数据
    #`pure_instructions`属性 表示此区仅包含机器指令
    .section __TEXT,__text,regular,pure_instructions
    #`cstring_literals`类型 表示该区存放以null结尾的c字符串
    .section __TEXT,__cstring,cstring_literals
  • .global symbol_name标记符号为外部符号;

  • .align对齐指令,指定汇编代码的对齐方式

    语法:

    .align    align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]]
    .p2align  align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]]
    .p2alignw align_expression [ , 2byte_fill_expression [,max_bytes_to_fill]]
    .p2alignl align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]]
    .align32  align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]]

    示例:

    # 以16(2^4)字节的方式对齐,不足的使用0x90补齐
    .p2align 4, 0x90
  • CFA

    An area of memory that is allocated on a stack called a “call frame.” The call frame is identified by an address on the stack. We refer to this address as the Canonical Frame Address or CFA. Typically, the CFA is defined to be the value of the stack pointer at the call site in the previous frame (which may be different from its value on entry to the current frame).引自DWARF规范-6.4

    1. .cfi_def_cfa_register REGISTERcfi_def_cfa_register这个指令让%ebp%rbp被设置为新值且偏移量保持不变。

      上述设置只是为了用来辅助调试的,比如打断点,获取调用堆栈信息。

  • CFI

    调用帧信息,英文全称:Call Frame Information

    1. cfi_startproc,表示函数或过程开始。
    2. .cfi_endproc,表示函数或过程结束。

更多细节请查看苹果官网

汇编器

这个阶段主要任务是运行目标架构的汇编程序(汇编器),将编译器的输出转换为目标架构的目标(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

最终输出如下图:

编译器与Clang编译过程
输出的可执行文件

执行这个可执行文件:

#执行
./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

系统符号

目标文件的显示工具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路径不一致。参数路径下的文件内容:编译器与Clang编译过程

  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


以上是关于了解-clang编译过程的主要内容,如果未能解决你的问题,请参考以下文章

Clang前端源码分析

IOS逆向-LLVM代码混淆

Impala中 LLVM 的交叉编译、调用过程

Clang如何处理MSVC的编译参数

一文带你梳理Clang编译步骤及命令

编译器:gcc, clang, llvm