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..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的二維碼關注公衆號進羣。

相關文章
相關標籤/搜索