GitHubじゃ!Pythonじゃ!

GitHubからPython関係の優良リポジトリを探したかったのじゃー、でも英語は出来ないから日本語で読むのじゃー、英語社会世知辛いのじゃー

intoli

exodus – コンテナなしで、Linuxバイナリとその依存関係の徹底的な再配置

投稿日:

コンテナなしで、Linuxバイナリとその依存関係の徹底的な再配置

出国

Exodusは、あるシステムから別のシステムにLinux ELFバイナリを簡単に再配置するツールです。 これは、マシン上でrootアクセス権を持たない場合や、特定のLinuxディストリビューションでパッケージが利用できない場合に便利です。 たとえば、CentOS 6.XやAmazon LinuxにはGoogle Chromearia2用のパッケージはありません。 サーバ指向のディストリビューションは、デスクトップディストリビューションよりもパッケージの制限があり、時代遅れになる傾向があります。そのため、ラップトップにソフトウェアをインストールしてリモートマシンに簡単にインストールできない場合があります。

出国では、あるコンピュータで動作しているソフトウェアを別のコンピュータに転送するのは簡単です。

exodus aria2c | ssh intoli.com

Exodusはバイナリのすべての依存関係を束ね、再配置されたリンカを直接呼び出す実行可能ファイルの静的にリンクされたラッパーをコンパイルし、リモートマシン上の~/.exodus/にバンドルをインストールします。 あなたはここで実際にそれを見ることができます。

目次

解決される問題

あるシステムから別のシステムに実行ファイルを単純にコピーすると、問題が発生する可能性が非常に高くなります。 Linuxで利用可能なバイナリのほとんどは、動的にリンクされ、いくつかの外部ライブラリファイルに依存しています。 依存関係がない場合、再配置されたバイナリを実行すると、このようなエラーが発生します。

aria2c: error while loading shared libraries: libgnutls.so.30: cannot open shared object file: No such file or directory

これらのライブラリを手動でインストールするか、それらを再配置し、 LD_LIBRARY_PATHをどこに置いても設定できますが、 ld-linuxリンカとglibcライブラリの場所はハードコードされていることがわかります。 物事は非常に迅速に再配置エラーの混乱に変わる可能性がありますが、

aria2c: relocation error: /lib/libpthread.so.0: symbol __getrlimit, version
GLIBC_PRIVATE not defined in file libc.so.6 with link time reference

セグメンテーション障害、

Segmentation fault (core dumped)

あなたが本当に不運な場合、欠けているリンカーのこの非常に混乱した症状。

$ ./aria2c
bash: ./aria2c: No such file or directory
$ ls -lha ./aria2c
-rwxr-xr-x 1 sangaline sangaline 2.8M Jan 30 21:18 ./aria2c

Exodusは、ハードコードされたRPATHライブラリパスをオーバーライドして直接再配置されたリンカを呼び出す小さな静的にリンクされたランチャバイナリをコンパイルすることにより、これらの問題を回避します。 再配置されたバイナリは、元のマシン上で実行されたリンカとライブラリと全く同じリンカで実行されます。

インストール

パッケージはpypiのパッケージからインストールできます。 次のexodus実行すると、現在のユーザー用にローカルにexodusがインストールされます。

pip install --user exodus-bundler

exodus実行ファイルを実行するには、 PATH変数に~/.local/bin/を追加する必要があります(まだ実行していない場合)。 これは、

export PATH="~/.local/bin/:${PATH}"

~/.bashrcファイルに~/.bashrcます。

オプション/推奨依存関係

バイナリをパッケージ化するマシンにgccmusl libcまたはdiet libcのいずれかをインストールすることを強くお勧めします。 存在する場合、これらの小さなCライブラリは、バンドルされたアプリケーションのための静的にリンクされた小さなランチャーをコンパイルするために使用されます。 同等のシェルスクリプトがフォールバックとして使用されますが、コンパイルされたランチャーと比較してかなりのオーバーヘッドが発生します。

使用法

コマンドラインインターフェイス

コマンドラインインターフェイスは、次のオプションをサポートしています。

usage: exodus [-h] [-c CHROOT_PATH] [-a DEPENDENCY] [-d] [--no-symlink FILE]
              [-o OUTPUT_FILE] [-q] [-r [NEW_NAME]] [--shell-launchers] [-t]
              [-v]
              EXECUTABLE [EXECUTABLE ...]

Bundle ELF binary executables with all of their runtime dependencies so that
they can be relocated to other systems with incompatible system libraries.

positional arguments:
  EXECUTABLE            One or more ELF executables to include in the exodus
                        bundle.

optional arguments:
  -h, --help            show this help message and exit
  -c CHROOT_PATH, --chroot CHROOT_PATH
                        A directory that will be treated as the root during
                        linking. Useful for testing and bundling extracted
                        packages that won run without a chroot. (default:
                        None)
  -a DEPENDENCY, --add DEPENDENCY, --additional-file DEPENDENCY
                        Specifies an additional file to include in the bundle,
                        useful for adding programatically loaded libraries and
                        other non-library dependencies. The argument can be
                        used more than once to include multiple files, and
                        directories will be included recursively. (default:
                        [])
  -d, --detect          Attempt to autodetect direct dependencies using the
                        system package manager. Operating system support is
                        limited. (default: False)
  --no-symlink FILE     Signifies that a file must not be symlinked to the
                        deduplicated data directory. This is useful if a file
                        looks for other resources based on paths relative its
                        own location. This is enabled by default for
                        executables. (default: [])
  -o OUTPUT_FILE, --output OUTPUT_FILE
                        The file where the bundle will be written out to. The
                        extension depends on the output type. The
                        "{{executables}}" and "{{extension}}" template strings
                        can be used in the provided filename. If omitted, the
                        output will go to stdout when it is being piped, or to
                        "./exodus-{{executables}}-bundle.{{extension}}"
                        otherwise. (default: None)
  -q, --quiet           Suppress warning messages. (default: False)
  -r [NEW_NAME], --rename [NEW_NAME]
                        Renames the binary executable(s) before packaging. The
                        order of rename tags must match the order of
                        positional executable arguments. (default: [])
  --shell-launchers     Force the use of shell launchers instead of attempting
                        to compile statically linked ones. (default: False)
  -t, --tarball         Creates a tarball for manual extraction instead of an
                        installation script. Note that this will change the
                        output extension from ".sh" to ".tgz". (default:
                        False)
  -v, --verbose         Output additional informational messages. (default:
                        False)

SSHでの配管

実行可能バンドルをリモートマシンにインストールする最も簡単な方法は、 exodusコマンドの出力をSSH経由でパイプすることです。 たとえば、次のコマンドを実行すると、 intoli.comサーバーにaria2cコマンドがインストールされます。

exodus aria2c | ssh intoli.com

これには、リモートユーザーのデフォルトシェルをbash (または互換シェル)に設定する必要があります。 cshを使用する場合は、このようにリモートサーバー上でbashをさらに実行する必要があります。

exodus aria2c | ssh intoli.com bash

明示的に余分なファイルを追加する

追加ファイルは、さまざまな方法でバンドルに追加できます。 インクルードしたい特定のファイルまたはディレクトリがある場合は、– --addオプションを使用できます。 たとえば、次のコマンドはnmapをバンドルし、 /usr/share/nmap内容をバンドルに含めます。

exodus --add /usr/share/nmap nmap

また、依存関係のリストをexodusパイプすることもできます。 これにより、標準のLinuxユーティリティを使用して、依存関係を見つけてフィルタリングすることができます。 次のコマンドシーケンスはfindを使用して、 /usr/share/nmap下にあるすべてのLuaスクリプトをインクルードしfind

find /usr/share/nmap/ -iname '*.lua' | exodus nmap

これらの2つの方法を併用することができ、– --addフラグは1つのコマンドで複数回使用することもできます。

余分なファイルの自動検出

余分な依存関係が必要ないかどうかわからない場合は、– --detectオプションを使用してシステムのパッケージマネージャーに問い合わせて、対応するパッケージにすべてのファイルを自動的に組み込むことができます。 ランニング

exodus --detect nmap

/usr/share/nmapの内容とそのマニュアルページと/usr/share/zenmap/内容が含まれています。 デフォルトの設定では--detectしないバイナリを再配置しようとすると、 --detectオプションを試してみてください。

straceの出力をexodusパイプすることもでき、アクセスされたすべてのファイルは自動的にインクルードされます。 これは、共有ライブラリがプログラムによってロードされる状況で特に便利ですが、特定のコマンドを実行するために必要なファイルを特定するためにも使用できます。 次のコマンドは、 nmapがデフォルトスクリプトのセットを実行している間にアクセスするすべてのファイルを決定します。

strace -f nmap --script default 127.0.0.1 2>&1 | exodus nmap

straceの出力は、 exodusによって解析され、すべてのファイルが含まれます。 一般的に、ライブラリ以外の依存関係を見つけるために--detectを使用する方がより堅牢ですが、プログラムがdlopen()を使用してプログラムでライブラリをロードする場合、 straceパターンは不可欠です。 また、プログラムがアクセスするファイルは、この方法に従うときにバンドルに含まれることに注意してください。 公開したくないファイルを誤って入れていないことを確認しないで、バンドルを配布しないでください。

バイナリの名前を変更する

--rename / -rオプションを使用すると、同じ名前の複数のバイナリを並行してインストールできます。 ローカルマシンに/bin/grep/usr/local/bin/grep 2種類のgrepバージョンがあるとします。 その場合、 -rフラグを使用すると、各バイナリにエイリアスを割り当てることができます。

exodus -r grep-1 -r grep-2 /bin/grep /usr/local/bin/grep

上記のコマンドは、2つのgrepバージョンを/bin/grepgrep-1/usr/local/bin/grepgrep-2と並列にインストールします。

手動抽出

--tarballオプションを指定すると、デフォルトのスクリプトの代わりに圧縮されたtarballを直接作成できます。 tarballを作成してそれをリモートサーバーにコピーし、それを~/custom-locationで展開すると、以下を実行できます。

# Create the tarball.
exodus --tarball aria2c --output aria2c.tgz

# Copy it to the remote server and remove it locally.
scp aria2c.tgz intoli.com:/tmp/aria2c.tgz
rm aria2c.tgz

# Make sure that `~/custom-location` exists.
ssh intoli.com "mkdir -p ~/custom-location"

# Extract the tarball remotely.
ssh intoli.com "tar --strip 1 -C ~/custom-location -zxf /tmp/aria2c.tgz"

# Remove the remote tarball.
ssh intoli.com "rm /tmp/aria2c.tgz"

さらに、 ~/custom-location/binをリモートサーバのPATH変数に追加する必要があります。 これは、リモートサーバの~/.bashrcに以下を追加することで可能になります。

export PATH="~/custom-location/bin:${PATH}"

ドッカー画像に追加する

Tarball形式の流出バンドルは、 ADD命令を使用してDockerイメージに簡単に組み込むことができます。 まず、– --tarballオプションを使用してバンドルを作成する必要があります

# Create and enter a directory for the Docker image.
mkdir jq
cd jq

# Generate the `exodus-jq-bundle.tgz` bundle.
exodus --tarball jq

次の内容のjqディレクトリ内にDockerfileファイルを作成します。

FROM scratch
ADD exodus-jq-bundle.tgz /opt/
ENTRYPOINT ["/opt/exodus/bin/jq"]

Dockerイメージは、

docker build -t jq .

jqはコンテナの内部で実行できます。

docker run jq

この単純なイメージには、 jqバイナリと依存関係のみが含まれますが、同じ方法でバンドルを既存のドッカーイメージに含めることができます。 たとえば、

ENV PATH="/opt/exodus/bin:${PATH}"
ADD exodus-jq-bundle.tgz /opt/

既存のDockerfileすると、 jqバイナリをコンテナ内で使用できるようになります。

使い方

エクソダスの仕組みには2つの主要な要素があります:

  1. バイナリの依存関係をすべて見つけてバンドルする。

  2. バイナリを起動すると、ターゲットマシン上のシステムライブラリから潜在的なやりとりがなく、適切な依存関係が使用されます。

最初のコンポーネントは実際にはかなりシンプルです。 LD_TRACE_LOADED_OBJECTS環境変数を1設定してld-linuxを起動すると、バイナリの解決済みライブラリの依存関係がすべて一覧表示されます。 たとえば、実行中

LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 /bin/grep

以下を出力します。

    linux-vdso.so.1 =>  (0x00007ffc7495c000)
    libpcre.so.0 => /lib64/libpcre.so.0 (0x00007f89b2f3e000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f89b2b7a000)
    libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f0e95e8c000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f89b3196000)

linus-vdso.so.1依存関係は、ユーザー空間にエクスポートされるカーネル空間ルーチンを指しますが、他の4つはgrepを実行するために必要なディスク上の共有ライブラリファイルです。 特に、これらの依存関係の1つは/lib64/ld-linux-x86-64.so.2リンカそのものです。 このファイルの場所は通常、ELFバイナリのINTERPヘッダにハードコードされており、プログラムを実行するときにカーネルによってリンカが呼び出されます。 今すぐ分かりますが、今の主なポイントは、リンカーを使ってバイナリの直接の依存関係を見つけることができることです。

もちろん、これらの直接の依存関係には、独自の依存関係が存在する可能性があります。 ライブラリの依存関係ごとにリンカを再度呼び出すのと同じ方法で、必要なすべての依存関係を繰り返し見つけることができます。 これはgrepでは実際には必要ではありませんが、exodusはあなたのための完全な依存関係を見つけるのに役立ちます。

すべての依存関係が見つかった後、exodusはそれらをバイナリとともに(通常は/opt/exodus/または~/.exodus )抽出できるtarballに入れます。 長いSHA-256ハッシュを8桁に切り詰めるために、 sed one-linerと組み合わせたツリーを使って、 grepバンドルの構造を調べることができます。 ランニング

alias truncate-hashes="sed -r 's/([a-f0-9]{8})[a-f0-9]{56}/\1.../g'"
tree ~/.exodus/ | truncate-hashes

grepバンドルに含まれるすべてのファイルとフォルダを表示します。

/home/sangaline/.exodus/
├── bin
│   └── grep -> ../bundles/3124cd96.../usr/bin/grep
├── bundles
│   └── 3124cd96...
│       ├── lib64
│       │   └── ld-linux-x86-64.so.2 -> ../../../data/dfd5de26...
│       └── usr
│           ├── bin
│           │   ├── grep
│           │   ├── grep-x -> ../../../../data/7477c1a7...
│           │   └── linker-dfd5de26...
│           └── lib
│               ├── libc.so.6 -> ../../../../data/6d0e1d45...
│               ├── libpcre.so.1 -> ../../../../data/a0862ebc...
│               └── libpthread.so.0 -> ../../../../data/85cb56a5...
└── data
    ├── 6d0e1d45...
    ├── 7477c1a7...
    ├── 85cb56a5...
    ├── a0862ebc...
    └── dfd5de26...

8 directories, 13 files

~/.exodus/bin~/.exodus/ 、およびdataには3つの最上位ディレクトリがあることがわかりdata dataディレクトリから始めて、これらをアルファベット順の逆順に説明しましょう。

dataディレクトリには、SHA-256ハッシュに対応する名前のバンドルからの実際のファイルが含まれています。 これは、同じファイル名を持つ複数のバージョンのファイルを互いに上書きすることなくdataディレクトリに抽出できるようにするdataです。 一方、同じ内容のファイルは互いに上書きされます。 これにより、同じファイルが異なるバンドルまたはディレクトリに存在する場合でも、同じデータの複数のコピーを格納する必要がなくなります。

次に、SHA-256ハッシュを名前として持つサブフォルダでいっぱいのbundlesディレクトリがあります。 今回のハッシュは、バンドルに含まれるすべてのディレクトリ構造とコンテンツの組み合わせに基づいて決定されます。 ハッシュはバンドルに固有のフィンガープリントを提供し、複数のバンドルをディレクトリー内容の混在なしに抽出できるようにします。

各バンドルサブディレクトリの内側には、ホストマシン上のバンドルコンテンツの元のディレクトリ構造が反映されています。 この特定のgrepバンドルには、 lib64usr/bin 、およびusr/libディレクトリがあります。 より複雑なバンドルには、 /usr/share/opt/local 、ユーザーのホームディレクトリ、またはシステムの実際の場所の追加ファイルが含まれます( --addおよび--detectオプションを参照)。 lib64usr/lib両方のファイルは、トップレベルのdata/ディレクトリにある実際のライブラリファイルへのシンボリックリンクで構成されています。 usr/binディレクトリはもう少し複雑です。

grepファイルは実際にはオリジナルのgrepバイナリではありませんが、 “ランチャー”と呼ばれるexodus構造の特別な実行可能ファイルです。 ランチャーは、リンカーを起動し、システムライブラリが使用されず、互換性がないために問題が発生することなくオリジナルのバイナリを実行できるように、ライブラリ検索パスをオーバーライドする小さなプログラムです。 この場合のlinker-dfd5de26...linker-dfd5de26...ファイルです。 これは実行中の実行可能ファイルに対してリソースパスを解決できるように、同じディレクトリにあります。 最後に、 grep-x symlinkは、トップレベルのdata/ディレクトリ(これはリンカが解釈するELFファイル)にバンドルされ抽出された実際のgrepバイナリを指しています。

Cコンパイラとmusl libcまたはdiet libcが利用可能な場合、exodusは静的にリンクされたバイナリランチャをコンパイルします。 これらのいずれも存在しない場合は、シェルスクリプトを使用してランチャーのタスクを実行することに戻ります。 バイナリランチャに比べてオーバーヘッドが少し追加されますが、ランチャの動作を理解するのに役立ちます。 例えば、 grep-launcherのシェルスクリプト版です。

#! /bin/bash

current_directory="$(dirname "$(readlink -f "$0")")"
executable="${current_directory}/./grep-x"
library_path="../../lib64:../lib64:../../lib:../lib:../../lib32:../lib32"
library_path="${current_directory}/${library_path//:/:${current_directory}/}"
linker="${current_directory}/./linker-dfd5de2638cea087685b67786050dcdc33aac7b67f5f8c2753b7da538517880a"
exec "${linker}" --library-path "${library_path}" --inhibit-rpath "" "${executable}" "$@"

ランチャーは、まず、すべてのLD_LIBRARY_PATHディレクトリ、実行可能ファイル、およびリンカの完全なパスを独自の場所に基づいて構築することがわかります。 その後、適切なライブラリディレクトリを検索し、ハードコードされたRPATH無視し、渡されたコマンドライン引数でバイナリを実行できるように、一連の引数を使用してリンカを実行します。 これは、バイナリのINTERPRPATHを変更するINTERPような目的にも役立ちますが、リンカとライブラリの両方の場所を相対的な位置だけに基づいて指定することもできます。 これは、 ~/.exodus/opt/exodus/などの内部バンドル構造が保存されている限り、外部バンドルが~/.exodus/opt/exodus/などで抽出されるようにします。

アルファベット順の逆順を続けると、最終的に最上位のbinディレクトリに移動します。 最上位のbinディレクトリは、対応するランチャへのバイナリ名のシンボリックリンクで構成されています。 これにより、移行されたexodusバイナリをアクセス可能にするために、ユーザーのPATH変数に単一のディレクトリを追加することができます。 たとえば、 ~/.bashrcファイルにexport PATH="~/.exodus/bin:${PATH}"を追加すると、これらのエントリポイントがすべてユーザのPATH追加され、フルパスを指定せずに実行できるようになります。

既知の制限事項

exodusでアプリケーションをバンドルすると失敗するいくつかのシナリオがあります。 これらの多くは私たちが取り組んでいるものであり、将来的には改善することを望んでいますが、基本的には設計上のものであり、変更する可能性は低いです。 ここでは、exodusが実行可能ファイルを正常に再配置できない状況の概要を見ることができます。

  • 非ELFバイナリ – Exodusは現在、完全にバンドルされているELFバイナリのみをサポートしています。 シェルスクリプトのように解釈された実行可能ファイルはバンドルに含めることができますが、shebangインタプリタの指示は変更されません。 これは一般に、 bashpythonperlなどのシステムバージョンを使用して解釈されることを意味します。 エクソダスが解決しようとしている問題は、主にELFバイナリのダイナミックリンクを中心に行われているため、近い将来変わることはありません。
  • 互換性のないCPUアーキテクチャー – あるCPUアーキテクチャー用にコンパイルされたバイナリーは、通常、別のアーキテクチャーのCPU上では実行できません。 これにはいくつかの例外があります。たとえば、x64プロセッサはx86命令セットと下位互換性がありますが、x64バイナリをx86またはARMマシンに移行することはできません。 これを行うには、プロセッサのエミュレーションが必要です。これは間違いなく、プロジェクトの範囲外です。 あなたがこの問題の解決策を探しているなら、 QEMUをチェックしたいかもしれません。
  • 互換性のないGlibcとカーネルバージョン – glibcがコンパイルされると、特定のカーネルバージョンを対象にするように設定されます。 glibcのターゲットバージョンより古いカーネルバージョンを使用するシステムでglibcに対してコンパイルされたソフトウェアを実行しようとすると、 FATAL: kernel too oldエラーが発生します。 バイナリでサポートされている最も古いカーネルバージョンを確認するには、 file /path/to/binary実行しfile /path/to/binary 出力には、バイナリと互換性がある最も古いカーネルバージョンを示すfor GNU/Linux 2.6.32ような文字列を含める必要があります。 この問題を回避するには、古いカーネルをサポートしているオペレーティングシステムイメージ(古いバージョンのオペレーティングシステムなど)を使用して、Dockerイメージで流出バ​​ンドルを作成することができます。
  • ドライバ依存ライブラリ – 他のアプリケーションバンドルとは異なり、バンドルの作成時に必要なすべてのライブラリを組み込み、転送先のマシンのシステムライブラリから転送されたバイナリを完全に分離することを目指しています。 つまり、特定のハードウェアドライバ用にコンパイルされたライブラリは、同じドライバを持つマシン上でのみ動作します。 これの重要な例は、 libGLX_indirect.soライブラリですlibGLX_mesa.soまたはlibGLX_nvidia.soリンクできるのは、特定のシステムで使用されているグラフィックカードドライバによって異なります。 ソースマシンでローカルに利用できないバンドルの依存関係は、基本的には、流出が意図する範囲外であり、これは決して変更されません。

開発

開発環境は、以下を実行することで設定できます。

# Clone the repository.
git clone git@github.com:intoli/exodus.git
cd exodus

# Create and enter a virtualenv.
virtualenv .env
. .env/bin/activate

# Install the development requirements.
pip install -r development-requirements.txt

# Install the exodus package in editable mode.
pip install -e .

テストスイートはtoxを使用して実行できます。

tox

貢献する

寄付は歓迎ですが、 CONTRIBUTING.mdに記載されている寄稿者のガイドラインに従ってください。

ライセンス

ExodusはBSD 2条項ライセンスのもとで使用許諾され、 Intoli、LLCの著作権です







-intoli
-, , , , , , , , ,

執筆者: