我々の書くコードは完璧ではありません。ですが、アプリをクラッシュさせるバグの原因がAppleのコードにある場合、一般的にはバグを報告することぐらいしかできません。ですが、Sash Zatsはこの講演の中で、我々のようなAppleのコードへアクセスできない開発者が、Swiftでswizzlingを用いてプライベートフレームワーク中のバグに対してパッチを当て修正する方法をデモしてくれます。
我々が使うフレームワークのほとんどはまだObjective-Cで書かれているため、メソッドを入れ替え(swizzle)たり、元の実装に修正を加えたりすることができます。Sashは元のソースコードにアクセスできない状況でどうやってバグを探すか、また他人のコードをリバースエンジニアリングして自分のコードを加えるための便利なツールについてデモし、さらにSwiftで関数ポインタを用いてC言語の関数をパッチを当てる方法を説明します。
イントロダクション (0:00)
こんにちは。私はSashです、どのようにプロプライエタリなコードベースを扱い、そのバグやクラッシュをサードパーティのコード内でどのように修正するかについて、話し合いたいと思います。
少し私について話しますと、この講演をすることになるより、前の先週末に髪と髭を剃るまで——これはトラウマ的な経験でした——過去2年間とても髪が伸びていました。5年前ロシアからイスラエルに移り、さらに5か月前に米国に来ました。みなさん思われたかも知れませんが、私は変化が好きです。そして新しいことを試してみるのを楽しんでいます。
Swift によるSwizzling (0:55)
Swiftがリリースされたおよそ1年前、私はAppleの新言語と遊ぶのにわくわくしていました。Swiftには新しいツールとともに実験したくなる、多くの素晴らしい機能や新機能が含まれています。ですが、まずはSwiftの背景に注目してみましょう。よく見てみると、それがなんとなく馴染みのあるものであると気づきます。というのも、Swiftはプラットフォームに依存して、CocoaやCocoa Touchを扱っているからです。つまり、Swiftを使いながら、どうやってこれらのObjective-Cフレームワークを扱うか学ばないといけません。
ファーストパーティのフレームワークのうちどれだけを、AppleはSwiftを使って書いているのでしょうか、ということで簡単なRubyスクリプトを書いてみました:
#!/usr/bin/ruby
developers_dir = `xcode-select -p`.chomp
frameworks_dir = "#{developers_dir}/Platforms/iPhoneSimulator.platform"
frameworks_dir += "/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/*"
swift_frameworks_count = 0
Dir[frameworks_dir].each { |file|
framework = "#{file}/#{File.basename(file, ".*")}"
next unless File.exist?(framework)
puts framework unless `otool -l #{framework} | grep -i swift`.empty?
}
システムフレームワークのフォルダに対してこのスクリプトを走らせてみると、衝撃的な全ての合計を見ることになるでしょう……「0」、完全なゼロです。したがって、Swiftを使う際にどうやってObjective-Cのフレームワークと共存するか学ぶ必要があります。なぜ我々はそのことに気を使うのでしょうか?なぜなら我々のユーザーは気にしないからです。あなたのアプリがクラッシュしたとき、ユーザーは誰のコードにその責任があるのかなんて、まったく気にしません。それがAppleのせいでもあなたのせいでも。ユーザーはバグ報告する方法を知りません。したがって、良い品質を保証するためには、これから話す通り、プロプライエタリなコード内の問題への回避策を見つけ出す必要があります。
Objective-C メソッドのSwizzling(デモ) (2:35)
では、このポイントについて説明するためのUIKittyというアプリのデモを行います。このアプリはInstagramと似ていますが、猫の写真を1枚だけ友達とシェアするためのものです。
補足: ⌘ +
で文字サイズを大きくできるXcodeのプラグインを書きました。
このアプリ内では、iOSの共有シート内の”プリント”は、ビューが正しいサイズでないため正常に表示されません。私のパッチはこれを修正します。そしてこの方法は他のもっと役立つバグ修正にも応用できると思います。
ではその解決策と、それをどうやって達成したかについてひと通りお話しします。ちょっと恐ろしくみえるかもしれませんが、1行1行見て行きましょう。
if let cls = NSClassFromString("UIPrinterSearchingView") {
let block: @objc_block (AspectInfo) -> Void = { (aspectInfo) in
if let view = aspectInfo.instance() as? UIView {
view.frame.size.height = view.superview!.frame.height - 44
}
}
let blockObject = unsafeBitCast(block, AnyObject.self)
cls.aspect_hookSelector(
Selector("layoutSubviews"),
withOptions: .PositionAfter,
usingBlock: blockObject,
error: nil
)
}
まず、ビジュアルデバッガを使って、この整列されていないビューの後ろには実際どのクラスが潜んでいるかを理解することができます: UIPrinterSearchingView
です。1行目で、このクラスを実行時に取得しています。そしてその後は要するにswizzlingという技術を使っています。Swizzlingはフレームワークのコードを、直接的なアクセスをせずに、メソッドをあなたのカスタムコードで実行時に置き換えることを可能にします。今回の場合は、layoutSubviews()
が起こる際に毎回呼び出されてほしいコードブロックを定義します。
フレームワーク自身そのものは、何も変えていません。UIKit
はlayoutSubviews()
をレイアウトパスの中で今までどおりに呼び出します。ですがこの特定のケースでは、layoutSubviews()
の実行が完了した後に、我々のメソッドも呼び出されます。繰り返しになりますが、この特定のケースでは、これはかなりまぬけな修正です。というのもAppleはステータスバーの高さを考慮していなかったからです。ですので44ポイントという値をハードコードすることにしました。
これは実際にうまくいきます。あなたが抱える問題によっては、もっと複雑な修正をする必要があるかもしれません。また、元の実装すらも呼び出さずに全てを置き換えたいときもあるかもしれません。
layoutSubviews()
をswizzleするために、Aspectsというライブラリを使いました。このライブラリと、純粋なswizzlingの違いは、このライブラリはすべてのクラスに渡って特定のメソッドに対してswizzlingの適用を可能にしてくれることです。これはプロジェクト全体の中で、Appleのビューも含めた全てのviewWillAppear()
メソッドに解析用のコードを追加したいときなどに非常に便利です。
このためには、1行のコードを加え、いつ実行されてほしいかを定義するだけです。我々の例では、viewWillAppear()
は元の実装が呼び出されるその直前に呼び出され、実行されます。そのときにスクリーン名や興味深い情報をログに出力することもできるでしょう。
Swizzlingノート (8:34)
UIKitty
のデモでは、Swiftが備える最適化にもかかわらず、swizzlingすることができました。これは、プリコンパイルされたフレームワーク中で動くコードは最適化されていないということを意味します。我々が関わる全てのフレームワークやUIKit
のすべてはまだObjective-Cで書かれているため、このようなswizzlingを加えて立ち去ることができます。フレームワークそれ自身内の呼び出しは最適化されていないため、実行時の実装の置き換えは今のところ安全に行うことができます。
さらには、ふさわしいバージョンのiOSやOS Xにのみパッチを行うべきでしょう。なぜならiOS 6からiOS 7への移行で多くのコードが壊れたのを目撃したように、とても劇的な移り変わりがあるからです。マイナーバージョンのみに限定し、未来のバージョンのOSにはswizzlingによるパッチを適用しないようにしたほうがよいでしょう。
さらにパッチをできる限り多くのデバイスでテストするべきです。多くのフレームワークはデバイスによって異なった方法でコンパイルされるのみではなく、シミュレータとデバイス間では異なって書かれています。
たびたび、パッチはシミュレータでの問題を直しても、デバイスには適用できなかったり問題を修正しないこともあるため、swizzleされたメソッドをシミュレータのみでテストすることは避けるべきです。
最後に、もし興味があるなら、アスペクト指向プログラミングは探検してみるべきコンセプトです。
C言語関数のパッチ (10:12)
もう一つの例は、私の友達が書いた Organizerというアプリ内にあります。このアプリは、古いカメラからたくさんの写真をインポートした時に、iPhotoが撮られた写真のメタデータを取り去ったりめちゃくちゃにする問題に対処するものです。
Organizerでは写真のメタデータを調整することができます。友達の彼が言うには、Appleでさえも社内でこれを使っているとのことです。これには、聞いたことがあると思いますが、Photos.frameworkを使います。1つ前のバージョンのOSで導入され、フォトライブラリを扱うのに便利な方法を提供することを目的としています。
このフレームワークのすばらしい機能として、フォトライブラリの変更を監視できる機能があり、UIの中でその変更をアニメーションさせることができます。例えば、写真がアプリ外で削除された場合、グリッド状に並んだ写真一覧でその変更をアニメーションできます。このフレームワークはこのような動作を、ただreloadData()
を呼ぶより見事に行ってくれます。
このデモでは、Appleのウェブサイトから直接ダウンロードできるサンプルプロジェクトをお見せします。これはフォトライブラリを表示するシンプルなアプリです。私が元のコードに加えた唯一の変更点は、フォトライブラリを監視し変化をアニメーションさせることです。実際に写真をタップすると、アプリは写真の変更日時を現在時刻に変更し、並び替えアニメーションを発動させます。
これらの変化がその場で発生することを確認でき、また誰かがバックグラウンドでその写真を変更しようとはしていません。ですが、写真をタップすると、アプリはただクラッシュしてしまいます。これはタップ部分を除いては、Appleのウェブサイトで見つかるサンプルコードそのままです。そしてタップ部分にはバグがないことをお約束します。この部分は意図したとおりに動いているのです。
クラッシュを突き止める(デモ) (13:04)
ではこの問題を追って、自分たちで解決してみましょう。
[デモ] - クラッシュ後のスタックトレースの中で、クラッシュは我々が作ったものではないメソッドの中で現れています。スタックのトップがobjc_msgSend
を表示しているとはいえ、問題はたいていそこにはなく、その前のステップにあります。objc_msgSend
はただの最後の足あとです。
ですが、その下にはどこかAppleのコードから来た2つのわからないシンボルがあります。これが我々が追跡して直さなければいけないものです。
コツ: コンソールにテキストが表示されるだけのものよりも良い、クラッシュのスタックトレースを得るためには、”All Exceptions”を有効にするとよいでしょう。これにより例外が投げられてキャッチされなかった後に止まるのではなく、例外が発生したその瞬間にストップしてくれます。そうすると、スタックの中で変数の状態を確認することができるようになります。
クラッシュするシンボルを追跡するために、まずはQuick Openでその名前を検索することを試します。そしてAppleのパブリックヘッダの中に現れることを期待します。この例ではPhotos.frameworkの中の特定のクラスを示しているだけのシンボルだったので、ラッキーな状況です。
とはいえ何の結果も得られなかった場合には、GitHubで検索することをおすすめします。iOSの内部プレイベートヘッダをダンプした多くのリポジトリ(例えばこれなど)があります。そこでそのクラスやメソッドの名前を打ち込むと、それがAppleのフレームワークのどこから来たのかがわかります。これは全てのフレームワークの中から文字列をひっぱってくるよりも良い方法です。非常に考慮する価値があります。
Hopper によるディスアセンブル(デモ) (16:15)
これで、このクラッシュがPhotosフレームワークの中で起こったことがわかりました。ではこれからどうしましょう?どうやって実際にクラッシュしているものが何か特定しましょう?
Hopper Disassemblerはこのケースに驚くほど役立つツールです。もし単にiOSやOS Xがどのように動いているのか興味があれば、このツールを起動してフレームワークを開いてみましょう。コンパイル済みコードをディスアセンブルすることができ、元のコードに近い形でとても良い感じに表示してくれます。
Hopperを用いることにより、フレームワークを読み込み、不正なクラスを発見し、クラッシュしたメソッドを検索することができます。アセンブリが憑依���れますが、擬似コードボタンによりObjective-Cとアセンブリの混合物を見ることができ、問題の解読を試し、追跡することができます。ときどきこれは問題そのものを表示してくれますが、アセンブリの知識が助けになります。この例のような実際の問題は追跡して理解することが難しくなることがあります。
このアプリは明らかに8つのパラメータを持つ静的なC関数でクラッシュしています。面白いですね:
diffArrays(var_24, eax, edi->_changedItems, nil, nil, nil, nil, nil);
void diffArrays(NSArray <NSManagedObject *> *arg0,
NSArray <NSManagedObject *> *arg1,
NSArray <NSManagedObject *> *arg2,
NSIndexSet **arg3,
NSIndexSet **arg4,
NSIndexSet **arg5,
NSArray <NSManagedObject *> **arg6,
NSIndexSet **arg7);
この関数は写真を表すマネージドオブジェクトと挿入、削除、更新などの変化を表すインデックスセットを引数に取ります。
この関数は終わりで、何が変更されたかを返すはずです。それこそ、私が最初にやろうとしたことでした。このフレームワークは変更を監視し、UIの中でアニメーションをすることを可能にしてくれます。そしてこの関数はその中核にあるのです。
ですが、静的なC関数であるため、これをswizzleする方法はありません。swizzleしてこの関数が置き換わるようなクラスが単に存在しないため、前の例での解決策は使えません。
以下に示す我々のパッチでは、これを異なった方法で解決しています。要するに、Swiftの内部実装に手を伸ばし(Swift自体の不安定な解決策です)、実際の問題のある関数のアドレスにアクセスします。
クラッシュの修正 (19:14)
最も簡単なパートは、問題を見つけた後に、どうやって回避するかを単に発見する部分です。関数そのものの実装は、追うのはとてもつらいもので、途中のある場所で諦めました。ですが、私が取り戻そうとしていたのはデアロケートされたメモリへの典型的なバッドポインタで、しかし誰かが未だにそのポインタを使ってメモリにアクセスしようとしてるものであると気づきました。
これにパッチを当てるために、私は最終的に戻り値になるパラメータのひとつを余分にretainすることにしました。私友達がこの解決方法をAppleのQ&Aエンジニアに見せたところ、彼は、これでは8バイトのリークが発生するだろうとコメントしました。ですが、これはクラッシュしません——はるかに良い結果です。
こういった種類の問題を直すことは常に、どうやって動いているのかを理解しようとすること、過剰にメモリを犠牲にしないようにすること、そして、アプリの中核の機能としてユーザーを幸せにし続けることとのバランスです。もし日付を変更するたびに毎回アプリがクラッシュしたら、誰も幸せにはならないでしょう。
SwiftでのC言語のパッチ (21:02)
次は、このパッチの詳細を一つ一つ見て行きましょう。
// 内部の構造体
struct swift_func_wrapper {
var trampolinePtr: UnsafeMutablePointer<uintptr_t>
var functionObject: UnsafeMutablePointer<swift_func_object>
}
struct swift_func_object {
var original_type_ptr: UnsafeMutablePointer<uintptr_t>
var unknown: UnsafeMutablePointer<UInt64>
var address: uintptr_t
var selfPtr: UnsafeMutablePointer<uintptr_t>
}
このバージョン(2.0)までのSwiftは内部で関数をこのように表しています。もしSwift内部の関数の心臓部に到達したいなら、アドレスを得るためにこの構造体を切り取って内部に手を伸ばす必要があります。これはSwiftの関数にCから、あるいはCの関数にSwiftからアクセスするために必要なプロセスで、その中で関数のアドレスにアクセスできます。
次に、例としてシンプルな関数を定義し、どのようにこれを呼び出すか、元のバグを含んだ関数に紐付けるかをお見せします。実際は、Cの関数に対応した8つの引数を持ち、これらをretainするような実装となるでしょう。ここでは文字列をログに出力するシンプルなものを例とします:
// 呼び出したいメソッド
func hello(world: String) -> Void {
print("Hello, \(world)")
}
typedef helloFn = (String) -> Void
次に Swiftの関数の後ろに潜む実際のCの関数へのポインタを得ます:
// C の関数へのポインタ
let fn = UnsafeMutablePointer<helloFn>.alloc(1)
fn.initialize(hello)
let fnWrapper = UnsafeMutablePointer<swift_func_wrapper>(fn)
let opaque = COpaquePointer(bitPattern: fnWrapper.memory.functionObject.memory.address)
let cFunction = CFunctionPointer<helloFn>(opaque)
ここではまず我々が定義したSwiftの関数から始め、次にSwiftの関数を表す構造体の実装の詳細に手を伸ばすことで、アドレスを入手します。次に実際にCの関数ポインタとして得ます、これはC言語側に渡し、そこから呼び出されるもので、この部分は単純にうまくいきます。
ここでまだ言及していないことは、どうやって元の実装を呼び出すかということです。このケースでは、私はどのように元の関数を再実装するかは知りたいと思わないので、元の実装を呼び出して、返されるオブジェクトをretainしなければなりません。これはSwift 1.2での問題点です。Cの関数ポインタを用いて、Cの関数を呼び出すことはできません。明確に言うと、SwiftからCの関数を呼び出すことはできますが、他のツールを用い関数へのポインタを入手し、Objective-Cでやるようにポインタを介して関数を呼び出すことはできません。
結果として、この部分はObjective-Cで実装する必要があります。あまり誇らしいことではありませんが、今のところこうするしかありません。
結論とTips (24:30)
まず、そのバグの回避方法を見つけ出し、説明したツールを使ってパッチを適用する必要があります。
書いた回避用コードをパッチする実際のエンジンはFishhookです。これはObjective-C内で実行時にどんなCの関数でも取得することができ、基本的にswizzleできるライブラリです。
もう一度言いますと、その関数を呼ぶ場合はだれでもあなたの実装を呼び出すことになり、またパッチの中では元の実装を得ることができます。ここがSwiftの及ばないところです。ですが、Swift 2.0について私が理解していることから言うと、可能性として@convention(c)
がそれを直接Swift内で行うことを可能にしてくれそうです。
その他のフレームワーク内の問題の修正 (25:28)
もしObjective-Cのコード内に問題があるのなら、swizzleして終わりです。
もし問題がCの関数にあるのなら、プロジェクトにObjective-Cのコードを加える必要が有るため、やや不格好になってしまいます。見たとおり、Swiftはこの件に関してはとても悪いです。Swiftの最適化が原因で、最適化が存在しない時と同様に動的にディスパッチされるかは保証されません。したがって利点である実行時のメソッド置き換えの機会がないのです。
最適化は我々が好きなSwiftのスピードや利点をもたらしてくれますが、同時にこのようなパッチングによる解決法を妨げているのです。
クレジット, 参考文献と資料 (26:19)
-
Perceptual Debugging by Kendall Gelner: もっとも重要なパートである直感的なデバッグ能力を発達させるプロセスについての議論をするAltConfのおすすめのセッションです。残りのパッチの当て方は単なる技術ですが、どうやって問題を見つけるか、どうやってパッチを当てる方法を発見するかを理解する必要があります。これは学び、熟練するのに時間がかかります。
-
Reverse Engineering by Samantha Marshall: リバースエンジニアリングに関するツール、技術についてなどの有益なリンク集です。
-
Unsafe Swift: For Fun & Profit by Russ Bishop: C言語とSwiftのインタラクションについての綿密な議論です。このテーマで私が軽く触れたことの全てをカバーしています。構造体やセーフポインタなど。このトークを見ることをとてもおすすめします。かなり面白いです。
-
Peter Steinberger’s blog by 彼自身: 彼は、仕事でPSPDFKitに取り組む一環で、
UIKit
にすさまじくパッチを当てているため、素晴らしい資料集となっています。 -
WWDC のデバッグについてのセッション: これらはあなたをより良い人間にしてくれます。とにかく見ましょう。
-
Fishhook: C言語の関数をswizzleするのに役立つライブラリです。
-
Aspects:Peterによる素晴らしいライブラリで、アプリ全体にわたって適用される機能を追加してくれます。最初の例で示しました。
Q&A (29:09)
Q: 昔は関数のオフセットにもとづいて、スタック中のアドレスを元にしてコード中のその位置を示すロードマップを生成するツールがあったと思うのですが、iOSで、これと問うかなLLVMを用いた何かはありますか?
Sash: LLVMを用いて実行を一時停止したときに、ランループのどこかに着地してしまって、そこから調査しなければならいことがあると思います。これをもっと簡単にするような特定のツールについてはあまり知りません。
聴衆のひとり (@nevyn): これは単純にotool
or nm
を使えば達成できます─どっちだったかは覚えていませんが、バイナリのロードコマンドの一覧を得ることができます。
About the content
This content has been published here with the express permission of the author.