■What is Tweak?
「CydiaでよくダウンロードするTweakとは何なのか?」について割りと詳しく解説してみようと思う。様々な便利な機能の追加や、細かい変更をOSに施すTweakは単語であるTweakの意味「ちょっとした調整」から来ている。ソフトウェアの既存動作にちょっとした調整を加える目的のソフトウェアをさす。
では好みの動作をさせるにはどうしたらいいか。
端的に言えばある処理をフックして好みの処理に置き換える事で実現出来る。
フックを考えるにあたってまず、そのソフトウェアの動作を考えよう。
iPhoneひいてはOS Xのソフトウェアは専らObjective-Cで記述されている。
Objective-Cは非常に動的な言語で、コンパイル時点であまり警告・エラーを出さず実行時にうまいこと動けばOKって感じの言語だ。
オブジェクト指向言語であり、クラスとメソッドとインスタンス変数があってカプセル化されているように見えるが、C言語が皮をかぶっているだけで中でランタイムがゴリゴリ動いてる印象だ。
メソッドもC関数に皮を数枚かぶせた感じの実装で、実態はIMP型という関数ポインタだ。
更にランタイムが非常に強力で、カプセル化されていながら、見ようと思えば何でも見れるようになっていて実にフックしやすい言語なんだ。
フックの仕方には何種類か方法があって、木下さんの連載を参考にすると
- IMPの入れ替え(Method Swizzling)
- フォワーディング
- ポージング
IMP変数を取得するための関数もランタイムで提供されているため、実に簡単にフックが行える。
■例を見てみよう!
- (BOOL)shouldShowDictationKey;というインスタンスメソッドがUIKeyboardLayoutStarクラスに存在する。これは音声入力キーを表示すべきかどうかYESかNO(BOOL値)で返すメソッドだ。iOSの音声入力はSiriが可能な言語と今のところ同じであるので、使用中のキーボードがそれに該当するかどうかを判定してYESかNOを返していると想像がつくと思う。
このメソッドを書き換えて常にYESやNOを返す処理に変えてしまえば、あのマイクキーが常に出るようになったり、常に表示されなくなる事も想像がつくだろう。 このメソッドをフックするには以下のようなコードで実現できる。
IMP old, new; old = class_getMethodImplementation([UIKeyboardLayoutStar class], @selector(shouldShowDictationKey)); new = class_getMethodImplementation([UIKeyboardLayoutStar class], @selector(new_shouldShowDictationKey)); IMP tmp; tmp = old; old = new; new = tmp;オリジナルのメソッドを使う気がなければ最後のところは
old = new;だけで事足りてしまう。 また、method_exchangeImplementationsなんて素敵な関数もあるのでこうも書ける。
Method old, new; old = class_getInstanceMethod([UIKeyboardLayoutStar class], @selector(shouldShowDictationKey)); new = class_getInstanceMethod([UIKeyboardLayoutStar class], @selector(new_shouldShowDictationKey)); method_exchangeImplementations(old, new);いずれにせよ、これらの処理を行えば以後OSやアプリケーションがshouldShowDIctationKeyメソッドを呼び出すと、実際に処理が走るコードは自らが書いたnew_shouldShowDictationKeyメソッドのもととなる。 ちなみにnew_shouldShowDictationKeyは追加しなければならないので、class_addMethod関数で追加できる。
static BOOL replaced_UIKeyboardLayoutStar_shouldShowDictationKey(UIKeyboardLayoutStar *self, SEL _cmd) { return NO; } class_addMethod([UIKeyboardLayoutStar class], @selector(new_shouldShowDictationKey), &replaced_UIKeyboardLayoutStar_shouldShowDictationKey, "c@:");メソッドの実装であるIMPを上書きするだけでも書き方は色々ある。
Method method = class_getMethodImplementation([UIKeyboardLayoutStar class], @selector(shouldShowDictationKey)); static BOOL replaced_UIKeyboardLayoutStar_shouldShowDictationKey(UIKeyboardLayoutStar *self, SEL _cmd) { return NO; } method_setImplementation(method, &replaced_UIKeyboardLayoutStar_shouldShowDictationKey);さて、これらの処理を適当な関数にまとめて、その関数を呼び出せば晴れてフックが完了するが、元々は存在しない関数だ。一体どうやって呼べばいいのか。
そもそもこのコードを任意のタイミングで読みこませる(ロード)にはどうしたらいいのか。
まず、動的なコードのロードはライブラリが同様の形態を持っているため、これを読み込むための環境が配備されている。OS XのサブセットであるiOSではdyld(Linuxならld-linux.so)によって読み込まれる.dylib形式(Linuxで言えば.so、Windowsなら.dll)にコンパイルしてdyldに読み込んでもらえばいい。
次に関数の呼び出しだがアプリケーション側が呼んでくれるはずもないので、ロードされたタイミングで実行する関数をdyldに指定するオプションがある。これはgccの-initオプションの引数に関数名を記述すればいい。このオプションはDarwin linker(dyldのことだ)に引き渡される。man dyldでマニュアルを読むとこのオプションで指定された関数はdyldによって実行される事が書いてある。
そこには__attribute__((constructor))が指定されたものも実行する事が記述されている。
これらをまとめると、このTweakは以下のようなコードになる。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
#import <CoreFoundation/CoreFoundation.h> | |
#import <objc/runtime.h> | |
#import <UIKit/UIKit.h> | |
@class UIKeyboardLayoutStar; | |
static BOOL replaced_UIKeyboardLayoutStar_shouldShowDictationKey(UIKeyboardLayoutStar *self, SEL _cmd) { | |
return NO; | |
} | |
__attribute__((constructor)) | |
static void OverrideMethodFunction() { | |
Method method = class_getMethodImplementation([UIKeyboardLayoutStar class], @selector(shouldShowDictationKey)); | |
method_setImplementation(method, &replaced_UIKeyboardLayoutStar_shouldShowDictationKey); | |
} |
この例だとメソッドの置換ではなく上書きにしたため、わりと短いコードですんでいるが、置換するとなると行うべき処理は多くなっていき、フックするメソッドが増えていくととても長いコードになっていく。そこで、このランタイムによる実装の置き換えを便利な関数でまとめてくれたライブラリを提供してくれるパッケージがCydiaにはある。そう、MobileSubstrateだ。
■MobileSubstrate
MobileSubstrateの提供する関数はメソッドの置換や関数の置換、インスタンス変数の取得を行える関数を提供してくれている。では上記のコードをMobileSubstrateの一般的な関数を使用した形にすると、以下の様になる。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
#import <CoreFoundation/CoreFoundation.h> | |
#import <objc/runtime.h> | |
#import <UIKit/UIKit.h> | |
#import <substrate.h> | |
@class UIKeyboardLayoutStar; | |
static BOOL replaced_UIKeyboardLayoutStar_shouldShowDictationKey(UIKeyboardLayoutStar *self, SEL _cmd) { | |
// can be able to call original method. | |
NSLog(@"original return BOOL value = %@", [self PREFIX_shouldShowDictationKey] ? @"YES" : @"NO"); | |
return NO; | |
} | |
__attribute__((constructor)) | |
static void OverrideMethodFunction() { | |
MSHookMessage([UIKeyboardLayoutStar class], | |
@selector(shouldShowDictationKey), | |
(IMP)&replaced_UIKeyboardLayoutStar_shouldShowDictationKey, | |
"PREFIX_"); | |
} |
- MSHook系関数を提供するライブラリlibsubstrate.dylib
- TweakをロードするSubstrateLoader.dylib
- plistによるTweakのロードを制御(SubstrateLoader)
- DYLD_INSERT_LIBRARIES環境変数へのMobileLoader.dylib(MobileSubstrate->SubstrateInjection->SubstrateBootstrap->SubstrateLoader)の追加
- DYLD_INSERT_LIBRARIESを元に戻すMobileSafety
これはDynamicLibrariesディレクトリ以下のdylibを同名のplistの内容からロードの可否を判定してロードしてくれる。特定のアプリケーションにのみフックを適応させたい場合、このplistに条件を記述しておくことでコード本体にそのif文をかかずにすむようになっている。これがないと各Tweakは自分で自分のコードをロードする為のコードをかかなくてはならなくなってしまう。
TweakをロードするのがSubstrateLoaderなら、SubstrateLoaderはどのようにロードされているのだろうか。SubstrateLoaderだって勝手に書かれた関数でしかないのだ。これにはDYLD_INSERT_LIBRARIESという環境変数へdylibを指定する事でdyldがロードするdylibの一つに加える事で実現している。
DYLD_INSERT_LIBRARIESはLinuxでいうところのLD_PRELOADに非常によく似ていて、テストのために一時的に使用するライブラリのバージョンを違うものを指定したい時などに使われるものだ。LD_PRELOADを使ったフックについてはこちらのページ(LD_PRELOADを使ったテスト(C言語編))が非常にわかりやすく、行なっている内容もiOSのTweakと非常に似ている。そんなに長くないので是非読んでみてほしい。
現在のMobileSubstrateはMobileSubstrate.dylibをDYLD_INSERT_LIBRARIESに追加している。/Library/MobileSubstrate/MobileSubstrate.dylibは/Library/Framework/CydiaSubstrate.framework/Libraries/SubstrateInjection.dylibへのシンボリックリンクとなっていて、SubstrateInjection.dylibは同ディレクトリのSubstrateBootstrap.dylibへのシンボリックリンクとなっている。
SubstrateBootstrapからSubstrateLoader.dylibが読み込まれ、各種Tweakが読み込まれる流れだ。
最後にMobileSafetyについて。SpringboardをクラッシュさせるTweakをインストールしてしまった場合、無限にRespringする事態におちいるのを防ぐためにTweakの導火線であるSubstrateLoader.dylibをロードするDYLD_INSERT_LIBRARIES環境変数を元の状態に戻した上でSpringboardを起動する役目を担っている。これが俗にいうSafe Modeだ。Tweakのロードを行わないだけなので、Cydiaからインストールしたフックを行わないアプリケーション(iFileとか)は普通に起動できる。SpringboardのクラッシュまでならMobileSafetyで対応できますが、Rebootループになるようなものには無力なので、最新のMobileSubstrateでは音量アップボタンを押しながら起動する事でTweakはおろかMobileSafetyもロードせずに起動できるようになりました。この場合もやっぱりiFileとかは動かせます。
ちなみにOS XではSIMBLというLoaderが有名だ。MSHook系の関数を使用する前のようなコードを書く事でOS X向けのTweakも作成できる。
■theos
最後にtheosについて。theosはiOS向けのMakefileプラットフォームで、長ったらしいMakefileを非常に簡素化できます。また、詳細は過去の記事を参照として割愛しますが、Tweak向けにマクロを用意してあり、直感的に記述できるようになります。上記の例示コードでいくとtheosを使用するとたった3行で記述できてしまいます。簡単だね!:)
%hook UIKeyboardLayoutStar - (BOOL)shouldShowDictationKey { return NO; } %end
■終わりに、Jailbreak developerが増える事を願って。
iOSのTweakのソースコードの多くはgithubで公開されています。記事上部で長々と書いたコードもtheosを使えば簡単に書けますので、「ここの動きが気に食わない」と感じるところがあれば、是非チャレンジしてみてはどうでしょうか。私も最初は大学でのC言語の授業(ポインタぐらいまで)程度の知識からスタートしましたので、そんなに難しくはないと思います。
何より自分の作ったものが動くというのは楽しいものですよ?