Android ホットフィックス Tinker ソースコード分析
tinker の最大のハイライトの 1 つは、一連の dex diff および patch 関連のアルゴリズムを自社開発したことです。この記事の主な目的は、このアルゴリズムを分析することです。もちろん、分析の前提条件として、dex ファイルの形式をある程度理解する必要があることに注意してください。理解していないと混乱する可能性があります。
したがって、この記事では、最初に dex ファイル形式の簡単な分析を行い、いくつかの簡単な実験も行い、最後に dex diff およびパッチのアルゴリズム部分に入ります。
まず第一に、Dex ファイルについて簡単に理解しましょう。逆コンパイルすると、apk に 1 つ以上の *.dex ファイルが含まれることは誰もが知っています。このファイルには、作成したコードが保存されます。通常の状況では、変換ツールも使用します。それを jar に保存し、逆コンパイルしていくつかのツールで表示します。
jar ファイルはクラス ファイルの圧縮パッケージに似ていることを誰もが知っているはずです。通常の状況では、各クラス ファイルを直接解凍して表示できます。 dex ファイルを解凍しても内部クラス ファイルを取得できません。これは、dex ファイルが独自の特定の形式を持つことを意味します:
dex は Java クラス ファイルを再配置し、すべての JAVA クラス ファイルの定数プールを分解し、冗長な情報を削除し、それらを再結合して定数プールを形成します。すべてのクラス ファイルは同じ定数プールを共有し、同じ文字列と定数を作成します。DEX 内で 1 回だけ表示されます。ファイルを削除することにより、ファイル サイズが削減されます。
次に、dex ファイルの内部構造がどのようなものかを見てみましょう。
ファイルの構成を分析するには、分析用に最も単純な dex ファイルを作成するのが最善です。
まず、クラス Hello.java:
を作成します。 リーリー次にコンパイルします:
リーリー最後に dx 作業を通じて dex ファイルに変換します:
リーリーdx パスは、Android-sdk/build-tools/versionnumber/dx の下にあります。dx コマンドが認識できない場合は、パスを path の下に置くか、絶対パスを使用してください。
このようにして、非常に単純な dex ファイルを取得します。
まず、dex ファイルの大まかな内部構造図を示します。
もちろん、単に画像から説明するだけでは絶対に十分ではありません。後ほど diff と patch のアルゴリズムを学習するからです。理論的には、dex ファイルの各要素に至るまで、さらに詳細を知る必要があります。バイトは何を表しますか?
バイナリのようなファイルの場合、メモリに依存しないことが最善の方法です。幸いなことに、分析に役立つソフトウェアがあります:
- ソフトウェア名:010エディター
ダウンロードしてインストールした後、dex ファイルを開くと、dex ファイルの解析テンプレートをインストールするように案内されます。
最終的なレンダリングは次のとおりです:
上部は dex ファイルの内容 (16 進形式で表示) を表し、下部は dex ファイルの各領域を示します。下部をクリックすると、対応するコンテンツ領域と内容が表示されます。
もちろん、dex ファイルについての理解を深めるために、いくつかの特別記事を読むことも強くお勧めします。
- DEX ファイル形式の分析
- Android リバース エンジニアリングの旅 - コンパイルされた Dex ファイル形式の解析
この記事では、dex ファイルの簡単な形式分析のみを行います。
dex_header
まず、dex_header の大まかな分析を行います。ヘッダーには次のフィールドが含まれます:
まず第一に、ヘッダーの役割を推測します。ヘッダーにはいくつかの検証関連フィールドと、dex ファイル全体のブロックのおおよその分布 (off はオフセット) が含まれていることがわかります。
この利点は、仮想マシンが dex ファイルを読み取るときに、ヘッダー部分を読み取るだけで dex ファイルのおおよそのブロック分布がわかり、ファイル形式が正しいかどうか、およびファイル形式が正しいかどうかを確認できることです。ファイルが改ざんされているなど
- ファイルが dex ファイルであることを証明できる
- チェックサムと署名は主にファイルの整合性を検証するために使用されます
- file_size は dex ファイルのサイズです
- head_size はヘッダー ファイルのサイズです
- endian_tag のデフォルト値は 12345678 で、ロゴのデフォルトはリトルエンディアン (自己検索) です。
残りのほとんどは、ペアで表示されるサイズとオフであり、そのほとんどは、各ブロックに含まれる特定のデータ構造の数とオフセットを表します。例: string_ids_off は 112、つまり string_ids 領域がオフセット 112 から始まることを意味し、string_ids_size は 14、つまり string_id_item の数が 14 であることを意味します。他も同様なので紹介は省略します。
010Editorと組み合わせると、各エリアに含まれるデータ構造とそれに対応する値が確認できますので、ぜひご覧ください。
ヘッダーの他に、dex_map_list という重要な部分があります。最初に図を見てみましょう:
最初はmap_item_listの番号で、その後に各map_item_listの説明が続きます。
map_item_list は何に役立ちますか?
各map_list_itemには、列挙型、2バイトの未使用メンバー、現在の型の番号を示すサイズ、および現在の型のオフセットを示すオフセットが含まれていることがわかります。
次の例を見てみましょう:
- 最初は TYPE_HEADER_ITEM タイプで、1 つのヘッダー (サイズ = 1) が含まれ、オフセットは 0 です。
- 次は TYPE_STRING_ID_ITEM で、これには 14 個の string_id_item (size=14) が含まれており、オフセットは 112 です (覚えていると思いますが、ヘッダーの長さは 112 で、その後にヘッダーが続きます)。
残りは順番に推測できます~~
この場合、map_list により、完全な dex ファイルが固定領域 (この例では 13) に分割でき、各領域の開始とその領域に対応するデータ形式の数がわかることがわかります。 。
map_list を通じて各エリアの先頭を見つけます。各エリアは特定のデータ構造に対応します。010 Editor を通じて表示するだけです。
dex の基本的な形式を理解したので、dex diff と patch を行う方法を考えてみましょう。
最初に考慮すべきことは、私たちが持っているものです:
- 古いデックス
- 新しいデックス
古い dex を使用したパッチ アルゴリズムを通じて新しい dex も生成できるパッチ ファイルを生成したいと考えています。
-
###だから何をすべきか?
###ヘッダ###
- さまざまな分野
- マップリスト
- ヘッダーは実際に次のデータに基づいてその内容を決定でき、112 の固定長です。各領域については後述します。マップ リストは実際に各領域の開始位置を特定できます。
ヘッダーは他のデータに基づいて生成できるため、処理する必要はありません;
マップリストの場合、主に必要なのは各エリアの開始(オフセット)です
- 各領域のオフセットがわかったら、新しい dex を生成するときに、各領域の開始位置と終了位置を特定できるので、各領域にデータを書き込むだけで済みます。
- 次に、領域の差分を見てみましょう。主に文字列を格納するために使用される文字列領域があるとします。
- 古い dex のこの領域の文字列は次のとおりです: Hello、World、zhy 新しい dex のこの領域の文字列は次のとおりです: Android、World、zhy
この領域では、Hello を削除し、Android を追加したことがわかります。
その後、パッチは次のようにこの領域を記録できます:
「del Hello, add Android」(実際にはバイナリに変換する必要があります)。
アプリケーションで直接読み取ることができる古い dex について考えてみましょう。つまり、次のことがわかります。
この領域には、Hello、World、zhyが含まれていることが判明しました。
パッチのこの領域には次の内容が含まれます:「del Hello, add Android」
次に、新しい dex に次のものが含まれることを非常に簡単に計算できます:
- Android、世界、zhy。
- このようにして、1 つの領域の大まかな diff と patch のアルゴリズムが完成しましたが、他の領域の diff と patch も上記と同様です。
アルゴリズムの一般的な概念を理解したら、ソース コードを見てみましょう。
3. Tinker DexDiff ソース コードの簡単な分析
ここにはコードを読むためのコツがあります。実際にはかなり多くのいじくりコードがあり、コードの束にはまってしまうことがよくあります。 diff アルゴリズムのように、入力パラメータは古い dex と新しい dex で、出力はパッチ ファイルです。
その場合、上記のパラメータを受け入れて出力するクラスまたはメソッドが存在する必要があります。実際、このクラスは DexPatchGenerator:
diff の API 使用コードは次のとおりです:
リーリーコードは tinker-build の tinker-patch-lib の下にあります。
単体テストまたはメイン メソッドを作成します。上記のコード行は diff アルゴリズムです。
コードを見るときは、ターゲットを絞る必要があります。たとえば、差分アルゴリズムを見る場合は、差分アルゴリズムへの入り口を見つけます。gradle プラグインでは心配する必要はありません。
渡した dex ファイルを Dex オブジェクトに変換します。
リーリーまずファイルを byte[] 配列として読み取り (これは非常にメモリを消費します)、次にそれを ByteBuffer でラップし、バイト順序をリトル エンディアンに設定します (ここでは ByteBuffer が非常に便利であることがわかります)。メソッドは、Dex オブジェクトの tableOfContents に値を割り当てます。
リーリーReadHeaderとreadMapは内部で実行されており、上記ではヘッダーとマップリストを大まかに分析しましたが、実際にはこの2つの領域はあるデータ構造に変換されて読み込まれ、メモリに格納されます。
最初に readHeader を見てみましょう:
リーリーここで 010 Editor を開いたり、前面の図を見てみると、実際にはヘッダー内のすべてのフィールドを定義し、応答バイトを読み取り、値を割り当てています。
次に、readMap を見てください:
リーリーここで注意していただきたいのは、ヘッダーを読み込む際に、実際にはマップリスト領域を除いたオフセットが読み込まれ、mapList.offに格納されるということです。したがって、マップ リストは実際にはこの位置から始まります。最初に読み取るのはmap_list_itemの番号であり、次に読み取るのは各map_list_itemに対応する実際のデータです。
type、unused、size、offset の順に読み込まれていることがわかります。前に map_list_item について説明した印象がまだ残っている方は、これがこれに相当し、対応するデータ構造は TableContents.Section オブジェクトです。
computeSizesFromOffsets() は主にセクションの byteCount (複数バイトを占有する) パラメータに値を割り当てます。
これで、dex ファイルから Dex オブジェクトへの初期化が完了しました。
Dex オブジェクトを 2 つ取得したら、diff 操作を実行する必要があります。
ソース コードに戻ります:
リーリー2 つの Dex オブジェクトのコンストラクターに直接:
リーリー最初に oldDex と newDex に値を割り当て、次に 15 個のアルゴリズムを順番に初期化することを確認してください。各アルゴリズムは各領域を表します。アルゴリズムの目的は前に説明したとおりです。「どのアルゴリズムが」を知る必要があります。どれが削除され、どれが追加されたか。"どれ";
引き続きコードを見てみましょう:
リーリーdexPatchGenerator オブジェクトでは、executeAndSaveTo メソッドを直接指します。
リーリーexecuteAndSaveTo メソッド:
リーリー15 個のアルゴリズムが関係しているため、コードは非常に長くなります。ここでは説明するためにアルゴリズムのうちの 1 つだけを使用します。
各アルゴリズムは、execute メソッドと SimulatePatchOperation メソッドを実行します:
まず実行を見てみましょう:
リーリーoldDex と newDex の対応する領域のデータが最初に読み込まれ、それぞれ調整されたOldIndexedItemsとadjustedNewIndexedItemsに並べ替えられることがわかります。
次にトラバースが始まります。else 部分を直接見てください:
現在のカーソルに従って、oldItem と newItem をそれぞれ取得し、それらの値のペアを比較します。
-
>0 の場合、newItem は新しく追加されたとみなされ、PatchOperation.OP_ADD として記録され、newItem のインデックスと値が PatchOperation オブジェクトに記録され、patchOperationList に追加されます。
- =0 の場合、PatchOperation は生成されません。
コードの後半に進みます:
リーリー
- まず、インデックスに従って patchOperationList を並べ替えます。インデックスが一貫している場合は、最初に DEL を実行し、次に ADD を実行します。
- すべての操作の次の反復では、主に、一貫したインデックスと連続性を持つ DEL と ADD を REPLACE 操作に変換します。
- 最後に、patchOperationList は 3 つのマップ、つまり、indexToDelOperationMap、indexToAddOperationMap、indexToReplaceOperationMap に変換されます。
先ほど、execute() に加えて、各アルゴリズムには SimulatePatchOperation() があると言いました
リーリー
渡されるオフセットはデータ領域のオフセットです。リーリー
oldIndex と newIndex をトラバースし、それぞれ、indexToAddOperationMap、indexToReplaceOperationMap、indexToDelOperationMap を検索します。ここに注意してください。最終的な結果は、patchedOffset-baseOffset によって取得される this.patchedSectionSize です。
patchedOffset =itemSize:
が発生する状況はいくつかあります。
- indexToAddOperationMap には patchIndex が含まれています
- indexToReplaceOperationMap には patchIndex が含まれています
- oldDex.
- これは、indexToDelOperationMap および IndexToReplaceOperationMap にありません
この時点で、アルゴリズムが実行されました。
このようなアルゴリズムの後、PatchOperationList と対応する領域のセクションサイズを取得します。すべてのアルゴリズムを実行した後、各アルゴリズムの PatchOperationList と各領域のセクションサイズを取得する必要があります。各領域のセクションサイズは実際には各領域のオフセットに変換されます。
各領域のアルゴリズム、実行、およびシミュレートPatchOperationのコードは再利用されているため、その他の部分には小さな変更しかありません。ご自身で確認してください。
次に、すべてのアルゴリズムを実行した後の writeResultToStream メソッドを確認します。
- 最初に MAGIC を書きました。CURRENT_VERSION は主に、ファイルが合法的な修正パッチ ファイルであることを確認するために使用されます。
- 次に、patchedDexSizeを記述します
- 4 番目のビットはデータ領域のオフセットに書き込まれますが、最初に 0 局の位置が使用され、マップ リストに関連するすべてのオフセットが書き込まれた後、現在位置が書き込まれていることがわかります。
- 次に、マップリストの各領域に関連するすべてのオフセットを書き込みます (ここでは各領域の順序は重要ではありません。同じように読み書きするだけです)
- その後、各アルゴリズムを実行して、対応する領域に情報を書き込みます
- 最後にパッチファイルを生成します
ここではまだ stringDataSectionDiffAlg アルゴリズムのみを確認します。
リーリーまず、patchOperationList を DEL、ADD、REPLACE に対応する 3 つの OpIndexList に変換し、すべての項目を newItemList に保存します。
次に、順番に書きます:
- del 操作の数、各 del のインデックス
- 追加操作の数、各追加のインデックス
- 置換操作の数、置換が必要な各インデックス
- 最後にnewItemListを書きます。
インデックスはここで実行されます (インデックス - lastIndex 操作はここで実行されます)
他のアルゴリズムも同様の操作を実行します。
生成したパッチがどのようなものかを確認するのが最善です:
- 最初に、あなたが改造パッチであることを証明するためにいくつかのフィールドを含めます
- newDex の各領域を生成するためのオフセットが含まれています。つまり、newDex を複数の領域に分割して開始点に配置できます。
- newDex の各領域に、削除されたインデックス (oldDex)、新しいインデックスと値、およびアイテムの置換されたインデックスと値が含まれます
このように見ると、Patch のロジックは次のように推測されます:
- まず各エリアのオフセットに基づいて各エリアの開始点を決定します
- oldDex の各領域の項目を読み取り、パッチに従って oldDex で削除および置換する必要がある項目を削除し、新しい項目と置換された項目を追加して、の領域の項目を形成します新しい古い。
つまり、newDex の特定の領域には次のものが含まれます:
リーリーこれは非常に明確です。以下のコードを見てみましょう~
diff と同様に、古い dex ファイルとパッチ ファイルを受け入れ、最終的に新しい Dex を生成するクラスまたはメソッドが必要です。大量のセキュリティ検証コードや APK 解凍コードに引っかからないようにしてください。
このクラスは、tinker-commons では DexPatchApplier と呼ばれています。
パッチに関連するコードは次のとおりです:
リーリーdiff コードと似ていることがわかります。以下のコードを参照してください。
oldDex は Dex オブジェクトに変換されます。これは主に readHeader と readMap で上で分析されました。patchFile が DexPatchFile オブジェクトに変換されることに注意してください。
リーリーまずパッチ ファイルを byte[] として読み取り、次に init を呼び出します
リーリーパッチをどのように書いたかまだ覚えていますか? 私たちは最初に MAGIC と Version を書いてファイルがパッチ ファイルであることを確認し、次に patchedDexSize とさまざまなオフセットに値を割り当て、最後にデータ領域 (firstChunkOffset) を見つけました。書くときは、このフィールドが 4 番目の位置にあることに注意してください。
位置特定後、後から読み込むのがデータであり、保存時には以下の形式で保存されます。
- del 操作の数、各 del のインデックス
- 追加操作の数、各追加のインデックス
- 置換操作の数、置換が必要な各インデックス
- 最後にnewItemListを書きます。
リーリー
oldDex と patchFile に加えて、patchedDex も最終出力 Dex オブジェクトとして初期化されます。構築が完了すると、executeAndSaveTo メソッドが直接実行されます。
リーリー
executeAndSaveTo(os) に直接移動します。このメソッドのコードは比較的長いので、3 つの段落で説明します。 リーリー実際には、ここで、patchFileに記録された値が読み取られ、patchedDexのTableOfContent内のさまざまなSection(マップリストの各map_list_itemにほぼ対応)に割り当てられます。
並べ替えの次に、byteCount などのフィールド情報を設定します。
###続く:### リーリーこの部分は明らかに多数のアルゴリズムを初期化し、それらを個別に実行します。分析には引き続き stringDataSectionPatchAlg を使用します。
リーリー書くときのルールを投稿しましょう:
del 操作の数、各 del のインデックス
追加操作の数、各追加のインデックス
- 置換操作の数、置換が必要な各インデックス
- 最後にnewItemListを書きます。
- コードを見ると、読み取り順序は次のとおりです。
に格納されます。
追加の数、追加のすべてのインデックスは int[];- に格納されます。
- 置換の数、置換のすべてのインデックスは int[]; に格納されます。
- それは書かれたときと同じですか?
- 続行して、oldDex の oldItems と oldItemCount を取得します。
これで次のようになります:
del数とインデックス
add count インデックスを追加
- カウントとインデックスを置き換える
- oldItems と oldItemCount
- 今あるものをそのまま使用して、doFullPatch の実行を続けます
- リーリー 全体を見てみましょう。ここでの目的は、patchedDex の stringData 領域にデータを書き込むことです。書き込まれたデータは、理論的には次のようになります。
新しいデータ
代替データ
- oldDex の新しいデータと置換されたデータ
- もちろん、それらは連続して書き込む必要があります。
- コードを見ると、最初に newItemCount=oldItemCount addCount - delCount を計算してから走査を開始します。走査条件は 0~oldItemCount または 0~newItemCount です。
項目はコードを通じて記述されており、次のことがわかります:
まず、patchIndex が addIndices に含まれているかどうかを確認し、含まれている場合はそれを書き込みます。
さらに、replicaIndices にあるかどうかを判断し、含まれているかどうかを書き込みます;次に、oldIndex が削除または置換されたかどうかを判断し、直接スキップします。
- その後、最後のインデックスは、削除と置換が行われていない oldIndex を参照します。これは、newDex の項目と同じ部分です。
- 1.2.4 の上記 3 つの部分は、完全な newDex のこの領域を形成できます。
これでstringData領域のパッチアルゴリズムが完成します。
残りの 14 個のアルゴリズムの実行コードは同じ (親クラス) であり、実行される操作も同様であり、パッチ アルゴリズムのすべての部分が完了します。
すべての領域が復元されると、残っているのはヘッダーとマップリストだけになるので、すべてのアルゴリズムの実行が完了した場所に戻ります。
リーリーヘッダー領域を見つけてヘッダー関連データを書き込み、マップ リスト領域を見つけてマップ リスト関連データを書き込みます。両方が完了したら、ヘッダーに 2 つの特別なフィールド (signature と checkSum) を記述する必要があります。これら 2 つのフィールドはマップ リストに依存するため、マップ リストの後に記述する必要があります。
これで完全な dex リカバリが完了し、最後にメモリ内のすべてのデータがファイルに書き込まれます。
Hello.dex ができたので、別のクラスを作成しましょう:
リーリー次に、このクラスをコンパイルして dx ファイルに入力します。
リーリーこのようにして、Hello.dex と World.dex という 2 つの dex を準備しました。
010 エディターを使用して 2 つの dex をそれぞれ開きます。主に string_id_item に焦点を当てます;
両側に 13 個の文字列があります。上で紹介した diff アルゴリズムによれば、次の操作が得られます:
両側の文字列のトラバースと比較を開始します:
- >0 の場合、newItem は新しく追加されたとみなされ、PatchOperation.OP_ADD として記録され、newItem のインデックスと値が PatchOperation オブジェクトに記録され、patchOperationList に追加されます。
- =0 の場合、PatchOperation は生成されません。
その後、インデックスに従って並べ替えますが、変更はありません;
次に、すべての操作を繰り返し、一貫したインデックスと隣接する DEL と ADD を使用して操作を置換します。
リーリー最後に、書き込み時にトラバーサルが実行され、操作が DEL、ADD、REPLACE に従って分類され、表示される項目が newItemList に配置されます。
リーリーnewItemList は次のようになります:
リーリー次に書き込みます。書き込み順序は次のようになります:
リーリーここでは、DexPatchGenerator の writeResultToStream の関連位置に直接ログインします。 リーリー
出力は次のようになります:リーリー
上記の分析結果と一致しています ~~その後、他の領域も同様の方法で検証でき、パッチも同様であるため、詳細は説明しません。
以上がAndroid ホットフィックス Tinker ソースコード分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

インターネットは単一のオペレーティングシステムに依存していませんが、Linuxはその上で重要な役割を果たしています。 Linuxは、サーバーやネットワークデバイスで広く使用されており、安定性、セキュリティ、スケーラビリティに人気があります。

Linuxオペレーティングシステムのコアは、コマンドラインインターフェイスで、コマンドラインを介してさまざまな操作を実行できます。 1.ファイルおよびディレクトリ操作は、ファイルとディレクトリを管理するために、LS、CD、MKDIR、RM、その他のコマンドを使用します。 2。ユーザーおよび許可管理は、useradd、passwd、chmod、その他のコマンドを介してシステムのセキュリティとリソースの割り当てを保証します。 3。プロセス管理は、PS、Kill、およびその他のコマンドを使用して、システムプロセスを監視および制御します。 4。ネットワーク操作には、Ping、Ifconfig、SSH、およびネットワーク接続を構成および管理するためのその他のコマンドが含まれます。 5.システムの監視とメンテナンスは、TOP、DF、DUなどのコマンドを使用して、システムの動作ステータスとリソースの使用を理解します。

導入 Linuxは、柔軟性と効率性により、開発者、システム管理者、およびパワーユーザーが好む強力なオペレーティングシステムです。しかし、頻繁に長く複雑なコマンドを使用することは退屈でERです

Linuxは、サーバー、開発環境、埋め込みシステムに適しています。 1.サーバーオペレーティングシステムとして、Linuxは安定して効率的であり、多くの場合、高電流アプリケーションの展開に使用されます。 2。開発環境として、Linuxは効率的なコマンドラインツールとパッケージ管理システムを提供して、開発効率を向上させます。 3.埋め込まれたシステムでは、Linuxは軽量でカスタマイズ可能で、リソースが限られている環境に適しています。

はじめに:Linuxベースの倫理的ハッキングでデジタルフロンティアを保護します ますます相互に接続されている世界では、サイバーセキュリティが最重要です。 倫理的なハッキングと浸透テストは、脆弱性を積極的に特定し、緩和するために不可欠です

基本的なLinux学習の方法は次のとおりです。1。ファイルシステムとコマンドラインインターフェイス、2。LS、CD、MKDIR、3。ファイルの作成と編集などのファイル操作を学習するマスター基本コマンド、4。

Linuxは、サーバー、組み込みシステム、デスクトップ環境で広く使用されています。 1)サーバーフィールドでは、Linuxは、その安定性とセキュリティにより、Webサイト、データベース、アプリケーションをホストするための理想的な選択肢となっています。 2)埋め込みシステムでは、Linuxは高いカスタマイズと効率で人気があります。 3)デスクトップ環境では、Linuxはさまざまなユーザーのニーズを満たすために、さまざまなデスクトップ環境を提供します。

Linuxの欠点には、ユーザーエクスペリエンス、ソフトウェア互換性、ハードウェアサポート、学習曲線が含まれます。 1.ユーザーエクスペリエンスは、WindowsやMacOほどフレンドリーではなく、コマンドラインインターフェイスに依存しています。 2。ソフトウェアの互換性は他のシステムほど良くなく、多くの商用ソフトウェアのネイティブバージョンがありません。 3.ハードウェアサポートはWindowsほど包括的ではなく、ドライバーは手動でコンパイルされる場合があります。 4.学習曲線は急で、コマンドラインの操作をマスターするには時間と忍耐が必要です。


ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

AI Hentai Generator
AIヘンタイを無料で生成します。

人気の記事

ホットツール

VSCode Windows 64 ビットのダウンロード
Microsoft によって発売された無料で強力な IDE エディター

ドリームウィーバー CS6
ビジュアル Web 開発ツール

WebStorm Mac版
便利なJavaScript開発ツール

Safe Exam Browser
Safe Exam Browser は、オンライン試験を安全に受験するための安全なブラウザ環境です。このソフトウェアは、あらゆるコンピュータを安全なワークステーションに変えます。あらゆるユーティリティへのアクセスを制御し、学生が無許可のリソースを使用するのを防ぎます。

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境
