2008 年,iPhone 3G 與第一座 App Store 讓許多人第一次接觸行動應用程式。我們開發 AirBuild3G,讓這支手機有件新鮮事可做。在 iPhone 3G 或 3GS 上,你可以描述一個新的應用程式,透過程式設計模型處理原始碼,再於同一支手機上編譯並執行。模型會在你選擇的服務上執行。專案檔案與開發工具則留在手機上。
AirBuild3G 本身是在 Mac 上使用 Xcode 27 建置。你用它建立的應用程式會使用另一套安裝在手機上的編譯器。要讓兩者都能運作,我們處理了四個部分:裝置準備、連結、HTTPS,以及手機本身的工具鏈。以下各節會依序說明這些工作,直到你完成第一個應用程式。
準備原始硬體
小巧的顯示器和實體主畫面按鈕,讓 iPhone 3G 一眼就能辨認。再次為它開發,意味著必須尊重原有的比例與限制,連那些讓應用程式用起來熟悉的控制項也不例外。AirBuild3G 本身使用 iOS 4 可用的 UIKit 控制項,包括群組式設定表格與導覽列操作。
AirBuild3G 可在 iPhone 3G 與 3GS 上執行。3G 需要 armv6 二進位檔;3GS 使用 armv7。韌體會決定每支手機能安裝哪個環境套件。我們作為參考的 armv6 裝置,是一支執行 iOS 4.2.1 並安裝自訂韌體的 iPhone 3G。
- 準備回復 / 啟用
- 連線SSH + 網路
- 安裝應用程式 + 環境套件
- 環境設定 → 環境
先準備一支能開機進入主畫面,並接受 Mac SSH 連線的手機。它需要可用的網路連線與套件管理器,才能接收並安裝 Debian 套件。硬體型號與韌體版本會決定要傳送哪些套件,網路位址則決定傳送位置。你也需要一條可靠的 USB 連接線和電源。
部分舊款手機需要額外步驟,才能完成啟用與系統存取。若是支援的硬體,請使用 Legacy iOS Kit,並依照其中針對手機型號與韌體提供的指引操作。開始前請先備份手機。AirBuild3G 本身不會啟用手機。
AirBuild3G 的 TLS 支援也需要 Substrate。在 3G 上,建置必須包含 armv6:只有 armv7 的套件無法在這裡執行,無論版本為何。透過 Legacy iOS Kit 準備的手機,可能已經有相容的版本。如果 Substrate 運作正常,請保留原狀。
設定 → 環境也遵循同樣的原則。它會加入開發工具,同時保留可用的套件管理器、SSH 服務和其他基礎元件。一次升級所有這些元件,可能會安裝舊系統無法執行的二進位檔。
使用 Xcode 27 建置
Xcode 27 的編譯器仍能產生 armv6 與 armv7 程式碼,也接受 iOS 4.0 部署目標。Xcode 本身只隨附 iOS 27 SDK,因此我們使用舊版 SDK 的標頭來編譯。請在 make 命令列傳入 SDK 路徑作為 LEGACY_SDK,或將它匯出。建置環境套件的指令稿也會讀取這個值,只在 Makefile 內設定的值無法傳遞給它們。
連結器的限制更嚴格。它拒絕將 armv6 作為連結目標,也不接受低於 iOS 4.3 的部署目標,並將舊版 SDK 的程式庫回報為針對未知平台建置。針對這些程式庫,我們改為連結至產生的 stub,舊版 SDK 則繼續用於標頭。
- 已編譯clang
- 架構
- armv6
- 最低 iOS
- 4.0
- 程式碼
- e92d40f0 …
- 已連結重新標記,再執行 ld
- 架構
armv6armv7- 最低 iOS
4.05.0- 程式碼
- e92d40f0 …
- 已交付重新標回,再修補
- 架構
armv7armv6- 最低 iOS
5.04.0- 程式碼
- e92d40f0 …
建置流程會為 iOS 4.0 編譯兩個 slice,以 iOS 5.0 連結,再將最低版本修補回 4.0。針對 armv6,它也會將每個目的檔重新標記為 armv7,讓連結器接受,再把連結完成的 slice 重新標回 armv6。重新標記只會變更中繼資料,不會改動指令。編譯後的程式碼,以及連結器加入的符號 stub,都必須原本就能在 armv6 上執行。
MH_MAGIC = 0xFEEDFACE
SUBTYPES = {"armv6": 6, "armv7": 9}
def main():
…
path, want = sys.argv[1], SUBTYPES[sys.argv[2]]
with open(path, "rb") as f:
buf = bytearray(f.read())
if struct.unpack_from("<I", buf, 0)[0] != MH_MAGIC:
…
struct.pack_into("<i", buf, 8, want)
with open(path, "wb") as f:
f.write(buf)- 每種架構在 Mach-O 中的 cpusubtype:6 是 armv6,9 是 armv7。
- 整個重新標記只涉及位於位移 8 的四個位元組,也就是標頭的 cpusubtype 欄位。指令會原封不動地通過。
建置流程保留 -no_pie,因為 iOS 在 4.3 之前沒有 ASLR。它也會關閉 Objective-C 類別合併,否則合併後的方法會失去 Thumb 位元,導致 AirBuild3G 在 3GS 上當機。make verify 會檢查架構、載入命令、armv6 指令,以及 armv7 方法的 Thumb 位元。成功連結並不足以證明這些項目都正確。
執行 make 來建置 AirBuild3G 及其特權輔助程式。make payload 會將每個鎖定版本的套件下載並驗證至本機快取,make package 則會離線使用這些套件,建置 AirBuild3G 套件和兩個環境套件。封裝也需要 LEGACY_SDK,因為每個環境套件都包含供手機使用的 SDK 封存檔。
make install-device DEVICE=root@<ip> 會安裝 AirBuild3G 與 iOS 4 環境套件、重新整理主畫面,並重新啟動應用程式。若是執行 iOS 5 或更新版本的 3GS,請改為將 ios6 作為第三個引數傳給 tools/deploy_device.sh。應用程式會以 mobile 使用者身分執行,旁邊的小型輔助程式則會執行所有需要 root 權限的工作。環境套件現在已在手機上;設定 → 環境會安裝其中包含的開發工具。
連接模型服務
在設定中,你要在 Endpoint 下輸入三個值:Base URL、API Key 和 Model。要求會直接從手機傳送到該 endpoint。我們不會在中間執行任何伺服器,也不需要帳號或裝置註冊。你的模型服務會執行模型、收取使用費,並告訴你該輸入哪些內容。如果你的手機是我們贈送的,則會改為連接 air.build。
原廠 iOS 4 只能透過 SecureTransport 使用 TLS 1.0,而目前的 endpoint 可能要求 TLS 1.2。我們使用 TLSFix 加入 TLS 1.2,這是一項針對 CFNetwork 的 Substrate tweak。
手機仍須驗證伺服器身分。它的舊信任儲存區不一定能建立目前 endpoint 所用的憑證鏈,而加入 TLS 1.2 並不會更新該儲存區。連線失敗時,請將加密與憑證信任視為兩個不同的問題。
AirBuild3G 預設會驗證憑證。如果要求失敗,請先檢查 Base URL、API Key、Model 與憑證鏈。Trust Any Certificate 設定是一項明確的例外。開啟後會移除伺服器身分檢查,任何能冒充該 endpoint 的人都可能取得你的 API 金鑰。除非 endpoint 位於你自己的網路,否則請保持關閉。AirBuild3G 每次評估信任時都會讀取這項設定,因此關閉後會套用至下一個要求。
static OSStatus ABLSecTrustEvaluate(SecTrustRef trust, SecTrustResultType *result) {
…
OSStatus status = ABLOriginalSecTrustEvaluate(trust, result);
if (!ABLTrustAnyCertificate()) {
return status;
}
if (result != NULL) {
*result = kSecTrustResultUnspecified;
}
return errSecSuccess;
}- 手機會先自行執行評估。
- Trust Any Certificate 預設為關閉,因此該結果會原樣傳回。每次呼叫都會從
NSUserDefaults讀取這項設定。
串流回覆也必須配合手機的能力。AirBuild3G 會在背景執行緒接收並解碼串流,只將完成的文字傳給介面。逐字稿會定時重新整理,而不是每收到一個 token 就重新測量並捲動,讓回覆抵達時的版面配置工作維持在較低程度。看到回覆就表示連線運作正常。若要編譯模型寫出的內容,手機上仍須安裝環境。
安裝手機能執行的編譯器
設定 → 環境會從環境套件安裝編譯器、SDK 標頭、連結程式庫與簽署工具。iOS 4 套件包含供 armv6 使用的 GCC;iOS 5 與更新版本的套件則包含供 armv7 使用的 clang。每支手機只會安裝符合其韌體的套件。
每個套件都依下載位址與 checksum 鎖定,安裝時會使用這些確切檔案,不會要求套件庫提供最新版本。已安裝鎖定版本的套件會被略過,因此第二次執行環境安裝會立刻完成。接著,已安裝的套件會被保留,Cydia 便不會再提供可能破壞工具鏈的升級。
在 iPhone 3G 上,環境使用 2008 年版 iphone-gcc 4.2。2009 年版會為預先編譯的標頭保留一 GB 空間,導致部分內容超出 armv6 手機的位址空間。它的編譯器後端還沒編譯任何內容就會當機。兩個版本都識別為 Apple build 5555,但只有 2008 年版能放進該位址空間。
0x300000002008 iphone-gcc · 每個 segment 均小於 6 MB可執行
512 MB array · 測試二進位檔可執行
768 MB array · 測試二進位檔SIGBUS
2009 iphone-gcc · 1 GB 預先編譯標頭區域SIGBUS
SDK 標頭也需要修改:這個 GCC 無法解析其中的 block 宣告。在封裝 iOS 4 標頭前,我們會移除這些宣告,以及所有依賴已移除型別的宣告。clang 套件會保留 Apple 的標頭原貌,因為 clang 支援 block。每個套件也包含一項簽署工具,可為其編譯器產生的內容簽署。
手機將系統 framework 保存在 dyld shared cache 中,因此 framework 目錄內沒有可供連結器讀取的程式庫檔案。我們提供產生的 stub,其中包含匯出的符號與真正的程式庫名稱。連結器會透過 stub 解析每項參照,並記錄它來自哪個程式庫;啟動時,dyld 會將它繫結至 shared cache 中的真正實作。由於 stub 帶有真正的符號名稱,遺漏 framework 會讓連結失敗,而不是等到應用程式啟動時才當機。
環境也會設定 include path,讓 gcc 與 cc 指向已安裝的編譯器,並匯出連結旗標以納入 Objective-C runtime 和 CoreFoundation。隨附的 Makefile 會讀取這些設定,因此新專案不需要自行設定工具鏈。從這時起,你的應用程式會直接在手機上建置。
build/%.o: src/%.m | build
@echo " compile $<"
@gcc -c $(IOS_CFLAGS) $< -o $@
# Compiling and linking take different flags, so they are separate steps.
# A missing -framework is a link error here, which is the point: link stubs
# with real symbol names are what make it one instead of a crash at launch.
build/$(NAME): $(OBJECTS)
@echo " link $@"
@gcc $(OBJECTS) -o $@ $(IOS_LDFLAGS) $(addprefix -framework ,$(FRAMEWORKS))
@# Unsigned is SIGKILL at launch, not a warning.
@ldid -S $@IOS_CFLAGS與IOS_LDFLAGS來自環境所安裝的/etc/profile.d/airbuild.sh,因此專案不必指定 SDK 或編譯器路徑。- 二進位檔也會在手機上簽署。若未簽署,啟動時就會遭到終止。
建立並執行你的第一個應用程式
新專案會從隨附的 UIKit 範本開始。AirBuild3G 將它複製到專案目錄,並把檔案放進模型的 context。模型會從一個已能建置的專案開始,因此你的第一個要求可以直接描述應用程式,不必要求它建立 Makefile。
適合當作第一個應用程式的,是一個能讓畫面上數字遞增的按鈕。它小到可以完整閱讀,卻能涵蓋在手機上編輯、編譯、封裝與執行的過程。提出要求時,請指定 iOS 4 為目標,並要求模型建置及安裝應用程式。介面程式碼必須限於手機所執行 iOS 版本已實作的 API。標頭中有宣告,不代表該 API 在執行階段確實存在。
模型有四項工具。Execute 會在專案目錄中執行命令。EditFile 會寫入完整檔案。PatchFile 會取代完全相符的段落。CheckBuild 會載入環境設定後執行 make,並傳回輸出的結尾。工具結果會傳回模型,讓它繼續處理或修改失敗的變更。推論會全程留在你的模型服務上。
工具呼叫
工具結果
Execute EditFile PatchFile CheckBuild這個循環會持續,直到模型在沒有工具呼叫的情況下回覆、你點一下「停止」,或已執行 16 輪。
安裝新的應用程式後,請在手機上開啟它。以計數器為例,點一下按鈕並看著數字改變,就能檢查它是否完全符合你的要求。後續要求可以變更版面配置或加入其他互動,模型會接續使用現有檔案與先前的工具結果。
AirBuild3G 會將每個專案及其對話保存在手機上,並在每輪工具流程後儲存。它也會記住目前開啟的專案,因為離開應用程式或記憶體不足,都可能終止其程序。重新開啟 AirBuild3G 時,它會回到該專案,連同對話與檔案一起恢復。你的下一則訊息會接續已儲存的對話,而原始碼仍在,可供下一次編輯與建置。
在這個畫面上,即使只是這個計數器,也把你曾經使用的應用程式與現在能親手做出的應用程式連在一起。
熟悉畫面上的新鮮事
iPhone 3G 於 2008 年問世,當時正值 App Store 開幕。一個新的應用程式,就能將它變成隨身攜帶的樂器、筆記本或遊戲。主畫面按鈕會帶你回到圖像方格,每一個圖像都邀請你試試別的東西。
如今再用這支手機,你會感受到 2008 年已經離我們多遠。顯示器沒有變大,處理器也沒有變快。原始介面仍要求應用程式謹慎運用少數控制項與有限空間。在這些限制內工作,你會注意到一些在較新裝置上很容易忽略的細節:按鈕是否回應、對話是否順暢捲動,以及下一個畫面是否如期出現。
AirBuild3G 讓這個熟悉的畫面多了一項工作。目前的模型協助你撰寫應用程式,手機當年同時代的編譯器則負責建置。原有圖像可以留在原位。
在它們旁邊,還容得下一個從未存在過的應用程式。