2008 年、iPhone 3G と最初の App Store によって、多くの人がモバイルアプリに出会いました。私たちは、その端末に新しい役割を加えるために AirBuild3G を開発しました。iPhone 3G または 3GS で新しいアプリの内容を伝え、コーディングモデルとソースコードを編集し、同じ端末上でコンパイルして実行できます。モデルは、選択したサービス上で動作します。プロジェクトファイルと開発ツールは端末内に残ります。
AirBuild3G 自体は、Mac 上の Xcode 27 でビルドしています。AirBuild3G で作成するアプリには、端末にインストールされた別のコンパイラを使用します。両方を動かすには、端末の準備、リンク、HTTPS、端末側のツールチェーンという 4 つの領域で変更が必要でした。以下では、最初のアプリが動くまでの作業を順に説明します。
当時のハードウェアを準備する
小さなディスプレイと物理 Home ボタンを見れば、すぐに iPhone 3G だとわかります。この端末向けに再び開発するには、アプリらしさを形づくっていたコントロールを含め、当時の画面比率と制約を尊重する必要があります。AirBuild3G 自体も、グループ化された設定テーブルやナビゲーションバーのアクションなど、iOS 4 で利用できる UIKit コントロールを使用しています。
AirBuild3G は iPhone 3G と 3GS で動作します。3G には armv6 バイナリが必要で、3GS は armv7 を使用します。各端末にインストールできる環境パッケージは、ファームウェアによって決まります。検証に使った armv6 端末は、カスタムファームウェアで iOS 4.2.1 を実行する iPhone 3G です。
- 準備復元 / アクティベート
- 接続SSH + ネットワーク
- インストールアプリ + 環境パッケージ
- 環境設定 → 環境
まず、Home 画面まで起動し、Mac からの SSH 接続を受け付ける端末を用意します。Debian パッケージを受信してインストールするには、動作するネットワーク接続とパッケージマネージャが必要です。送信するパッケージはハードウェアモデルとファームウェアのバージョンで決まり、送信先はネットワークアドレスで決まります。信頼できる USB ケーブルと電源も必要です。
古い端末によっては、アクティベートとシステムアクセスに追加の手順が必要です。対応するハードウェアでは、Legacy iOS Kit を使い、端末のモデルとファームウェアに合った案内に従ってください。作業を始める前に端末をバックアップしてください。AirBuild3G 自体には端末をアクティベートする機能はありません。
AirBuild3G の TLS 対応には Substrate も必要です。3G 向けのビルドには armv6 を含める必要があります。バージョンにかかわらず、armv7 のみのパッケージは 3G では動作しません。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 のライブラリを不明なプラットフォーム向けとして報告します。そのため、これらのライブラリには生成したスタブをリンクし、旧 SDK はヘッダ用として残します。
- コンパイルclang
- アーキテクチャ
- armv6
- 最小 iOS
- 4.0
- コード
- e92d40f0 …
- リンクラベル変更後、ld
- アーキテクチャ
armv6armv7- 最小 iOS
4.05.0- コード
- e92d40f0 …
- 配布ラベルを戻してパッチ適用
- アーキテクチャ
armv7armv6- 最小 iOS
5.04.0- コード
- e92d40f0 …
ビルドでは両方のスライスを iOS 4.0 向けにコンパイルし、iOS 5.0 向けとしてリンクした後、最小バージョンを 4.0 に戻します。armv6 では、リンカが受け付けるように各オブジェクトファイルのラベルを armv7 に変更し、リンク済みスライスのラベルを armv6 に戻します。ラベル変更で変わるのはメタデータであり、命令ではありません。コンパイル済みコードとリンカが追加するシンボルスタブは、あらかじめ 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 にある 4 バイト、つまりヘッダの cpusubtype フィールドです。命令には一切触れません。
iOS 4.3 より前には ASLR がなかったため、ビルドでは -no_pie を残しています。また、Objective-C カテゴリのマージも無効にします。有効にすると、マージされたメソッドから Thumb ビットが失われ、3GS 上で AirBuild3G がクラッシュするためです。make verify は、アーキテクチャ、ロードコマンド、armv6 命令、armv7 メソッドの Thumb ビットを検証します。リンクの成功だけでは、これらが正しいことは証明できません。
make を実行すると、AirBuild3G と権限付きヘルパーがビルドされます。make payload は、バージョンを固定した全パッケージをローカルキャッシュへダウンロードして検証します。make package は、そのキャッシュから AirBuild3G のパッケージと 2 種類の環境パッケージをオフラインでビルドします。各環境パッケージには端末用の SDK アーカイブが含まれるため、パッケージ作成にも LEGACY_SDK が必要です。
make install-device DEVICE=root@<ip> は AirBuild3G と iOS 4 用の環境パッケージをインストールし、Home 画面を更新してアプリを再起動します。iOS 5 以降を実行する 3GS では、代わりに tools/deploy_device.sh の 3 番目の引数として ios6 を渡します。アプリは mobile ユーザーとして動作し、隣に置かれた小さなヘルパーが root 権限を必要とする処理をすべて実行します。これで環境パッケージが端末に入りました。設定 → 環境から、パッケージ内の開発ツールをインストールできます。
モデルサービスに接続する
設定の「エンドポイント」で、ベース URL、API キー、モデルの 3 つを入力します。リクエストは端末からそのエンドポイントへ直接送信されます。中継するサーバはなく、アカウントや端末の登録もありません。モデルサービスがモデルを実行し、利用料金を請求し、入力すべき値を案内します。端末が私たちからの贈り物だった場合は、代わりに air.build へ接続します。
標準の iOS 4 が SecureTransport を通じて対応するのは TLS 1.0 だけですが、現在のエンドポイントでは TLS 1.2 が必要な場合があります。そこで、CFNetwork 用の Substrate tweak である TLSFix を使って TLS 1.2 を追加します。
端末は引き続き、サーバの身元を検証する必要があります。古い信頼ストアでは、現在のエンドポイントが使用する証明書チェーンを構築できない場合があり、TLS 1.2 を追加しても信頼ストアは更新されません。接続に失敗した場合は、暗号化と証明書の信頼を別々の問題として調べます。
AirBuild3G は、デフォルトで証明書を検証します。リクエストが失敗したら、まずベース URL、API キー、モデル、証明書チェーンを確認してください。「すべての証明書を信頼」設定は、明示的な例外です。オンにするとサーバの身元確認が省かれ、エンドポイントになりすませる相手に API キーが漏れるおそれがあります。自分のネットワーク上にあるエンドポイント以外では、オフのままにしてください。AirBuild3G は信頼評価のたびに設定を読み取るため、オフに戻すと次のリクエストから反映されます。
static OSStatus ABLSecTrustEvaluate(SecTrustRef trust, SecTrustResultType *result) {
…
OSStatus status = ABLOriginalSecTrustEvaluate(trust, result);
if (!ABLTrustAnyCertificate()) {
return status;
}
if (result != NULL) {
*result = kSecTrustResultUnspecified;
}
return errSecSuccess;
}- まず、端末自身の評価が実行されます。
- 「すべての証明書を信頼」はデフォルトでオフのため、その結果が変更されずに返されます。設定は呼び出しのたびに
NSUserDefaultsから読み取られます。
返信のストリーミングも、端末の処理能力に収める必要があります。AirBuild3G はバックグラウンドスレッドでストリームを受信してデコードし、完成したテキストだけをインターフェイスへ渡します。返信中は、トークンごとに再計測してスクロールせず、一定間隔で会話を更新します。これにより、返信の受信中もレイアウト処理を抑えられます。返信が表示されれば、接続は機能しています。モデルが書いたコードをコンパイルするには、端末上の環境も必要です。
端末で動くコンパイラをインストールする
設定 → 環境では、環境パッケージからコンパイラ、SDK ヘッダ、リンクライブラリ、署名ツールをインストールします。iOS 4 パッケージには armv6 用の GCC が含まれ、iOS 5 以降のパッケージには armv7 用の clang が含まれます。各端末には、ファームウェアに合うものだけがインストールされます。
すべてのパッケージはダウンロード先とチェックサムで固定され、インストール時にはリポジトリへ最新版を問い合わせず、そのファイルをそのまま使用します。固定されたバージョンがすでにインストール済みならスキップされるため、2 回目の環境セットアップはすぐに完了します。インストールしたパッケージはその後保留され、ツールチェーンを壊すアップグレードを Cydia が提示しなくなります。
iPhone 3G の環境では、2008 年版の iphone-gcc 4.2 を使用します。2009 年版はプリコンパイル済みヘッダ用に 1 GB を予約するため、その一部が armv6 端末のアドレス空間を越えます。コンパイラのバックエンドは、何もコンパイルしないうちにクラッシュします。どちらも Apple build 5555 として識別されますが、このアドレス空間に収まるのは 2008 年版だけです。
0x300000002008 iphone-gcc · すべてのセグメントが 6 MB 未満動作
512 MB 配列 · テストバイナリ動作
768 MB 配列 · テストバイナリSIGBUS
2009 iphone-gcc · 1 GB のプリコンパイル済みヘッダ領域SIGBUS
SDK ヘッダにも変更が必要です。この GCC はブロック宣言を解析できません。iOS 4 のヘッダをパッケージ化する前に、それらの宣言と、削除した型に依存する宣言をすべて取り除きます。clang はブロックをサポートするため、clang パッケージでは Apple のヘッダを変更せずに使います。各パッケージには、そのコンパイラが生成したものに署名できる署名ツールも含まれます。
端末はシステムフレームワークを dyld 共有キャッシュに保持しているため、フレームワークのディレクトリにはリンカが読み込めるライブラリファイルがありません。そこで、エクスポートされたシンボルと実際のライブラリ名を持つスタブを生成して提供します。リンカは各参照をスタブに対して解決し、その参照元となるライブラリを記録します。起動時には、dyld が共有キャッシュ内の実装へ結び付けます。スタブに実際のシンボル名が含まれるため、フレームワークが不足している場合は、アプリの起動時にクラッシュするのではなくリンクに失敗します。
環境は include パスも設定し、gcc と cc をインストール済みのコンパイラに向け、Objective-C ランタイムと 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 はテンプレートをプロジェクトディレクトリにコピーし、そのファイルをモデルのコンテキストに入れます。モデルはすでにビルドできるプロジェクトから作業を始めるため、最初のリクエストでは Makefile の作成ではなく、アプリの内容を伝えられます。
最初のアプリには、画面上の数値を増やすボタンが適しています。全体を読めるほど小さい一方で、端末上での編集、コンパイル、パッケージ作成、実行をひと通り試せます。リクエストでは iOS 4 を対象に指定し、アプリをビルドしてインストールするようモデルに依頼します。インターフェイスのコードでは、端末の iOS に実装されている API だけを使う必要があります。ヘッダに宣言があっても、その API が実行時に存在するとは限りません。
モデルには 4 つのツールがあります。Execute はプロジェクトディレクトリでコマンドを実行します。EditFile はファイル全体を書き込みます。PatchFile は一致する箇所を置き換えます。CheckBuild は環境設定を読み込んで make を実行し、出力の末尾を返します。ツールの結果はモデルへ返され、モデルは作業を続けるか、失敗した変更を修正できます。推論処理は一貫してモデルサービス上で行われます。
ツール呼び出し
ツール結果
Execute EditFile PatchFile CheckBuildモデルがツール呼び出しなしで返信するか、「停止」をタップするか、16 ラウンドが完了するまで、このループが続きます。
新しいアプリをインストールしたら、端末上で開きます。カウンターでは、ボタンをタップして数値が変わるのを確認すれば、依頼した動作を正確に検証できます。その後のリクエストでは、レイアウトを変えたり、別の操作を追加したりできます。モデルは、既存のファイルと以前のツール結果をもとに作業を続けます。
AirBuild3G は各プロジェクトとその会話を端末上に保持し、ツールラウンドが終わるたびに保存します。開いているプロジェクトも記憶します。アプリから離れたり、メモリが不足したりすると、プロセスが終了することがあるためです。AirBuild3G を再び開くと、会話とファイルを含む同じプロジェクトに戻ります。次のメッセージは保存済みの会話から続き、次の編集とビルドに使うソースも残っています。
この画面では、あの簡単なカウンターでさえ、かつて使っていたアプリと、いま作れるアプリをつないでいます。
見慣れた画面に、新しいものを
iPhone 3G は、App Store が開設されたばかりの 2008 年に登場しました。新しいアプリを入れれば、いつも持ち歩く楽器にも、ノートにも、ゲームにもなりました。Home ボタンを押すとアイコンが並ぶ画面に戻り、どのアイコンからも次の何かを試せました。
いまその端末に触れると、2008 年がどれほど昔だったかを感じます。ディスプレイは大きくなっていません。プロセッサも速くなっていません。当時のインターフェイスのまま、少ないコントロールとわずかな画面を慎重に使うことがアプリに求められます。その制約の中で作業すると、新しい端末では見過ごしやすい細部に気づきます。ボタンが反応するか、会話が滑らかにスクロールするか、次の画面が適切なタイミングで表示されるか。
AirBuild3G は、その見慣れた画面にもう 1 つの役割を加えます。現在のモデルがアプリのコード作成を助け、端末と同じ時代のコンパイラがそれをビルドします。古いアイコンは、そのまま残しておけます。
その隣には、これまで存在しなかったアプリを置く余地があります。