Xcode如何找到FrameWork對應的原始碼的

weixin_34236869發表於2018-01-03

背景描述:

在cocpoads管理下自研庫和第三方的開源庫可以使用frameWork的方式整合到主工程中,在我們編譯過程中,只要不改動pod下整合的庫程式碼,第一次編譯後,第三方庫會編譯成frameWork,通過Xcode 的 buildSetting 的 framework search Path 記錄所有生成的framework地址,在專案的編譯後期去link這些framework 。在沒有修改pod子工程的庫檔案前提下,之後的每一次build都直接link快取中編譯過的framework檔案。但是當檔案編譯成framework,成為了可執行的二進位制檔案,我們卻任然可以藉助Xcode做原始碼除錯。那麼Xcode工具是如何通過一個二進位制檔案執行時找到它對應的原始碼檔案的?

鑑於上個問題,我們可以先看看Xcode的編譯過程

Xcode作為一個GUI工具,實際上是通過呼叫一系列的命令列工具,將命令列工具處理的結果彙總輸出。Xcode 使用clang編譯器進行編譯 會使用一系列 xcrun clang 的命令來編譯檔案,xcrun是用來定位clang工具位置的

接著看編譯過程中都經歷了些什麼?
大致過程如下:

1.檔案預處理

符號化 (Tokenization)
巨集定義的展開
include 的展開

2.語法語義分析

將符號化後的內容轉化為一棵解析樹 (parse tree)
解析樹做語義分析
輸出一棵抽象語法樹(Abstract Syntax Tree* (AST))

3.程式碼生成和優化

將 AST 轉換為更低階的中間碼 (LLVM IR)
對生成的中間碼做優化
生成特定目的碼
輸出彙編程式碼

4.彙編器

將彙編程式碼轉換為目標物件檔案。

5.link檔案

將多個目標物件檔案合併為一個可執行檔案 (或者一個動態庫)

整個過程比較複雜,特別是語義分析,和程式碼優化過程,可以先簡單的看一下與處理過程。clang 有很多命令,可以通過

`$xcrun clang —help `

檢視提示資訊

其中關於預編譯的:

-c                      Only run preprocess, compile, and assemble steps
-E                      Only run the preprocessor

我嘗試使用上面的命令預編譯了main.c檔案
main.c檔案:

#include <stdio.h>
int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    return 0;
}

在main.c當前目錄下執行
xcrun clang -E main.c | open -f

Open -f 是將預編譯結果在檔案中開啟,預編譯部分結果截圖如下:

3101146-62fc76bdd4ad55e7.png
1.png
3101146-31225bc3dd3ad9cb.png
2.png

預編譯過程因為要展開所有的巨集定義,inclue , import。 輸出的檔案中行前面的 “#”號 (hash) 接著的數字 代表的插入的原始檔中的行號, 後面的檔案路徑代表的是需要引入的檔案的路徑 後面的數字代表的是引入的內容在新生成的檔案所在的行數,hash下面跟的內容是指定檔案 指定行數 所引入的內容,例如第二張圖中
將 /Application/Xcode.app/Contents/Developer/platforms/MacOSX10.12.sdk/usr/include/machine/_types.h 檔案中的第33行,34行,55行的程式碼段
插入新生成的檔案中。

Xcode也支援檢視檔案的預編譯結果 ,選中檔案點選product—>perform Action —>preprocess,之後便能看到預編譯的結果,可以對比所引入的檔案所在行數和引入的內容是否對應。

Xcode基於預編譯的結果去構建語法樹,生成中間位元組碼,下面是hello world的LLVM中間位元組碼


3101146-3e734eb8bfa6b3a7.png
3.png

可以簡單瞭解一下上面llvm中間碼的意思,@符號是LLVM中間位元組碼(IR)的變數字首,@代表全域性變數標誌,%代表區域性變數標誌。@.str是全域性變數名,%cast210 是區域性變數的名稱 。i8, i32,i64代表的儲存位元組型別,char是i8,一個位元組,int是i32,4個位元組。@main,@put,@puts是方法名,在IR中function和全域性變數統稱全域性值(global values)都是@字首標誌。

根據中間位元組碼(LLVM的位元組碼)做程式碼優化,優化後輸出彙編程式碼。

可以使用下面命令來檢視生成的彙編程式碼:
xcrun clang -S -o - main.c | open -f
結果如下圖:

3101146-a8cc27349f0bd8d8.png
4.png

按照上面說的步驟,就進入了呼叫匯編器編譯彙編程式碼了,彙編器將彙編程式碼轉換成機器碼,稱為目標物件檔案,在MacOSX, ios下為 Mach-O檔案格式下圖是蘋果官方文件對mach-o檔案格式的介紹:

3101146-9fb1d7abb0dbb334.png
5.png

Mach-O 的組成結構如下圖所示包括了Header、Load commands、Data(包含Segement的具體資料)
我們可以在工程的編譯路徑下找到編譯後生成的可執行檔案APP,我本地路徑為~/Library/Developer/Xcode/DerivedData/App-ackwqnrbjrdvslercxzskjictqru/Build/Intermediates/App.build/Release-iphonesimulator/App.build/Objects-normal/i386
在此路徑下使用size工具檢視檔案結構
xcrun size -x -l -m App
輸出
3101146-19ad719718547b7c.png
6.png

剛好與蘋果給出的結構圖相匹配,可以暫時先簡單的瞭解下,__TEXT segment 包含了被執行的程式碼,它被以只讀和可執行的方式對映。__DATA segment 中包含了可讀寫資料, 以可讀寫和不可執行的方式對映,它包含了將會被更改的資料。__LINKEDIT segment 包含了動態連結器的原始資料,如符號,字串和重定位的表的入口.

3101146-c881a6a4707cda68.png
7.png

最後進入聯結器連線的階段
連結器解決了目標檔案和庫之間的連結,連結器需要將所需的lib函式(可以理解為系統提供的函式),庫函檔案的記憶體地址編碼進最後的可執行檔案中,接著連結器會輸出可以執行的執行檔案:a.out,得到a.out 命令為:
xcrun clang man.c
用size 工具檢視a.out的檔案結構

3101146-ba2585c4b165ce99.png
8.png

可以看到 main.c檔案生成的可執行檔案a.out 的虛擬地址空間從0x10000000 開始的,之前的地址是不可訪問

此時在藉助mac下可執行檔案的閱讀器。machOView工具,看生成的a.out的檔案結構和內容,發現了一張叫symbols的表


3101146-8c6a0631a56a7f4d.png
9.png

這裡面就很清楚的記錄了,編譯前的字串,在編譯成二進位制可執行檔案後的虛擬地址,和app打包生成的dsym基本類似,這就可以猜想,蘋果是如何通過crash的二進位制棧資訊解析出可讀性的程式程式碼呼叫棧的。藉助symbols表給我的希望,我用machOView開啟了我編譯生成的frameWork

原諒我把圖片糊得像翔。。。。因為是公司的專案的frameWork

首先第一張裡表示了在不同平臺下的可執行資料,當前frameWork可在ARM,X86,Arm64,X86_64平臺下執行


3101146-891ed00df9fc68a3.png
一.png

第二張圖是所有frameWork內的類名對應的編譯生成的虛擬地址


3101146-49e53381e5bc91eb.png
二.png

第三張是挑了第二張圖中的一個類的symbols表,記錄的是當前檔案編譯後的字元對照表。


3101146-69508b26005b5ed1.png
三.png

那麼有了以上三張表的對比後我們很容易的瞭解到Xcode工具如何通過執行期間的二進位制地址對應到具體檔案的具體程式碼呼叫了。

相關文章