コグノスケ


2024年2月2日

SSLに対応

目次: 自宅サーバー

今更感がありますが、このサイトもSSL対応にしました。ブラウザに「安全ではない接続です!」と怒られなくなりました。まあ、それだけですね……。


SSL対応

SSL証明書は年990円のJPRSドメイン認証型SSLにしました。とてもお安いです。購入も簡単で、さくらインターネットからボタン1つで買えました。買った後にLet's Encryptを使えばタダだったのでは?と気づきましたけど、気にしないことにします。

日記システムも直した

サーバー側の設定以外に日記システム側も改修が必要です。一部のリンクや画像のURL出力などでhttpに決め打ちしていた部分があって、http経由/https経由でアクセスするファイルが混在していました。ブラウザはこのようなサイトを表示させると「完全に安全ではない!」と警告してくれます。良く見てます。ありがたい。

さらに面倒なことに自宅に置いているミラーサーバーはSSL非対応なので、手あたり次第httpを全部httpsに置換するとミラーサーバーの表示が崩壊してしまいます。サイトごとにhttp経由/https経由を設定できるように変更しましょう。

編集者:すずき(2024/02/02 01:54)

コメント一覧

  • hdkさん(2024/02/02 08:54)
    さくらのレンタルサーバの設定でLet's Encryptを有効にしただけでほとんど何もした記憶がない... しかし最近のデスクトップ版Firefoxは http:// と指定しても https:// で開くんですね。Android版Firefoxはプロトコル省略すると未だに黙って http:// なのに。
  • すずきさん(2024/02/02 18:17)
    サーバー側の設定はとても簡単でした。ちょっと苦戦したのはこの日記システムがいまいちなのと、ミラーサーバーを置いているのが原因ですね。

    そういえばテストしていて、ChromeとFirefoxでURLのプロトコル指定の挙動が違うことに気づきました。
    Chrome: httpは表示する、httpsは表示しない
    Firefox: httpは表示しない、httpsは表示する
open/close この記事にコメントする



2024年2月7日

複数の音声ファイルのラウドネスを統一したい

PCやデジタル音楽プレーヤーで音楽を聞いていると、曲によって音量の大小が激しく違っていて鬱陶しいです。片や何も聞こえず、片やリスナーの耳を壊す勢いの爆音で同じ音量で聞き続けるのが困難です。

人が感じる音の大きさをラウドネス(Loudness)と呼びますけども、全ての曲のラウドネスを一定レベルに修正すれば、極端な沈黙や爆音を避け同じくらいの音量で楽しめるはずです。ラウドネスの修正を行うツールはffmpeg-normalizeが簡単でわかりやすかったです。Pythonで実装されていて内部ではffmpegのloudnormフィルターを使っているそうです。

インストール方法はpipを実行するだけで簡単です。ローカルのPython環境に影響が及ぶのが嫌な方はvenv(venv - 仮想環境の作成 - Python 3.12 ドキュメント)を作成し、仮想環境にインストールすると良いです。

ffmpeg-nomalizeのインストール
$ python3 -m venv ./venv
$ ./venv/bin/activate
$ pip install ffmpeg-normalize

基本的な使い方はffmpeg-normalize (入力ファイル名) -o (出力ファイル名)ですが、デフォルトパラメータがターゲットレベル-23LUFS、サンプリングレート192kHz、Matroska(拡張子.mkv)形式となっています。サンプリングレートとかは好み次第ですけど、ターゲットレベル-23LUFSはすっっっごい音が小さいです……。最初試したときに、音が消えてしまうぞ?おかしいなあ??と悩みました。

これらの設定を変更するときは、-tオプションでターゲットレベルを、-arオプションでサンプリングレートを、-extオプションで出力形式を変更してください。

2パス処理を諦めないで

基本的にffmpeg-normalizeは2パス(1回目でファイル全体を解析、2回目でノーマライズ処理)でノーマライズを試みますが、曲のラウドネスの幅がターゲットLRA(Loudness Range、曲全体のラウドネスの幅、デフォルトは7.0LUFS)を超えると2パスのノーマライズを諦めてしまいます。

LRAを超えたときの警告メッセージ
$ ffmpeg-normalize test.mp3

WARNING: Output directory 'normalized' does not exist, will create

★★↑出力ディレクトリを指定しないとnormalizedというディレクトリを自動的に作る


WARNING: Input file had loudness range of 7.8. This is larger than the loudness range target (7.0). Normalization will revert to dynamic mode. Choose a higher target loudness range if you want linear normalization. Alternatively, use the --keep-loudness-range-target or --keep-lra-above-loudness-range-target option to keep the target loudness range from the input.

★★↑2パスを諦めたよ、というWARNING


WARNING: In dynamic mode, the sample rate will automatically be set to 192 kHz by the loudnorm filter. Specify -ar/--sample-rate to override it.

★★↑2パスを諦めてdynamicモードになると192kHzで再エンコードし始める

WARNINGを見ると、この挙動を回避したければ--keep-lra-above-loudness-range-targetオプションを付けてねと書いてありますが、オプションによる悪影響の有無が良くわかりません。変換後の音楽をいくつか確認した限りでは、破綻やクリッピングが発生している様子はなさそうです、たぶん付けておいて良いのかな……??

複数ファイルの扱い

複数のファイルを処理する際はシェルスクリプトを組んだほうが良いと思います。ffmpeg-normalizeに複数ファイルを渡すと1つのディレクトリ(デフォルトではnormalizedという名前のディレクトリ)に出力するからです。

例えばアルバムごとにディレクトリを分けて音声ファイルを格納している場合に困ります。ノーマライズ後のファイルが1つのディレクトリにまとめられてしまうと、再度アルバムのディレクトリに分配しなければならず面倒です。

複数ファイルを処理するワンライナースクリプト

for i in */*.flac; \
  do echo $i -----; \
  ffmpeg-normalize -t -14 --keep-lra-above-loudness-range-target \
    -ar 48000 -ext wav "$i" \
    -o "`echo $i | sed -e 's/flac\$/wav/g'`"; \
done

全アルバムディレクトリのflacを、ノーマライズしたwavに変換するならこんな感じです。適当スクリプトなので入力と出力を同じファイル名にできません。一度別名を経由して元の名前にリネームするように改造していただければと思います。

編集者:すずき(2024/02/12 03:03)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月9日

気難しいM5Stamp C3のAutomatic Bootloader

目次: Arduino

M5Stamp C3の接続を変更したところ、Arduino Sketchが書き込めなくなってしまいました。こんなエラーで怒られます。

M5Stamp C3にてArduino Sketch書き込みエラーのログ
Sketch uses 1003156 bytes (76%) of program storage space. Maximum is 1310720 bytes.
Global variables use 38676 bytes (11%) of dynamic memory, leaving 289004 bytes for local variables. Maximum is 327680 bytes.
esptool.py v4.5.1
Serial port COM3
Connecting......................................

A fatal error occurred: Failed to connect to ESP32-C3: Wrong boot mode detected (0xc)! The chip needs to be in download mode.
For troubleshooting steps visit: https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html
Failed uploading: uploading error: exit status 2

回避策としてはboot modeを変えれば良いです。ジャンパーケーブルなどでGPIO 9をLowにして(隣のGNDと繋ぐと簡単)リセットします。リセットしたらジャンパーケーブルは外しましょう。GPIO 9はPull-upされているので、未接続だと勝手にHighになります。


M5Stamp C3のGPIO 9の位置

リセット後のシリアルに下記のようにROM serial bootloader for esptool Modeに入ったと表示されれば成功で、Arduinoなどから書き込みができる状態です。

ROM serial bootloader for esptool Modeのときのシリアル出力
ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x1 (POWERON),boot:0x4 (DOWNLOAD(USB/UART0/1))
waiting for download

これでArduino Sketchが書き込みできますが、いちいちジャンパーケーブル挿してリセットなどやっていられません。面倒臭すぎます。そもそも今までそんなことしていませんでした。

Automatic Bootloader

M5Stamp C3が採用しているEspressif ESP32-C3というSoCの書き込みツールesptoolは、Automatic Bootloaderといって先ほど紹介した「GPIO 9をLowにしてリセット」という操作を不要にする機能、つまりDownload Modeに勝手に切り替えてSketchを書き込んで、Normal Bootしてくれる素敵な機能があります(Boot Mode Selection - ESP32-C3 - esptool.py latest documentation)。

とても便利なAutomatic Bootloaderですが、USB接続のトポロジーに敏感で結構気難しいやつだということがわかりました。私の環境ではUSBハブを介して接続すると発動しなくなります。なんでだー。

  • CPU: AMD Ryzen 7 2700
  • マザーボード: ASUS ROG STRIX B450-F GAMING
  • OS: Windows 10 Pro 22H2
  • USBポート: フロントパネル用ポート(オンボードコネクタから引き出すタイプ)

Automatic Bootloaderがうまく行くパターンはRootハブに直接接続したときです。接続例を示します。


Rootハブに直接M5Stamp C3を接続

うまく行くとログはこんな感じになります。

Automatic Bootloaderが正常動作したログ
Sketch uses 1003156 bytes (76%) of program storage space. Maximum is 1310720 bytes.
Global variables use 38676 bytes (11%) of dynamic memory, leaving 289004 bytes for local variables. Maximum is 327680 bytes.
esptool.py v4.5.1
Serial port COM3
Connecting....
Chip is ESP32-C3 (revision v0.3)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 84:f7:03:27:f8:14
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Changed.
Configuring flash size...
(...以下略...)

うまく行かないパターンはUSBハブを介して接続したときです。接続例を示します。


USBハブを経由してM5Stamp C3を接続

ハブに依存するかもしれないと思い、USBハブはUSB 2.0とUSB 3.0のものを試しましたがどちらもダメでした。嫌な感じの挙動です。バグっぽいなー。

複数台接続はOK

もう一つの可能性として、M5Stamp C3を複数台繋ぐこと自体がだめな可能性を考えました。が、こちらは特に問題ありませんでした。せめてもの救いですね。

1台でも2台でも、USBハブを入れるとAutomatic Bootloaderが無効になってしまうようです。接続したい台数は3台なんですが、フロントポートは2つしかありません。困ったね……。

編集者:すずき(2024/02/10 23:00)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月12日

M5Stamp C3をBluetooth LE子機にする

目次: Arduino

M5Stamp C3はWi-FiとBluetooth LEが使えます。私はBluetooth規格は全く知らないですが、Arduino-ESP32にはBLEライブラリがあってお手軽にBluetooth LEによる通信ができます。Arduino-ESP32とは何者か?については以前の日記(2024年1月7日の日記参照)をご覧ください。

ライブラリがあるとはいえゼロからコードを書くのは結構大変です。しかし世の中にはArduino-ESP32 BLEのサンプル(ESP_32_BLE_Arduino)を公開している素敵なリポジトリがありまして、これを元に改造すると簡単です。ありがたいですね。

動作確認にはスマホを使います。Bluetooth LEに対応しているアプリならなんでも良いです。参考までに私はSerial Bluetooth Terminalというアプリ(Serial Bluetooth Terminal - Google Play)で確認しています。iPhoneは持ってないのでわかりません、アプリがあると思いますので頑張って探してください……。

TXとRXの割当

Arduino-ESP32 BLEのサンプル側はNordic nUARTのCharacteristic GATTのUUIDを使っていて、こんな設定になっています。

  • 6E400001-B5A3-F393-E0A9-E50E24DCCA9E: Service GATT UUID
  • 6E400002-B5A3-F393-E0A9-E50E24DCCA9E: RX(受信用、アプリから見たらWritable)Characteristic GATT UUID
  • 6E400003-B5A3-F393-E0A9-E50E24DCCA9E: TX(送信用、アプリから見たらReadable)Characteristic GATT UUID

TXとRXがどちらから見た方向なのか混乱しますが、Serial Bluetooth TerminalのPredefined設定は賢いので間違えてTXとRXの定義が逆になっていても通信できてしまいます。

通信できれば問題ないとはいえ、正解を調べておいて損はないでしょう。Nordic Semiconductorのドキュメント(UART/Serial Port Emulation over BLE - Nordic Semiconductor Infocenter)を見ると、下記の記述があります。

RX/TXとUUIDについての説明(Nordic Semiconductorのドキュメントより一部抜粋)
This service exposes two characteristics: one for transmitting and one for receiving (as seen from the peer).

  - RX Characteristic (UUID: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E)
    The peer can send data to the device by writing to the RX Characteristic of the service.
    ATT Write Request or ATT Write Command can be used. The received data is sent on the UART interface.
  - TX Characteristic (UUID: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E)
    If the peer has enabled notifications for the TX Characteristic, the application can send data to the
    peer as notifications. The application will transmit all data received over UART as notifications.

RX Characteristic(UUID: 6E400002)の説明はsend data to deviceとあります。すなわちアプリ側にとってTXでありサンプル側にとってはRXとなるはずです。TXはその逆ですね。

  • 6E400002-B5A3-F393-E0A9-E50E24DCCA9E: サンプルのRX Characteristic GATT UUID
  • 6E400003-B5A3-F393-E0A9-E50E24DCCA9E: サンプルのTX Characteristic GATT UUID
  • 6E400003-B5A3-F393-E0A9-E50E24DCCA9E: アプリから見たらRX方向
  • 6E400002-B5A3-F393-E0A9-E50E24DCCA9E: アプリから見たらTX方向

とするのが正しそうですね。

編集者:すずき(2024/02/20 02:14)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月14日

LinuxでBluetooth LEデバイスをスキャンする

目次: Arduino

毎回BlueZというかhcitoolの使い方を忘れてググっているのでメモしておきます。

Bluetooth LEデバイスのスキャンはhcitool lescanです。Bluetoothアダプタが2つ以上あるなら-iオプション(-i hci0など)でアダプタを指定できます。lescanは別の機器Crtl+Cなどで止めるまで発見したBluetooth LEデバイスのMACアドレスが延々と流れ続けます。

デバイス情報を見る場合はhcitool leinfoです。

hcitool leinfoの出力例
# hcitool leinfo (xx:xx:xx:xx:xx:xx)

Requesting information ...
        Handle: 55 (0x0037)
        LMP Version: 5.0 (0x9) LMP Subversion: 0x16
        Manufacturer: Espressif Incorporated ( ?鑫信息科技(上海)有限公司 ) (741)
        Features: 0x01 0xf9 0x01 0x00 0x00 0x00 0x00 0x00

M5Stamp C3のMACアドレスを指定して実行するとこんな情報が表示されました。SoCのESP32シリーズの製造元はEspressifなので合ってそうですね。

編集者:すずき(2024/02/20 02:11)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月16日

JavaとM5Stamp C3とBluetooth LE - 導入編

目次: Arduino

以前の日記(2024年2月12日の日記参照)でM5Stamp C3をBluetooth LEデバイスにして、スマホのアプリとお話ができるようになりました。とくれば、次はM5Stamp C3とLinux PCもしくはRaspberry PiなどのLinux SBCとお話したくなります。

LinuxでBluetoothを扱う場合は、BlueZという実装を使います。BlueZはDBusというマシン内のイベントを配信するシステムに依存しており、軽く調べた感じではC言語からDBusを制御するのは結構大変そうです。Pythonだと簡単らしいですがGUIも実装したいんですよね。ちょっと悩みましたが久しぶりにJavaで書くことにしました。

素晴らしいことにJavaからBlueZを制御する場合、bluez-dbusというbluez-dbusのリポジトリ)求めていた用途そのもののライブラリがあります。DBus制御は同作者さんのdbus-java(dbus-javaのリポジトリ)を使うようです。Unixソケットを使うライブラリにも依存しているようですね。

ちなみにbluez-dbusはJavaで実装されているものの、Javaの外の世界にOSやシステム依存(DBus、Unixソケットの存在に依存)があるため、Linuxでしか動作しないことに注意が必要です。用途次第ではWindowsやMacOSで動作しないと困るのでしょうけど、私が今回想定している用途ではLinux専用で問題ありません。ありがたくbluez-dbusを使わせていただきます。

bluez-dbusのドキュメント

ドキュメント(bluez-dbusのJavaDoc)を自分でビルドするのは大変なので、javadoc.ioを参照(Overview (bluez-dbus 0.1.4 API))すると楽です。

ちなみにjavadoc.ioって何?と思ったらMavenに収容されているリポジトリのJavaDocを生成して公開してくれているサイトなのだそうです。ありがとうMavenプロジェクト、とても便利です。

編集者:すずき(2024/02/20 02:22)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月18日

JavaとM5Stamp C3とBluetooth LE - ビルド準備編

目次: Arduino

M5Stamp C3をBluetooth LEデバイスにして、Linux PCもしくはRaspberry PiなどのLinux SBCとお話する取り組みの続編です。

今回はbluez-dbusを使ったJavaアプリのビルド準備をします。目的は依存するライブラリが全て含まれた(つまり単独で実行可能な)JARファイルを生成することです。ビルドシステムは昔はAntでしたが今はMavenがメジャーでしょうか?Mavenは依存関係の解消が楽で良いですね。

アプリ開発そのものはテキストエディタでもIntelliJ IDEAやEclipseなどのIDEでも、お好きなツールを使っていただければ良いかと思います。

基本的な設定

ビルドを試すだけならJavaのソースコードは不要で、プロジェクトのディレクトリにpom.xmlを作成するだけで試せます。今回作成するアプリはbluez-dbusに依存するので依存設定を記述するdependenciesタグはこんな感じにします。

pom.xmlのdependenciesタグ周辺(2つに分ける書き方)

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.github.hypfvieh</groupId>
                <artifactId>bluez-dbus</artifactId>
                <version>0.1.4</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.github.hypfvieh</groupId>
            <artifactId>bluez-dbus</artifactId>
        </dependency>
    </dependencies>

Mavenでは記述を2つに分けるのが推奨のようです、例えば依存関係の記述ならdependenciesタグとdependencyManagementタグに分けます。特に大きなプロジェクトだと複数のpom.xmlに渡ってビルドの設定を記述することが増え、各pom.xmlのdependenciesタグの中でバージョンなどを何度も書くと間違うし更新漏れが発生します。トップレベルにあるpom.xmlのdependencyManagementタグの中でバージョンなどを定義し、子レベルにあるpom.xmlではdependenciesタグから参照する使い方が良いのだとか。

今回はpom.xmlが1ファイルしかないので分けなくても問題ありません。分けない方の例も挙げましょう。プラグイン設定も同様にpluginsタグとpluginMnagementタグの2つに分けますが、ここではあえて1つのpluginsタグに全て書きます。

pom.xmlのpluginsタグ周辺(2つに分けない書き方)

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifest>
                                    <mainClass>main.java.Main</mainClass>
                                </manifest>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>

上記に出てくるmaven-assembly-pluginは依存ライブラリを全て含んだJARファイルを生成できるプラグインです。jar-with-dependenciesという定義済みのdescriptorRefを指定すると良いそうです。階層構造といい変な名前といい何かの呪文のようです、とても覚えられません。

テストの設定などを除けば、基本的には依存ライブラリ設定とプラグイン設定で最低限の役目は果たすはずです。ですがビルドしてみたらめっちゃハマりました。Mavenつらい……。

ハマりポイントその1 - maven-assembly-pluginのバージョン

バージョンを特に指定しない場合、現状maven-assembly-pluginは2.2-beta-5というバージョンになります。が、このバージョンは動作がおかしいようでmvn packageがビルドエラーになります。

mvn package実行時のエラー
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.211 s
[INFO] Finished at: 2024-02-19T19:40:23+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-assembly-plugin:2.2-beta-5:single (default) on project bluetooth-test: Failed to create assembly: Error creating assembly archive jar-with-dependencies: A zip file cannot include itself -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

エラーの原因が全くわかりません。Mavenのエラーメッセージは全般的に何が言いたいのかわからず、pom.xmlのデバッグは困難を極めます。色々調べたり試した限りでは、プラグインのバージョンを2.1に固定するとこの現象は発生しなくなることがわかりました。何だそりゃ?どういうことだ……??

maven-assembly-pluginのバージョンを2.1に固定

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.1</version>    ★★バージョン2.1に固定した★★
                <executions>
                    <execution>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifest>
                                    <mainClass>main.java.Main</mainClass>
                                </manifest>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

本当に意味がわからないです。Mavenつらすぎる。

ハマりポイントその2 - 2回目のビルドとmaven-clean-plugin

次のハマりポイントはmvn packageを2回連続で実行すると下記のエラーが出て失敗することです。mvn-assembly-pluginどうなってんだお前……。

mvn packageを2回実行した時のエラー
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ bluetooth-test ---
[INFO] Building jar: /home/katsuhiro/ble-test/target/bluetooth-test-0.1.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.332 s
[INFO] Finished at: 2024-02-20T02:41:20+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-jar-plugin:2.4:jar (default-jar) on project bluetooth-test: Error assembling JAR: A zip file cannot include itself -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

なぜかmaven-jar-pluginがエラーを起こします。メッセージの意味がさっぱりわかりません。なぜかmaven-clean-pluginを追加して最初にcleanを実行すると現象が発生しなくなります。ビルド時のゴミが残っているのか?ビルドが毎回全てやり直しになるので遅いですけど、ビルドエラーよりはマシでしょう……。

maven-clean-pluginとビルド開始時にcleanする設定

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <executions>
                    <execution>
                        <id>auto-clean</id>
                        <phase>initialize</phase>
                        <goals>
                            <goal>clean</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

やっとmvn packageが動作するようになりました。思い出していただきたいのは、Mavenの設定ファイルpom.xmlたった1つしか作っておらず、Java言語を1文字足りとも書いていないのにこの有様だということです。Mavenは著名ツールの割にエラーメッセージが意味不明すぎてつらいです。Maven使っている人はpom.xmlをどうやってデバッグしているんでしょうね?

ソースコード

ソースコードという程でもないですが、こちらからどうぞ。

編集者:すずき(2024/02/20 02:54)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月24日

国立科学博物館の乗り物体験ツアー

昨年8月ころに国立科学博物館が資金難に陥りクラウドファンディングをしていました(地球の宝を守れ - 国立科学博物館500万点のコレクションを次世代へ(国立科学博物館2023/08/07公開) - クラウドファンディング READYFOR)。科博にはこれからも頑張ってほしいなと思い、微力ながら「【体験】【寄付控除あり】乗り物体験<昔の電気自動車など>」コースに寄付しました。

んで、今日は寄付の返礼品のツアーに行ってきました。

残念ながら建物外観も含め写真を撮ってはダメで、写真がありません。車に乗っているところは写真を撮って良かったのですが、公開してはいけませんとのことで、日記に使える写真はありません。

ツアーの内容 - 車

科博の筑波地域の簡単な歴史や説明、注意事項、ツアーの時間を説明したオリエンテーリングのあと、実験植物園の裏口から理工資料棟に向かいました。こんな入り口あったんだ……。資料棟に入る前には靴底を洗い、靴にカバーを付けてから入場します(資料保護のためです)。

車は6台ありました。

  • Milburn 電気自動車
  • マツダ RX-8
  • マツダ コスモスポーツ
  • マツダ R360クーペ
  • スバル 360
  • 謎の三輪自動車

Milburn電気自動車(理工電子資料館:Milburn 電気自動車 - 国立科学博物館)は、名前だけ聞くと電気自動車?EVの仲間?と思いますが、なんと100年前(1920年くらい)のアメリカの車です。

リンク先の写真を見てもらうとわかりますが、形がほぼ「馬車」です。御者台のない馬車というのかな。当時はガソリンエンジンが圧倒的シェアを取る前で、蒸気、電気、ガソリンなど様々な動力で走る車がいたそうです。面白い時代です。

マツダの各車、スバル 360は名前は知ってましたが乗ったのは初めてです。スバル 360は屋根が低すぎます。マツダ R360クーペは車内の狭さが尋常じゃないです。後部座席なんかほぼ壁です。4人乗りと名乗るには無茶が過ぎないか……??

ツアーの内容 - おまけ

車に乗ったり撮ったりするのが一巡したところでおまけツアーが始まりました。今日のツアー担当の方は3人いらっしゃって、車、天文、物理が専門の方々でした。たぶん2回目のツアーは担当の方が違うはずで、ツアーの内容も変わると思いますので参考程度。

最初は望遠鏡のツアーでした。

ちょうど車と同じフロアに天体望遠鏡が置いてあったため、望遠鏡の説明をしていただきました。今はデジタル撮像素子を使いますが、昔は写真乾板を使って撮影していたため数十分レベルの長時間露光が必要でした。単に数十分露光すると星が動いてしまって正しく撮影できないので、望遠鏡には望遠鏡の撮影部分を星の動きに追尾させる機構があります。

当時はモーターなどが精度良いものはなかったそうで、錘と調速機構(=遠心ガバナー)を使って一定速度で望遠鏡を回すという仕組みだそうです。望遠鏡は何台か所蔵されており上野の日本館の屋上で稼働していたニコンの60cm(だったかな?)反射式望遠鏡も置いてありました。反射式望遠鏡はどれもでかいですねえ……。

望遠鏡の次はコンピュータ系のツアーでした。

地球シミュレータが置いてありました。コンピュータはカッコいい系のデザインにされることが多いですが、地球シミュレータのデザインはかわいいですね。愛嬌があって良いと思います。京コンピュータもあったみたいですが、カバーが掛かったままで良く見えませんでした。

あと日立 HITAC5020もありました。前面パネルを開けて配線を見せてもらいましたが、辿ることすら困難なグチャグチャ配線でゾッとしました。良く配線したものだ、メンテも地獄だったのではなかろうか……。気合と根性の日本って感じがします。

編集者:すずき(2024/02/25 02:48)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月25日

100万回のHello, World!

目次: ベンチマーク

Twitterで100万回のHello, World!問題を見かけ、面白そうだったのでやってみました。消えてしまった時のためにレギュレーションを書き写しておくと、

  • 設問: ループや再帰なしで100万回Hello, World!するには?
  • 条件: for, while, goto, 再帰, 1000バイト越え, 外部ファイルは使用禁止

Cだとマクロを入れ子にする、setjmp/longjmpやsignalでgotoの代わりにする、辺りが設問意図のようです。C++だとクラスAのコンストラクタでHello, World!を書いておいて、A hoge[100 * 10000]のように配列宣言する方法があるみたいです。なるほどスマート。スクリプト言語だと大抵は繰り返し処理の構文があるので、あまり難しくないみたいです。最近の言語にとっては手ごたえのない問題かもしれません。

ツイートの引用をざっと見た感じだとアセンブラでやっている人がいなかったので、Cのふりしたアセンブラ(というかほぼバイナリ直書き)でチャレンジしました。

Cのふりしたバイナリ直書き版

const char _start[] __attribute__((section(".text"))) = {
        0xbb, 0x40, 0x42, 0x0f, 0x00, //ebx 1000000
        0x66, 0xb8, 0x01, 0x00, //ax
        0x89, 0xc7, //edi
        0x66, 0xba, 0x0e, 0x00, //dx
        0x48, 0x8d, 0x35, 0x0c, 0x00, 0x00, 0x00, //esi
        0x0f, 0x05, //syscall
        0xff, 0xcb, //dec ebx
        0x75, 0xe9, //jne
        0x66, 0xb8, 0x3c, 0x00, //ax
        0x0f, 0x05, // syscall
        'H', 'e', 'l', 'l', 'o', ',', ' ',
        'W', 'o', 'r', 'l', 'd', '!', '\n', '\0',
};

これだけです。ソースコードのサイズは433バイトで、コメントなどは削っていませんが余裕で1000バイト以下に収まります。何が書いてあるのかわからんと思うので逆アセンブルした結果も載せます。

逆アセンブル
0000000000401000 <_start>:
  401000:       bb 40 42 0f 00          mov    $0xf4240,%ebx         ★0xf4240 = 100万
  401005:       66 b8 01 00             mov    $0x1,%ax
  401009:       89 c7                   mov    %eax,%edi
  40100b:       66 ba 0e 00             mov    $0xe,%dx
  40100f:       48 8d 35 0c 00 00 00    lea    0xc(%rip),%rsi        # 401022 <_start+0x22>
  401016:       0f 05                   syscall                      ★write(1, "Hello, World!\n", 14)に相当する
  401018:       ff cb                   dec    %ebx
  40101a:       75 e9                   jne    401005 <_start+0x5>   ★goto 2行目
  40101c:       66 b8 3c 00             mov    $0x3c,%ax
  401020:       0f 05                   syscall                      ★exit()に相当、libcを使っていないのでretするとSEGVする

★ここから下はHello, World!の文字列なので命令列としては無意味

  401022:       48                      rex.W
  401023:       65 6c                   gs insb (%dx),%es:(%rdi)
  401025:       6c                      insb   (%dx),%es:(%rdi)
  401026:       6f                      outsl  %ds:(%rsi),(%dx)
  401027:       2c 20                   sub    $0x20,%al
  401029:       57                      push   %rdi
  40102a:       6f                      outsl  %ds:(%rsi),(%dx)
  40102b:       72 6c                   jb     401099 <_start+0x99>
  40102d:       64 21 0a                and    %ecx,%fs:(%rdx)

次に動作確認しましょう。

動作確認
$ gcc -static -nostdlib a.c

/tmp/ccdifyE1.s: Assembler messages:
/tmp/ccdifyE1.s:4: Warning: ignoring changed section attributes for .text

$ ./a.out | head
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!

$ ./a.out | wc
1000000 2000000 14000000

Hello, World!が繰り返し出ていること、100万行出ていること、異常事態(SEGVなど)が発生しないことを見ています。これで良さそうですね。

アセンブラで書いただけではつまらない

アセンブラでやる人がいないのは、jmp命令がgotoと同等なので当たり前にクリアできて何も面白くないからだと思います。なのでもう少しこだわって「1000バイト以下」のレギュレーションを極めましょう。

  • 設問: ループや再帰なしで100万回Hello, World!するには?
  • 条件: for, while, goto, 再帰, 1000バイト越え(ソースコード、バイナリともに), 外部ファイルは使用禁止

レギュレーションを上記のように強化しました。

1000バイト以下に収めよう(バイナリも) - まずはstrip

まずは先ほど動作確認したバイナリのサイズを確認します。

バイナリのサイズ
$ gcc -static -nostdlib a.c

/tmp/ccdifyE1.s: Assembler messages:
/tmp/ccdifyE1.s:4: Warning: ignoring changed section attributes for .text

$ ls -la a.out

-rwxr-xr-x 1 katsuhiro suzuki 4864  2月 25 03:23 a.out

$ strip -s a.out

$ ls -la a.out

-rwxr-xr-x 1 katsuhiro suzuki 4544  2月 25 03:25 a.out

$ strip -s -R .note.gnu.build-id -R .comment a.out

strip: a.out: warning: empty loadable segment detected at vaddr=0x400000, is this intentional?

$ ls -la a.out

-rwxr-xr-x 1 katsuhiro suzuki 4360  2月 25 03:28 a.out

実質の命令列+文字列で50バイトくらいなのにバイナリは4KBを超えています。なんだこれは。stripしても300バイトほどしか減りません。readelfで確認すると.note.gnu.build-idと.commentというセクションがstripされずに残っていたため、排除しましたが500バイトほどしか減りません。

おっと……これは?もしかして意外と難しいのか?

1000バイト以下に収めよう(バイナリも) - 変な空白を消そう

バイナリのセクションヘッダを眺めていると、なぜか.textが0x1000から配置されています。

セクションヘッダ
セクションヘッダ:
  [番] 名前              タイプ           アドレス          オフセット
       サイズ            EntSize          フラグ Link  情報  整列
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000 ★この時点でバイナリサイズ4KBが確定
       0000000000000031  0000000000000000  AX       0     0     32
  [ 2] .shstrtab         STRTAB           0000000000000000  00001031
       0000000000000011  0000000000000000           0     0     1

デフォルトリンカースクリプト(gccに-Wl,-verboseオプションを渡すと表示できます)を調べると、特に何も指定しない場合.textは0x400000+SIZEOF_HEADERSというアドレスに配置されることがわかります。

デフォルトリンカースクリプトの先頭部分

SECTIONS
{
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000));
  . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;

SIZEOF_HEADERSを決定する仕組みがいまいち分からないですが、恐らくSIZEOF_HEADERS = 0x1000でしょう。0x400000のような大きなアドレスの場合はプログラムヘッダのロードアドレスで調整するので、0x400000の0データがバイナリに書かれることはありません。しかし一定サイズ以下(今回のような0x1000など)の場合はバイナリに0データを埋め込んで開始位置を調整するようです。

この仕組みを逆手にとって.textの開始アドレス下位をもっと手前のアドレスにずらすことで、ELFヘッダやプログラムヘッダを避けながら開始位置調整用の無駄な0データを削除できるはずです。

.textの開始アドレスを手前にずらす
$ gcc -static -nostdlib -Wl,-Ttext=0x400160 a.c

$ ls -la a.out

-rwxr-xr-x  1 katsuhiro suzuki 1120  2月 25 03:43 a.out


セクションヘッダ:
  [番] 名前              タイプ           アドレス          オフセット
       サイズ            EntSize          フラグ Link  情報  整列
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000400160  00000160 ★0x1000から0x160に詰められた
       0000000000000031  0000000000000000  AX       0     0     32
  [ 2] .note.gnu.bu[...] NOTE             00000000004000e8  000000e8
       0000000000000024  0000000000000000   A       0     0     4
  [ 3] .comment          PROGBITS         0000000000000000  00000191
       000000000000001f  0000000000000001  MS       0     0     1
  [ 4] .symtab           SYMTAB           0000000000000000  000001b0
       0000000000000090  0000000000000018           5     2     8
  [ 5] .strtab           STRTAB           0000000000000000  00000240
       000000000000001d  0000000000000000           0     0     1
  [ 6] .shstrtab         STRTAB           0000000000000000  0000025d
       000000000000003d  0000000000000000           0     0     1

開始アドレス下位を0x1000から0x160までずらすとバイナリ中の0データ領域が減り、一気に1120バイトまで減りました。劇的な効果です。変更により動作不良を起こしていないかも忘れずにチェックします(動作チェックのログは省略)。

もう一息削れば1000バイトはすぐそこですが、、、

もう削れない?
$ gcc -static -nostdlib -Wl,-Ttext=0x400140 a.c

/tmp/cc5LlyKi.s: Assembler messages:
/tmp/cc5LlyKi.s:4: Warning: ignoring changed section attributes for .text
/usr/bin/ld: section .text LMA [0000000000400140,0000000000400170] overlaps section .note.gnu.build-id LMA [0000000000400120,0000000000400143]
collect2: error: ld returned 1 exit status

残念ながら.textの開始アドレス下位を0x140にすると「別のセクションと重なってるぞ!」と怒られてリンクエラーになります。.note.gnu.build-idセクションと重なっているとのこと。

1000バイト以下に収めよう(バイナリも) - さようならbuild-id

とりあえず.note.gnu.build-idセクションは無くても実行できるので、-Wl,--build-id=noneで消し去ります。.note.gnu.build-idセクションがいなくなった分、.textセクションをさらに前の0x100まで詰められます。

1000バイト以下達成!
$ gcc -static -nostdlib -Wl,-Ttext=0x400100 -Wl,--build-id=none a.c

/tmp/cctmYikq.s: Assembler messages:
/tmp/cctmYikq.s:4: Warning: ignoring changed section attributes for .text

$ ls -la a.out

-rwxr-xr-x  1 katsuhiro suzuki  936  2月 25 03:52 a.out

やりました。stripに頼らずとも1000バイトを下回ることができました。最初の4KBを見たときはどうなるかと思いましたが、意外と何とかなるものです。

1000バイト以下に収めよう(バイナリも) - ダメ押しのstrip

既に目標は達成しましたが、さらに.commentセクションの排除と、stripするとどこまで減るか試しましょう。まずは.commentセクションの排除です。

.commentセクション内に取り込まれる.identの排除
$ gcc -static -nostdlib -Wl,-Ttext=0x400100 -Wl,--build-id=none -fno-ident a.c

$ ls -la a.out

-rwxr-xr-x  1 katsuhiro suzuki  840  2月 25 04:55 a.out

次はstripです。.symtabセクションと.strtabセクションが消えます。

strip
$ strip -s ./a.out

$ ls -la a.out

-rwxr-xr-x  1 katsuhiro suzuki  520  2月 25 04:56 a.out

サイズは520バイトまで減らせました。1000バイトの約半分です。もっと複雑な処理でも詰め込めるでしょう。私はx86_64アセンブラが得意じゃないので、何か書いてやろうという気は起きませんけど……。

編集者:すずき(2024/02/25 05:57)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月26日

100万回のHello, World! - バイナリサイズを削って遊ぼう

目次: ベンチマーク

前回(2024年2月25日の日記参照)はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対し、アセンブラというかほぼバイナリ直書きのC言語で挑戦しました。その後、バイナリサイズを削って遊んでみたところgccのみで840バイト、stripありで520バイトとなり、バイナリでも1000バイト以下を達成しました。

今回はどこまでバイナリのサイズを切り詰められるか試してみたいと思います。ツール頼みだとこれ以上削れないと思うのでバイナリエディタで削っていきます。

バイナリの見直し

前回の私のバイナリ実装はかなり適当だったので猛者に叩きなおしてもらいました。変化点としては、

  • レジスタに0x1を代入するとき、movではなくpushとpopを使う
  • exitを呼ぶときwriteを呼ぶ処理を流用してmovを減らす
  • アドレスが固定であることを期待して、幅を取ってたlea命令をmovに置き変える

といったところです。

バイナリの改良

const char _start[] __attribute__((section(".text"))) = {
        0xbb, 0x40, 0x42, 0x0f, 0x00, //ebx 1000000
        0x6a, 0x01, //push 0x1
        0x58, //pop rax
        0x89, 0xc7, //mov eax,edi
        0x6a, 0x0e, //push 0xe
        0x5a, //pop rdx
        0xbe, 0x1c, 0x01, 0x40, 0x00, //mov 0x4011c, esi
        0x0f, 0x05, //syscall
        0xff, 0xcb, //dec ebx
        0x75, 0xed, //jne -> push 0x1
        0x6a, 0x3c, //push 0x3c
        0xeb, 0xeb, //jmp -> pop rax
        'H', 'e', 'l', 'l', 'o', ',', ' ',
        'W', 'o', 'r', 'l', 'd', '!', '\n',
};

この改良の時点で520→512バイト(8バイト減)に改善します。素晴らしい〜。

動作確認、逆アセンブルなど
$ gcc -static -nostdlib -Wl,-Ttext=0x400100 -Wl,--build-id=none -fno-ident a.c
/tmp/cczsMtLk.s: Assembler messages:
/tmp/cczsMtLk.s:4: Warning: ignoring changed section attributes for .text

$ ls -la a.out
-rwxr-xr-x 1 katsuhiro suzuki 832  2月 25 13:47 a.out

$ strip -s a.out
$ ls -la a.out
-rwxr-xr-x 1 katsuhiro suzuki 512  2月 25 13:47 a.out  ★8バイト改善

$ objdump -DrS a.out
(略)
0000000000400100 <.text>:
  400100:       bb 40 42 0f 00          mov    $0xf4240,%ebx
  400105:       6a 01                   push   $0x1
  400107:       58                      pop    %rax
  400108:       89 c7                   mov    %eax,%edi
  40010a:       6a 0e                   push   $0xe
  40010c:       5a                      pop    %rdx
  40010d:       be 1c 01 40 00          mov    $0x40011c,%esi
  400112:       0f 05                   syscall
  400114:       ff cb                   dec    %ebx
  400116:       75 ed                   jne    0x400105
  400118:       6a 3c                   push   $0x3c
  40011a:       eb eb                   jmp    0x400107

★ここから下はHello, World!の文字列なので命令列としては無意味

  40011c:       48                      rex.W
  40011d:       65 6c                   gs insb (%dx),%es:(%rdi)
  40011f:       6c                      insb   (%dx),%es:(%rdi)
  400120:       6f                      outsl  %ds:(%rsi),(%dx)
  400121:       2c 20                   sub    $0x20,%al
  400123:       57                      push   %rdi
  400124:       6f                      outsl  %ds:(%rsi),(%dx)
  400125:       72 6c                   jb     0x400193
  400127:       64 21 0a                and    %ecx,%fs:(%rdx)


$ ./a.out | head
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!

$ ./a.out | wc
1000000 2000000 14000000

動作確認もできました。バイナリはこんな感じです。

サイズ削減前のバイナリ(512バイト)
$ hexdump -C remove_prg_section_org.out
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  00 01 40 00 00 00 00 00  |..>.......@.....|
00000020  40 00 00 00 00 00 00 00  40 01 00 00 00 00 00 00  |@.......@.......|
00000030  00 00 00 00 40 00 38 00  03 00 40 00 03 00 02 00  |....@.8...@.....|
00000040  01 00 00 00 04 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 40 00 00 00 00 00  00 f0 3f 00 00 00 00 00  |..@.......?.....|
00000060  e8 00 00 00 00 00 00 00  e8 00 00 00 00 00 00 00  |................|
00000070  00 10 00 00 00 00 00 00  01 00 00 00 05 00 00 00  |................|
00000080  00 01 00 00 00 00 00 00  00 01 40 00 00 00 00 00  |..........@.....|
00000090  00 01 40 00 00 00 00 00  2a 00 00 00 00 00 00 00  |..@.....*.......|
000000a0  2a 00 00 00 00 00 00 00  00 10 00 00 00 00 00 00  |*...............|
000000b0  51 e5 74 64 06 00 00 00  00 00 00 00 00 00 00 00  |Q.td............|
000000c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000000e0  10 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000100  bb 40 42 0f 00 6a 01 58  89 c7 6a 0e 5a be 1c 01  |.@B..j.X..j.Z...|
00000110  40 00 0f 05 ff cb 75 ed  6a 3c eb eb 48 65 6c 6c  |@.....u.j<..Hell|
00000120  6f 2c 20 57 6f 72 6c 64  21 0a 00 2e 73 68 73 74  |o, World!...shst|
00000130  72 74 61 62 00 2e 74 65  78 74 00 00 00 00 00 00  |rtab..text......|
00000140  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000180  0b 00 00 00 01 00 00 00  06 00 00 00 00 00 00 00  |................|
00000190  00 01 40 00 00 00 00 00  00 01 00 00 00 00 00 00  |..@.............|
000001a0  2a 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |*...............|
000001b0  20 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ...............|
000001c0  01 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
000001d0  00 00 00 00 00 00 00 00  2a 01 00 00 00 00 00 00  |........*.......|
000001e0  11 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000001f0  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000200

サイズは小さいですが0データの羅列が目立ちます。削れそうな気がしてきませんか?

削れそうなもの - プログラムヘッダ、セクション(512→162バイト)

大物から削りましょう。プログラムヘッダがなぜか3つありますが2つ不要、セクションヘッダは実行には不要、セクションヘッダ削除に伴って.shstrtabも不要です。

プログラムヘッダが3つあるが、2つは不要
プログラムヘッダ:
  タイプ        オフセット          仮想Addr           物理Addr
            ファイルサイズ        メモリサイズ         フラグ 整列
  LOAD           0x0000000000000000 0x0000000000400000 0x00000000003ff000  ★いらない
                 0x00000000000000e8 0x00000000000000e8  R      0x1000
  LOAD           0x0000000000000100 0x0000000000400100 0x0000000000400100
                 0x000000000000002a 0x000000000000002a  R E    0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000  ★いらない
                 0x0000000000000000 0x0000000000000000  RW     0x10

削除データの位置は下記の図の通りです。


削除するデータ(プログラムヘッダ、セクションヘッダなど)の位置

単純に削除すると.textセクションの位置とヘッダに記載されているアドレスがズレて動かなくなりますから、

  • ELFヘッダ: e_entry, e_phoff, e_phnum
  • プログラムヘッダ: p_offset, p_vaddr, p_paddr, p_filesz, p_memsz

上記の値を調整します。調整後のバイナリが下記です。

削減第一弾(162バイト)
$ ls -la remove_prg_section.out
-rwxr-xr-x 1 katsuhiro suzuki 162  2月 25 14:05 remove_prg_section.out

$ hexdump -C remove_prg_section.out
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............| ★ELFヘッダ
00000010  02 00 3e 00 01 00 00 00  78 00 40 00 00 00 00 00  |..>.....x.@.....|
00000020  40 00 00 00 00 00 00 00  40 01 00 00 00 00 00 00  |@.......@.......|
00000030  00 00 00 00 40 00 38 00  01 00 40 00 03 00 02 00  |....@.8...@.....|
00000040  01 00 00 00 05 00 00 00  60 00 00 00 00 00 00 00  |........`.......| ★プログラムヘッダ
00000050  60 00 40 00 00 00 00 00  60 00 40 00 00 00 00 00  |`.@.....`.@.....|
00000060  40 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |@.......@.......|
00000070  00 10 00 00 00 00 00 00  bb 40 42 0f 00 6a 01 58  |.........@B..j.X| ★0x78〜: 実行する命令列
00000080  89 c7 6a 0e 5a be 94 00  40 00 0f 05 ff cb 75 ed  |..j.Z...@.....u.|
00000090  6a 3c eb eb 48 65 6c 6c  6f 2c 20 57 6f 72 6c 64  |j<..Hello, World|
000000a0  21 0a                                             |!.|
000000a2

0データの羅列が減ってだいぶスリム化しました。サイズは162バイトです。

削れそうなもの - ELFヘッダ(162→120バイト)

ELFヘッダには色々な値が並んでいますが、実行ファイルを実行(execveシステムコール)するときに全ての値をチェックしているわけではありません。これを逆手にとってELFヘッダに実行命令列やデータを詰め込むことができます。図で示した黄色い部分以外は好き勝手に変えて大丈夫です。


ELFヘッダのなかで正しい値を維持しなければならない部分

最初の空き地はe_identの後半(アドレス: 0x04〜0x0f、12バイト)とe_version(アドレス: 0x14〜0x17、4バイト)です。実行バイナリの先頭4命令、10バイト分、そのあとの2バイト分を入れます。各領域の終端2バイトはジャンプ命令に使います。そうしないと命令列ではないところまで実行してクラッシュするからからです。

e_identの後半に入れる命令列、e_versionに入れる命令列
★e_identに入れる分
  400100:       bb 40 42 0f 00          mov    $0xf4240,%ebx
  400105:       6a 01                   push   $0x1
  400107:       58                      pop    %rax
  400108:       89 c7                   mov    %eax,%edi

★e_versionに入れる分
  40010a:       6a 0e                   push   $0xe

次の空き地はe_shoff, e_flags, e_ehsize(アドレス: 0x28〜0x35、14バイト)です。ちょうど"Hello, World!\n"と同じ長さなので文字列を置きます。

最後の空き地はELFヘッダの終端、e_shentsize, e_shnum, e_shstrndx(アドレス: 0x3a〜0x3f、6バイト)です。ここは命令列を置くよりe_phnumの値とプログラムヘッダの先頭p_typeも値が0x01であることを利用して、プログラムヘッダを8バイト手前にずらした方が良いでしょう。命令列を置くとジャンプ命令分を除いて、4バイトしか改善できないからです。


ELFヘッダの終端とプログラムヘッダの先頭8バイトを重ねる

プログラムヘッダの方は空き地はほぼなく、ヘッダ終端のp_align(8バイト)だけ変更OKでした。ここにも命令列を置きましょう。

最後にアドレスを調整するとこんなバイナリです。120バイトになりました。当然、実行できて100万回のHello, World!を出力します。

削減第二弾(120バイト)
$ hexdump -C overwrap_elfh.out
00000000  7f 45 4c 46 bb 40 42 0f  00 6a 01 58 89 c7 eb 04  |.ELF.@B..j.X....|
00000010  02 00 3e 00 6a 0e eb 50  04 00 40 00 00 00 00 00  |..>.j..P..@.....|
00000020  38 00 00 00 00 00 00 00  48 65 6c 6c 6f 2c 20 57  |8.......Hello, W|
00000030  6f 72 6c 64 21 0a 38 00  01 00 00 00 05 00 00 00  |orld!.8.........|
00000040  60 00 00 00 00 00 00 00  60 00 40 00 00 00 00 00  |`.......`.@.....|
00000050  60 00 40 00 00 00 00 00  30 00 00 00 00 00 00 00  |`.@.....0.......|
00000060  30 00 00 00 00 00 00 00  5a be 28 00 40 00 0f 05  |0.......Z.(.@...|
00000070  ff cb 75 95 6a 3c eb 93                           |..u.j<..|
00000078


$ ./overwrap_elfh.out | head
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!

$ ./overwrap_elfh.out | wc
1000000 2000000 14000000

あくまでもこのバイナリは私が使っているLinux kernel 6.1系のチェックを掻い潜って実行できるだけ、です。gdbは実行ファイルとして認識してくれませんし、readelfも大量のエラーを出します。将来のLinuxカーネルでも実行できなくなる可能性があります。

GDBやreadelfには怒られる
$ gdb ./remove_elf.out
GNU gdb (Debian 13.1-3) 13.1
(略)
"(略)/./remove_elf.out": not in executable format: file format not recognized

(gdb)

$ readelf -a remove_elf.out
ELF ヘッダ:
  マジック:  7f 45 4c 46 bb 40 42 0f 00 6a 01 58 89 c7 eb 04
  クラス:                            <不明: bb>
  データ:                            <不明: 40>
  Version:                           66 <unknown>
  OS/ABI:                            AROS
  ABI バージョン:                    0
  型:                                EXEC (実行可能ファイル)
  マシン:                            Advanced Micro Devices X86-64
  バージョン:                        0x50eb0e6a
  エントリポイントアドレス:          0x400004
  プログラムヘッダ始点:            0 (バイト)
  セクションヘッダ始点:              56 (バイト)
  フラグ:                            0x0
  Size of this header:               25928 (bytes)
  Size of program headers:           27756 (bytes)
  Number of program headers:         11375
  Size of section headers:           22304 (bytes)
  Number of section headers:         29295
  Section header string table index: 25708
readelf: 警告: The e_shentsize field in the ELF header is larger than the size of an ELF section header
readelf: エラー: Reading 653395680 bytes extends past end of file for セクションヘッダ
readelf: エラー: セクションヘッダが利用できません!
readelf: エラー: Too many program headers - 0x2c6f - the file is not that big

このファイルには動的セクションがありません。
readelf: エラー: Too many program headers - 0x2c6f - the file is not that big

さらにLinuxの裏をかいて削減できるような気もしますけど……、キリがないのでこれくらいにしておきます。

余談

GDBはセクションヘッダを削除した時点でELFファイルとして認識しなくなります。ジャンプ先を間違ったとき、アドレスが間違っていてSEGVするときのデバッグができずしんどいです。

ELFヘッダを書き換えるとreadelfすら訳の分からない表示になるので、デバッグがさらに辛いですね……。

バイナリの種類GDBreadelf
オリジナル512バイト版認識するエラーなし
セクション削除162バイト版エラーセクションヘッダがない、エラー
ELFヘッダ改変120バイト版エラー読む場所がずれてる(OS/ABIが間違っているから?)
編集者:すずき(2024/02/26 00:53)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月27日

100万回のHello, World! - バイナリサイズを削って遊ぼう、究極編その1

目次: ベンチマーク

前回(2024年2月26日の日記参照)はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対して、バイナリサイズをどこまで削れるかに挑戦しました。結果120バイトのバイナリとなりました。

今回は削減の限界値と思われる値まで達成したので、その内容を紹介したいと思います。空き地の開拓は私ですが、アセンブラの短縮に関しては私ではなくhdkさん(hdkのページ)がほぼやってくれました。結構難しいので、わかるように説明できているか怪しいです……。

バイナリの最小サイズ(削減の限界)は112バイト

正常に動作するELF形式の実行ファイルには、ELFヘッダ(64バイト)とプログラムヘッダ(56バイト)が必要です。120バイト必要に見えますが、2つのヘッダは破綻しない範囲で重ねて良いです。今のところ112バイトが最小サイズだと思われます


ELFヘッダのなかで正しい値を維持しなければならない部分

前回紹介した通り、ELFヘッダには適当な値を入れるとエラーになって動かなくなる部分があります。プログラムヘッダも同様に制約があり、先頭のp_typeとp_flags(01 00 00 00 05 00 00 00)は変更できません。そのあとに続くp_offsetも上位バイトは変更できず、p_vaddr = p_paddr、p_filesz = p_memszでなければなりません。結構厳しいです。

これらの条件を満たせるのはe_phnum, e_shentsize, e_shnum, e_shstrndx(計8バイト)とp_type, p_flags(計8バイト)を重ねることだけだと思われます。単純な合計サイズから8バイト減った64 + 56 - 8 = 112バイトが最小サイズのはず。もし間違っていたら教えてください。私が喜びます。

削減のアイデア - プログラムヘッダの空き地p_fileszとp_memsz

前回は見落としましたが、プログラムヘッダ中に命令列を置ける空き地が1つ残っていました。p_fileszとp_memsz(どちらも8バイト)です。あまりに大きな値にするとエラーで動かなくなりますが、6バイトくらいなら何を書いても大丈夫そうです。

注意点としてはp_fileszとp_memszを同じ値にしないとエラーになることです。両方書き換えますが実行に使うのはどちらか片方だけとなるでしょう。

削減のアイデア - 命令の途中にジャンプ

RISC系のCPUを使う人からすると信じがたい動きですが、x86 CPUは命令のアラインメントがなく「命令の途中」にジャンプしても良いです。例を示しましょう。

命令の途中にジャンプしても正常に実行できる例
0000: be 28 01 b0 3c: mov $0x3cb00128, %esi

この命令の4バイト目(アドレス3)にジャンプすると

0003: b0 3c: mov $0x3c, %al

という命令に見えるので、継続して実行可能。

もし命令の一部が有効な命令として解釈可能ならそのまま実行を続けられます。x86 CPUの無茶苦茶な仕様には驚きですが、今回はこの動きが役立ちます。

削減のアイデア - 4バイトジャンプの代わりにtest命令

ELFヘッダのe_identの後半(アドレス: 0x04〜0x0f、12バイト)とe_version(アドレス: 0x14〜0x17、4バイト)の間には、e_type, e_machineという書き換えてはいけない4バイトの情報があります。単純に実装するとe_identの最後に4バイトジャンプ(eb 04)を置いて飛ばせば良いです。


test命令を4バイトスキップの代わりにする例

しかし上記のようにアドレス0x000fをtest命令(a9)に置き換えますと、後続の4バイトと合わせて1命令(a9 02 00 3e 00: test $0x3e0002, %eax)とみなせます。test命令はフラグレジスタ以外には影響を及ぼさないので、a9 02 00 3e 00は実行されるものの何も効力を発揮しません。

ジャンプ命令2バイトからtest命令の先頭1バイトのみで済みますので、空き地が1バイト増加します。test命令に変更する代償はフラグレジスタの内容が破壊されることです。例えば条件分岐命令の直前などでtest命令を使うと、分岐の結果が変わる可能性があります。

削減のアイデア - デクリメント+条件分岐の代わりにpush, pop, loop命令

100万回のループを行うときは、ebxレジスタなどcallee-saveレジスタ(関数呼び出しで値が壊れないレジスタ)に1,000,000を入れ、デクリメントし(ff cb: dec %ebx)、条件不一致ならループの先頭にジャンプ(75 xx: jne +-7bit幅ショートジャンプ)する、2命令、合計4バイトで実現するのが普通だと思います。

実はx86_64にはloop命令といってdec %ecxとjne xxを実行する命令があります。サイズは2バイト(e2 xx: loop +-7bit幅ショートループ)です。ただしecxレジスタはcallee-savedではないため、関数呼び出しやsyscallで内容が壊れます。必ずecxレジスタの保存(51: push %rcx)と復帰(59: pop %ecx)を伴うため合計サイズは4バイトで変わりません。

それなら置き換える意味がないのでは?と思うかもしれませんが、同じ4バイトでも2バイト命令2つ(dec, jne)と1バイト命令2つ+2バイト命令1つ(push, pop, loop)を比べると、後者の方が部品が多くてより柔軟に配置できるのです。

もう1つ良い点はdec命令とjne命令は近くに配置しないといけない制約(フラグを壊す算術命令やtest命令を間に入れると動かなくなるから)がありますが、push/pop命令はloop命令から多少離れても問題ありません。これもloopの方が柔軟に配置できる理由です。

続きはまた明日書きます。

編集者:すずき(2024/02/27 01:55)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年2月28日

100万回のHello, World! - バイナリサイズを削って遊ぼう、究極編その2

目次: ベンチマーク

前回(2024年2月27日の日記参照)はループ、再帰なし、1000バイト以下で100万回のHello, World!を実施する問題に対し、バイナリサイズを極限まで削るためのアイデアをご紹介しました。

今回はバイナリ実装の説明を紹介したいと思います。

使用する命令列の説明

今回使用する100万回のHello, World!を呼ぶ命令列と処理の流れを説明します。エントリアドレスは0x983cb004なので、一番上の行から実行開始します。

今回使用する命令列
    983cb004:   b9 40 42 0f 00          mov    $0xf4240,%ecx
    983cb009:   6a 01                   push   $0x1
    983cb00b:   58                      pop    %rax
    983cb00c:   6a 0e                   push   $0xe
    983cb00e:   5a                      pop    %rdx
    983cb00f:   a9 02 00 3e 00          test   $0x3e0002,%eax
    983cb014:   89 c7                   mov    %eax,%edi
    983cb016:   eb 50                   jmp    983cb068 <_start+0x68>

    983cb028:   "Hello, World!\n"

    983cb060:   0f 05                   syscall
    983cb062:   59                      pop    %rcx
    983cb063:   e2 a4                   loop   983cb009 <_start+0x9>
    
    
    ----- (★1)loop命令の条件に一致せず、983cb065に進んだ時に見える命令列
    
    983cb065:   25 00 00 be 28          and    $0x28be0000,%eax
    983cb06a:   b0 3c                   mov    $0x3c,%al
    983cb06c:   98                      cwtl
    983cb06d:   51                      push   %rcx
    983cb06e:   eb f0                   jmp    983cb060 <_start+0x60>


    ----- (★2)983cb068に飛んだ時に見える命令列

    983cb068:   be 28 b0 3c 98          mov    $0x983cb028,%esi
    983cb06d:   51                      push   %rcx
    983cb06e:   eb f0                   jmp    983cb060 <_start+0x60>

まず0x983cb004〜0x983cb016まで実行して、ジャンプ命令で0x983cb068に飛びます。上の図でいう(★2)の方です。

  • 983cb068: mov: esiレジスタに"Hello, World\n"のアドレスを入れる
  • 983cb06d: push: rcxレジスタを保存
  • 983cb060: syscall: writeシステムコールに相当
  • 983cb062: pop: syscallによってrcxレジスタが壊れるのでrcxを復帰
  • 983cb063: loop: rcxレジスタが0になるまでループ

ループ100万回が終了してrcxレジスタが0になるとloop命令の次のアドレス983cb065に進みます。eaxレジスタに0x3cを入れてsyscall(exitシステムコールに相当する)するので、プログラムが終了します。

命令重ね合わせの妙

この命令列の凄いところはand命令とmov命令の重ね合わせです。アドレス0x983cb065から見たときと、アドレス0x983cb068から見たときで命令列の意味が大きく変わります。図示するとこんな感じです。

アドレス0x983cb065から見たときと、アドレス0x983cb068から見たときの命令列の見え方
      and      mov  cwtl push  jmp
      |        |      |  |     |
<------------> <---> <> <> <--->    : アドレス983cb065から見たときの解釈
25 00 00 be 28 b0 3c 98 51 eb f0
         <------------> <> <--->    : アドレス983cb068から見たときの解釈
                |        |     |
                mov      push  jmp

この工夫によってmovとjmpの合計7バイトを重ね合わせることができていて、結果112バイトにきっちり収まっています。

ELF実行ファイル全体、動作確認
$ hexdump -C 112byte.out

00000000  7f 45 4c 46 b9 40 42 0f  00 6a 01 58 6a 0e 5a a9  |.ELF.@B..j.Xj.Z.|
00000010  02 00 3e 00 89 c7 eb 50  04 b0 3c 98 00 00 00 00  |..>....P..<.....|
00000020  38 00 00 00 00 00 00 00  48 65 6c 6c 6f 2c 20 57  |8.......Hello, W|
00000030  6f 72 6c 64 21 0a 38 00  01 00 00 00 05 00 00 00  |orld!.8.........|
00000040  00 00 00 00 00 00 00 00  00 b0 3c 98 00 00 00 00  |..........<.....|
00000050  00 b0 3c 98 00 00 00 00  0f 05 59 e2 a4 25 00 00  |..<.......Y..%..|
00000060  0f 05 59 e2 a4 25 00 00  be 28 b0 3c 98 51 eb f0  |..Y..%...(.<.Q..|
00000070

$ ./112byte.out | head
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!

$ ./112byte.out | wc
1000000 2000000 14000000

$ ./112byte.out > /dev/null

最終的なバイナリはこんな感じです。動作もOKです。

なんとエラー処理もある

ループ終了後exitシステムコールを呼ぶとき、アドレス983cb06aのmov命令にて0x3c(exitシステムコールを表す番号)をalレジスタに入れてsyscall命令を実行します。もしこのmov命令のみだと、writeシステムコールがエラーを返してeaxレジスタが負の値になっていると問題が起きます。alレジスタを0x3cに書き換えても上位ビットが0ではないため、eaxレジスタとして見たとき値が0x3cになりません。よってexitシステムコールが呼ばれず終了しません。

今回の112バイト版は前後のand命令とcwtl命令によってこの問題を解決しています。loop命令を通過した後の命令列を再掲します。

ループ終了後の命令列、再掲
    983cb065:   25 00 00 be 28          and    $0x28be0000,%eax
    983cb06a:   b0 3c                   mov    $0x3c,%al
    983cb06c:   98                      cwtl
    983cb06d:   51                      push   %rcx
    983cb06e:   eb f0                   jmp    983cb060 <_start+0x60>

ポイントは先頭の2命令です、簡単に解説します。

  • and: axレジスタ相当の部分(eaxレジスタの下位16ビット)を0クリア
  • cwtl: axレジスタをeaxレジスタに符号拡張

もしwriteシステムコールがエラーを返してeaxレジスタが負の値になっていたとしても、and命令がaxレジスタ相当の下位16ビットを0クリアし、cwtlで符号拡張するので必ずeaxレジスタは0x3cになる仕組みです。

この手のコードゴルフではエラー処理まで考えないことが多いですが、きれいに解決されています。ちなみに私はcwtl命令を初めて知りました。こんな命令あるんだ……。

編集者:すずき(2024/02/27 09:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報