Shogo's Blog

Dec 21, 2023 - 2 minute read - perl

Perlで超簡易アセンブラーを書いた話

この記事は、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のサブタイトルのふりかえり」です。 お楽しみに!

参考