Shell Scriptにバイナリ(例:tarball)を埋め込み、実行時にバイナリを取り出す方法
前書き:スクリプトサイズが大きい理由
プロプラエタリソフト(例:商用ソフト)のShell Scriptインストーラのサイズを見たら、数百MBだった事はありませんか?
そのような場合は、.deb/.rpmパッケージやtarball等のバイナリがShell Scriptに埋め込まれている可能性が高いです。このようなインストーラは、実行時にバイナリ部分だけを取り出してから、バイナリを操作します。
上記の作りにする理由は、インストーラを単一スクリプト(一つのファイル)にしたいからでしょう。インストール手順がシンプルになりますし、ユーザが操作ミスする可能性も減ります。また、”$ wget $INSTALLER_URL | sh”という形式でインストールもできます(複数のURLに対してwgetする必要がなくなります)。
本記事では、Shell Scriptにバイナリを埋め込み、実行時にバイナリを取り出す方法を紹介します。
検証環境
Ubuntu 21.04、Bash 5.1.4、tar (GNU tar) 1.34を使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
./oydmMMMMMMmdyo/. nao@nao :smMMMMMMMMMMMhs+:++yhs: ------- `omMMMMMMMMMMMN+` `odo` OS: Ubuntu Budgie 21.04 x86_64 /NMMMMMMMMMMMMN- `sN/ Host: B450 I AORUS PRO WIFI `hMMMMmhhmMMMMMMh sMh` Kernel: 5.11.0-31-generic .mMmo- /yMMMMm` `MMm. Uptime: 15 hours, 59 mins mN/ yMMMMMMMd- MMMm Packages: 2830 (dpkg), 11 (snap) oN- oMMMMMMMMMms+//+o+: :MMMMo Shell: bash 5.1.4 m/ +NMMMMMMMMMMMMMMMMm. :NMMMMm Resolution: 2560x1080 M` .NMMMMMMMMMMMMMMMNodMMMMMMM DE: Budgie 10.5.2 M- sMMMMMMMMMMMMMMMMMMMMMMMMM WM: Mutter(Budgie) mm` mMMMMMMMMMNdhhdNMMMMMMMMMm Theme: Yaru-dark [GTK2/3] oMm/ .dMMMMMMMMh: :dMMMMMMMo Icons: ubuntu-mono-dark [GTK2/3] mMMNyo/:/sdMMMMMMMMM+ sMMMMMm Terminal: tilix .mMMMMMMMMMMMMMMMMMs `NMMMm. CPU: AMD Ryzen 5 3400G (8) @ 3.700GHz `hMMMMMMMMMMM.oo+. `MMMh` GPU: AMD ATI 09:00.0 Picasso /NMMMMMMMMMo sMN/ Memory: 8887MiB / 30099MiB `omMMMMMMMMy. :dmo` :smMMMMMMMh+-` `.:ohs: ./oydmMMMMMMdhyo/. |
検証用のtarball作成
Shell Script(test.sh)を含むbinディレクトリを圧縮して、tarballを作成します。tarballの中に、他のファイル(例:パッケージや画像)を含んでも問題ありません。
1 2 3 4 5 6 7 8 9 10 11 |
$ ls bin/ test.sh $ cat bin/test.sh #/bin/bash echo "Hello World" $ tar cvfz bin.tar.gz bin $ ls bin bin.tar.gz |
tarballを埋め込んだShell Scriptの作り方
Shell Scriptの末尾にバイナリを埋め込み、バイナリを実行時に取り出す考え方は、以下の通りです。
- 編集時:Shell Scriptの末尾に、tarball開始位置の目印(検索用マーカ)を付与
- 編集時:Shell Scriptの末尾(検索用マーカの後)に、tarballを連結(結合)
- 実行時:検索用マーカの次の行からShell Script末尾までを抽出(抽出した内容=tarball)
以下、Shell Script(embed.sh)の例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#!/bin/bash # 本スクリプト名 SCRIPT_NAME="$0" # tarballの展開先 WORK_DIR=/tmp # 検索用マーカの次行(行番号) EMBEDDED_START="" # 検索用マーカの次行(行番号)を取得 function getEmbeddedLine() { # ・__EMBEDDED_MARKER__=検索用のマーカ(tarball開始行の一つ前) # ・NR=現在処理しているレコード(行)番号。 EMBEDDED_START=$(awk '/^__EMBEDDED_MARKER__/ { print NR + 1; exit 0; }' $SCRIPT_NAME) } # Shell Scriptに埋め込まれたtarballを展開する。 function extractTarball() { # base64コマンドでバイナリをテキストフォーマットにデコード。 tail -n +${EMBEDDED_START} $SCRIPT_NAME | base64 -d | tar -zpx -C $WORK_DIR } # tarball展開後の処理(任意の処理) function afterExtractTarball() { cd ${WORK_DIR} ./bin/test.sh } getEmbeddedLine extractTarball afterExtractTarball # 明示的に終了する。終了しない場合、後続のtarballまで処理が進んでしまう exit 0 __EMBEDDED_MARKER__ |
embed.shのextractTarball()内では、tarballの内容をbase64コマンドでデコードしています。デコードする事が重要ではなく、tarballをbase64エンコード(英数字、記号2つ、パディングによる表現)でShell Scriptに埋め込む事が目的です。
以下、base64エンコードでtarballをShell Scriptに埋め込む例です。
1 2 3 4 5 6 7 8 9 10 |
$ base64 bin.tar.gz >> embed.sh (注釈) 埋め込み後、 embeded.sh中の__EMBEDDED_MARKER__の次行にテキストが 来ていなければ調整してください。以下、埋め込み後の状態例です。 __EMBEDDED_MARKER__ H4sIAAAAAAAAA+3SQQqDMBRF0YxdRbDzmtTEbKE76FhbwYIYMHb/JpSCIx2FUnrP5AfyIQ9euudU icxU5JxNUzurtvNDaKP1pdGmNlqk7TikzR0seYWlnaUUU+t3947uf1QX+1/6sJzDkO2Nw/5r9+7f OtdYI1Q8ukZIlS3Rxp/3f6rSD+jaMBRFfx+8LK/9OHp58/P4KItvxwMAAAAAAAAAAAAAAACwYwW8 6GfKACgAAA== |
base64エンコーディングされていない状態、すなわちtarballをShell Scriptにそのまま連結した場合は、Shell Scriptの編集が出来なくなります。正確には、編集後に保存できますが、実行時に以下のエラーが出ます(保存時にtarball部分が意図しない形式に変換され、展開に失敗してしまう)。
1 2 3 |
gzip: stdin: unexpected end of file tar: Child returned status 1 tar: Error is not recoverable: exiting now |
また、Shell Scriptを編集していなくても、base64エンコーディングしていない場合はBashがtarballに含まれるNULL文字を省略する可能性があります(”warning: command substitution: ignored null byte in input”がtarball展開時に表示される事があります)。
NULL文字が省略された場合、tarball展開時にチェックサムチェックエラーが発生し、処理が継続できなくなります。
tarball結合後のShell Script実行例
embed.shは、tarballを/tmp以下に展開後、/tmp/bin/test.shを実行します。test.shは”Hello World”を表示し、終了します。
1 2 |
$ ./embed.sh Hello World |
おまけ:Shell Scriptをバイナリ化する方法
shc(Shell Script Compiler)でスクリプトをバイナリ化(暗号化)する方法
ロシア人と国際結婚した地方エンジニア。
小学〜大学院、就職の全てが新潟。
大学の専攻は福祉工学だったのに、エンジニアとして就職。新卒入社した会社ではOS開発や半導体露光装置ソフトを開発。現在はサーバーサイドエンジニアとして修行中。HR/HM(メタル)とロシア妻が好き。サイトに関するお問い合わせやTwitterフォローは、お気軽にどうぞ。
1件の返信
[…] この手法は、他言語でも一般的です。例えば、シェルスクリプトにバイナリを埋め込んで、インストーラを作る手法があります。 […]