この記事は、Perl Advent Calendar 2023 18日目の記事(代打)です。 17日目は@tecklで「Perlのレガシーシステムを少し更新した話」でした。
背景
最小のELFを作る記事を見かけて、「自分もやってみよう!」と思ってやってみました。
ただ、僕はバイナリーエディターとはあまりお友達になれていないので、 ELFを出力するPerlスクリプトを書くことにしました。 そうしたらPerl製のアセンブラーっぽいものができた、というお話です。
Minimum ELF
ELFとして実行できるだけの必要最低限の機能しか実装していないので、 プログラムの終了処理を書く以外、大したことはできません。 それでも実行可能なバイナリーを吐くので、 「これはアセンブラーだ!」 と言い張ることにします。
完成したELFファイルはDockerイメージに焼き込み、 GitHub Container Registry にあげてあるので、
docker pull
で完成品をダウンロードできます。
$ docker pull ghcr.io/shogo82148/minimum-elf:latest
$ docker images ghcr.io/shogo82148/minimum-elf:latest
REPOSITORY TAG IMAGE ID CREATED SIZE
ghcr.io/shogo82148/minimum-elf latest cd926faef0a2 13 days ago 188B
わずか188バイト!
もちろん docker run
で実行できます。
x86_64, arm64 どちらでも動くはずです。
実装
NASM っぽい構文を PerlのDSLとして実装し、ELFファイルを書き出しています。 たとえばELFのヘッダーを書き出す部分は以下のようになっています。
label($elf_start);
my $elf_size = $elf_end - $elf_start;
db 0x7F, "ELF"; # e_ident
db 2; # 64-bit architecture.
db 1; # 2's complement little-endian.
db 1; # ei_version ABI Version: Current
db 0; # ei_osabi UNIX System V ABI
db 0; # ei_abiversion ABI Version
db 0; # padding
dw 0, 0, 0; # padding
dw 2; # e_type: executable
dw $machine; # e_machine
dd 1; # e_version
dq $entrypoint; # e_entry
dq $phdr1-$start; # e_phoff
dq 0; # e_shoff
dd 0; # e_flags
dw $elf_size; # e_ehsize
dw 0x38; # e_phentsize
dw 2; # e_phnum
dw 0x40; # e_shentsize
dw 0; # e_shnum
dw 0; # e_shstrndx
label($elf_end);
なかなかアセンブリ言語っぽいと思いませんか?
db
, dw
, dq
などのキーワードは、すべてPerlの関数として実装しています。
内部では pack 関数をつかって地道にバイト列に変換しています。
# output byte sequence
sub db {
for my $v(@_) {
if (is_string($v)) {
push @output, Lazy::lazy { $v };
$pos += length $v;
} else {
push @output, Lazy::lazy { pack("C", $v) };
$pos++;
}
}
}
Lazy::lazy
は遅延評価を行うユーティリティー関数です。
ラベルをうまく扱うために導入しました。
ラベルのアドレス計算がプログラムの途中にでてくるのですが、
ラベルのアドレスが具体的にいくつになるかは、プログラムを全部読み込んだあとでないとわかりません。
そこで以下のようなユーティリティー関数を用意し、 アドレスの計算をバイナリー出力時まで遅延することにしました。
package Lazy;
use v5.32;
use utf8;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw/lazy/;
sub lazy(&) {
return Lazy->new($_[0]);
}
use overload
'""' => sub {
my $s = shift->{sub}->();
return "$s";
},
"0+" => sub { int(shift->{sub}->()) },
"+" => sub {
my ($self, $other, $reverse) = @_;
return $reverse ?
lazy { int($other) + int($self) }:
lazy { int($self) + int($other) };
},
"-" => sub {
my ($self, $other, $reverse) = @_;
return $reverse ?
lazy { int($other) - int($self) }:
lazy { int($self) - int($other) };
};
sub new {
my ($class, $sub) = @_;
return bless { sub => $sub }, $class;
}
sub set {
my ($self, $v) = @_;
$self->{sub} = sub { $v };
}
1;
まとめ
Perlはバイナリーデータの扱いも可能です。 少し頑張ればELFファイルの出力もできます。
明日19日目は@kfly8で「Hack For Perlのサブタイトルのふりかえり」です。 お楽しみに!