アセンブラー挿入を使用したJava













ご存じのように、Javaのような任意の言語で書くことができ、javistaの最初の愛はガベージコレクターとJITコンパイラーを書くことです。 たとえば、管理されたコードからマシンコードやアセンブラーを直接操作するにはどうすればよいでしょうか。







さらに、この記事はC#の小さな例になります。 ある時点で、常に1つのJavaを学習できるわけではないことが明らかになりました。 動的言語の牧場は一般的な理論を使用し、実際には同様の問題の枠組み内で作業します。 あなたの作品を宣伝する最も簡単な方法は、あなたの隣人がどのようにそこにいるかを見て、自分に良いものをコピーすることです。







次に、アセンブラとマシンコードについて説明します。 これが必要な理由は未解決の問題です。 たとえば、Meltdownについて多くのことを聞いて、そのための素敵なAPIを書きたいと思います:-)まあ、Oracleは神ではないことを忘れないでください。一部の標準メソッドは、SDKなどにあるものよりも適切に実装できます。 -常に掘り下げるものがあります!







問題には2つのレベルがあります。









ただし、後で説明するハックを悪用することもできます。







素朴なオプション:実行してください!



ご存知のように、JNIを使​​用してネイティブコードを実行できます。

つまり、C ++では、マシンコードを静かに動的に生成してからプルできます。







これを行うには、同時に書き込みと実行が可能なメモリセグメントを作成する必要があります。







VirtualAllocExを使用したWindowsの例を次に示します( posixにmprotect(2)がありますが、私は面倒です)。

彼が何をしているかを理解してみてください。 PAGE_EXECUTE_READWRITE



がない場合、このコードはWindowsのデータ実行防止を即座に打ち負かします。







 #include <stdio.h> #include <windows.h> typedef unsigned char byte; int arg1; int arg2; int res1; typedef void (*pfunc)(void); union funcptr { pfunc x; byte* y; }; int main( void ) { byte* buf = (byte*)VirtualAllocEx( GetCurrentProcess(), 0, 1<<16, MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if( buf==0 ) return 0; byte* p = buf; *p++ = 0x50; // push eax *p++ = 0x52; // push edx *p++ = 0xA1; // mov eax, [arg2] (int*&)p[0] = &arg2; p+=sizeof(int*); *p++ = 0x92; // xchg edx,eax *p++ = 0xA1; // mov eax, [arg1] (int*&)p[0] = &arg1; p+=sizeof(int*); *p++ = 0xF7; *p++ = 0xEA; // imul edx *p++ = 0xA3; // mov [res1],eax (int*&)p[0] = &res1; p+=sizeof(int*); *p++ = 0x5A; // pop edx *p++ = 0x58; // pop eax *p++ = 0xC3; // ret funcptr func; func.y = buf; arg1 = 123; arg2 = 321; res1 = 0; func.x(); // call generated code printf( "arg1=%i arg2=%i arg1*arg2=%i func(arg1,arg2)=%i\n", arg1,arg2,arg1*arg2,res1 ); }
      
      





もちろん、手作業で行うことは最も賢明な考えではなく、何らかのAsmJitをドラッグする必要があります。 次に、Javaコードから特定のデータを転送し、バッファをどのように埋めるか、そしてすごい-ロールしました!







ここでの問題は、インラインからいくぶんより大胆な機能を期待することです。 呼び出しのコンテキスト全体にアクセスし、さらにSDKからさまざまなシステム要素を取得したいと思います。 あなたはおそらく自分でそれを行うことができます-しかし、長い間、痛みを伴う。 幸いなことに、すべてがすでに私たちの前に盗まれています。







Javaネイティブインターフェイス



JNIは引き続き使用できますが、別の方法で使用できます。







次のようなクラスがあるとしましょう:







 public class MyJNIClass { public native void printVersion(); }
      
      





アイデアは、JNI命名規則に従ってキャラクターに名前を付けることです。そうすれば、それ自体がすべてを行います。 この場合、 Java_MyJNIClass_printVersion



ようなものになります。







シンボルは他の翻訳単位から見える必要があります。これは、 global



ディレクティブを使用してNASMで、またはpublicを使用してFASMで実行できます。







asm自体は、使用するアーキテクチャの呼び出し規約を理解して作成する必要があります(引数はレジスタ、スタック、他のメモリ構造などにあります)。 関数に到達する最初の引数はJNIEnv



へのポインターになり、JNI関数のテーブルへのポインターになります。







たとえば、x86_64の下のNASMは次のようになります。







 global Java_MyJNIClass_printVersion section .text Java_MyJNIClass_printVersion: mov rax, [rdi] call [rax + 8*4] ; pointer size in x86_64 * index of GetVersion ...
      
      





GetVersion



マジックインデックスはGetVersion



ですか? 非常に簡単:それらはドキュメントにリストされています







GetVersion



説明は次のようになります。







 GetVersion jint GetVersion(JNIEnv *env); Returns the version of the native method interface. LINKAGE: Index 4 in the JNIEnv interface function table. PARAMETERS: env: the JNI interface pointer. RETURNS: Returns the major version number in the higher 16 bits and the minor version number in the lower 16 bits. In JDK/JRE 1.1, GetVersion() returns 0x00010001. In JDK/JRE 1.2, GetVersion() returns 0x00010002. In JDK/JRE 1.4, GetVersion() returns 0x00010004. In JDK/JRE 1.6, GetVersion() returns 0x00010006.
      
      





ご覧のとおり、関数テーブルは単なるポインタの配列の一種です。 もちろん、これらのインデックスにターゲットアーキテクチャのポインタのサイズを掛けることを忘れてはなりません。







2番目の引数は、関数を呼び出したクラスまたはオブジェクトへの参照です。 以下の引数はすべて、Javaコードでnative



として宣言されたメソッドのパラメーターです。







次に、アセンブラーからオブジェクトをアセンブルする必要がありますnasm -f elf64 -o GetVersion.o GetVersion.asm





オブジェクトコレクション-ライブラリから: gcc -shared -z noexecstack -o libGetVersion.so GetVersion.o





最後に、ファイル自体を収集します: javac MyJNIClass.java









より複雑な操作を実行できます。 配列要素追加する例を次に示します







そして、すべてがうまくいくようですが、私はいくつかのものが欲しいです。







まず、Javaでコーディングする場合、アセンブラーを作成するための静的チェックを備えた美しい構文(この場合はどういう意味でも)が必要です。 私は金髪で、IDEのオートコンプリートでレジスタを選択します。1文字で封印されることを恐れません。 さて、少なくともJava APIにしましょう。







第二に、コードの隣に手でライブラリをアセンブルすることはお勧めできません。 さて、ファイルを1つずつ収集するには-一番下です。 そのようなことを気にしないインフラストラクチャが必要です。 たとえば、asmコードをインライン、Mavenのプラグイン、または変更されたJDKの一部として配布します。







第三に、多くの異なる表現があるため、抽象化としてアセンブラを選択する価値があるかどうかは明らかではありません。







図書館



すぐに、Javaのマシンコードに携わっているOracleの男性に手紙を書いて、答えを受け取りました。彼らは通常の美しいライブラリを知らないのです。







ただし、Googleにアクセスする必要があります。怠け者ではありません。 「java call x86 assembly library」キーワードを使用して、結果を瞑想します。







結果は、ライブラリの観点から、すべてが本当に悪いことを示しています。 The Machine Level Javaを含むいくつかの未完成のものをGoogleで検索しました。







そして、そこには美しいAPIさえあります(Javaの構造を使用するときの美しさ)。







 public class SimpleNativeDemo extends X86InlineAssembly // X86InlineAssembly is a successor of InlineAssembly { static // static initializer { InlineAssemblyHelper.initializeNativeCode_deleteExistingLibraryAndNoExceptions(new SimpleNativeDemo(System.out)); } // constructor, which defines x86 architecture as a native method's target public SimpleNativeDemo(OutputStream debugStream) { super(Architectures.X86.architecture, false, debugStream); } // native method declaration public static native long multiply(int x, int y); // native method implementation @Override public void writeNativeCode() { parameterIn(r.EAX,IP.In0.ordinal()); parameterIn(r.EBX,IP.In1.ordinal()); mul.x32(r.EBX); } }
      
      





構文(Javaパーサーを変更せずに)と実行方法の両方が得られます。







ここでの主な問題は、内部が非常に複雑なコードであり、サポートする必要があることです。 Javaでオペレーティングシステム全体を作成するプロジェクトであるjEmbryOSの一部です。 そして、見たところ、このプロジェクトはあまり活発ではありません:2014年の最新の投稿がある空のフォーラムであるSourceforge(およびGitHubやその他の人気のある最新のホスティングサービス)にはまだありません。 coの最後の釘は、Sourceforgeの2015リリースではライセンスファイルがないことです。ライセンスなしではコードを使用できません(このような読み取り専用コードを作成するデフォルトの著作権規則が適用されます)。







さて、うまくいきませんでした。 しかし、将来的には、アイデアが明確なので、自分で書くことができます。







真性



この機会に、レポート全体がここにあります:









会議に行く価値がある場合、これらはレポートです。 Volker zhzhot。

ちなみに、彼はノボシビルスクの次のJBreakに参加しクラスデータの共有について別のアニーリングを行います。







つまり、OpenJDKにはマジックファイルsrc/share/vm/classfile/vmSymbols.hpp



ます。 リンクでブラウザで開くだけで、すべてを理解できます。 特定のメソッドをキャッチし、それらをアセンブラーで置き換えることができます。 もちろん、これらの変更を加えてOpenJDKを再構築します。







私見、本当にうまくいけば、これを行うことができます: __asm("ret")



ような構造体への呼び出しをキャッチする.javaクラスのプリプロセッサを作成し、それらから組み込み関数のパッチを生成し、OpenJDKを自動的に再構築します。







どうしてこの決定が私にはあまり楽しくないように見えるのですか? まず、組み込み関数の変更により、OpenJDKの重要な部分が再構築されます。 だから、お茶を飲んで、非常に頻繁に喫煙しなければなりません 悲しみに酔う ストーブと同じくらい暑いラップトップがC ++を打ち負かしながら、他の方法で時間を潰します







第二に、本質主義者はネイティブメソッドと同じようには機能しません。 JNIで通常モードで作業しており、JVMが常にセーフポイントにロールバックできる場合、組み込み関数の場合、これは機能しません。 致命的な何かを壊さないように汗をかく必要があります。







そして第三に、OpenJDKのこの部分に普通の人が連絡するのはあまり快適ではないという疑いがあります。 そこにあるコードの大部分は深刻なソーサリーで構成されており、そこで動けなくなる可能性があります。







.NETはどうですか?



いくつかのショックは、ドナーのアプローチがまったく異なることでした。 アセンブラをまったくラップしないかもしれませんが、C#から直接ネイティブコードを実行します!







このアイデアは、2005年に書き戻された例によって設定されました。 残念ながら、リンクコードは機能しません。DEPはすぐにそれを打ち負かすからです。 kernel32.dll



からガベージをドラッグして少し変更する必要がありましたMemoryProtection



VirtualAllocEx



と、必要なフラグAllocationType



MemoryProtection



ます。 これは、C ++の例で使用したトリックとまったく同じです。







例を簡単にするために、Life、Universe、Everything Elseの最も重要な質問に対する答えを返すメソッドがあるとします。







 using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; class Program { [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [Flags] public enum AllocationType { Commit = 0x1000, Reserve = 0x2000, Decommit = 0x4000, Release = 0x8000, Reset = 0x80000, Physical = 0x400000, TopDown = 0x100000, WriteWatch = 0x200000, LargePages = 0x20000000 } [Flags] public enum MemoryProtection { Execute = 0x10, ExecuteRead = 0x20, ExecuteReadWrite = 0x40, ExecuteWriteCopy = 0x80, NoAccess = 0x01, ReadOnly = 0x02, ReadWrite = 0x04, WriteCopy = 0x08, GuardModifierflag = 0x100, NoCacheModifierflag = 0x200, WriteCombineModifierflag = 0x400 } private delegate int IntReturner(); private static void Main() { List<byte> bodyBuilder = new List<byte>(); bodyBuilder.Add(0xb8); bodyBuilder.AddRange(BitConverter.GetBytes(42)); bodyBuilder.Add(0xc3); byte[] body = bodyBuilder.ToArray(); IntPtr buf = VirtualAllocEx(Process.GetCurrentProcess().Handle, (IntPtr) 0, Convert.ToUInt32(body.Length), AllocationType.Commit, MemoryProtection.ExecuteReadWrite); Marshal.Copy(body, 0, buf, body.Length); IntReturner ptr = (IntReturner) Marshal.GetDelegateForFunctionPointer(buf, typeof(IntReturner)); Console.WriteLine(ptr()); Console.ReadKey(); } }
      
      





パラメータを使用したよりスマートなサンプルが突然必要になった場合、 Marshal



AllocHGlobal



を使用してアンマネージメモリをAllocHGlobal



AllocHGlobal



をクリーンFreeHGlobal



し、同じトピックFreeHGlobal



メソッドを使用できます。







このような超大国を使用すると、実際のゲームを作成できます。たとえば、 クラスのメソッドを置き換えます 。 この記事を公開する前に、GitHubで多数のプロジェクトを見てきましたが、残念ながら、これらのハックはC ++、安全でない、そして最も悲しいことに非常に膨大なコードなしではできませんでした。 したがって、ここではすべてを説明するのではなく、.NETのハッキングに関する別の記事に記載します。







JVMコンパイラインターフェイス



脳を正しい方向に向けることで、Javaでも同様の方法で問題を解決できることが明らかになります。 実際、Java 9はJEP 243:Java-Level JVM Compiler Interfaceを実装しています。







この機能の開発者は、JITコンパイラが深刻であることを理解していました たわごと 優れた無料のIDEなど、Javaエコシステムのすべての可能な機能を使用して、個別に開発するとよいソフトウェア。 これらの機能のすべてがOpenJDK内で使用できるわけではありません。通常、そのコードをIDEで開くと、すべてが赤くなり、エラーとして10回下線が引かれます。 さまざまな内部メカニズムへの直接アクセスを必要とするサブシステムのモノリシックアーキテクチャにはある程度の正当性があります(たとえば、これにはバイトコードインタープリターまたはガベージコレクターが必要です)-これはコンパイラーには関係ありません。







したがって、コンパイラを個別のエンティティに分離するという考え方。 接続は、コマンドラインに含まれるプラグインの形式で、便利に行う必要があります。 そこで、JVMCIが生まれました。







つまり、最もシンプルなインターフェイスがあります。







 interface JVMCICompiler { byte[] compileMethod(byte[] bytecode); }
      
      





Javaバイトコードが入り、ネイティブコードが出てきます。 C#で上にあったものと非常によく似ています。 私たちにとって、これはいくつかの点でさらに優れています。そのような使用は汚い副作用ではなく、最も基本的な使用パターンだからです。







実際には、バイトコードだけでは十分ではありません。 これは、フィールドが追加されたCompilationRequest



です。







 interface JVMCICompiler { void compileMethod(CompilationRequest request); } interface CompilationRequest { JavaMethod getMethod(); } interface JavaMethod { byte[] getCode(); int getMaxLocals(); int getMaxStackSize(); ProfilingInfo getProfilingInfo(); ... }
      
      





どれだけ長い間、短い間、バイトコードをコンパイルしたので、 HotSpot.installCode(...);



を使用して安全にインストールできますHotSpot.installCode(...);









特に、このアプローチは、組み込み関数の初期問題-OpenJDKを再構築する必要性を解決できます。







ここでの問題点は、JVMCI実装の作成が非常に高速で簡単なタスクではないことです。 この機能に関するドキュメントはほとんどありません。 唯一の包括的なドキュメントはOpenJDK C ++コードです。これは私は本当に読みたくありません。







しかし、ここではすべてが盗まれています。







グラールとトリュフ



Oracle Labsの暗くて恐ろしいセラーでは、OpenJDKを近い将来に拡張する人のために状況を変えるいくつかのクールなツールが開発されています。 これらのプロジェクトは、一般名Graalの下に統合され、 GitHubのこのリポジトリにあります







含む:









興味深いことに、GraalとTruffleはJVMCIの実装を提供します。 そして、このサポートはすでにOpenJDK 9にあります-必要なフラグを有効にするだけです。 もちろん、これらのフラグを接続しても、Graal自体を再構築することから私たちを救うことはできませんが、開発者がこの問題にどれほど真剣に取り組んだかを示しています。 これはすべて、すでにテストされており、公式機能になるのに十分なほど成熟していることを示しています。







Graalの仕組みについて非常に良いChris Seaton氏は述べています。 ところで、この記事はJoker 2017でのスピーチに基づいて書かれています。







次に、これらすべてがうまく機能し、実際に適用可能であるかという質問に移ります。 同じジョーカーで、クリスチャン・タリンジャーは 、ツイッターのかなりの部分がすでにGraalに翻訳されていると話し 、言った。 コンパイラとして使用することは実用的であるだけでなく、既存のコードのパフォーマンスを10%以上向上させました。







さらに、 JEP 317:Experimental Java-Based JIT Compilerがあり 、これはJava 10の一部になる可能性があります。







このセクションでは、Graalを目的にどのように使用できるかを示す小さな勝利例を作成したいと考えました。 残念ながら、この例はまだ書かれており、長い間書かれているようです。 これは別の記事のトピックです。







ここに欠けているもの



VMStructsJava Native Runtime (この場合はJNR-x86asm )、 Project Panama全体は、 不当に考慮されていません。 4月、 アパンギンレポートの後、スピンオフを作成してこれらのトピックを明らかにする必要があります。







おわりに



この記事では、ネイティブコードをJavaから直接実行する方法を示しました。







これはシリーズの最初の記事にすぎません。 次のステップは何ですか?







最初に、グラールを理解しているクリスチャン・タリンジャーとのインタビューを行う必要があります。 インタビューは近い将来にHabréで公開されます。







ちなみに、彼はノボシビルスクのJBreak 2018に新しいレポート「Graal:実際の生活で新しいJVM JITコンパイラを使用する方法」を持って来ています。このレポートは価値があります。







このテーマに関する以下の記事では、GraalとTruffleのアーキテクチャと構成をさらに深く掘り下げ、簡単な変更を加えて迅速な効果を実現する方法を示す必要があります。







さらに、Graalのデザインに影響を与えた、古くても失われていないユーティリティと現代の素材を結び付けてみることができます。 たとえば、 Habréでそのような記事(文脈依存トレースのインライン化)をすでに公開しています 。 関連する開発で大量の資料が蓄積されています。たとえば、現在のGraal開発者であるDoug Simonは、以前Maxine VMを扱っていましたが、これについては非常に多くの出版物があります。








All Articles