SandHook第3弾-パフォーマンス最適化&Xposedモジュール&VM Inlineの阻止


概要
前述したSandHookでは、HookのエントリとしてHookメソッドを手動で書く必要があるため、Xposed Callback式APIと互換性を持つには、Xposed Callbackロジックを転送するためにHookメソッドを空で生成する必要があります.
だから私はDexMakerを選びましたが、Dexmakerはコードを生成してロードするのが遅すぎて、最初の生成(後でロードするだけでいい)だけで、最初のHookの関数は100 msぐらいかかります.
Backupメソッドについては、書くか書かないかを書くことができ、書かない場合は、元のArtMethodのデータを直接新しいMethodで埋め込むことができますが、ここでは卵が痛いという問題があります.実在するArtMethodはNon-Movingスタックの中にあります.つまり、オリジナルのArtMethodはMoving GCはありませんが、Newが出てくるのは普通のオブジェクトだけで、GCのときにアドレスが移動し、backupメソッドを呼び出すたびに移動されるかどうかをチェックする必要があります.もちろんSandHookは事前に空の方法を書いておきました.
最適化シナリオ
実は最適化案は簡単で、依然としてStub関数をたくさん書いていますが、これらのStubはhook関数になり、Non-Moving Spaceに穴を占めるだけではありません.元のメソッドのパラメータを受信し、XposedBridgeを呼び出し、戻り値を返す必要があります.
パラメータ受信
Epicはスタックにパラメータを1つずつ取り出し、SandHookはパラメータをすべてlong(32 bitはint)に設定する
理由は簡単です.
  • 関数呼び出し側は、パラメータリストの各Appendを64ビットの下ですべてのパラメータが8バイトを占め、longも8バイトである.基本タイプは数値で、オブジェクトタイプはオブジェクトアドレスです.32ビットではlongとdoubleが8バイトを占め、その他は4バイトである.
  • でhookメソッドは受信者としてlong(32 bitはint)で統一的に受信し、パラメータを復元する.
  • 戻り値とパラメータ類似処理
  • パラメータリストは、できるだけ長くすることができ、パラメータの少ない関数を両立させることができる.

  • パラメータへんかん
    変換方法
    オブジェクト
    オブジェクトアドレスは、jweak JavaVMExt::AddWeakGlobalRef(Thread*self,ObjPtrmirror::Object obj)を使用してjavaオブジェクトに変換されます.
    jweak JavaVMExt::AddWeakGlobalRef(Thread* self, ObjPtr<mirror::Object> obj) {
      if (obj == nullptr) {
        return nullptr;
      }
      MutexLock mu(self, *Locks::jni_weak_globals_lock_);
      // CMS needs this to block for concurrent reference processing because an object allocated during
      // the GC won't be marked and concurrent reference processing would incorrectly clear the JNI weak
      // ref. But CC (kUseReadBarrier == true) doesn't because of the to-space invariant.
      while (!kUseReadBarrier && UNLIKELY(!MayAccessWeakGlobals(self))) {
        // Check and run the empty checkpoint before blocking so the empty checkpoint will work in the
        // presence of threads blocking for weak ref access.
        self->CheckEmptyCheckpointFromWeakRefAccess(Locks::jni_weak_globals_lock_);
        weak_globals_add_condition_.WaitHoldingLocks(self);
      }
      std::string error_msg;
      IndirectRef ref = weak_globals_.Add(kIRTFirstSegment, obj, &error_msg);
      if (UNLIKELY(ref == nullptr)) {
        LOG(FATAL) << error_msg;
        UNREACHABLE();
      }
      return reinterpret_cast<jweak>(ref);
    }
    

    dlsym/fake_を使用できますdlsymが検索しました.
    基本タイプ
    longはint char byte shortなどに直接変換でき、booleanは0であるかどうかを判断すればよいが、floatとdoubleは浮動小数点レジスタが存在し、longは汎用レジスタであり、パラメータリストと戻り値の中でこの2つのタイプがdexmakerを直接転用すればよいことを発見し、一般的にこの2つのパラメータも少ない.また32 bitでlong/doubleは8バイトで、パラメータリストの間にこの2種類のパラメータが挟まれているとパラメータリストが混乱し、Dexmakerをスキップします.
    最後にpythonスクリプトを書いてstub関数を自動的に生成し、基本的に9割以上の関数hookが直接stubを歩き、1-3 msしかかかりません.
    Xposedモジュールのサポート
    最後にVirtualAppと組み合わせてVXPのようなRoot Xposedフリーコンテナを簡単に実現し,現在Q++,適用変数,XPrivacyLua,MDWechatなどのモジュールが使用可能であることを測定した.
    https://github.com/ganyao114/SandVXposed
    インラインを止める
    げんしょう
    VMのInline最適化は私たちのHookの最大の障害であり、ここで実験を行います.
    Android 7.1:
  • 被Hook的試験方法
  • public String testInline(String a, int b, int c) {
            b++;
            c++;
            return "origin res" + a;
        }
    
  • 試験方法を呼び出す大循環
  • new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        Log.e("TestInline", "res = " + testInline.testInline("xxx", 1, 3));
                    }
                }
            }).start();
    
  • Hookポイント
  • 元のメソッドの戻り値が主に変更されました
    XposedHelpers.findAndHookMethod(TestInline.class, "testInline", String.class, int.class, int.class, new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                    param.setResult("hooked res");
                }
    
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                }
            });
    
  • 実験結果
  • 最初の1 s、メソッドHookは正常で、Hookを出力した後の戻り値:
    02-25 09:32:22.099 28532-28548/com.swift.sandhook E/TestInline: res = hooked res
    

    1 s以降:
    res = origin resxxx
        res = origin resxxx
        res = origin resxxx
        res = origin resxxx
        res = origin resxxx
        res = origin resxxx
        res = origin resxxx
    

    これは,1秒足らずで,Hookメソッドによって単純すぎるため,このメソッドを呼び出すメソッドの熱が高く,呼び出し者がInline最適化を行い,スタック上で置換したことを示している.
    解決策
    ARTソースコードを調べたところ、inlineメソッドのcode unitsが設定したしきい値より大きい場合、メソッドInlineは失敗することが分かった.このしきい値はCompilerOptions->inline_ですmax_code_units_
    
    const CompilerOptions& compiler_options = compiler_driver_->GetCompilerOptions();
      if ((compiler_options.GetInlineDepthLimit() == 0)
          || (compiler_options.GetInlineMaxCodeUnits() == 0)) {
        return;
      }
    
     bool should_inline = (compiler_options.GetInlineDepthLimit() > 0)
          && (compiler_options.GetInlineMaxCodeUnits() > 0);
      if (!should_inline) {
        return;
      }
    
      size_t inline_max_code_units = compiler_driver_->GetCompilerOptions().GetInlineMaxCodeUnits();
      if (code_item->insns_size_in_code_units_ > inline_max_code_units) {
        VLOG(compiler) << "Method " << PrettyMethod(method)
                       << " is too big to inline: "
                       << code_item->insns_size_in_code_units_
                       << " > "
                       << inline_max_code_units;
        return false;
      }
    

    では、なんとかこの閾値を0にすればいいです.
    検索すると、CompilerOptionsは一般的にJitCompilerにバインドされます.
    class JitCompiler {
     public:
      static JitCompiler* Create();
      virtual ~JitCompiler();
    
    ..............
     private:
      std::unique_ptr<CompilerOptions> compiler_options_;
    
    	............
    };
    
    }  // namespace jit
    }  // namespace art
    

    一方、ARTのJitCompilerはグローバルな例です.
      // JIT compiler
      static void* jit_compiler_handle_;
    
      jit->dump_info_on_shutdown_ = options->DumpJitInfoOnShutdown();
      if (jit_compiler_handle_ == nullptr && !LoadCompiler(error_msg)) {
        return nullptr;
      }
    
      jit_compiler_handle_ = (jit_load_)(&will_generate_debug_symbols);
    
    extern "C" void* jit_load(bool* generate_debug_info) {
      VLOG(jit) << "loading jit compiler";
      auto* const jit_compiler = JitCompiler::Create();
      CHECK(jit_compiler != nullptr);
      *generate_debug_info = jit_compiler->GetCompilerOptions()->GetGenerateDebugInfo();
      VLOG(jit) << "Done loading jit compiler";
      return jit_compiler;
    }
    

    OK、それでは「static void*jit_compiler_handle_」を手に入れましたのC++記号"_ZN3 art 3 jit 3 Jit 20 jit_compiler_handle_E"
    最後に中の値を修正すればいいです.
    ART Invokeコード生成解析
    https://blog.csdn.net/ganyao939543405/article/details/88079544