6

私は自分の小さな逆アセンブラを書くことを計画しています。実行可能ファイルを読み取ったときに取得したオペコードをデコードしたい。次のオペコードが表示されます。

69 62 2f 6c 64 2d 6c

以下に対応する必要があります。

imul   $0x6c2d646c,0x2f(%edx),%esp

現在、「imul」命令は 2 つまたは 3 つのオペランドを持つことができます。そこにあるオペコードからこれをどのように理解するのですか?

Intel の i386 命令セットに基づいています。

4

5 に答える 5

12

x86 命令セットは非常に複雑であり (とにかく CISC です)、ここで多くの人がそれを理解しようとする試みを思いとどまらせているのを見てきましたが、私は逆に言います: それはまだ理解可能であり、途中で学ぶことができます。なぜそんなに複雑なのか、Intel が 8086 から最新のプロセッサに至るまで何度もそれを拡張した方法を説明します。

x86 命令は可変長エンコーディングを使用するため、複数のバイトで構成できます。各バイトはさまざまなものをエンコードするためにあり、そのうちのいくつかはオプションです (オプションのフィールドが使用されているかどうかにかかわらず、オペコードでエンコードされています)。

たとえば、各オペコードの前に 0 ~ 4 バイトのプレフィックス バイトを付けることができますが、これはオプションです。通常、それらについて心配する必要はありません。これらは、オペランドのサイズを変更するため、または最新の CPU (MMX、SSE など) の拡張命令を使用してオペコード テーブルの「2 階」へのエスケープ コードとして使用されます。

次に、実際のオペコードがあります。通常は 1 バイトですが、拡張命令の場合は最大 3 バイトになる場合があります。基本的な命令セットのみを使用する場合は、それらについても心配する必要はありません。

次に、アドレッシング モードとオペランド タイプをエンコードする、いわゆるModR/Mバイト ( と呼ばれることもあります) があります。これは、そのようなオペランドを持つmode-reg-reg/memオペコードによってのみ使用されます。3 つのビット フィールドがあります。

  • 最初の 2 ビット (左から最上位ビット) は、アドレッシング モード (4 つの可能なビットの組み合わせ) をエンコードします。
  • 次の 3 ビットは、最初のレジスタをエンコードします (8 つの可能なビットの組み合わせ)。
  • 最後の 3 ビットは、最初の 2 ビットの設定に応じて、別のレジスタをエンコードしたり、アドレッシング モードを拡張したりできます。

バイトの後に、ModR/M(アドレス指定モードに応じて) 別のオプションのバイト ( calendex ase ) が存在する可能性SIBSありますIBこれは、スケーリング ファクタ (1x、2x、4x)、ベース アドレス/レジスタ、および使用されるインデックス レジスタをエンコードするために、よりエキゾチックなアドレッシング モードで使用されます。バイトと同様のレイアウトですがModR/M、名前が示すように、左から最初の 2 ビット (最上位) はスケールをエンコードするために使用され、次の 3 ビットと最後の 3 ビットはインデックスとベース レジスタをエンコードします。

ディスプレイスメントが使用されている場合は、その直後に移動します。アドレッシング モードと実行モード (16 ビット/32 ビット/64 ビット) に応じて、0、1、2、または 4 バイトの長さになります。

最後のデータは、存在する場合は常に即時データです。また、0、1、2、または 4 バイト長にすることもできます。

したがって、x86 命令の全体的な形式がわかったら、これらすべてのバイトのエンコーディングを知る必要があります。そして、一般的な信念に反して、いくつかのパターンがあります。

たとえば、すべてのレジスタ エンコーディングはきちんとしたパターンに従いますACDB。つまり、8 ビット命令の場合、レジスタ コードの最下位 2 ビットが A、C、D、および B レジスタをエンコードします。

00=Aレジスタ (アキュムレータ)
01=Cレジスタ (カウンタ)
10=Dレジスタ (データ)
11=Bレジスタ (ベース)

彼らの 8 ビット プロセッサは、次のようにエンコードされた 4 つの 8 ビット レジスタだけを使用していたのではないかと思います。

       second
      +---+---+
f     | 0 | 1 |          00 = A
i +---+---+---+          01 = C
r | 0 | A : C |          10 = D
s +---+ - + - +          11 = B
t | 1 | D : B |
  +---+---+---+

次に、16 ビット プロセッサでは、このレジスタ バンクを 2 倍にし、バンクを選択するためにレジスタ エンコーディングにもう 1 ビット追加しました。

       second                second         0 00  =  AL
      +----+----+           +----+----+     0 01  =  CL
f     | 0  | 1  |     f     | 0  | 1  |     0 10  =  DL
i +---+----+----+     i +---+----+----+     0 11  =  BL
r | 0 | AL : CL |     r | 0 | AH : CH |
s +---+ - -+ - -+     s +---+ - -+ - -+     1 00  =  AH
t | 1 | DL : BL |     t | 1 | DH : BH |     1 01  =  CH
  +---+---+-----+       +---+----+----+     1 10  =  DH
    0 = BANK L              1 = BANK H      1 11  =  BH

ただし、これらのレジスタの両方の半分を一緒に使用して、完全な 16 ビット レジスタとして使用することも選択できるようになりました。これは、オペコードの最後のビット(最下位ビット、右端のビット) によって行われます。これが の場合0、これは 8 ビット命令です。ただし、このビットが設定されている場合 (つまり、オペコードが奇数の場合)、これは 16 ビット命令です。このモードでは、ACDB以前と同様に、2 ビットがレジスタの 1 つをエンコードします。パターンはそのままです。しかし、現在は完全な 16 ビット レジスタをエンコードしています。しかし、3 番目のバイト (最上位バイト) も設定されると、インデックス/ポインター レジスターと呼ばれるまったく別のレジスター バンクに切り替わります。これらはSP(スタック ポインター)、BP(ベース ポインター)、SI(ソース インデックス)、DI(宛先/データ インデックス)。したがって、アドレス指定は次のようになります。

       second                second         0 00  =  AX
      +----+----+           +----+----+     0 01  =  CX
f     | 0  | 1  |     f     | 0  | 1  |     0 10  =  DX
i +---+----+----+     i +---+----+----+     0 11  =  BX
r | 0 | AX : CX |     r | 0 | SP : BP |
s +---+ - -+ - -+     s +---+ - -+ - -+     1 00  =  SP
t | 1 | DX : BX |     t | 1 | SI : DI |     1 01  =  BP
  +---+----+----+       +---+----+----+     1 10  =  SI
    0 = BANK OF           1 = BANK OF       1 11  =  DI
  GENERAL-PURPOSE        POINTER/INDEX
     REGISTERS             REGISTERS

32 ビット CPU を導入したとき、これらのバンクは再び 2 倍になりました。しかし、パターンは同じままです。奇数オペコードは 32 ビット レジスタを意味し、偶数オペコードは以前と同様に 8 ビット レジスタを意味します。CPUとその現在の動作モードに応じて16/32ビットバージョンが使用されるため、奇妙なオペコードを「ロング」バージョンと呼びます。16 ビット モードで動作する場合、奇数 (「長い」) オペコードは 16 ビット レジスタを意味しますが、32 ビット モードで動作する場合、奇数 (「長い」) オペコードは 32 ビット レジスタを意味します。命令全体に66プレフィックス (オペランド サイズのオーバーライド) を付けることで、反転させることができます。偶数オペコード (「短い」オペコード) は常に 8 ビットです。したがって、32 ビット CPU では、レジスタ コードは次のようになります。

0 00 = EAX      1 00 = ESP
0 01 = ECX      1 01 = EBP
0 10 = EDX      1 10 = ESI
0 11 = EBX      1 11 = EDI

ご覧のとおり、ACDBパターンは同じままです。こちらもSP,BP,SI,SI柄はそのまま。より長いバージョンのレジスタを使用するだけです。

オペコードにもいくつかのパターンがあります。そのうちの 1 つについては既に説明しました (偶数対奇数 = 8 ビットの「短い」対 16/32 ビットの「長い」もの)。それらの多くは、簡単な参照と手動でのアセンブル/逆アセンブルのために一度作成したこのオペコードマップで見ることができます: ここに画像の説明を入力 (まだ完全なテーブルではなく、いくつかのオペコードが欠落しています. いつか更新するかもしれません.)

ご覧のとおり、算術命令と論理命令はほとんど表の上半分に配置され、左半分と右半分は同様のレイアウトに従います。データ移動命令は下半分にあります。すべての分岐命令 (条件付きジャンプ) は行 にあり7*ます。また、命令B*用に予約された完全な行が 1 つありmovます。これは、即値 (定数) をレジスタにロードするための省略形です。これらはすべて 1 バイトのオペコードであり、直後に定数が続きます。これは、オペコードの宛先レジスタをエンコードするため (この表の列番号によって選択されます)、最下位 3 バイト (右端のもの) に格納されます。 . これらは、レジスタ エンコーディングと同じパターンに従います。4 番目のビットは、「ショート」/「ロング」のどちらかを選択します。あなたはそれを見ることができますimul命令はすでにテーブルにあり、正確にその69位置にあります (huh.. ;J)。

多くの命令では、「ショート/ロング」ビットの直前のビットは、オペランドの順序をエンコードするためのものです。ModR/Mバイトにエンコードされた 2 つのレジスタのどちらがソースで、どちらがデスティネーションであるか (これは命令に適用されます) 2 つのレジスタ オペランドを使用)。

ModR/Mバイトのアドレッシング モード フィールドについては、次のように解釈します。

  • 11は最も単純で、レジスタからレジスタへの転送をエンコードします。1 つのレジスタは次の 3 ビット (フィールド) によってエンコードされ、もう 1 つのレジスタはこのバイトregの残りの 3 ビット (フィールド) によってエンコードされます。R/M
  • 01このバイトの後に、1 バイトの変位が存在することを意味します。
  • 10同じ意味ですが、使用される変位は 4 バイトです (32 ビット CPU の場合)。
  • 00フィールドの内容に応じて、間接アドレス指定または単純な置換を意味しR/Mます。

バイトが存在する場合は、ビット内のビット パターンSIBによって通知されます。バイトをまったく使用しない 32 ビット ディスプレイスメントのみのモードのコードもあります。100R/M101SIB

これらすべてのアドレッシング モードの概要を以下に示します。

Mod R/M
 11 rrr = register-register  (one encoded in `R/M` bits, the other one in `reg` bits).
 00 rrr = [ register ]       (except SP and BP, which are encoded in `SIB` byte)
 00 100 = SIB byte present
 00 101 = 32-bit displacement only (no `SIB` byte required)
 01 rrr = [ rrr + disp8 ]    (8-bit displacement after the `ModR/M` byte)
 01 100 = SIB + disp8
 10 rrr = [ rrr + disp32 ]   (except SP, which means that the `SIB` byte is used)
 10 100 = SIB + disp32

それでは、あなたのをデコードしましょうimul

69はそのオペコードです。imulこれは、8 ビット オペランドを符号拡張しない のバージョンをエンコードします。バージョンはそれら6Bを符号拡張します。(誰かが尋ねた場合、それらはオペコードのビット 1 によって異なります。)

62RegR/Mバイトです。バイナリでは0110 0010または01 100 010です。最初の 2 バイト (Modフィールド) は間接アドレッシング モードを意味し、ディスプレースメントは 8 ビットになります。次の 3 ビット (regフィールド) は、宛先レジスタとしてレジスタ (この場合は 32 ビット モードであるため)100をエンコードします。最後の 3 ビットはフィールドであり、使用される他の (ソース) レジスタとしてレジスタ (この場合は)をエンコードします。SPESPR/M010DEDX

ここで、8 ビットの変位が予想されます。2f変位、正の変位 (10 進数で +47) です。

最後の部分は、imul命令に必要な即値定数の 4 バイトです。あなたの場合、これは6c 64 2d 6cリトルエンディアンでは$6c2d646c.

そして、それがクッキーが崩れる方法です;-J

于 2013-08-19T04:14:00.070 に答える
3

マニュアルには、1つ、2つ、または3つのオペランドバージョンを区別する方法が記載されています。

IMUL命令

F6 / F7:1つのオペランド。0F AF:2つのオペランド。6B / 69:3つのオペランド。

于 2011-08-03T10:41:58.347 に答える
3

いくつかのアドバイスは、最初にあなたが手に入れることができるすべての命令セットドキュメントを入手してください。このx86の場合は、Intelやネット上の豊富なオペコードテーブルから、古い8088/86マニュアルだけでなく、最近のマニュアルも試してみてください。さまざまな解釈と文書化には、最初に微妙な文書化の誤りや違いがあるかもしれません。次に、一部の人々は、異なる、より理解しやすい方法で情報を提示するかもしれません。

次に、これが最初の逆アセンブラである場合、x86を避けることをお勧めします。これは非常に困難です。あなたの質問は可変ワード長の命令セットが難しいことを示唆しているので、リモートで成功する逆アセンブラを作成するには、メモリの順序ではなく、実行の順序でコードに従う必要があります。したがって、逆アセンブラは、命令をデコードして出力するだけでなく、ジャンプ命令をデコードし、宛先アドレスを命令へのエントリポイントとしてタグ付けするために、ある種のスキームを使用する必要があります。たとえば、ARMは固定命令長であり、ramの先頭から始まり、各単語をまっすぐに逆アセンブルするARM逆アセンブラを記述できます(もちろん、armとthumbコードの混合ではないと仮定します)。サム(thumb2ではない)は、32ビット命令のフレーバーが1つしかないため、この方法で分解できます。それ以外はすべて16ビットです。

(可変長の命令セットを使用して)すべてを分解することはできません。また、いくつかのハンドコーディングのニュアンスや、コードを実行順序でウォークする先行コードの分解を防ぐための意図的な戦術により、私が呼ぶものが含まれる場合があります。衝突、例えば上記の指示。1つのパスで命令へのエントリポイントである0x69に移動し、それから7バイトの命令であると判断するとしますが、他の場所に、宛先が命令のオペコードである0x2fとして計算される分岐命令があるとします。非常に巧妙なプログラミングはそのようなことをやってのけるかもしれません、それは逆アセンブラーがデータを逆アセンブルするように導かれた可能性が高いです。例えば

clear condition flag
branch if condition flag clear
data

逆アセンブラはデータがデータであることを認識せず、追加のスマートがなければ、逆アセンブラは条件分岐が実際には無条件分岐であることを認識しません(条件クリアと条件クリアの場合は分岐の間の異なる分岐パスに多くの命令がある可能性があります)。条件分岐後のバイトは命令です。

最後に、私はあなたの努力を称賛します。私はしばしば、命令セットを非常によく学ぶために、単純な逆アセンブラ(コードが非常に短く、意図的に作成されたコードであると想定するもの)を書くことを説きます。逆アセンブラを実行順序で実行する必要があり、代わりにメモリ順序で実行できる状況にしない場合(基本的に、命令間にデータを埋め込まないで、最後または別の場所に配置して、逆アセンブルする命令の文字列のみを残します) )。命令セットのオペコードデコードを理解すると、低水準言語と高水準言語の両方で、そのプラットフォームのプログラミングが大幅に向上します。

短い答え、インテルはプロセッサ用のテクニカルリファレンスマニュアルを発行していたが、おそらく今でもそうしている。私はまだ8088/86マニュアル、電気関連のもの用のハードウェア、命令セットとその仕組み用のソフトウェアを持っている。私は486とおそらく386を持っています。Igorの回答のスナップショットは、Intelのマニュアルに直接似ています。命令セットは時間の経過とともに大きく進化したため、x86はせいぜい難しい獣になります。同時に、プロセッサ自体がこれらのバイトを通り抜けて実行できる場合は、同じことを実行できるがデコードできるプログラムを作成できます。

于 2011-08-03T14:25:29.047 に答える
1

これはマシン コード命令ではありません (オペコードと 0 個以上のオペランドで構成されます)。

これはテキスト文字列の一部であり、次のように変換されます。

$ echo -e "\x69\x62\x2f\x6c\x64\x2d\x6c"
ib/ld-l

これは明らかに string の一部です"/lib/ld-linux.so.2"

于 2011-08-04T20:23:35.497 に答える
0

オペコード テーブル/マニュアルを参照する気がない場合は、オープン ソースの逆アセンブラーであるbea-engineなどの他のプロジェクトから学ぶことが常に役に立ちます。あなたが何のためにそれをしているかに応じて。

于 2011-08-03T10:53:35.833 に答える