8
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

VAXの機械語を総当たり調査

VAXの命令を調べるため総当たりで逆アセンブルしてみました。VAXの機械語はシンプルなので総当たり調査が簡単にできました。

VAXのクロス開発環境を前提とします。構築方法は次の記事を参照してください。

この記事には姉妹編があります。

基本形

簡単な機械語を見てみます。

$ cat test1.s
incl %r0
incl %r1
movl %r0,%r1
movl %r2,%r3
$ vax-netbsdelf-as test1.s
$ vax-netbsdelf-objdump -d a.out
(略)
00000000 <.text>:
   0:   d6 50           incl r0
   2:   d6 51           incl r1
   4:   d0 50 51        movl r0,r1
   7:   d0 52 53        movl r2,r3

アセンブリ言語ではレジスタに%を付けますが、逆アセンブル結果にはありません。オプションで揃えられるのかもしれませんが、本質ではないので無視します。

機械語を見ると、以下の対応関係が見て取れます。

d6 -> incl
d0 -> movl
50 -> r0
51 -> r1
52 -> r2
53 -> r3

最初の1バイト目がオペコード(命令の種類)で、2バイト目からオペランドが始まります。バイト単位で区切られているため、オペコードとオペランドをビット操作で分離する必要がありません。

拡張コード

1バイト目がオペコードが表すと命令は256個しか表現できません。これでは将来的な拡張の余地がなくなってしまうため、0xfd-0xffは次の1バイトも含んだ2バイトがオペコードです。

  • 00 - fc
  • fd 00 - fd ff
  • fe 00 - fe ff
  • ff 00 - ff ff

最大で0xfd + 3 * 0x100 = 1021個の命令が扱えます。このようなマルチバイトの扱い方は文字コードに似ています。

総当たり(オペコード)

適当にオペランドを並べてオペコードを総当たりすれば、すべての命令フォーマットが分かるはずです。

0x00で試します。オペランドの個数が不明なので、適当に0x50を並べます。

$ cat 00.s
.byte 0x00, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50
$ vax-netbsdelf-as 00.s
$ vax-netbsdelf-objdump -d a.out
(略)
00000000 <.text>:
   0:   00              halt
   1:   50 50 50        movf r0,r0
   4:   50 50 50        movf r0,r0

0x00はオペランドなしのhalt命令だと分かりました。

この要領ですべてを試します。0xfd-0xffの扱いに注意が必要です。

オペランドの個数によって命令の切れ目が変わるため、1つのファイルに並べて書くと不具合が生じます。1つずつ別ファイルで試して、最後に結合します。シェルスクリプトで自動化しますが、連番生成を使うためbash依存です。

allop.sh
#!/usr/bin/env bash

arch=vax-netbsdelf
as=$arch-as
dis="$arch-objdump -d"

rm -f allop.d
mkdir -p tmp
cd tmp

func() {
    echo $1
    echo ".byte $2, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50" > $1.s
    $as -o $1.o $1.s
    $dis $1.o > $1.d
    head -n8 $1.d | tail -n1 | cut -f2- >> ../allop.d
}

for i in {0..252}
do
    hex=`printf "%02x" $i`
    func $hex "0x$hex"
done

for i in {253..255}
do
    hex1=`printf "%02x" $i`
    for j in {0..255}
    do
        hex2=`printf "%02x" $j`
        func "$hex1-$hex2" "0x$hex1, 0x$hex2"
    done
done

実行するとallop.dというファイルが生成されます。

allop.d
00              halt
01              nop
02              rei
(略)
ff fd 50        bugl r0
ff fe 50        bugw r0
ff ff           .word 0xffff

.wordは未定義であることを表します。

命令長が4バイト以上の場合、ダンプが途中で切れるのに注意が必要です。cvtpsは実際には5バイト命令です。

temp/08.d(抜粋)
   0:   08 50 50 50     cvtps r0,r0,r0,r0
   4:   50

拡張コードの部分を見ると、fdは割と使われていますが、feは未使用、ffは2つしか命令がありません。

命令数を確認します。

$ grep -v .word allop.d | wc -l
306

上限が1021個なので、まだ余裕があります。

オペランドの組み合わせ

オペランドの組み合わせと出現数を確認します。

$ grep -v .word allop.d | cut -f2 | cut -s -d' ' -f2 | sort | uniq -c
      2 0x5053
     14 0x52
     37 r0
      4 r0,0x53
    131 r0,r0
     10 r0,r0,0x54
     57 r0,r0,r0
      5 r0,r0,r0,0x5056
      2 r0,r0,r0,0x5057
     19 r0,r0,r0,r0
      8 r0,r0,r0,r0,r0
      8 r0,r0,r0,r0,r0,r0

いくつか即値が現れています。0x50からずれているため、相対アドレスだと推測できます。

例としてbsbbを見ます。

temp/10.d(抜粋)
   0:   10 50           bsbb 0x52
   2:   50 50 50        movf r0,r0

相対アドレスは次の命令のアドレスを基点とします。2 + 0x50 = 0x52と確認できます。

オペランドの個数

allop.dを見れば、命令ごとのオペランドの個数が分かります。スクリプトで確認します。シェル芸には通じていないためPerlに逃げました。

allop-c.pl
#!/usr/bin/env perl

while (<>) {
    chop while substr($_, -1) le " ";
    @f = split(/\t/);
    @b = split(/ /, $f[0]);
    if ($f[1] =~ /^(.*?) (.*)/) {
        $mne = $1;
        @opr = split(/,/, $2);
    } else {
        $mne = $f[1];
        @opr = ();
    }
    if ($mne ne ".word") {
        $c = @opr;
        $r = "";
        if ($c > 0) {
            $l = length($opr[$c - 1]);
            if ($l > 2) {
                --$c;
                $r = ($l - 2) / 2;
            }
        }
        $op = hex($b[0]) < 0xfd ? $b[0] : "$b[0]-$b[1]";
        print "$op\t$c\t$r\t$mne\n";
    }
}

【注】 hexの意味はPythonと逆です。

実行結果をファイルに保存して確認します。

$ ./allop-c.pl < allop.d > allop-c.txt
$ cat allop-c.txt
00  0       halt
01  0       nop
(略)
10  0   1   bsbb
11  0   1   brb
(略)
ff-fd   1       bugl
ff-fe   1       bugw

相対アドレスはカウントせずに、3番目のカラムにサイズ(1または2)で示しています。

オペランドのサイズ

オペランドはそれぞれ固有のサイズを持っています。

即値を指定して確認します。$0x00-$0x3fは短い形式にエンコードされるため$0x40を指定します。

$ cat test2.s
tstb $0x40
tstw $0x40
tstl $0x40
$ vax-netbsdelf-as test2.s
$ vax-netbsdelf-objdump -d a.out
(略)
00000000 <.text>:
   0:   95 8f 40        tstb $0x40
   3:   b5 8f 40 00     tstw $0x0040
   7:   d5 8f 40 00     tstl $0x00000040
   b:   00 00

即値は8fで表されていますが、サイズは命令で決まるのが確認できます。

逆に、機械語からオペランドのサイズを調べます。調べたいオペランドに8fを入れておけば、出て来た即値からサイズが分かります。

cvtpsの第2オペランドを調べる例です。

$ cat test3.s
.byte 0x08, 0x50, 0x8f, 0x50, 0x50, 0x50, 0x50, 0x50
$ vax-netbsdelf-as test3.s
$ vax-netbsdelf-objdump -d a.out
(略)
00000000 <.text>:
   0:   08 50 8f 50     cvtps r0,$0x50,r0,r0
   4:   50 50
   6:   movf r0,Address 0x00000008 is out of bounds.

第2オペランドが$0x50と出ていることからサイズが1バイトだと確認できます。

総当たり(オペランド)

オペランドのサイズを総当たりで確認します。先ほど調査したオペランドの個数を使用します。

allopr.sh
#!/usr/bin/env bash

arch=vax-netbsdelf
as=$arch-as
dis="$arch-objdump -d"

rm -f allopr.d
mkdir -p tmp
cd tmp

func() {
    $as -o $1.o $1.s
    $dis $1.o > $1.d
    head -n8 $1.d | tail -n1 | cut -f2- >> ../allopr.d
}

data=`for i in {1..32}; do echo -n ", 0x50"; done`

cat ../allop-c.txt | while read line
do
    echo $line
    set -- $line
    fn=$1
    c=$2
    hex=`echo 0x$fn | sed 's/-/, 0x/'`
    if [ $c -eq 0 ]
    then
        echo ".byte $hex$data" > $fn.s
        func $fn
    else
        i=1
        while [ $i -le $c ]
        do
            echo -n ".byte $hex" > $fn-$i.s
            j=1
            while [ $j -lt $i ]
            do
                echo -n ", 0x50" >> $fn-$i.s
                j=`expr $j + 1`
            done
            echo ", 0x8f$data" >> $fn-$i.s
            func $fn-$i
            i=`expr $i + 1`
        done
    fi
done

実行するとallopr.dというファイルが生成されます。

allopr.d
00              halt
01              nop
(略)
08 8f 50 50     cvtps $0x5050,r0,r0,r0
08 50 8f 50     cvtps r0,$0x50,r0,r0
08 50 50 8f     cvtps r0,r0,$0x5050,r0
08 50 50 50     cvtps r0,r0,r0,$0x50
(略)
ff fd 8f 50     bugl $0x50505050
ff fe 8f 50     bugw $0x5050

cvtpsのオペランドのサイズが 2, 1, 2, 1 となっていることが分かります。

即値の種類を確認します。

$ cut -f2 allopr.d | cut -s -d' ' -f2- | sed 's/,/\n/g' | grep '\$' | sort | uniq
$0x50
$0x5050
$0x50505050
$0x50505050 [f-float]
$0x5050505050505050
$0x5050505050505050 [d-float]
$0x5050505050505050 [g-float]
$0x50505050505050505050505050505050
$0x50505050505050505050505050505050 [h-float]

5つの整数型(b, w, l, q, o)と4つの浮動小数点数型(f, d, g, h)があります。括弧内は型の省略形です。

分析

総当たりの結果を分析して、命令ごとのオペランドの型を特定します。

allopr-t.pl
#!/usr/bin/env perl

%im = (
    '$0x50'                                         => 'b',
    '$0x5050'                                       => 'w',
    '$0x50505050'                                   => 'l',
    '$0x50505050 [f-float]'                         => 'f',
    '$0x5050505050505050'                           => 'q',
    '$0x5050505050505050 [d-float]'                 => 'd',
    '$0x5050505050505050 [g-float]'                 => 'g',
    '$0x50505050505050505050505050505050'           => 'o',
    '$0x50505050505050505050505050505050 [h-float]' => 'h'
);

$cur = "";
@opt = ();

sub output {
    if ($cur ne "") {
        print "$cur\t$mne\t", join("", @opt), "\n";
        @opt = ();
    }
}

while (<>) {
    chop while substr($_, -1) le " ";
    @f = split(/\t/);
    @b = split(/ /, $f[0]);
    $op = hex($b[0]) < 0xfd ? $b[0] : "$b[0]-$b[1]";
    if ($cur ne $op) {
        output();
        $cur = $op;
    }
    if ($f[1] =~ /^(.*?) (.*)/) {
        $mne = $1;
        @opr = split(/,/, $2);
    } else {
        $mne = $f[1];
        @opr = ();
    }
    $c = @opr;
    for (my $i = 0; $i < $c; ++$i) {
        $o = $opr[$i];
        $t = $im{$o};
        if ($t ne "") {
            $opt[$i] = $t;
        } elsif ($o =~ /^0x..$/) {
            $opt[$i] = "1";
        } elsif ($o =~ /^0x....$/) {
            $opt[$i] = "2";
        }
    }
}
output();

実行結果をファイルに保存して確認します。

$ ./allopr-t.pl < allopr.d > allopr-t.txt
$ cat allopr-t.txt
00  halt    
01  nop 
(略)
10  bsbb    1
11  brb 1
(略)
3c  movzwl  wl
3d  acbw    www2
(略)
ff-fd   bugl    l
ff-fe   bugw    w

オペランドの型を省略形で並べていますが、相対アドレスはサイズ(1または2)で示しています。

以上で総当たりによる調査は完了しました。

まとめ

今回のスクリプトや出力結果は以下に置いてあります。

調査結果に基づいてJavaで300行程度の逆アセンブラを作りました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
8
Help us understand the problem. What are the problem?