LLVM与Clang的一些事儿

9,352 阅读10分钟

在说这篇文章之前,首先我们带入一个问题,在Xcode中我们最常使用的一个组合键cmd+b按下之后都进行了哪一些工作?伟大的ARC内存管理方式又是如何实现内存管理的?

又或者我不了解编译过程代码照样撸得飞起,摸透这晦涩难理解的东西有什么用?

下面要开始啰嗦了

LLVM简介-来自https://zh.wikipedia.org/wiki/LLVM

LLVM项目的发展起源于2000年伊利诺伊大学厄巴纳-香槟分校维克拉姆·艾夫(Vikram Adve)与克里斯·拉特纳(Chris Lattner)的研究,他们想要为所有静态及动态语言创造出动态的编译技术。LLVM是以BSD授权来发展的开源软件。2005年,苹果电脑雇用了克里斯·拉特纳及他的团队为苹果电脑开发应用程序系统,LLVM为现今Mac OS X及iOS开发工具的一部分。

LLVM的命名最早源自于底层虚拟机(Low Level Virtual Machine)的首字母缩写,由于这个项目的范围并不局限于创建一个虚拟机,这个缩写导致了广泛的疑惑。LLVM开始成长之后,成为众多编译工具及低级工具技术的统称,使得这个名字变得更不贴切,开发者因而决定放弃这个缩写的意涵,现今LLVM已单纯成为一个品牌,适用于LLVM下的所有项目,包含LLVM中介码(LLVM IR)、LLVM除错工具、LLVM C++标准库等。


关于swift之父加入Apple有个有趣的故事

Xcode3之前,用的是GCC
Xcode3,GCC仍然保留,但是也推出了LLVM,苹果推荐LLVM-GCC混合编译器,但还不是默认编译器
Xcode4,LLVM-GCC成为默认编译器,但GCC仍保留
Xcode4.2,LLVM3.0成为默认编译器,纯用GCC不复可能
Xcode4.6,LLVM升级到4.2版本
Xcode5,LLVM-GCC被遗弃,新的编译器是LLVM5.0,从GCC过渡到LLVM的时代正式完成

当时苹果对Objective-C新增了许多特性,但这时的Apple使用的是当时一手遮天的GCC作为前端。GCC并不为这些新特性买账--不给实现,因此索性后来两者 分成两条分支分别开发,这也造成Apple的编译器版本远落后于GCC的官方版本。并且GCC的代码耦合度太高,不好独立,而且越是后期的版本,代码质量越差7,但Apple想做的很多功能(比如更好的IDE支持)需要模块化的方式来调用GCC,但GCC一直不给做。《GCC运行环境豁免条款 (英文版)8》从根本上限制了LLVM-GCC的开发。 所以,这种不和让Apple一直在寻找一个高效的、模块化的、协议更放松的开源替代品。而UIUC的高材生Chris Lattner的LLVM显然是一个很棒的选择。


Clang - a C language family frontend for LLVM

Clang(发音为/ˈklæŋ/) 是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。它采用了底层虚拟机(LLVM)作为其后端。它的目标是提供一个GNU编译器套装(GCC)的替代品。作者是克里斯·拉特纳,在苹果公司的赞助支持下进行开发,而源代码授权是使用类BSD的伊利诺伊大学厄巴纳-香槟分校开源码许可。 Clang项目包括Clang前端和Clang静态分析器等。

Clang的在出生之前就已经明确了他的使命——干掉该死的GCC。有了LLVM+Clang,从此,苹果的开发面貌焕然一新。从此摆脱了GCC的限制。客观的说GCC是有很多的优点,例如支持多平台,很流行,基于C无需C++编译器即可编译。这些优点到苹果那就可能是缺点了,苹果需要的是——快。这正是Clang的优点,除了快,它还有与GCC兼容,内存占用小,诊断信息可读性强,易扩展,易于IDE集成等等优点。有个测试数据:Clang编译Objective-C代码时速度为GCC的3倍。


LLDB

GCC有个强大的诊断工具——GDB,相对应的Clang下纠错工具就是LLDB。对于LLDB大家应该都不陌生,它继承了GDB的优点,弥补GDB的不足。iOS开发者从gbd过渡到lldb没有任何不适应感,最直白的原因就是lldb和gdb常用的命令很多都是一样的,例如常用的po等。

啰嗦到此结束


iOS编译过程

image.png

Objective-C与swift都采用Clang作为编译器前端,编译器前端主要进行语法分析,语义分析,生成中间代码,在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

image.png

编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化,根据不同的系统架构生成不同的机器码。

C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码。

image.png

如上图所示,在xcode按下cmd+B之后的工作流程。

预处理(Pre-process):他的主要工作就是将宏替换,删除注释展开头文件,生成.i文件。 词法分析 (Lexical Analysis):将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。 语法分析(Semantic Analysis):验证语法是否正确,然后将所有节点组成抽象语法树 AST 。由 Clang 中 Parser 和 Sema 配合完成 静态分析(Static Analysis):使用它来表示用于分析源代码以便自动发现错误。 中间代码生成(Code Generation):开始IR中间代码的生成了,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入。 优化(Optimize):LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass,官方有比较完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation 。如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。

image.png

生成目标文件(Assemble):生成Target相关Object(Mach-o) 链接(Link):生成 Executable 可执行文件

经过这一步步,我们用各种高级语言编写的代码就转换成了机器可以看懂可以执行的目标代码了。


环境搭建

cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`

git clone -b release_39 git@github.com:llvm-mirror/llvm.git llvm
git clone -b release_39 git@github.com:llvm-mirror/clang.git llvm/tools/clang
git clone -b release_39 git@github.com:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_39 git@github.com:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

mkdir llvm_build
cd llvm_build
cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release
make -j`sysctl -n hw.logicalcpu`

文件很多很大,需要下载一段时间


###Clang Static Analyzer静态代码分析

clang 静态分析是通过建立分析引擎和 checkers 所组成的架构,这部分功能可以通过 clang —analyze 命令方式调用。

####命令行执行 通过clang -cc1 -analyzer-checker-help可以列出能调用的 checker,但这些checker并不是所有都是默认开启的

这里使用一个默认关闭的checker-alpha.security.ArrayBoundV2作为例子进行操作
$ clang -cc1 -analyzer-checker-help
 alpha.core.BoolAssignment       Warn about assigning non-{0,1} values to Boolean variables
  alpha.core.CastSize             Check when casting a malloced type T, whether the size is a multiple of the size of T
  alpha.core.CastToStruct         Check for cast from non-struct pointer to struct pointer
  alpha.core.FixedAddr            Check for assignment of a fixed address to a pointer
  alpha.core.IdenticalExpr        Warn about unintended use of identical expressions in operators
  alpha.core.PointerArithm        Check for pointer arithmetic on locations other than array elements
  alpha.core.PointerSub           Check for pointer subtractions on two pointers pointing to different memory chunks
  alpha.core.SizeofPtr            Warn about unintended use of sizeof() on pointer expressions
  alpha.cplusplus.NewDeleteLeaks  Check for memory leaks. Traces memory managed by new/delete.
  alpha.cplusplus.VirtualCall     Check virtual function calls during construction or destruction
  ...
  alpha.security.ArrayBound       Warn about buffer overflows (older checker)
  alpha.security.ArrayBoundV2     Warn about buffer overflows (newer checker)
  alpha.security.MallocOverflow   Check for overflows in the arguments to malloc()
  alpha.security.ReturnPtrRange   Check for an out-of-bound pointer being returned to callers
  ...
  core.CallAndMessage             Check for logical errors for function calls and Objective-C message expressions (e.g., uninitialized arguments, null function pointers)
  core.DivideZero                 Check for division by zero
  core.DynamicTypePropagation     Generate dynamic type information
  core.NonNullParamChecker        Check for null pointers passed as arguments to a function whose arguments are references or marked with the 'nonnull' attribute
  core.NullDereference            Check for dereferences of null pointers
  core.StackAddressEscape         Check that addresses to stack memory do not escape the function
  ...
  unix.API                        Check calls to various UNIX/Posix functions
  unix.Malloc                     Check for memory leaks, double free, and use-after-free problems. Traces memory managed by malloc()/free().
  unix.MallocSizeof               Check for dubious malloc arguments involving sizeof
  unix.MismatchedDeallocator      Check for mismatched deallocators.
  unix.cstring.BadSizeArg         Check the size argument passed into C string functions for common erroneous patterns
  unix.cstring.NullArg            Check for null pointers being passed as arguments to C string functions

可以使用 -enable-checker 和 -disable-checker 开启和禁用具体的 checker 或者 某种类别的 checker。

$ scan-build -enable-checker alpha.security.ArrayBoundV2 ... # 启用数组边界检查

当然,使用scan-build启用的checker只适用于使用scan-build生成的html报告。 scan-build在编译安装 llvm/clang 之后可以在/llvm/tools/clang/tools/scan-build目录下找到

//允许未被默认允许的check并进行代码分析并将输出结果输出至网页
./scan-build  -enable-checker alpha.security.ArrayBoundV2 --use-analyzer=/opt/llvm/llvm_build/bin -V xcodebuild  -project /Users/yuhao/TestClang/TestClang.xcodeproj -sdk iphonesimulator10.3

我们在TestClang.xcodeproj的main.m文件中插入一段数组越界的代码

int main(){
    @autoreleasepool {
        int a[2];
        int i;
        for (i = 0; i < 3; i++){
            a[i] = 0;
        }
    }
    return 0;
}

然后执行上面的命令,会导出这样的一个界面

image.png

查看报表

image.png

报表中提示了该代码有数组越界的问题。

####Xcode执行 Xcode本身已经自带了静态检测的功能,可以通过Product-Analyze来执行静态检测,这也只是用自带的clang去执行,如果想用其他的版本,比如自己编译clang,就需要通过命令来设置。

image.png

在Xcode的Product选项卡下有Analyze的选项,Xcode中默认提供了一些checkers。

Usage: set-xcode-analyzer [options]

Options:
  -h, --help            show this help message and exit
  --use-checker-build=PATH
                        Use the Clang located at the provided absolute path,
                        e.g. /Users/foo/checker-1
  --use-xcode-clang     Use the Clang bundled with Xcode

可以看到,它有2个选项,

--use-checker-build:用于将xcode的clang版本切换成设定的版本 --use-xcode-clang:用于将xcode的clang版本切换回去

注:在执行上面命令的时候,需要退出xcode执行;且需要用sudo的方式运行。

依然使用上面的project文件,在Build Settings添加参数,如图

image.png

-Xanalyzer -analyzer-checker=alpha.security.ArrayBoundV2

然后cmd+shift+b

image.png

在Xcode中也出现了和报表同样的提示。 关于checker的开发可以看这里


###关于ARC(AUTOMATIC REFERENCE COUNTING)

ARC是ios5.0引入的新特性,完全消除手动管理内存的繁琐,编译器会自动在适合的代码里面插入适当的retain,release,autorelease的语句。我们不要再担心内存管理,因为编译器帮我们做了这一切。 我们都知道ARC的规则就是只要对象没有强指针引用,就会被释放掉。那么,该对象是什么时候被释放,又是谁操作去释放该对象的?

自动添加release

int main(int argc, const char * argv[]) {
    id a;
    return 0;
}

上面的代码中有强引用的对象,通过以下命令将代码编译成中间语言:

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

结果如下:

define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = 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
  store i8* null, i8** %6, align 8
  store i32 0, i32* %3, align 4
  call void @objc_storeStrong(i8** %6, i8* null) #1
  %7 = load i32, i32* %3, align 4
  ret i32 %7
}

alloca函数申请内存地址,而store表示将值存到指定地址。 函数的最后调用了函数objc_storeStrong,查询ARC文档可以知道objc_storeStrong的实现。

void objc_storeStrong(id *object, id value) {
  id oldValue = *object;
  value = [value retain];
  *object = value;
  [oldValue release];
}

call void @objc_storeStrong(i8** %6, i8* null)null进行了retain,对a进行了release。 综上,在__strong类型的变量的作用域结束时,自动添加release函数进行释放。

自动添加retain 查阅ARC文档,发现有objc_retain这样一个函数,顾名思义,该函数就是将对象进行retain操作。

id objc_retainAutorelease(id value) {
  return objc_autorelease(objc_retain(value));
}

objc_retainAutorelease(id value)valuenull或指针指向有效对象,如果valuenull,则此调用不起作用。否则,它执行保留操作,然后执行自动释放操作。即对一个变量先进行一次retain,再添进行autorelease

weak的实现 runtime是如何实现在weak修饰的变量的对象在被销毁时自动置为nil的呢?一个普遍的解释是:runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil

weak指针的实现借助Objective-C的运行时特性,runtime通过 objc_storeWeak, objc_destroyWeakobjc_moveWeak等方法,直接修改__weak对象,来实现弱引用。

objc_storeWeak函数,将附有__weak标识符的变量的地址注册到weak表中,weak表是一份与引用计数表相似的散列表。

而该变量会在释放的过程中清理weak表中的引用,变量释放调用以下函数:

dealloc
_objec_rootDealloc
object_dispose
objc_destructInstance
objc_clear_deallocating

在最后的objc_clear_deallocating函数中,从weak表中找到弱引用指针的地址,然后置为nil,并从weak表删除记录。

关于ARC更多实现请参阅探究ARC