WWDC18 What’s New in LLVM 我的笔记

前言

LLVM 做为 Apple 御用的编译基础设施其重要性不言而喻,Apple 从未中止对 LLVM 的维护和更新,而且几乎在每一年的 WWDC 中都有专门的 Session 来针对 LLVM 的新特性作介绍和讲解,刚刚过去的 WWDC18 也不例外。swift

WWDC18 Session 409 What’s New in LLVM 中 Apple 的工程师们又为咱们介绍了 LLVM 最新的特性,这篇文章将会结合 WWDC18 Session 409 给出的 官方演示文稿 分享一下 LLVM 的新特性并谈谈笔者本身我的对这些特性的拙见。api

Note: 本文不会对官方演示文稿作逐字逐句的翻译工做,亦不会去过多介绍 LLVM 的基本常识。数组

索引

  • ARC 更新
  • Xcode 10 新增诊断
  • Clang 静态分析
  • 增长安全性
  • 新指令集扩展
  • 总结

ARC 更新

本次 ARC 更新的亮点在于 C struct 中容许使用 ARC Objective-C 对象。安全

在以前版本的 Xcode 中尝试在 C struct 的定义中使用 Obj—C 对象,编译器会抛出 Error: ARC forbids Objective-C objects in struct,以下图所示:bash

嘛~ 这是由于以前 LLVM 不支持,若是在 Xcode 10 中书写一样的代码则不会有任何 Warning 与 Error:微信

那么直接在 C struct 中使用 Objective-C 对象的话难道就没有内存上的问题吗?Objective-C 所占用的内存空间是什么时候被销毁的呢?网络

// ARC Object Pointers in C Structs!
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];
}
复制代码

如上述代码所示,编译器会在 C struct MenuItem 建立后 retain 其中的 ARC Objective-C 对象,并在 orderMenuItem(item); 语句以后,即其余使用 MenuItem item 的函数调用结束以后 release 掉相关 ARC Objective-C 对象。闭包

思考,在动态内存管理时,ARC Objective-C 对象的内存管理会有什么不一样呢?架构

Note: 动态内存管理(Dynamic Memory Management),指非 int a[100];MenuItem item = {name, [NSNumber numberWithInt:0]}; 这种在决定了使用哪一存储结构以后,就自动决定了做用域和存储时期的代码,这种代码必须服从预先制定的内存管理规则。app

咱们知道 C 语言中若是想要灵活的建立一个动态大小的数组须要本身手动开辟、管理、释放相关的内存,示例:

void foo() {
	int max;
	double *ptd;
	    
	puts("What is the maximum number of type double entries?");
	scanf("%d", &max);
	ptd = malloc(max * sizeof(double));
	if (ptd == NULL) {
	    // memory allocation failed
	    ...
	}
	    
	// some logic
	...
	
	free(ptd);
}
复制代码

那么 C struct 中 ARC Objective-C 的动态内存管理是否应该这么写呢?

// Structs with ARC Fields Need Care for Dynamic Memory Management
typedef struct {
	NSString *name;
	NSNumber *price;
} MenuItem;

void testMenuItems() {
	// Allocate an array of 10 menu items
	MenuItem *items = malloc(10 * sizeof(MenuItem));
	orderMenuItems(items, 10);
	free(items);
}
复制代码

答案是否认的!

能够看到经过 malloc 开辟内存初始化带有 ARC Objective-C 的 C struct 中 ARC Objective-C 指针不会 zero-initialized

嘛~ 这个时候天然而然的会想起使用 calloc ^_^

Note: callocmalloc 都可完成内存分配,不一样之处在于 calloc 会将分配过来的内存块中所有位置都置 0(然而要注意,在某些硬件系统中,浮点值 0 不是所有位为 0 来表示的)。

另外一个问题就是 free(items); 语句执行以前,ARC Objective-C 并无被清理。

Emmmmm... 官方推荐的写法是在 free(items); 以前将 items 内的全部 struct 中使用到的 ARC Objective-C 指针手动职位 nil ...

因此在动态内存管理时,上面的代码应该这么写:

// Structs with ARC Fields Need Care for Dynamic Memory Management
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);
}
复制代码

瞬间有种日了狗的感受有木有?

我的观点

嘛~ 在 C struct 中增长对 ARC Objective-C 对象字段的支持意味着咱们从此 Objective-C 能够构建跨语言模式的交互操做

Note: 官方声明为了统一 ARC 与 manual retain/release (MRR) 下部分 function 按值传递、返回 struct 对 Objective-C++ ABI 作出了些许调整。

值得一提的是 Swift 并不支持这一特性(2333~ 谁说 Objective-C 的更新都是为了迎合 Swift 的变化)。

Xcode 10 新增诊断

Swift 与 Objective-C 互通性

咱们都知道 Swift 与 Objective-C 具备必定程度的互通性,即 Swift 与 Objective-C 能够混编,在混编时 Xcode 生成一个头文件将 Swift 能够转化为 Objective-C 的部分接口暴露出来。

不过因为 Swift 与 Objective-C 的兼容性致使用 Swift 实现的部分代码没法转换给 Objective-C 使用。

近些年来 LLVM 一致都在尝试让这两种语言能够更好的互通(这也就是上文中提到 Objective-C 的更新都是为了迎合 Swift 说法的由来),本次 LLVM 支持将 Swift 中的闭包(Closures)导入 Objective-C

@objc protocol Executor {
	func performOperation(handler: () -> Void)
}
复制代码
#import “Executor-Swift.h”
@interface DispatchExecutor : NSObject<Executor>
- (void)performOperation:(void (^)(void))handler; 
@end
复制代码

Note: 在 Swift 中闭包默认都是非逃逸闭包(non-escaping closures),即闭包不该该在函数返回以后执行。

Objective-C 中与 Swift 闭包对应的就是 Block 了,可是 Objective-C 中的 Block 并无诸如 Swift 中逃逸与否的限制,那么咱们这样将 Swift 的非逃逸闭包转为 Objective-C 中无限制的 Block 岂不是会有问题?

别担忧,转换过来的闭包(非逃逸)会有 Warnning 提示,并且咱们说过通常这种状况下 Apple 的工程师都会在 LLVM 为 Objective-C 加一个宏来迎合 Swift...

// 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
复制代码

我的观点

若是 Swift 5 真的能够作到 ABI 稳定,那么 Swift 与 Objective-C 混编的 App 包大小也应该回归正常,相信不少公司的项目都会慢慢从 Objective-C 转向 Swift。在 Swift 中闭包(Closures)做为一等公民的存在奠基了 Swift 做为函数式语言的根基,本次 LLVM 提供了将 Swift 中的 Closures 与 Objective-C 中的 Block 互通转换的支持无疑是颇有必要的。

使用 #pragma pack 打包 Struct 成员

Emmmmm... 老实说这一节的内容更底层,因此可能会比较晦涩,但愿本身能够表述清楚吧。在 C 语言中 struct 有 内存布局(memory layout) 的概念,C 语言容许编译器为每一个基本类型指定一些对齐方式,一般状况下是以类型的大小为标准对齐,可是它是特定于实现的。

嘛~ 仍是举个例子吧,就拿 WWDC18 官方演示文稿中的吧:

struct Struct { 
	uint8_t a, b;
	// 2 byte padding 
	uint32_t c;
};
复制代码

在上述例子中,编译器为了对齐内存布局不得不在 Struct 的第二字段与第三字段之间插入 2 个 byte。

|   1   |   2   |   3   |   4   |
|   a   |   b   | pad.......... |
|  c(1) |  c(2) |  c(3) |  c(4) |
复制代码

这样本该占用 6 byte 的 struct 就占用了 8 byte,尽管其中只有 6 byte 的数据。

C 语言容许每一个远程现代编译器实现 #pragma pack,它容许程序猿对填充进行控制来依从 ABI。

From C99 §6.7.2.1:

12 Each non-bit-field member of a structure or union object is aligned in an implementation- defined manner appropriate to its type.

13 Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.

实际上关于 #pragma pack 的相关信息能够在 MSDN page 中找到。

LLVM 本次也加入了对 #pragma pack 的支持,使用方式以下:

#pragma pack (push, 1) 
struct PackedStruct {
	uint8_t a, b;
	uint32_t c; 
};
#pragma pack (pop)
复制代码

通过 #pragma pack 以后咱们的 struct 对齐方式以下:

|   1   |
|   a   | 
|   b   |
|  c(1) |
|  c(2) |
|  c(3) |
|  c(4) |
复制代码

其实 #pragma pack (push, 1) 中的 1 就是对齐字节数,若是设置为 4 那么对齐方式又会变回到最初的状态:

|   1   |   2   |   3   |   4   |
|   a   |   b   | pad.......... |
|  c(1) |  c(2) |  c(3) |  c(4) |
复制代码

值得一提的是,若是你使用了 #pragma pack (push, n) 以后忘记写 #pragma pack (pop) 的话,Xcode 10 会抛出 warning:

我的观点

嘛~ 当在网络层面传输 struct 时,经过 #pragma pack 自定义内存布局的对齐方式能够为用户节约更多流量。

Clang 静态分析

Xcode 一直都提供静态分析器(Static Analyzer),使用 Clang Static Analyzer 能够帮助咱们找出边界状况以及难以发觉的 Bug。

点击 Product -> Analyze 或者使用快捷键 Shift+Command+B 就能够静态分析当前构建的项目了,固然也能够在项目的 Build Settings 中设置构建项目时自动执行静态分析(我的不推荐):

本地静态分析器有如下提高:

  • GCD 性能反模式
  • 自动释放变量超出自动释放池
  • 性能和可视化报告的提高

GCD 性能反模式

在以前某些无可奈何的状况下,咱们可能须要使用 GCD 信号(dispatch_semaphore_t)来阻塞某些异步操做,并将阻塞后获得的最终的结果同步返回:

__block NSString *taskName = nil;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self.connection.remoteObjectProxy requestCurrentTaskName:^(NSString *task) {
	taskName = task;
	dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return taskName;
复制代码

嘛~ 这样写有什么问题呢?

上述代码存在经过使用异步线程执行任务来阻塞当前线程,而 Task 队列一般优先级较低,因此会致使优先级反转

那么 Xcode 10 以后咱们应该怎么写呢?

__block NSString *taskName = nil;
id remoteObjectProxy = [self.connection synchronousRemoteObjectProxyWithErrorHandler:
	^(NSError *error) { NSLog(@"Error: %@", error); }];
[remoteObjectProxy requestCurrentTaskName:^(NSString *task) {
	taskName = task; 
}];
return taskName;
复制代码

若是可能的话,尽可能使用 synchronous 版本的 API。或者,使用 asynchronous 方式的 API:

[self.connection.remoteObjectProxy requestCurrentTaskName:^(NSString *task) { 
	completionHandler(task);
}];
复制代码

能够在 build settings 下启用 GCD 性能反模式的静态分析检查:

自动释放变量超出自动释放池

众所周知,使用 __autoreleasing 修饰符修饰的变量会在自动释放池离开时被释放(release):

@autoreleasepool {
	__autoreleasing NSError *err = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
}
复制代码

这种看似不须要咱们注意的点每每就是引发程序 Crash 的隐患:

- (void)findProblems:(NSArray *)arr error:(NSError **)error {
	[arr enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) {
		if ([value isEqualToString:@"problem"]) { 
			if (error) {
				*error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
			}
		}
	}];
}

复制代码

嘛~ 上述代码是会引发 Crash 的,你能够指出为何吗?

Objective-C 在 ARC(Automatic Reference Counting)下会隐式使用 __autoreleasing 修饰 error,即 NSError *__autoreleasing*。而 -enumerateObjectsUsingBlock: 内部会在迭代 block 时使用 @autoreleasepool,在迭代逻辑中这样作有助于减小内存峰值。

因而 *error-enumerateObjectsUsingBlock: 中被提早 release 掉了,这样在随后读取 *error 时会出现 crash。

Xcode 10 中会给出具备针对性的静态分析警告:

正确的书写方式应该是这样的:

- (void)findProblems:(NSArray *)arr error:(NSError *__autoreleasing*)error { 
	__block NSError *localError;
	[arr enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) {
		if ([value isEqualToString:@"problem"]) {
			localError = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
		} 
	}];
	if (error) {
		*error = localError;
	} 
}
复制代码

Note: 其实早在去年的 WWDC17 Session 411 What's New in LLVM 中 Xcode 9 就引入了一个须要显示书写 __autoreleasing 的警告。

性能和可视化报告的提高

Xcode 10 中静态分析器能够以更高效的方式工做,在相同的分析时间内平都可以发现比以前增长 15% 的 Bug 数量。

不只仅是性能的提高,Xcode 10 在报告的可视化方面也有所进步。在 Xcode 9 的静态分析器报告页面有着非必要且冗长的 Error Path:

Xcode 10 中则对其进行了优化:

我的观点

嘛~ 对于 Xcode 的静态分析,我的认为仍是聊胜于无的。不过不建议每次构建项目时都去作静态分析,这样大大增长了构建项目的成本。

我的建议在开发流程中自测完毕提交代码给组内小伙伴们 Code Review 以前作静态分析,能够避免一些 issue 的出现,也能够发现一些代码隐患。有些问题是可使用静态分析器在提交代码以前就暴露出来的,不必消耗组内 Code Review 的宝贵人力资源。

还能够在 CI 设置每隔固定是时间间隔去跑一次静态分析,生成报表发到组内小群,根据问题指派责任人去检查是否须要修复(静态分析在比较复杂的代码结构下并不必定准确),这样按期维护从某种角度讲能够保持项目代码的健康情况。

增长安全性

Stack Protector

Apple 工程师在介绍 Stack Protector 以前很贴心的带领着在场的开发者们复习了一遍栈 Stack 相关的基础知识:

如上图,其实就是简单的讲了一下 Stack 的工做方式,如栈帧结构以及函数调用时栈的展开等。每一级的方法调用,都对应了一张相关的活动记录,也被称为活动帧。函数的调用栈是由一张张帧结构组成的,因此也称之为栈帧

咱们能够看到,栈帧中包含着 Return Address,也就是当前活动记录执行结束后要返回的地址。

那么会有什么安全性问题呢?Apple 工程师接着介绍了经过不正当手段修改栈帧 Return Address 从而实现的一些权限提高。嘛~ 也就是历史悠久的 缓冲区溢出攻击

当使用 C 语言中一些不太安全的函数时(好比上图的 strcpy()),就有可能形成缓冲区溢出。

Note: strcpy() 函数将源字符串复制到指定缓冲区中。可是丫没有指定要复制字符的具体数目!若是源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会形成缓冲区溢出

针对缓冲区溢出攻击,LLVM 引入了一块额外的区域(下图绿色区域)来做为栈帧 Return Address 的护城河,叫作 Stack Canary,已默认启用:

Note: Canary 译为 “金丝雀”,Stack Canary 的命名源于早期煤矿工人下矿坑时会携带金丝雀来检测矿坑内一氧化碳是否达到危险值,从而判断是否须要逃生。

根据咱们上面对缓冲区溢出攻击的原理分析,你们应该很容易发现 Stack Canary 的防护原理,即缓冲区溢出攻击旨在利用缓冲区溢出来篡改栈帧的 Return Address,加入了 Stack Canary 以后想要篡改 Return Address 就必然会通过 Stack Canary,在当前栈帧执行结束后要使用 Return Address 回溯时先检测 Stack Canary 是否有变更,若是有就调用 abort() 强制退出。

嘛~ 是否是和矿坑中的金丝雀很像呢?

不过 Stack Canary 存在一些局限性:

  • 能够在缓冲区溢出攻击时计算 Canary 的区域并假装 Canary 区域的值,使得 Return Address 被篡改的同时 Canary 区域内容无变化,绕过检测。
  • 再粗暴一点的话,能够经过双重 strcpy() 覆写任意不受内存保护的数据,经过构建合适的溢出字符串,能够达到修改 ELF(Executable and Linking Format)映射的 GOT(Global Offset Table),只要修改了 GOT 中的 _exit() 入口,即使 Canary 检测到了篡改,函数返回前调用 abort() 退出仍是会走已经被篡改了的 _exit()

Stack Checking

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

Stack Clash 问题的产生源于 Stack 和 Heap,Stack 是从上向下增加的,Heap 则是自下而上增加的,二者相向扩展而内存又是有限的。

Stack Checking 的工做原理是在 Stack 区域规定合理的分界线(上图红线),在可变长度缓冲区的函数内部对将要分配的缓冲区大小作校验,若是缓冲区超出分界线则调用 abort() 强制退出。

Note: LLVM 团队在本次 WWDC18 加入 Stack Checking,大几率是由于去年年中 Qualys 公布的一份 关于 Stack Clash 的报告

新指令集扩展

Emmmmm... 这一节的内容是针对于 iMac Pro 以及 iPhone X 使用的 指令集架构(ISA - Instruction set architecture) 所作的扩展。坦白说,我对这块并非很感兴趣,也没有深刻的研究,因此就不献丑了...

总结

本文梳理了 WWDC18 Session 409 What’s New in LLVM 中的内容,并分享了我我的对这些内容的拙见,但愿可以对各位由于种种缘由尚未来得及看 WWDC18 Session 409 的同窗有所帮助。

文章写得比较用心(是我我的的原创文章,转载请注明 lision.me/),若是发现错误会优先在个人我的博客中更新。若是有任何问题欢迎在个人微博 @Lision 联系我~

但愿个人文章能够为你带来价值~


补充~ 我建了一个技术交流微信群,想在里面认识更多的朋友!若是各位同窗对文章有什么疑问或者工做之中遇到一些小问题均可以在群里找到我或者其余群友交流讨论,期待你的加入哟~

Emmmmm..因为微信群人数过百致使不能够扫码入群,因此请扫描上面的二维码关注公众号进群。

相关文章
相关标签/搜索