被嫌弃的源码的一生:从Clang、LLVM到MachO

2,522 阅读8分钟

Hi 👋

我的个人项目扫雷Elic 无尽天梯梦见账本
类型游戏财务
AppStoreElicUmemi

一、 编程语言类型

1.1 解释型语言

pythonJS 就属于常见的 解释型语言 。他们无需生成 可执行文件 就能够执行。

他们的执行过程如下:

LLVM-解释型语言.png

1.2 编译型语言

我们常用的 C\C++Objective-CSwift 则属于 编译型语言 需要经过下面的过程生成 可执行文件 后才能执行。

LLVM-编译型语言.png

二、 LLVM

LLVM构架编译器(compiler)框架系统 ,以 C++ 编写而成,用于优化以任意程序语言编写的程序的 编译时间(compile-time)链接时间(link-time)运行时间(run-time) 以及 空闲时间(idle-time) ,对开发者保持开放,并兼容已有脚本。

LLVM 计划启动于2000年,最初由美国UIUC大学的 ChrisLattner 博士主持开展。2006年 ChrisLattner 加盟 AppleInc 并致力于 LLVMApple 开发体系中的应用。 Apple 也是 LLVM 计划的主要资助者。

目前 LLVM 已经被 苹果IOS开发工具Xilinx VivadoFacebookGoogle 等各大公司采用。

ChrisLattner 也是 Swift 之父。给大佬🧎‍♂️了

2.1 传统编译器设计

LLVM-传统编译器设计.png

编译器前端

编译器前端的任务是 解析源代码 。它会进行: 词法分析语法分析语义分析检查源代码是否存在错误 ,然后构建 抽象语法树(Abstract Syntax Tree, AST)LLVM 的前端还会生成 中间代码(intermediate representation,IR)

优化器

优化器负责进行 各种优化改善代码的运行时间 ,例如消除冗余计算等。

后端 / 代码生成器

将代码映射到 目标指令集生成机器语言 , 并且 进行机器相关的代码优化

2.2 iOS的编译器架构

ObjectiveC / C / C++ 使用的编译器前端是 ClangSwiftSwift后端 都是 LLVM

LLVM-iOS的编译器架构.png

2.3 LLVM的设计

当编译器决定支持多种源语言或多种硬件架构时, LLVM 最重要的地方就来了。

其他的编译器如 GCC 是非常成功的一款编译器,但由于它是作为整体应用程序设计的,因此它的用途受到了很大的限制。

LLVM 设计的最重要方面是,使用 通用的代码表示形式(IR) ,它是用来在编译器中表示代码的形式。所以 LLVM 可以为任何编程语言 独立编写前端 ,并且可以为任意硬件架构 独立编写后端

三、 Clang

对于我们的开发人员来说,接触最多的就是我们的 Clang

ClangLLVM 项目中的一个子项目。它是基于 LLVM架构轻量级编译器 ,诞生之初是为了替代 GCC ,提供更快的编译速度。它是负责编译 C、C++、Objecte-C 的编译器,它属于整个 LLVM 架构中的,编译器前端。对于开发者来说,研究 Clang 可以给我们带来很多好处。

四、 Objective-C 的编译流程探索

我们以下面的代码为例进行探索

#import <Foundation/Foundation.h>

int addResult(int a, int b) {
    return a + b;
}

#define ADD_FUNCTION(x, y) addResult(x, y)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int numA = 1;
        int b1 = 1;
        int b2 = 2;
        int numB = b1 + b2;
        int result = ADD_FUNCTION(numA, numB);
        NSLog(@"结果是:%d", result);
    }
    return 0;
}

4.1 编译阶段概览

通过下面的命令我们可以列出完整的编译阶段:

clang -ccc-print-phases main.m

01-01.png

  • 0:输入文件
    • 找到源文件
  • 1:预处理
    • 这个过程处理包括宏的替换,头文件的导入
  • 2:编译
    • 进行词法分析、语法分析、检测语法是否正确,最终生成IR
  • 3:后端
    • 这里LLVM会通过一个一个的Pass(可以理解为一个节点)去优化,每个Pass做一些事情,最终生成汇编代码
  • 4:汇编
    • 生成目标文件
  • 5:链接
    • 链接需要的动态库和静态库,生成相应的镜像可执行文件
  • 6:绑定结构
    • 根据不同的系统架构,生成对应的可执行文件

4.2 预处理

clang -E main.m >> 预处理.m

01-02.png

通过对比,很明显的发现这里的宏定义被替换掉了。

4.3 编译阶段

词法分析

这里会把代码切成一个个 Token ,比如 大小括号等于号 还有 字符串 等。

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

01-03.png

语法分析

语法分析的任务是 验证语法是否正确 。在词法分析的基础上将单词序列组合成各类语法短语,如 “程序”“语句”“表达式” 等,然后将所有节点组成 抽象语法树(AbstractSyntaxTree,AST) 。语法分析其目的就是 对源程序进行分析判断,在结构上是否正确

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

01-04.png

生产中间代码IR

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

这里会生成 .ll 文件,抽取一段 addResult 相关代码简单进行一下分析:

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @addResult(i32 %0, i32 %1) #1 {
  %3 = alloca i32, align 4 // 开辟 32 bit 空间, 4个字节, 为 %3
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4 // 将 %0 参数存到 %3
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %3, align 4 // 从 %3 中读取到 %5
  %6 = load i32, i32* %4, align 4
  %7 = add nsw i32 %5, %6 // %5 + %6 的值存到 %7
  ret i32 %7 // 返回 %7 32bit
}

main 函数:

; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32 %0, i8** %1) #2 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca i32, align 4
  %7 = alloca i32, align 4
  %8 = alloca i32, align 4
  %9 = alloca i32, align 4
  %10 = alloca i32, align 4
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  %11 = call i8* @llvm.objc.autoreleasePoolPush() #3
  store i32 1, i32* %6, align 4
  store i32 1, i32* %7, align 4
  store i32 2, i32* %8, align 4
  %12 = load i32, i32* %7, align 4
  %13 = load i32, i32* %8, align 4
  %14 = add nsw i32 %12, %13
  store i32 %14, i32* %9, align 4
  %15 = load i32, i32* %6, align 4
  %16 = load i32, i32* %9, align 4
  %17 = call i32 @addResult(i32 %15, i32 %16)
  store i32 %17, i32* %10, align 4
  %18 = load i32, i32* %10, align 4
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 %18)
  call void @llvm.objc.autoreleasePoolPop(i8* %11)
  ret i32 0
}

编译优化

这里 debug 模式下默认没有进行优化。

01-05.png

我们传入对应的优化等级,再生成一次 .ll 看看(这里用了和 release 一样的 Os ):

clang -Os -S -fobjc-arc -emit-llvm main.m -o main优化过.ll

addResult

; Function Attrs: norecurse nounwind optsize readnone ssp uwtable willreturn
define i32 @addResult(i32 %0, i32 %1) local_unnamed_addr #1 {
  %3 = add nsw i32 %1, %0
  ret i32 %3
}

main

; Function Attrs: optsize ssp uwtable
define i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #2 {
  %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #3
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 4) #5, !clang.arc.no_objc_arc_exceptions !9
  tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #3
  ret i32 0
}

这里发现优化后的中间代码简介了不少,并且代码中冗余的计算逻辑直接生成了结果

01-06.png

bitCode

这是 xcode7 以后开启 bitcode 苹果会做进一步的优化,生成 bc 的中间代码。我们可以试试用优化后的 IR 代码生成 bc 代码。

clang -emit-llvm -c main优化过.ll -o main优化过.bc

这里的 bc 文件无法使用文本方式进行查看

生成汇编代码

可以通过的 .bc 或者 .ll 代码生成汇编代码

clang -S -fobjc-arc main优化过.bc -o main优化过.s
或
clang -S -fobjc-arc main未优化.ll -o main未优化.s

对比一下优化前后的汇编代码,发现优化后的指令少了很多:

01-07.png

生成目标文件

clang -fmodules -c main未优化.s -o main未优化.o

目标文件的生成,是 汇编器 以汇编代码作为 输入 ,将 汇编代码 转换为 机器代码 ,最后输出 目标文件(object-file) ,这个阶段就是属于 编译器后端 的工作了。

查看符号

xcrun nm -nm main未优化.o

输出:

                 (undefined) external _NSLog
                 (undefined) external ___CFConstantStringClassReference
                 (undefined) external _objc_autoreleasePoolPop
                 (undefined) external _objc_autoreleasePoolPush
0000000000000000 (__TEXT,__text) external _addResult
0000000000000020 (__TEXT,__text) external _main
000000000000008e (__TEXT,__ustring) non-external l_.str
  • _NSLog 是一个是 undefined external
    • undefined
      • 表示在当前文件暂时找不到符号 _NSLog
    • external
      • 表示这个符号是外部可以访问的

有兴趣可以移步 通过符号表找到符号 有介绍查找 _NSLog 的过程

生成可执行文件

连接器 把编译产生的 .o文件(dylib .a)文件 ,生成一个 mach-o文件(可执行文件)

clang main.o -o main

报错:

Undefined symbols for architecture x86_64:
  "_NSLog", referenced from:
      _main in main.o
  "___CFConstantStringClassReference", referenced from:
      CFString in main.o
  "_objc_autoreleasePoolPop", referenced from:
      _main in main.o
  "_objc_autoreleasePoolPush", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture x86_64

01-08.png

因为这里用到了 Foundation 框架内的符号 NSLog 不能直接生成,我们加上参数

clang -framework Foundation main.o -o main

生成成功:

01-09.png

查看符号

xcrun nm -nm main

输出:

                 (undefined) external _NSLog (from Foundation)
                 (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                 (undefined) external _objc_autoreleasePoolPop (from libobjc)
                 (undefined) external _objc_autoreleasePoolPush (from libobjc)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003ed0 (__TEXT,__text) external _addResult
0000000100003ef0 (__TEXT,__text) external _main
0000000100008018 (__DATA,__data) non-external __dyld_private

这里的符号就已经和对应的框架对应起来了,如: _NSLog (from Foundation) 。而且 mach-o 文件中的偏移地址也有了。

参考

LLVM

Clang

Which OSX library to link against (command line) to use NSLog?