1001001

73。CTFのWrite-upや技術的な備忘録を書きとめたいです。

PyInstallerで32bitシステム用のELFを作る

はじめに

64bitアーキテクチャLinux上にて、Dockerで32bit用コンテナを作成し、その上でPyInstallerを使って32bitシステム用ELFを作った際の備忘録です。

背景

Linux環境で動かしたいプログラムをPythonで書いていた。対象環境に必要なライブラリがなくても動くように、PyInstallerとstaticxを使ってonefileに固めて利用するつもりだった。
しかし、対象のLinux環境が32bit環境であることが後から分かった。開発環境は64bit環境である。(なんでこんな環境なのかは割愛します。)
64bit環境において、PyInstallerで32bit用ELFファイルを作ることは可能かつ方法は単純明快で、Python自体がi386向けのものであれば、インストールされるPyInstallerもi386向けとなり、生成されるELFも32bit用となるらしい。

問題

例えば、メインで使っているPython3がPython3.8であれば、あえてPython3.7:i386をインストールし、シンボリックリンクでpython3-64, python3-32などと区別すれば、64bit環境で32bit用Pythonが使えるらしい。
とはいえ、こんなこともなければi386用のPythonなんて滅多に使わないので、これのために環境を汚したくなかった。

環境汚したくないけど新しくVM立てるのは面倒臭い。

解決方針

dockerコンテナ上に一時的にビルドに必要な環境を構築して、終わったらコンテナを消してスッキリ、という方法を取ることにした。
docker searchを使ってi386で検索すると、i386Pythonが使えるイメージが見つかったので、それを使うことにした。

docker search i386                                            
NAME                               DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
balenalib/i386-ubuntu-python       This image is part of the balena.io base ima…   0
(snip)

最終的な解決策

以下のような手順で実行することで、PyInstallerを使って32bit用ELFを作ることができた。手動で1つずつ実行しても同じことができるはず。dockerfile化する方がスマートかもしれない。

#!/bin/bash

container_name='pyinstaller32'
docker rm $container_name # 前のコンテナが残っていたら消す
docker run -dit --name $container_name balenalib/i386-ubuntu-python /bin/bash 

# 1回目は署名キー取得エラーになってしまうため2回実行(解決策わからず)

docker exec $container_name bash -c 'apt update'
docker exec $container_name bash -c 'apt update'


docker exec $container_name bash -c 'apt install -y <必要な依存関係>'

# sconsはstaticxに必要
docker exec $container_name bash -c 'pip install cython wheel scons'
docker exec $container_name bash -c 'pip install <必要な依存関係>'

# pyinstaller+staticxに必要
docker exec $container_name bash -c 'pip install pyinstaller staticx patchelf-wrapper'

# docker側にPythonファイルを渡してELFを生成して、ホスト側に持ってくる
docker cp ./myapp.py $container_name:/tmp
docker exec $container_name bash -c 'cd /tmp && pyinstaller --onefile myapp.py && staticx /tmp/dist/myapp myapp-static'
docker cp $container_name:/tmp/myapp-static ./

docker stop $container_name

少し中身を説明。解消法がわからずに冗長になっている部分などあるため。

コンテナは毎回削除して1から構築し直すようにしているがとても時間がかかるので、何度も実行するなら構築後のイメージを固めておくかいちいちコンテナを消さないようにした方が良さそう。

apt update時に署名キーのエラーが出てしまうが、エラーになるのは1回目だけだったので2回実行するようにした。(冗長だが解消法わからず。)

sconsはstaticxに必要かつ、staticxと一緒のタイミングでinstallしようとするとエラーになるので、前段階でinstallしている。

cythonとwheelは、私が作っていたプログラムで必要になったものなのでなくても問題ないかもしれない。

ホストとコンテナ間のファイルのやりとりはdocker cpを使っている。ホスト側のディレクトリをマウントする方法もありそう。その方がスムーズかもしれない。

これを実行すると、良い感じにonefileで動くELFを作ってくれます。

余談

staticxってなに?

PyInstallerだけだと動的リンクになる。動的リンクされるはずの共有オブジェクトが対象の環境に存在しない場合は動かないので、onefileあれば動くという要件に反する。
それを解消するためにstaticxを利用する。
以下の記事がlddコマンドで動的リンクを確認し、動的な依存関係がなくなる様子を示していてわかりやすい。
【python】Pyinstaller でバイナリ化した実行ファイルにダイナミックリンクしているものがあったので完全に静的なライブラリ化をめざす方法 - ものづくりのブログ

docker-pyinstallerについて

pyinstaller環境をdockerで用意して使いたいという内容を調べていると、docker-pyinstallerというのに行き着いた。日本語の紹介記事もちらほら。

Docker環境のPyInstallerでキレイにExe化する #Python - Qiita

最終コミットが4年前とかなり古いが、2022年11月時点で最新化を試みている人もいた。
docker-pyinstaller を雑に最新にする

32bitELFを作ろうとしているわけではないが、リポジトリを見ると32bit環境用のものもあり有用かと思われたが、やはりメンテナンスされてないのは気がかりなので、これは使わずに自分で試行錯誤してみることにした。

balenalib/i386-ubuntu-pythonってなんなの

docker hubで調べると、Verified Publisherなので問題はなさそう。以下を見ると、IOT向けのイメージを作成して配布しているようなPublisherみたい。
Balena base images - Balena Documentation

まとめ

PyInstaller+staticxは便利。32bitシステム用のELFも作れる。でも別の言語でかけるなら最初からそうした方が良い、きっと。