オープンソースソフトウェアの開発プロジェクトでは「企業が開発に貢献せずに修正依頼ばかり送りつけてくる」といった問題が発生しがちです。オープンソースCMS「Drupal」の開発チームはプロジェクトの貢献度を可視化することで問題を回避しようと試みています。
「オープンソースの開発に貢献せず搾取するだけの大企業」の解決方法をDrupal開発コミュニティが示す - GIGAZINE
⇧「修正依頼」があるってことは、レビューで見逃されていた不具合を指摘できているということで、貢献できている気がするんですけどね...
なので、
『「企業が開発に貢献せずに修正依頼ばかり送りつけてくる」』
の認識が間違っている気はしますけどね...
何も言わずに利用するのを止めて離れていく人もいると思いますし、況やビジネスの現場においてをや、じゃないですが、「修正依頼」も「課題発見」という意味において「貢献活動」に値すると思いますが...
Decompiler(逆コンパイラ)とは
Wikipediaによりますと、
A decompiler is a computer program that translates an executable file to high-level source code. It does therefore the opposite of a typical compiler, which translates a high-level language to a low-level language.
While disassemblers translate an executable into assembly language, decompilers go a step further and translate the code into a higher level language such as C or Java, requiring more sophisticated techniques.
Decompilers are usually unable to perfectly reconstruct the original source code, thus will frequently produce obfuscated code. Nonetheless, they remain an important tool in the reverse engineering of computer software.
⇧ とあり、完全に元のソースコードの状態に戻せるわけではないが、「リバースエンジニアリング」において重要なツールであることに変わりはない、とのこと。
消去法的に、他に代替できるような手段が無いというのが本音な気がしますかね...
Reverse engineering(リバースエンジニアリング)
「リバースエンジニアリング」はというと、
Reverse engineering (also known as backwards engineering or back engineering) is a process or method through which one attempts to understand through deductive reasoning how a previously made device, process, system, or piece of software accomplishes a task with very little (if any) insight into exactly how it does so.
Depending on the system under consideration and the technologies employed, the knowledge gained during reverse engineering can help with repurposing obsolete objects, doing security analysis, or learning how something works.
Although the process is specific to the object on which it is being performed, all reverse engineering processes consist of three basic steps: information extraction, modeling, and review.
⇧ とありますが、一般的なソフトウェアのWeb系のアプリケーションにおいては、「設計書」が無い状態で、現状稼働している「ソースコード」を元に、全体の関連を推測し、開発できる状態を構築する、という不確定要素の大きい作業でありますと。
「新規開発」とかでは、発生しないのだけど、「とりあえず動けば良い」というようなプロジェクト、所謂「スタートアップ」の組織で作成されたアプリケーションなんかでは、
- 設計書(ドキュメント)作る気が無い
- 設計書(ドキュメント)は後回し
みたいな状態になっていることが多く、俗に言う、『ソースコードが正』という状態を生み出しますと。
で、『ソースコードが正』の弊害は、『要求・要件が本当に適切なのかは、ソースコードを見ても分からない』ということで、本来であれば「設計書(ドキュメント)」でそのあたりの背景が説明されているべきなのだが、歪な状態は放置され続けますと。
で、「技術的負債」を抱えた状態になることが多く、「リファクタリング」しようにも、結局、手の施しようが無い状態になることが多いですと。
要するに、誰が作ったか分からないようなシステムを「保守・運用」せざるを得ないようなフェーズで、「リバースエンジニアリング」せざるを得ないことが多くなりますと。
後は、「新規開発」などでも、「既存踏襲」みたいな感じで、現状稼働している別システムを参考にするような場合で、『ソースコードが正』という状態だと、『要求・要件が本当に適切なのかは、ソースコードを見ても分からない』が起こるので、惨憺たる結果になるのは目に見えてますと。
Javaの「.class」ファイルから「.java」ファイルへ。Java用のDecompiler(逆コンパイラ)は何があるのか
で、Javaにおいて「Decompiler(逆コンパイラ)」が必要になってくるのは、
- 拡張子が「.java」のファイルとして連携されない
という状況、所謂、
など、拡張子「.class」や「.jar」などになってしまっているものしか手元にない場合になってくるのかなと。
まぁ、止むに止まれぬといった状況でしか必要となってこないので、「Decompiler(逆コンパイラ)」を利用する機会は滅多にないのかなと。
とは言え、必要となる場面が来るかもしれないということで、
歴史
この言葉は孫子の謀攻からの言葉であり、勝つために必要なこととして挙げられている。孫子の謀攻ではこの言葉に続いて、敵のことを知らずに味方のことだけを分かっている場合には勝ち負けの割合は半々になり、敵のことも味方のことも分かっていないようでは、ほぼ負けるとされている。
2008年に行われたRSA Conference Japan 2008で小川和久は、この言葉を持ち出して日本人を批判する。小川によれば日本人は、敵と味方の両方を知って百戦危うからずということを信じてきたが、これは世界では常識ではないとする。世界では味方のみを知り敵を知らないで勝ち負けは半々にするというのが常識であるとする。味方を知るだけで危機管理の半分は成立しており、完全なことを追及するのではなくまずは味方を理解することが世界共通の危機管理への意識であるとする。
⇧ とありますが、Javaにおける「Decompiler(逆コンパイラ)」を味方にする意味でも、Java用の「Decompiler(逆コンパイラ)」を知っておいても損はないですと。
で、ネットの情報を漁っていたところ、
⇧ 上記サイト様によりますと、「jadx」というライブラリが開発が継続中とあるので、選択肢としては「jdax」の一択になってきそう。
jadx - Dex to Java decompiler
Command line and GUI tools for producing Java source code from Android Dex and Apk files
⇧ 説明だと、「Android」用に特化のように見えてしまうのだが、
input fileとして「.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc」という複数の形式が使えるのは便利ですね。
⇧ 参考サイト様によると、拡張子「.class」や「.jar」のファイルに対応しているとはあるので、「Android」以外で生成されているファイルにも対応しているのか分からない...
Javaの「.class」ファイルから「.java」ファイルへ。Decompiler(逆コンパイラ)を試してみる
とりあえず、Windows環境で試してみる。
なのだが、
After download unpack zip file go to bin
directory and run:
jadx
- command line versionjadx-gui
- UI version
On Windows run .bat
files with double-click
Note: ensure you have installed Java 11 or later 64-bit version. For Windows, you can download it from oracle.com (select x64 Installer).
⇧ Windows環境の場合、Java 11以上をインストールしている必要があるっぽい。
おそらく、
⇧「JAVA_HOME」に、Java 11以上を設定していれば良いような気がしている。
で、導入方法としては、
- releaseページからダウンロード
- git cloneしてビルドする
の2つの方法が用意されている模様。
せっかくなので、「2. git cloneしてビルドする」で導入してみる。
事前にインストール済みの「JDK 21(Java Development Kit 21)」を「JAVA_HOME」に一時的に設定しています。
git clone直後は、「bin」ディレクトリが無い状態。
ビルドします。
環境変数の値で、末尾にスペースとか入らないように注意。ダブルクォーテーションの範囲外とかにスペース入ってると気付きにくい。(実験談)
set JAVA_HOME="[JDKの配置ディレクトリ]"
■jadxの諸々をビルド
gradlew.bat dist
⇧ 凄まじく「文字化け」したコンソール出力になってるが、ビルドは成功した模様。
出力先が
⇧ドキュメントの記載内容と異なり、分かり辛ら過ぎるのだが、自分の場合は「E:\soft_work\jadx\jadx-gui\build\scriptsShadow\jadx-gui.bat」に配置されておりました。
で、実行したところ、エラー。
バグなのか、分からんが、「jadx-gui.bat」の中を確認してみると、
■E:\soft_work\jadx\jadx-gui\build\scriptsShadow\jadx-gui.bat
@if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem jadx-gui startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME%.. @rem Add default JVM options here. You can also use JAVA_OPTS and JADX_GUI_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xms128M" "-XX:MaxRAMPercentage=70.0" "-Dawt.useSystemAAFontSettings=lcd" "-Dswing.aatext=true" "-Djava.util.Arrays.useLegacyMergeSort=true" "-Djdk.util.zip.disableZip64ExtraFieldValidation=true" "-XX:+IgnoreUnrecognizedVMOptions" "--add-opens=java.base/java.lang=ALL-UNNAMED" "-Dsun.java2d.noddraw=true" "-Dsun.java2d.d3d=false" "-Dsun.java2d.ddforcevram=true" "-Dsun.java2d.ddblit=false" "-Dswing.useflipBufferStrategy=True" @rem Find javaw.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=javaw.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/javaw.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\lib\jadx-gui-dev-all.jar @rem Execute jadx-gui start "jadx-gui" /B "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JADX_GUI_OPTS% -jar "%CLASSPATH%" %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable JADX_GUI_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%JADX_GUI_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega
何か、参照しているパスのディレクトリが存在しないんよね...
■参照してるパス
E:\soft_work\jadx\jadx-gui\build\lib\jadx-gui-dev-all.jar
■gradle.batの結果、実際に自分の環境で作られたパス
E:\soft_work\jadx\jadx-gui\build\libs\jadx-gui-dev-all.jar
とりあえず、「lib」を「libs」に変更。
■E:\soft_work\jadx\jadx-gui\build\scriptsShadow\jadx-gui.bat
@if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem jadx-gui startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME%.. @rem Add default JVM options here. You can also use JAVA_OPTS and JADX_GUI_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xms128M" "-XX:MaxRAMPercentage=70.0" "-Dawt.useSystemAAFontSettings=lcd" "-Dswing.aatext=true" "-Djava.util.Arrays.useLegacyMergeSort=true" "-Djdk.util.zip.disableZip64ExtraFieldValidation=true" "-XX:+IgnoreUnrecognizedVMOptions" "--add-opens=java.base/java.lang=ALL-UNNAMED" "-Dsun.java2d.noddraw=true" "-Dsun.java2d.d3d=false" "-Dsun.java2d.ddforcevram=true" "-Dsun.java2d.ddblit=false" "-Dswing.useflipBufferStrategy=True" @rem Find javaw.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=javaw.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/javaw.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\libs\jadx-gui-dev-all.jar @rem Execute jadx-gui start "jadx-gui" /B "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JADX_GUI_OPTS% -jar "%CLASSPATH%" %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable JADX_GUI_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%JADX_GUI_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega
で、「E:\soft_work\jadx\jadx-gui\build\scriptsShadow\jadx-gui.bat」をダブルクリックしたところ、「jadx」の「GUI」が起動されました。
試しに、
⇧ 前回の環境で生成された「.class」ファイルを「jadx」のインプットとしてみる。
結果。
■「jadx」による「Decompiler(逆コンパイラ)」の結果
package defpackage; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.LinkedHashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /* loaded from: CmdExtractor.class */ public class CmdExtractor { public static void main(String[] args) { Path dirPath = Paths.get("E:\\soft_work\\oxidized\\lib\\oxidized\\model", new String[0]); Path outputPath = Paths.get("output.tsv", new String[0]); Throwable th = null; try { try { BufferedWriter writer = Files.newBufferedWriter(outputPath, StandardOpenOption.CREATE); try { writer.write("番号\tファイル名\tコマンド数\tコマンド\n"); int[] counter = {1}; Files.walk(dirPath, new FileVisitOption[0]).filter(path -> { return Files.isRegularFile(path, new LinkOption[0]); }).forEach(filePath -> { processFile(filePath, writer, counter); }); if (writer != null) { writer.close(); } } catch (Throwable th2) { if (writer != null) { writer.close(); } throw th2; } } catch (Throwable th3) { if (0 == 0) { th = th3; } else if (null != th3) { th.addSuppressed(th3); } throw th; } } catch (IOException e) { e.printStackTrace(); } } /* JADX INFO: Access modifiers changed from: private */ public static void processFile(Path filePath, BufferedWriter writer, int[] counter) { Throwable th = null; try { try { BufferedReader reader = Files.newBufferedReader(filePath); try { Set<String> commands = new LinkedHashSet<>(); Pattern pattern = Pattern.compile("class\\s+([^<\\s]+)(?:\\s*<|$)"); String extracted = null; boolean notYetCheck = true; String fileName = filePath.getFileName().toString(); System.out.println("■■■ファイル名■■■:" + fileName); int lineNo = 0; while (true) { String line = reader.readLine(); if (line == null) { break; } lineNo++; String line2 = line.trim(); if (!line2.trim().startsWith("#")) { if (notYetCheck) { Matcher matcher = pattern.matcher(line2); if (matcher.find()) { extracted = matcher.group(1); System.out.println("行:" + lineNo); System.out.println("文字列: " + line2); System.out.println("抽出された文字列: " + extracted); notYetCheck = false; } else { System.out.println("行:" + lineNo); System.out.println("一致する文字列が見つかりませんでした"); } } if (line2.startsWith("cmd")) { int cmdStartPosition = "cmd".length() + 1; int cmdEndPosition = line2.lastIndexOf("do"); System.out.println("行:" + lineNo); System.out.println("文字数:" + line2.length()); System.out.println("文字列:" + line2); System.out.println("コマンド開始位置:" + cmdStartPosition); System.out.println("コマンド終了位置:" + cmdEndPosition); if (cmdEndPosition == -1) { commands.add(line2.substring(cmdStartPosition, line2.length())); } else { commands.add(line2.substring(cmdStartPosition, cmdEndPosition)); } } } } int commandCount = commands.size(); String commandsStr = commandCount > 0 ? String.join("\t", commands) : ""; writer.write(String.valueOf(counter[0]) + "\t" + fileName + "\t" + extracted + "\t" + commandCount + "\t" + commandsStr + "\n"); counter[0] = counter[0] + 1; if (reader != null) { reader.close(); } } catch (Throwable th2) { if (reader != null) { reader.close(); } throw th2; } } catch (Throwable th3) { if (0 == 0) { th = th3; } else if (null != th3) { th.addSuppressed(th3); } throw th; } } catch (IOException e) { e.printStackTrace(); } } }
元の「.java」ファイルはというと、
■元の「.java」ファイル
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.LinkedHashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public class CmdExtractor { public static void main(String[] args) { // 対象ディレクトリのパスを指定 Path dirPath = Paths.get("E:\\soft_work\\oxidized\\lib\\oxidized\\model"); // 出力ファイルのパスを指定(例:output.csv) Path outputPath = Paths.get("output.tsv"); try (BufferedWriter writer = Files.newBufferedWriter(outputPath, StandardOpenOption.CREATE)) { // TSVヘッダを書き込む writer.write("番号\tファイル名\tコマンド数\tコマンド\n"); // 番号を付けるカウンター(ファイルに対して番号を付ける) // 番号をカウントするための配列 final int[] counter = { 1 }; // ディレクトリ内のすべてのファイルを再帰的に探索 Files.walk(dirPath) .filter(Files::isRegularFile) // ファイルのみ対象 .forEach(filePath -> processFile(filePath, writer, counter)); // 各ファイルを処理 } catch (IOException e) { e.printStackTrace(); } } // ファイルを処理するメソッド private static void processFile(Path filePath, BufferedWriter writer, int[] counter) { try (BufferedReader reader = Files.newBufferedReader(filePath)) { String line; // シングルクォーテーションまたはダブルクォーテーションで囲まれた部分を抽出するパターン // Pattern pattern = Pattern.compile("\"([^\"]*)\"|'([^']*)'"); // ダブルクォートとシングルクォートを両方サポート Set<String> commands = new LinkedHashSet<>(); Pattern pattern = Pattern.compile("class\\s+([^<\\s]+)(?:\\s*<|$)"); String extracted =null; boolean notYetCheck = true; // ファイル名 String fileName = filePath.getFileName().toString(); System.out.println("■■■ファイル名■■■:" + fileName); int lineNo = 0; while ((line = reader.readLine()) != null) { lineNo++; line = line.trim(); // コメント行はスキップ if (line.trim().startsWith("#")) { continue; } // クラス名のチェック if (notYetCheck) { Matcher matcher = pattern.matcher(line); // マッチがあれば抽出 if (matcher.find()) { extracted = matcher.group(1); // グループ1が目的の文字列 System.out.println("行:" + lineNo); System.out.println("文字列: " + line); System.out.println("抽出された文字列: " + extracted); notYetCheck = false; } else { System.out.println("行:" + lineNo); System.out.println("一致する文字列が見つかりませんでした"); } } // コマンドのチェック if (line.startsWith("cmd")) { int cmdStartPosition = "cmd".length() + 1; int cmdEndPosition = line.lastIndexOf("do"); System.out.println("行:" + lineNo); System.out.println("文字数:" + line.length()); System.out.println("文字列:" + line); System.out.println("コマンド開始位置:" + cmdStartPosition); System.out.println("コマンド終了位置:" + cmdEndPosition); // ブロックが無いパターン if (cmdEndPosition == -1) { commands.add(line.substring(cmdStartPosition, line.length())); } else { commands.add(line.substring(cmdStartPosition, cmdEndPosition)); } } } // コマンドが1つ以上見つかった場合、または見つからない場合でもファイル名とコマンド数を出力 int commandCount = commands.size(); // コマンド数 // コマンドがあればカンマ区切りで出力、なければ空欄 String commandsStr = commandCount > 0 ? String.join("\t", commands) : ""; // 書き込み writer.write(counter[0] + "\t" + fileName + "\t" + extracted + "\t" + commandCount + "\t" + commandsStr + "\n"); // 番号をインクリメント counter[0]++; } catch (IOException e) { e.printStackTrace(); } } }
⇧ という感じで、「jadx」の「Decompiler(逆コンパイラ)」は、「try-with-resources 文」が解釈できなかったりと、微妙な感じではあるが、おおまかな「.java」ファイルのコーディングの雰囲気が把握できる程度には、機能している模様。
コメントとかは根こそぎ無かったことにされておりますが...
そもそも、Windows環境における「jadx」のバグが放置されてるってことは、Windows環境では利用している人がいないってことんかね?
何やら、「E:\soft_work\jadx\jadx-gui\build\install\jadx-gui-shadow\bin\jadx-gui.bat」に配置されている方を利用するらしい。
つまり、
- [git cloneでできるディレクトリ]\jadx-gui\build\install\jadx-gui-shadow\bin\jadx-gui.bat
- [git cloneでできるディレクトリ]\jadx-gui\build\scriptsShadow\jadx-gui.bat
「1. [git cloneでできるディレクトリ]\jadx-gui\build\install\jadx-gui-shadow\bin\jadx-gui.bat」を利用するのが正解だったと。
ちなみに、「1. [git cloneでできるディレクトリ]\jadx-gui\build\install\jadx-gui-shadow\bin\jadx-gui.bat」で「Decompiler(逆コンパイラ)」した結果、内容は変わりませんでした。
兎に角、どれを使えば良いのかが、分かり辛ら過ぎる...
素直に、「releaseページ」からダウンロードする導入方法を選択した方が良さそうかも...
毎度モヤモヤ感が半端ない…
今回はこのへんで。