What is Tweak?



■What is Tweak?

「CydiaでよくダウンロードするTweakとは何なのか?」について割りと詳しく解説してみようと思う。

様々な便利な機能の追加や、細かい変更をOSに施すTweakは単語であるTweakの意味「ちょっとした調整」から来ている。ソフトウェアの既存動作にちょっとした調整を加える目的のソフトウェアをさす。

では好みの動作をさせるにはどうしたらいいか。
端的に言えばある処理をフックして好みの処理に置き換える事で実現出来る。

フックを考えるにあたってまず、そのソフトウェアの動作を考えよう。
iPhoneひいてはOS Xのソフトウェアは専らObjective-Cで記述されている。
Objective-Cは非常に動的な言語で、コンパイル時点であまり警告・エラーを出さず実行時にうまいこと動けばOKって感じの言語だ。

オブジェクト指向言語であり、クラスとメソッドとインスタンス変数があってカプセル化されているように見えるが、C言語が皮をかぶっているだけで中でランタイムがゴリゴリ動いてる印象だ。

メソッドもC関数に皮を数枚かぶせた感じの実装で、実態はIMP型という関数ポインタだ。

更にランタイムが非常に強力で、カプセル化されていながら、見ようと思えば何でも見れるようになっていて実にフックしやすい言語なんだ。

フックの仕方には何種類か方法があって、木下さんの連載を参考にすると
あたりが挙げられる。詳しくは連載の方が面白いのでそちらを見ていただくとして、簡単で便利なのはIMPの入れ替えだ。
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は以下のようなコードになる。

このコードを動的にロードできるようdylib形式でコンパイルしたファイルがTweakそのものだ。

この例だとメソッドの置換ではなく上書きにしたため、わりと短いコードですんでいるが、置換するとなると行うべき処理は多くなっていき、フックするメソッドが増えていくととても長いコードになっていく。そこで、このランタイムによる実装の置き換えを便利な関数でまとめてくれたライブラリを提供してくれるパッケージがCydiaにはある。そう、MobileSubstrateだ。

■MobileSubstrate

MobileSubstrateの提供する関数はメソッドの置換や関数の置換、インスタンス変数の取得を行える関数を提供してくれている。
では上記のコードをMobileSubstrateの一般的な関数を使用した形にすると、以下の様になる。

種々のTweakが一様にMobileSubstrateをパッケージの依存関係に指定しているのは、MobileSubstrateが提供する関数を使用しているのもあるが、他にも重要な機能をMobileSubstrateは担っている。 ざっと以下のような構成だ。
  • MSHook系関数を提供するライブラリlibsubstrate.dylib
  • TweakをロードするSubstrateLoader.dylib
  • plistによるTweakのロードを制御(SubstrateLoader)
  • DYLD_INSERT_LIBRARIES環境変数へのMobileLoader.dylib(MobileSubstrate->SubstrateInjection->SubstrateBootstrap->SubstrateLoader)の追加
  • DYLD_INSERT_LIBRARIESを元に戻すMobileSafety
この中で、TweakをロードするSubstrateLoaderが最重要機能といってもいいだろう。
これは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言語の授業(ポインタぐらいまで)程度の知識からスタートしましたので、そんなに難しくはないと思います。
何より自分の作ったものが動くというのは楽しいものですよ?

フリックキーボードの音声入力キーを無効にするNoDictation

iOS5.1からSiriが日本語に対応した事で、日本語キーボードでも音声入力が使えるようになりましたが
あまりの邪魔っぷりにこのキーを無効にするtweakを作りました。Require: iOS 5.1+です。

日本語フリックキーボードの場合だけ無効にするバージョンと全てのキーボードで無効にするバージョンを作りました。
 全てのキーボードで無効にするバージョンはBigBoss提出予定です。

https://github.com/r-plus/NoDictation/downloads

ちなみにSiriが要らないのならSiriをOFFにすれば純正でこのボタンは無効です。
iPadは設定から音声入力のON/OFFが純正で可能です。

メモリの初期不良を交換した!

先日UMAXの4GBx2メモリをSofmap-Onlineで買ったのですが、初期不良を掴んでしまった時のメモ。

とりあえずここから症状を報告しました。https://www.sofmap.com/inquiry_mail_ec/exec/
たしかこんな感じ。

Windows使用中に突然落ちてしまった為、Memtest86+を走らせるとErrorの数値が変化せず落ちるため初期不良と思われます。

1日後あたりにこんな返信、

この度、ご注文を頂いておりました上記商品でございますが、
ご連絡頂きました状況から、初期不良の可能性が考慮されます
為、弊社にて交換のお手続きをさせて頂きたく存じます。

なお、商品の交換方法につきましては、弊社から交換商品と
着払いの返送用伝票をお客様のご注文時の配送先ご住所へ
お送り致します。
交換商品をお受け取り頂く際に、お届けにあがった佐川急便
の配送担当者へお手元の不具合のある商品を、梱包頂いた状態
で返送用伝票とともにお渡し頂く事により、商品の交換と
させて頂きます。

また、商品交換の際には、不具合品とともに、下記一式を
梱包箱へお戻しくださいますようお願い致します。

○御買上票/納品書(商品に同梱)のコピー
(原本はお手元にお取り置き下さい。)
 もしくはお名前・ご注文番号、不具合の症状を記載したメモ書き
○マニュアル、メーカー保証書、付属品など購入時の
 製品に同梱されていた物全て。

梱包頂く箱や緩衝材がお手元に無い場合は、交換品をお届け
致します梱包材をそのままご利用頂いても結構でございます。

上記方法による交換にご了承頂けます場合は、大変お手数を
お掛け致しますが商品の交換につきまして、ご希望の配送時間を
下記にご記入頂き、本メールをご返信頂けますようお願い
申し上げます。

時間帯指定(【】内に○をお願い致します。)
【 】午前中
【 】12:00-14:00
【 】14:00-16:00
【 】16:00-18:00
【 】18:00-21:00
【 】指定無し

ってことなので、○をつけて返信。
メモリをパッケージに戻して、納品書は捨てたので指示通りに名前・注文番号、不具合の症状を記載したメモを書いて佐川を待ちます。

佐川が来た際にダンボール開けて不良メモリとメモ書きを入れて佐川の人に渡して完了!

返送料もかからなかったし、初期不良なら思ったよりも素晴らしい対応でした。

DocSetsを試そうとしてダメだった

DocSetsがAppStoreに並びました。
買う前に試してみたかったので、コンパイルしてみましたメモ。

まずはcloneします。

Air:git_clone hyde$ git clone git://github.com/omz/DocSets-for-iOS.git
Cloning into DocSets-for-iOS...
remote: Counting objects: 358, done.
remote: Compressing objects: 100% (206/206), done.
remote: Total 358 (delta 158), reused 343 (delta 145)
Receiving objects: 100% (358/358), 8.96 MiB | 107 KiB/s, done.
Resolving deltas: 100% (158/158), done.
で、そのままコンパイルしてみる。
何かエラーがでました。
そんなメソッド知らんと宣ってるので.hに宣言を追加してあげましょう。
-[RootViewController addDocSet:]の宣言をぴろっとな。
@interface RootViewController : UITableViewController  {
 
 DetailViewController *detailViewController;
}

@property (nonatomic, strong) DetailViewController *detailViewController;

- (void)addDocSet:(id)sender; ←これ
@end
今度は通りました。
が、ARM向けにコンパイルしようとすると認証があるらしい。

大人しく買えというオチになったのであった。

キャンペーン開始通知スクリプト

先日の#pyfesでjsonファイルを操作したことないなー、と思ったのでこんなのを作ってました。

@search_accountの最新3ツイートを取得し、それぞれ5分以内のものか判定した上でsearch wordが含まれているかを判定します。
最後の部分でメール送信なり、自分にmentionなり、boxcarにプッシュなりを設定し、
数分間隔で定期実行させればキャンペーン開始通知として使えそうです。

ところでPythonの変数って推奨の銘々則ってあるんですかね?PEPとかに。
ObjCみたくキャメルケースで書いてしまってるけど。