世界を動かす技術を、日本語で。

Linuxバイナリ互換性の聖杯:MuslとDlopen

概要

  • GoGodot でAndroid/iOS向けのバイナリ生成は容易
  • Linux ではバイナリ互換性と グラフィックス 周りに大きな課題
  • muslglibc の互換性問題が顕著
  • musl 対応のため独自パッチやビルド手法を導入
  • 静的バイナリ+グラフィックス実現には dlopen 問題の克服が鍵

Linuxバイナリ互換性の壁

  • Go の静的バイナリはLinuxサーバーやCLI向けに広く普及
  • グラフィックス 利用時はGPUドライバがC ABI経由で動的ライブラリを要求
  • glibcmusl 間の互換性問題
    • glibc製バイナリはmusl環境で動作不可、逆も同様
  • Void Linux (musl版)での実体験
    • Zed editorやgraphics.gdプロジェクトのビルド困難
  • Goはmusl向け c-shared / c-archive ビルドに未対応

muslサポートのための工夫

  • GOOS=musl という独自ビルドターゲットをgraphics.gdに導入
  • musl向け c-shared ビルドを廃止、 c-archive でGodotと直接リンク
  • これによりmuslサポートを実現
  • Linux向けリリース時は glibc版musl版 の2種類を用意する必要
    • ユーザーに正しいバイナリ選択を促す課題

静的バイナリ+グラフィックスの挑戦

  • muslは 静的リンク に優れるため、単一静的バイナリの実現を模索
  • Godotは依存ライブラリを内包、残りは動的ロード(dlopen)で対応
  • -static 指定でビルドすると「Dynamic loading not supported」エラー
  • muslは静的バイナリでの dlopen 実装を拒否
    • glibcとmuslの TLS 実装差異が原因

dlopen問題の突破口

  • dlopen は弱シンボルとしてコンパイルされるため、自前実装が可能
  • C言語のdetour技法やCosmopolitanのdlopen手法が参考
  • 小型Cプログラムを組み込んで実行時にホストのダイナミックリンカを呼び出し
    • システムのdlopenを「奪い」、graphics.gdに戻る
    • アセンブリトランポリンでlibc TLSを切り替えつつ動的関数をラップ
  • cgoに類似したアプローチ

シングル静的バイナリ+グラフィックスの実現

  • musl+独自dlopen実装で、 Go製シングル静的バイナリ+グラフィックス がLinuxで実現
  • どのLinux(3.2以降)でもハードウェアアクセラレーション付きで動作可能

サンプルとクロスコンパイル手順

  • Dodge The Creeps サンプルプロジェクトの静的バイナリ公開
    • https://release.graphics.gd/dodge_the_creeps.static
  • 任意のプロジェクトをクロスコンパイル可能
    • GOOS=musl GOARCH=amd64 gd build コマンド
    • export_presets.cfgを削除して新しいmuslエクスポートプリセットを追加

今後の展望と課題

  • helperバイナリ の埋め込みや配布方法の改善
  • glibc/musl 混在環境でのユーザー体験向上
  • シングルバイナリ配布の標準化に向けたさらなる工夫

Hackerたちの意見

実行可能ファイルを受け取って、必要な.soファイルを全部集めて、静的実行ファイルか、どこでも動くパッケージを作るツールってある?

必要な.soファイルを一つのファイルに「パッケージ」することはできるよ。そういうツールはたくさんあるし(例えばzipファイルみたいに)。でも、.soファイルを使って一つの「静的」バイナリを作ることはできないんだ。

こういうのはあるよ。思いつくのは次のものかな:1. appimage https://appimage.org/ 2. nix-bundle https://github.com/nix-community/nix-bundle 3. guixのguix pack 4. ほとんど誰も使わないランダムな小プロジェクトの小集まり(例: https://github.com/NilsIrl/dockerc ) 5. dockerイメージ(dockerランタイムがあればどこでも動くパッケージ) 6. https://flatpak.org/ 7. https://en.wikipedia.org/wiki/Snap_(software) AppImageが一番近いと思うよ。

Ermine: https://www.magicermine.com/ 意外とよく動くけど、料金が隠されてて、学生の時に連絡したら年間350ドル以上だったよ。

「実際にポータブルな実行可能ファイル」/cosmopolitan libcというプロジェクトがあるよ。https://github.com/jart/cosmopolitan これは、一度コンパイルすればどこでも実行できるスタイルのC++バイナリを可能にするんだ。

15〜30年前、SolarisとLinuxで動く商業用チップ設計EDAソフトウェアをたくさん管理してたんだ。各プログラムが必要とする特定のライブラリのバージョンを指すために、LD_LIBRARY_PATHやLD_PRELOADを使ったラッパーシェルスクリプトがたくさんあったよ。「ldd」を使って、プログラムが使用する共有ライブラリを表示させてた。

(推奨じゃないけど、知ってる)https://www.magicermine.com/

「これを一つ実行すればパッケージ化できる」っていうほど簡単じゃないと思うから、プロセスが重要ならこれじゃダメかも。でも、ユーザー視点から見るとAppImagesの動きに似てると思う。AppImageは基本的に静的バイナリと、そのアプリケーションの「ルート」を含む小さなファイルシステムイメージがペアになってるって理解してる。フォーマットのすべてが好きなわけじゃないけど、全体的にはflatpakやsnapみたいな他の「ユニバーサル」パッケージよりも、ずっと規定が少ない感じがする。簡単に抽出して再パッケージ化したい部分を選べるのも(バイナリには --appimage-extract みたいなフラグがあるから)助かるよね。

mkdir chroot cd chroot for lib in $(ldd ${executable} | grep -oE '/\S+'); do tgt="$(dirname ${lib})" mkdir -p .${tgt} cp ${lib} .${tgt} done mkdir -p .$(dirname ${executable}) cp ${executable} .${executable} tar cf ../chroot-run-anywhere.tgz .

誰かがすでにAppImageについて言及していたけど、POSIXシェルスクリプトとして実行されるこの別の実装にも注目してほしい。これにより、異なるアーキテクチャで異なるプログラムを動的にディスパッチできるんだ。例えば、ARMとx64用のファットバイナリ。 https://github.com/mgord9518/shappimage

Exodus (https://github.com/intoli/exodus) はこれに良かったけど、最近はPythonエラーを出してる。

デトゥアって初めて聞いた。結構クールなハックだね。

2005年頃のゲームハッキングでは目立ってたね。Windowsがゲームコードにフックするのをずっと簡単にしてくれた。

共有ライブラリがより良い代替手段として設計されているのに、みんなが静的リンクをしたがるのが面白いよね。さらに悪いのはコンテナで、両方の欠点があるし。

ソフトウェアを完全に自己完結型で配布する方が簡単だよ。ただ、全部を静的にリンクする手間を無視すればね :)

ダイナミックリンクは特定のトレードオフを作るために存在してるんだよね。一般的にはスタティックリンクより良くも悪くもない。

ダイナミックライブラリは、存在しない問題に対するひどい解決策として、登場以来ずっと敬遠されてきたんだ。一般的にバイナリサイズを増やしてパフォーマンスを悪化させるからね。ここに著名なキャラクターたちの面白い引用があるよ: https://harmful.cat-v.org/software/dynamic-linking/ 実際には、スタティックリンクされたシステムの方が、丁寧にダイナミックリンクされたものよりも小さいことが多いんだ。共通のルーチンがたくさんコピーされているけど、プログラムは使うシンボルの密に詰まった、特に最適化された、時にはインライン化されたバージョンだけを含んでいるからね。プログラムごとのスペースとパフォーマンスの向上はかなり大きいよ。現代のアプリやコンテナは全く別の問題で、グラフィック資産がギガバイト単位であったり、全世界を含むコンテナベースのイメージを使っている場合、リンクは役に立たないんだ。

もしその共有ライブラリがバイナリの後方互換性を壊さず、もっとwinapiのように振る舞うなら、それは良いポイントだね。

ダイナミックライブラリは、安定したAPIとABIを保証する場合、オペレーティングシステムインターフェースとしては理にかなってるよ(どうやってそれを実現するかはWindowsを見ればわかる)。DLLが意味を持つ他のシナリオはプラグインシステムだけど、それ以外はスタティックリンクの方が優れてる。最適化の障害を提示しないからね(特にデッドコードの排除に関して)。glibcがAPI+ABIの安定性を提供できない理由はわからないけど、Linuxではいつもglibcに関連する「DLL地獄」の問題に帰着するんだ(例えば、新しいglibcエントリポイントにアクセスしないプログラムでも、最近のLinuxシステムで作成された実行可能ファイルを古いLinuxシステムで実行できないことがある。通常の解決策は古いglibcバージョンでリンクすることだけど、それも簡単じゃない。Zigツールチェーンを使わない限り)。要するに、静的リンク対動的リンクではなく、ただglibcがオペレーティングシステムインターフェースとして非常にひどい解決策なだけなんだ。

自分が制御できないコードを常に呼び出す理由って何?存在するかどうかもわからないし、改ざんされてるかもしれない。実行状態の制御を失うし、フラグが壊れちゃう呼び出し規約に従わなきゃいけない。上記のすべてを放棄して、何のためにリンク時最適化を犠牲にするの?コンパイル中に生成されたすべてのオブジェクトファイルが動的にリンクされるCプログラムを開発することを想像してみて。これがバカなアイデアである理由は明らかだよね。別のライブラリを扱うときにそれがなぜ less stupid になるの?

Linuxがゲームや他のソフトウェアにとってダメ(ダメだった?)な理由って、無数の異なるライブラリがあってバージョンもバラバラだから、OSの状態について何の前提も持てないからじゃない?それがソフトウェア開発をこんなに面倒にしてるんだよ。

だから、基本的には「libcの仮想化」が必要なんだ。でもMuslはLinuxでしか使えないよね?Cosmopolitan(https://github.com/jart/cosmopolitan)はさらに進んで、MacやWindowsでも使えるし、例えばSIMDや他のパフォーマンス関連の改善も使ってる。残念ながら、マーケティングの「魔法」を切り抜けないと、主なエンジニアリングの価値を見つけるのが難しい。革新的な「ポリグロット」シェルスクリプトハックや「実際にポータブルな実行可能ファイル」コンテナを取り除くと、Cosmopolitanの核心的な利点は、プラットフォームに依存しないスタティックリンクされたC標準(プラス一部のPosix)ライブラリで、ランタイムのシステムコール翻訳を行うことなんだ。つまり「私たちが待っていたMusl」ってわけ。

この調子だと、コンテナ仮想化レイヤーも必要になるかもね。要するに、DockerのためのDockerって感じ。

どうしてもC/C++のコードを書きたいんだ。ウェブサーバーがあって、WebSocketと話せるやつを、Cosmopolitanでコンパイルしたい。Luaは使いたくない。Luaを使うのはすごく賢いけど、俺が求めてるものじゃないんだよね。とりあえず、コードを気楽に書いちゃおうかな。

C/C++コードのビルドがこんなに長い間混乱していたことが、技術や経済、さらには政治の方向性に影響を与えているのがすごいと思う。もしこの問題がちゃんと解決されていたら、世界はどうなっていたんだろう?インターネットの中央集権化やマネタイズは同じ道を辿ったのかな?Windowsはそんなに支配的だったのかな?ソーシャルメディアは今の状態に進化していたのかな?私たちは進もうとしているテクノフェダリズムに対抗するチャンスがあったのかな?

APEのコンセプトが魅力的でないなら、LLVM libCの作業に興味があるかもしれない。最近、友達がそのビジョンについてあまり評価されていない講義をしたんだ: https://youtu.be/HtCMCL13Grg 要するに、Googleは静的リンクされたモジュラーでレイテンシに敏感なポータブルPOSIXランタイムの必要性を認識していて、今それを作っているんだ。

これは、muslがdlopen()を無効にすることであなたを救おうとした正確なトラブルを求めてるんじゃないの?

dlopenでシステムライブラリを開くのは、さまざまなライブラリやABIとの互換性を保つための「簡単な」ハックだよ。実際にはほとんど使われてないけど(SDL、Small HTTP Server、あとGodotくらいしか知らない)。dlopenなしで通常の動的リンクを使うと、古いディストリビューション向けにコンパイルするのがかなり難しくなるし、glibc/muslのクロス互換性を実装するのも簡単じゃないと思う。ValveがSteam Runtimeでやってることを見てみて:- https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/pressure-vessel.md - https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/subprojects/libcapsule/doc/Capsules.txt

バイナリ互換性は、プロセス内で動作するビデオを超えて広がってるよ。最近は、IPCを通じて多くの機能が実現されていて、インターフェースによってさまざまなワイヤプロトコルがある。例えば、dbusやWaylandプロトコル、varlinkなんかがあるね。ワイヤプロトコルとその上に構築されたAPIは、バイナリ互換性を確保するために後方互換性を維持する必要がある。そうしないと、さまざまなLinuxベースのプラットフォームで自由に動かすことができなくなる。カーネルとは違って、ユーザースペースの部分は後方互換性をそれほど重視してないんだ。5年前のシステムで利用可能なAPIのサブセットをターゲットにするのもかなり難しい。ウェブのAPIエンドポイントはここでのリスクが少ないと思うけど(とはいえ、そっちもよく壊れるけどね)。

関連のディスカッション(実際のプロジェクトはこの問題に言及されています): 「Detour: LinuxでLibcなしでの動的リンク」 https://news.ycombinator.com/item?id=45740241

つい最近、muslを使ったRustアプリでこのコンボを動かすことができたけど、すべてコンパイルしてライブラリを読み込んでも、まだlibcの関数に依存しているライブラリがあったから、ちゃんと動かなかった。巨大なモノリシックなmuslバイナリにすべてをコンパイルしても、グラフィック関連の何かを見つけられなかった。結局、tinyなmuslアプリを残して、必要に応じてセカンダリプロセスでコンパニオンアプリを作ることにした(muslをコンパイルした目的はクロスプラットフォームのLinux互換性/安定性だから)。

Linuxで得られる最高のバイナリ互換性はWineを通じてだよ。 出典: https://blog.hiler.eu/win32-the-only-stable-abi/