GraalVM をビルドすると生成される native-image ファイル。このファイルを実行することでバイナリファイルを生成することができるが、native-image ファイル自身はどのように生成されているのだろうか。native-image を生成するためにも自身のバイナリ生成機能を使っている? コンパイラ自身はどのようにコンパイルされているのか的な。

正体

なんてことはない、native-image ファイルはただのシェルスクリプトだった。勝手に ELF などの実行形式だと思い込んでいたのだけど、そうではなかった。substratevm ディレクトリで build して生成された native-image ファイルを file コマンドで確認した結果。

$ file ../sdk/latest_graalvm_home/lib/svm/bin/native-image
../sdk/latest_graalvm_home/lib/svm/bin/native-image: Bourne-Again shell script, ASCII text executable, with very long lines (5506)

生成元のテンプレートと思われるファイルも発見した。このテンプレート中の <> で囲まれている箇所を解決したものが native-image ファイル、ということのようである。

なにをしているのか

ではこのスクリプトはなにをしているのか。

以下が native-image ファイルの最終行。見やすいように改行を追加している。java コマンドでクラスを指定して実行していることがわかる。

exec "${location}/../../../bin/java" \
  -XX:MaxHeapSize=214748365 -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI \
  "${jvm_args[@]}" ${app_path_arg} "${cp_or_mp}" ${main_class} "${launcher_args[@]}"

${main_class} を出力してみた。つまり、native-image ファイルは NativeImage クラスを java コマンドで実行しているにすぎない。

--module org.graalvm.nativeimage.driver/com.oracle.svm.driver.NativeImage

さらにその先

native-image には --verbose という詳細出力を有効にするオプションがある。こちらを指定して実行してみる。

$ native-image --verbose HelloWorld
...
Executing [
...
/path/to/graal/sdk/mxbuild/linux-amd64/GRAALVM_0292E55835_JAVA21/graalvm-0292e55835-java21-24.2.0-dev/bin/java \
...
--module \
org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner \
...
]
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
[1/8] Initializing...                                                                                    (2.6s @ 0.17GB)
...

なんと、NativeImage クラス自身も java コマンドを実行していた。今度は以下のクラスが指定されている。

org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner

なお、以下の箇所で ProcessBuilder クラスの start メソッドにより java コマンドを実行しているようである。

p = pb.inheritIO().start();
imageBuilderPid = p.pid();
return p.waitFor();

おそらくここから本格的にバイナリファイル生成処理に移っていくと思われるが、今回はここまで。

追記

2024.12.31 追記

native-image ファイルはシェルスクリプト、と記載したが、native-image ファイルを実行形式としてビルドすることも可能だった。たとえば substratevm ディレクトリで、mx graalvm-show コマンドを実行した結果が以下。

$ mx graalvm-show
...
Launchers:
 - native-image (bash, rebuildable)
 - native-image-configure (bash, rebuildable)
 - polyglot (bash, rebuildable)
Libraries:
 - libjvmcicompiler.so (skipped, rebuildable)
 - libnative-image-agent.so (skipped, rebuildable)
 - libnative-image-diagnostics-agent.so (skipped, rebuildable)
 - libsvmjdwp.so (skipped, rebuildable)
No standalone

native-image (bash, rebuildable) と記載されている通り、このまま mx build を実行すると native-image はシェルスクリプトとして生成される。では、今度は --print-env というオプションを指定して実行してみる。

$ mx graalvm-show --print-env
...
Inferred env file:
DYNAMIC_IMPORTS=/compiler,/regex,/sdk,/substratevm,/truffle
COMPONENTS=antlr4,cmp,dis,icu4j,lg,llp,nfi,nfi-libffi,ni,nic,nil,nju,poly,rgx,sdk,sdkc,sdkl,sdkni,svm,svmjdwp,svml,svmnfi,svmsl,svmt,tfl,tfla,tflc,tflm,tflp,tflsm,truffle-json,xz
EXCLUDE_COMPONENTS=libpoly
NATIVE_IMAGES=false
NON_REBUILDABLE_IMAGES=False

すると、先ほどの出力結果に加えて上記の通り環境変数の設定状況も表示される。気になるのは NATIVE_IMAGES=false という部分。こちらを true にして再度実行。

$ NATIVE_IMAGES=true mx graalvm-show --print-env
...
Launchers:
 - native-image (native, rebuildable)
 - native-image-configure (native, rebuildable)
 - polyglot (native, rebuildable)
Libraries:
 - libjvmcicompiler.so (native, rebuildable)
 - libnative-image-agent.so (native, rebuildable)
 - libnative-image-diagnostics-agent.so (native, rebuildable)
 - libsvmjdwp.so (native, rebuildable)
No standalone
Inferred env file:
DYNAMIC_IMPORTS=/compiler,/regex,/sdk,/substratevm,/truffle
COMPONENTS=antlr4,cmp,dis,icu4j,lg,llp,nfi,nfi-libffi,ni,nic,nil,nju,poly,rgx,sdk,sdkc,sdkl,sdkni,svm,svmjdwp,svml,svmnfi,svmsl,svmt,tfl,tfla,tflc,tflm,tflp,tflsm,truffle-json,xz
EXCLUDE_COMPONENTS=libpoly
NATIVE_IMAGES=lib:jvmcicompiler,lib:native-image-agent,lib:native-image-diagnostics-agent,lib:svmjdwp,native-image,native-image-configure,polyglot
NON_REBUILDABLE_IMAGES=False

すると、先ほどは bash と表示されていた箇所が軒並み native に変わっている。ついでに Libraries の項目も skipped から native に変わっている。NATIVE_IMAGES の個別指定もできそうなので試してみる。

$ NATIVE_IMAGES=native-image mx graalvm-show --print-env
...
Launchers:
 - native-image (native, rebuildable)
 - native-image-configure (bash, rebuildable)
 - polyglot (bash, rebuildable)
Libraries:
 - libjvmcicompiler.so (skipped, rebuildable)
 - libnative-image-agent.so (skipped, rebuildable)
 - libnative-image-diagnostics-agent.so (skipped, rebuildable)
 - libsvmjdwp.so (skipped, rebuildable)
No standalone
Inferred env file:
DYNAMIC_IMPORTS=/compiler,/regex,/sdk,/substratevm,/truffle
COMPONENTS=antlr4,cmp,dis,icu4j,lg,llp,nfi,nfi-libffi,ni,nic,nil,nju,poly,rgx,sdk,sdkc,sdkl,sdkni,svm,svmjdwp,svml,svmnfi,svmsl,svmt,tfl,tfla,tflc,tflm,tflp,tflsm,truffle-json,xz
EXCLUDE_COMPONENTS=libpoly
NATIVE_IMAGES=native-image
NON_REBUILDABLE_IMAGES=False

案の定、native-image のみ変更された。それでは NATIVE_IMAGES=native-image を指定したうえでビルドを実行する。

$ NATIVE_IMAGES=native-image mx build

生成されたファイルを確認。

$ file ../sdk/latest_graalvm_home/bin/native-image
../sdk/latest_graalvm_home/bin/native-image: symbolic link to ../lib/svm/bin/native-image

どうやらシンボリックリンクになっているようなので本体のほうを確認。

$ file ../sdk/latest_graalvm_home/lib/svm/bin/native-image
../sdk/latest_graalvm_home/lib/svm/bin/native-image: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=56742ae6a7b3dd83171eb482a731dd3a43ab302e, for GNU/Linux 3.2.0, stripped

想定通り、シェルスクリプトではなく ELF ファイルが生成されている。