WWDC18《What’s new in LLVM》小记

TL;DR

上周末看的部分WWDC18的视频,不过当时没来得及做些记录。
我个人的历年习惯通常是先浏览下目录,再挑一部分感兴趣的先看——通常是《What’s new in Cocoa Touch》和《What’s new in LLVM》,后续的再根据主题和实际需要再进一步了解,比如关于如何提升开发效率的。
这里主要记录下WWDC18《What’s new in LLVM》中我个人的一些观后感。

零、背景简介

首先是LLVM相关团队的高级经理Jim Grosbach给大家介绍了下LLVM家族(表示LLVM不只是个编译器),以及LLVM这个开源项目目前的合作伙伴有哪些,并欢迎大家交流共建。

LLVM所支撑的相关工具和产品如下:
1. Clang(音[klæŋ]),从名字上也能感受到这是一个和C语言家族相关的东西,是一款主要面向C/C++/ObjC等语言的编译器前端。一个使用例子是clang -rewrite-objc xx.c,用来查看生成的C中间代码,了解语言特性的内部实现,比如我之前在12年写过的一篇关于block实现的探究
2. 静态分析器,同属于Clang项目的源码分析工具。
3. Sanitizers如AddressSanitizer、MemorySanitizer、ThreadSanitizer和 LeakSanitizer。
4. LLDB,Xcode内置的调试器。
5. GPU Shader Compiler下所使用的代码生成器框架。
6. Swift。
7. 视频里没有提到的但业界实际有在使用的,比如作为Java的编译器后端,将JVM字节码编译成机器码。

做iOS开发比较久的同学应该对Xcode中Compiler选项的过渡变迁有印象,从最早的GCC到现在的LLVM。
一开始的前端和后端都是采用GCC套件,到后来采用LLVM作为后端来处理GCC生成的中间代码,再到如今的Clang前端搭配LLVM后端套件。
类似的还有GDB到LLDB的变迁。

这些变迁的背景主要是由于一开始Apple所采用的GCC是一个面向多种语言的编译器套件,但GCC不太给Apple面子、支持力度不够,同时由于代码耦合度太高、不好做模块化定制,所以Apple招聘了如今的大神Chris Lattner到Apple继续做LLVM项目。
LLVM是Chris Lattner在校期间发起的项目,据说他当时啃烂了龙书。这本书也是我大学印象最深刻的书,当初也硬着头皮啃了好几遍,这也是我现在每年看WWDC都会优先看《What’s new in LLVM》的一个原因。
去年(2017年)Chris Lattner从Apple离职,先后去了特斯拉和Google,这让一部分人担心Swift的发展,也让另一部分人担心LLVM社区的发展。

以下是视频内容概要:

  • ARC的更新
  • Xcode 10中新的诊断功能
  • Clang静态分析器
  • 安全性提升
  • 新的指令集扩展

一、ARC的更新:结构体支持ARC对象指针

演讲者是Alex Lorenz,Clang的前端工程师。

在之前,ARC环境下的结构体中是不支持对象指针的,因为编译器还不支持自动为结构体成员插入retain/release等内存管理代码。

现在,针对栈上的结构体变量,支持自动插入内存管理代码了(以下代码来自WWDC18/409):

typedef struct {
    NSString *name;
    NSNumber *price;
} MenuItem;

void orderFreeFood(NSString *name) {
    MenuItem item = {
        name,
        [NSNumber numberWithInt:0]
    };
    // [item.name retain];
    // [item.price retain];
    orderMenuItem(item);
    // [item.name release]; 
    // [item.price release];
}

如果是堆上的结构体变量,那么需要做些额外的清理动作:

typedef struct {
    NSString *name;
    NSNumber *price;
} MenuItem;

void testMenuItems() {
    // Allocate an array of 10 menu items
    MenuItem *items = calloc(10, sizeof(MenuItem));
    orderMenuItems(items, 10);
    // ARC Object Pointer Fields Must be Cleared Before Deallocation
    for (size_t i = 0; i < 10; ++i) {
        items[i].name = nil;
        items[i].price = nil;
    }
    free(items);
}

这个新功能在ObjC/C++都能正常工作,但Swift不支持引入带有ARC对象指针成员的结构体。

二、Xcode 10中新的诊断功能

演讲者是Alex Lorenz,Clang的前端工程师。

2.1 在ObjC中引入Swift闭包的注意事项

由于在Swift中函数的闭包类型参数默认是non-escaping的,表明这个闭包的执行是不能“逃出”函数作用域的:

Thus, a closure argument is guaranteed to be executed (if executed at all) before the function returns. This enables the compiler to perform various optimizations, such as omitting unnecessary capturing/retaining/releasing of self.

在ObjC引入某个Swift声明的protocol并实现时,函数参数很经常会是闭包类型,而这个默认的闭包参数特性很容易被忘记,所以Xcode 10新增了相关警告,提示开发者要添加NS_NOESCAPE注解:

// Warning for Missing Noescape Annotations for Method Overrides

#import “Executor-Swift.h”
@interface DispatchExecutor : NSObject<Executor>
    - (void) performOperation:(NS_NOESCAPE void (^)(void)) handler;
@end

@implementation DispatchExecutor
- (void) performOperation:(NS_NOESCAPE void (^)(void)) handler {
    // Programmer must ensure that handler is not called after performOperation returns
}
@end

2.2 使用#pragma pack指示符的注意事项

编译器在处理结构体时会进行对齐,目的是加速CPU的访存,但会引发一些空间浪费。
所以在做一些需要节约内存空间或者流量传输的业务场景时,就需要使用#pragma pack指示符来做结构体的打包
和我们利用编译器指示符做一些临时性消除警告一样,#pragma pack指示符也需要push/pop成对出现。
由于程序员有时候会忘记配对使用,所以Xcode 10针对这种情况新增了警告,避免打包效果影响了其它结构体使得CPU处理速度变慢:

#pragma pack (push, 1) struct PackedStruct {
    uint8_t a, b;
    uint32_t c; 
};
// The programmer forgot #pragma pack (pop)

三、静态分析器

演讲者是George Karpenkov,程序分析工程师。
基本上每年介绍静态分析器都是说这是一款查找边界、难重现的bug的利器。

3.1 影响GCD性能的反模式

George Karpenkov举了个使用dispatch_semaphore来做线程同步的例子。
在GCD中,队列可以有不同的优先级,如果一个高优先级的队列同步等待一个低优先级的队列,导致优先级反转,那么静态分析器针对这种情况会做警告。

3.2 比自动释放池存“活得更久”的自释放变量

这里主要讲的是use-after-free的case,尤其是一些常用API中隐藏的自动释放池会让人踩坑,比如:

+ (void)findError:(NSError * __autoreleasing *)error inArray:(NSArray *)arr {
    [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isEqualToString:@"Error"]) {
            if (error) {
                *error = [NSError errorWithDomain:@"domain" code:-1 userInfo:nil];
            }
        }
    }];
}

目前Xcode是针对out类型参数,如(NSError * __autoreleasing *)error,要求显式增加__autoreleasing修饰符,但这不能消除这类bug隐患。
Xcode 10中静态分析器针对性地添加了Write to autoreleasing out parameter inside autorelease pool警告,提示开发者针对这种情况,先使用一个局部的强引用类型变量来赋值,再传递给out类型参数。

3.3 性能和可视化报告的优化

这里主要是一些优化细节了,比如更简洁的错误报告,然后推荐大家多用用静态分析器。

四、安全性提升

演讲者是Ahmed Bougacha,编译器后端工程师。

4.1 How stack works

Ahmed Bougacha先是简单地介绍下栈的工作方式,如栈帧结构、方法调用时栈的展开等,我以前也写过一篇相关博文可做参考。
这部分略过,如有需要建议翻阅CSAPP

4.2 Stack Buffer Overflows & Stack Protector

接着介绍了现有的缓冲区溢出问题和保护方式。
为什么要保护呢?主要是安全性考虑——比如修改return address从而实现一些权限提升,对应的就是历史悠久的缓冲区溢出攻击

开发者在使用一些不那么安全的API时,如strcpy,会引入一些安全隐患,比如下图的缓冲区溢出:

针对这种问题,现有的编译器技术加入了一个额外的区域(下图绿色区域),叫Stack Canary / Stack Protector,对应的编译选项是-fstack-protector / -fno-stack-protector
Canary的取名主要来源于早期煤矿工人携带金丝雀进矿坑来检测一氧化碳是否达到危险值,从而判断是否需要逃生。
当函数调用结束即将返回时,这个区域的值会被检查看是否被修改了,如果是则会abort()

当然,这些手段都是用来增加攻击者的难度,并不能百分百消除风险(比如这里提到的Stack Protector的局限性),类似的还有ASLR保护措施

4.3 Stack Clash & Stack Checking

Stack Protector是Xcode既有的、且默认开启的特性,而Stack Checking是Xcode 10引入的新特性,主要针对的是Stack Clash问题。

上面的问题(Stack Buffer Overflows)是栈反向向上溢出了,这里的问题(Stack Clash)是栈和下面的内存空间发生冲突了。
因为栈是向下增长的、堆是向上增长的,两者相向扩展而内存又是有限的。
LLVM团队在这届WWDC增加了这个安全检查特性,大概率是源于去年年中Qualys公布的报告

参考上图最右,OS Kernel针对Stack Clash问题引入了Guard Page,访问该区域会触发SIGSEGV
不过这块区域通常是一两张内存页的大小,可能被跳过。

Qualys的报告中提出了一套操作方法:

Step 1: Clash (the stack with another memory region)
Step 2: Run (move the stack-pointer to the start of the stack)
Step 3: Jump (over the stack guard-page, into the other memory region)
Step 4: Smash (the stack, or the other memory region)

第一步是让栈和其它内存区域相碰撞,比如图中的绿色部分(Heap)和紫色部分(mmap);
第二步是通过消耗栈区内存,将栈顶指针移到最底部以提高成功率;
第三步是通过如大块的栈缓冲区来跳过栈的保护页,从而访问到第一步提到的其它内存区域;
第四步是改写相关内存区域来获取如指令寄存器的控制权;

这份报告我看了两三遍,大意是能理解,但是还未能成功实践。

五、新的指令集扩展

演讲者是Ahmed Bougacha,编译器后端工程师。

这部分内容属于CPU体系架构,我没怎么细看,就稍微浏览了下:

结束。

发表评论

电子邮件地址不会被公开。 必填项已用*标注