Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19356] ファイルの概要

このコミットは、Go言語のcmd/objdumpツールにx86アーキテクチャ用のディスアセンブラを導入するものです。これにより、objdumpツールがx86バイナリの機械語命令を人間が読めるアセンブリコードに逆アセンブルする機能を持つようになります。

コミット

cmd/objdump: import x86 disassembler

The x86 disassembler lives in rsc.io/x86/x86asm for now.
We need to figure out what should live where in the long term,
but not before the 1.3 release.

The completed code reviews for the disassembler are at:
https://golang.org/cl/95350044
https://golang.org/cl/95300044
https://golang.org/cl/97100047
https://golang.org/cl/93110044
https://golang.org/cl/99000043
https://golang.org/cl/98990043

LGTM=crawshaw
R=crawshaw, jacek.masiulaniec
CC=golang-codereviews
https://golang.org/cl/92360043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e7ad1ebeac01ebfbc36b9848b05aa45712b3e2a2

元コミット内容

commit e7ad1ebeac01ebfbc36b9848b05aa45712b3e2a2
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 14 19:46:53 2014 -0400

    cmd/objdump: import x86 disassembler
    
    The x86 disassembler lives in rsc.io/x86/x86asm for now.
    We need to figure out what should live where in the long term,
    but not before the 1.3 release.
    
    The completed code reviews for the disassembler are at:
    https://golang.org/cl/95350044
    https://golang.org/cl/95300044
    https://golang.org/cl/97100047
    https://golang.org/cl/93110044
    https://golang.org/cl/99000043
    https://golang.org/cl/98990043
    
    LGTM=crawshaw
    R=crawshaw, jacek.masiulaniec
    CC=golang-codereviews
    https://golang.org/cl/92360043

変更の背景

このコミットの主な背景は、Go言語の標準ツールであるobjdumpにx86アーキテクチャのディスアセンブル機能を追加することです。Go言語はクロスコンパイルをサポートしており、様々なアーキテクチャ向けのバイナリを生成できます。objdumpはこれらのバイナリを解析し、機械語命令をアセンブリコードに変換して表示するための重要なツールです。

以前は、Goのobjdumpは特定のアーキテクチャ(例えばGoが動作するホストアーキテクチャ)に限定されたディスアセンブル機能しか持っていなかったか、あるいはx86に対する十分なサポートがありませんでした。このコミットにより、objdumpはx86バイナリ(Goプログラムだけでなく、一般的なx86バイナリも含む)を正確にディスアセンブルできるようになります。

コミットメッセージには「The x86 disassembler lives in rsc.io/x86/x86asm for now. We need to figure out what should live where in the long term, but not before the 1.3 release.」とあります。これは、このディスアセンブラが一時的にrsc.io/x86/x86asmという外部パッケージとして開発され、Go 1.3リリース前にGo本体のcmd/objdumpに取り込まれたことを示唆しています。長期的な配置については、Go 1.3リリース後に検討される予定でした。

この機能追加は、Go開発者が生成されたバイナリの低レベルな動作を理解したり、デバッグしたりする上で非常に役立ちます。特に、パフォーマンスの最適化や、コンパイラが生成するコードの検証において不可欠なツールとなります。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. ディスアセンブラ (Disassembler): 機械語(バイナリコード)を人間が読めるアセンブリ言語に変換するツールです。CPUが直接実行する命令列を、対応するニーモニック(例: MOV, ADD, JMP)とオペランドに変換します。デバッグ、リバースエンジニアリング、セキュリティ分析などに用いられます。

  2. x86アーキテクチャ: Intel 8086プロセッサから続く、広く普及している命令セットアーキテクチャ(ISA)です。デスクトップPCやサーバーで主に使用されます。x86命令セットは非常に複雑で、可変長の命令、多数のプレフィックス、多様なアドレッシングモード(ModR/M、SIBバイトなど)が特徴です。

  3. x86命令の構造: x86命令は、一般的に以下の要素で構成されます。

    • プレフィックス (Prefixes): 命令の動作を変更するオプションのバイト列です。
      • REXプレフィックス (REX Prefix): 64ビットモードでのみ使用され、レジスタの拡張(R8-R15など)、オペランドサイズの指定(REX.W)、SIBバイトやModR/Mバイトの拡張などに使われます。
      • セグメントオーバーライドプレフィックス (Segment Override Prefixes): メモリ参照の際に使用するセグメントレジスタ(CS, DS, ES, FS, GS, SS)を明示的に指定します。
      • オペランドサイズオーバーライドプレフィックス (Operand Size Override Prefix - 0x66): デフォルトのオペランドサイズ(例: 32ビットモードでの32ビット)を切り替えます(例: 16ビット)。
      • アドレスサイズオーバーライドプレフィックス (Address Size Override Prefix - 0x67): デフォルトのアドレスサイズ(例: 32ビットモードでの32ビット)を切り替えます(例: 16ビット)。
      • 繰り返しプレフィックス (Repeat Prefixes - 0xF2, 0xF3): 文字列操作命令などで繰り返し動作を指定します(例: REP, REPE, REPNE)。
      • ロックプレフィックス (LOCK Prefix - 0xF0): メモリ操作をアトミックに行うことを保証します。
    • オペコード (Opcode): 命令の種類を示すバイトです。1バイトから3バイト以上になることもあります。
    • ModR/Mバイト (ModR/M Byte): オペランドがレジスタかメモリか、どのアドレッシングモードを使用するか、どのレジスタが関与するかなどを指定します。
      • Mod (2ビット): オペランドがレジスタか、またはメモリ参照の場合のディスプレースメントの有無とサイズ。
      • Reg/Opcode (3ビット): レジスタオペランド、またはオペコードの拡張。
      • R/M (3ビット): レジスタオペランド、またはメモリ参照のベースレジスタ/SIBバイトの有無。
    • SIBバイト (Scale-Index-Base Byte): ModR/Mバイトが特定のメモリ参照(ベースレジスタ + インデックスレジスタ * スケール + ディスプレースメント)を示す場合に存在します。
      • Scale (2ビット): インデックスレジスタに乗算するスケールファクタ(1, 2, 4, 8)。
      • Index (3ビット): インデックスレジスタ。
      • Base (3ビット): ベースレジスタ。
    • ディスプレースメント (Displacement): メモリ参照のオフセット値。1, 2, 4バイトのいずれか。
    • イミディエイト (Immediate): 命令に直接埋め込まれる定数値。1, 2, 4, 8バイトのいずれか。
  4. Go言語のcmdパッケージ: Goの標準ツール群(コンパイラ、リンカ、アセンブラ、デバッガなど)が格納されているパッケージです。cmd/objdumpもその一つです。

  5. Goのコード生成ツール (bundle): このコミットではbundleというツールが使われています。これは、Goのソースコードを別のGoソースコードに埋め込む(バンドルする)ためのツールです。外部パッケージのコードをGo本体のリポジトリに直接取り込む際に利用されます。これにより、依存関係の管理が簡素化され、ビルドプロセスが安定します。

これらの知識は、x86.goファイル内のコードがどのようにx86命令を解析し、各バイトの意味を解釈しているかを理解する上で不可欠です。

技術的詳細

このコミットの技術的詳細の核心は、x86命令の複雑なデコードロジックをGo言語で実装している点にあります。特に注目すべきは、可変長命令、多数のプレフィックス、ModR/MおよびSIBバイトの解釈、そして異なるCPUモード(16/32/64ビット)への対応です。

src/cmd/objdump/x86.goファイルは、主に以下の要素で構成されています。

  1. 命令デコードのバイトコードプログラム: x86_decodeOp型とそれに対応する定数(x86_xFail, x86_xMatch, x86_xJump, x86_xCondByteなど)は、x86命令のデコードプロセスを記述するためのカスタムバイトコード命令セットを定義しています。このバイトコードは、命令ストリームからバイトを読み込み、条件分岐を行い、オペランドを解釈するためのものです。 x86_decoderというグローバル変数がこのバイトコードプログラムを保持しており、x86_Decode関数がこのプログラムを実行して命令をデコードします。このアプローチは、複雑な命令セットのデコードロジックを、より構造化された、テーブル駆動型またはバイトコード駆動型で表現するための一般的な手法です。

  2. プレフィックスの処理: x86命令のデコードにおいて、プレフィックスの処理は最も複雑な部分の一つです。

    • x86_Decode関数は、まず命令の先頭からプレフィックスを読み込みます。
    • 0xF0 (LOCK), 0xF2 (REPN), 0xF3 (REP), 0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65 (セグメントオーバーライド), 0x66 (オペランドサイズオーバーライド), 0x67 (アドレスサイズオーバーライド) などの各プレフィックスが識別され、inst.Prefix配列に格納されます。
    • 64ビットモードでは、REXプレフィックス(0x40から0x4F)も処理されます。REXプレフィックスは、レジスタの拡張や64ビットオペランドサイズ(REX.W)の指定に用いられます。
    • 複数のプレフィックスが存在する場合の優先順位や、意味を持たないプレフィックスの扱い(無視されるか、例外を発生させるか)に関する複雑なロジックが実装されています。特に、x86_xCondPrefixのセクションでは、F2/F3プレフィックスと66hプレフィックスの競合に関する詳細なコメントがあり、Intel XEDとGNU libopcodesの動作の違いが考慮されています。
  3. ModR/MおよびSIBバイトの解析:

    • x86_xCondSlashRx86_xReadSlashRなどのデコードオペレーションがModR/Mバイトの読み込みと解析をトリガーします。
    • ModR/Mバイトからmod, regop, rmフィールドが抽出されます。
    • modrmの値に基づいて、オペランドがレジスタ参照なのか、あるいはメモリ参照なのかが判断されます。
    • メモリ参照の場合、アドレスモード(16/32/64ビット)に応じて、ディスプレースメントの読み込みや、SIBバイトの有無がチェックされます。
    • SIBバイトが存在する場合、scale, index, baseフィールドが抽出され、メモリのアドレッシング計算に利用されます。
  4. イミディエイト値の読み込み: x86_xReadIb, x86_xReadIw, x86_xReadId, x86_xReadIoなどのオペレーションにより、命令に埋め込まれたイミディエイト値(即値)がバイトストリームから読み込まれます。リトルエンディアン形式で読み込まれることが明示されています。

  5. 命令の正規化と特殊ケースの処理:

    • デコードされた命令はx86_Inst構造体に格納されます。
    • XCHG EAX, EAXNOPとして扱われるなど、特定の命令の特殊な振る舞いが後処理で調整されます。
    • 文字列操作命令(MOVSB, LODSBなど)や入出力命令(INS, OUTS)のように、暗黙的なオペランドを持つ命令に対して、それらのオペランドが明示的に追加されます。
    • 条件分岐命令に対するブランチ予測プレフィックス(0x2E, 0x3E)の解釈や、Intel TSX (Transactional Synchronization Extensions) 関連のXACQUIRE/XRELEASEプレフィックスの処理も含まれています。
  6. GNUアセンブラ構文の生成 (gnu.go部分): x86_GNUSyntax関数は、デコードされた命令をGNUアセンブラ(AT&T構文)形式の文字列に変換します。

    • オペコード名の調整(例: FDIVFDIVRの入れ替え)。
    • MONITOR, MWAITなどの命令に対する暗黙的な引数の追加。
    • プレフィックスの表示ルール調整。特に、CRC32MOV命令におけるプレフィックスの表示に関する複雑なロジックが含まれています。
    • オペランドサイズを示すサフィックス(例: b, w, l, q)の付与ロジック。レジスタオペランドからサイズが推測できる場合はサフィックスを省略するなど、GNUアセンブラの慣習に従っています。

このディスアセンブラは、x86命令セットの膨大な複雑さを、Go言語の型システムとバイトコード駆動のアプローチで効率的に処理しようと試みていることがわかります。特に、プレフィックスの競合解決や、GNUアセンブラの出力形式に合わせるための調整は、x86ディスアセンブラ開発における一般的な課題であり、このコードがそれらに対応していることを示しています。

コアとなるコードの変更箇所

このコミットで追加された主要なファイルは以下の2つです。

  1. src/cmd/objdump/Makefile:

    --- /dev/null
    +++ b/src/cmd/objdump/Makefile
    @@ -0,0 +1,5 @@
    +x86.go: bundle
    +	./bundle -p main -x x86_ rsc.io/x86/x86asm >x86.go
    +
    +bundle:
    +	go build -o bundle code.google.com/p/rsc/cmd/bundle
    

    このMakefileは、x86.goファイルを生成するためのルールを定義しています。rsc.io/x86/x86asmパッケージのコードをbundleツールを使ってx86.goにバンドルしています。

  2. src/cmd/objdump/x86.go: このファイルは新規追加され、x86ディスアセンブラの主要なロジックを含んでいます。非常に大規模なファイルであり、その内容は以下の主要なセクションに分けられます。

    • decode.goセクション:

      • x86_decodeOp型と関連する定数(x86_xFail, x86_xMatch, x86_xJump, x86_xCondByteなど)の定義。これらはディスアセンブラのバイトコード命令です。
      • x86_Inst構造体(デコードされた命令を表す)の定義。
      • x86_Decode関数: 実際の命令デコードロジック。プレフィックスの解析、ModR/MおよびSIBバイトの処理、イミディエイト値の読み込み、そしてバイトコードプログラムの実行が含まれます。
      • x86_truncated, x86_instPrefixなどのヘルパー関数。
      • x86_ErrInvalidMode, x86_ErrTruncated, x86_ErrUnrecognizedなどのエラー定義。
      • x86_addr16, x86_baseReg, x86_prefixToSegment, x86_fixedArg, x86_memBytes, x86_isCondJmp, x86_isLoopなどの補助的なデータテーブルと関数。
    • gnu.goセクション:

      • x86_GNUSyntax関数: デコードされた命令をGNUアセンブラ(AT&T)構文でフォーマットするロジック。
      • GNUアセンブラの出力における特殊なケース(例: FDIVFDIVRの入れ替え、MOVNTSSの優先順位、暗黙的な引数の追加、プレフィックスの表示ルール、オペランドサイズサフィックスの付与)を処理します。

このコミットは、実質的にrsc.io/x86/x86asmパッケージの大部分をcmd/objdumpに統合したものです。

コアとなるコードの解説

src/cmd/objdump/x86.goのコアとなる部分は、x86_Decode関数と、それを駆動するx86_decoderバイトコードプログラムです。

x86_Decode関数の動作フロー

  1. 初期化:

    • 入力バイト列srcとCPUモード(16, 32, 64ビット)を受け取ります。
    • 最大命令長(15バイト)を超える場合はsrcを切り詰めます。
    • プレフィックス、ModR/M、SIB、イミディエイト値などを格納するための変数を初期化します。
  2. プレフィックスの読み込み (ReadPrefixesループ):

    • 命令の先頭からバイトを順に読み込み、それがプレフィックスであるかを判定します。
    • LOCK (0xF0), REP/REPN (0xF2, 0xF3), セグメントオーバーライド (0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65), オペランドサイズオーバーライド (0x66), アドレスサイズオーバーライド (0x67) などのプレフィックスを識別し、inst.Prefix配列に格納します。
    • 64ビットモードでは、REXプレフィックスもここで処理され、dataMode(オペランドサイズ)がREX.Wビットによって64ビットに設定される場合があります。
    • 複数の同じ種類のプレフィックスが存在する場合(例: 複数のREPプレフィックス)、後続のプレフィックスが以前のものを上書きする(または無視する)ロジックが実装されています。
  3. デコードループ (Decodeループ):

    • x86_decoderというバイトコード配列をプログラムカウンタpcで順に実行します。
    • 各バイトコード命令(x86_decodeOp)は、命令のデコードプロセスにおける特定のステップを表します。
    • x86_xCondByte: 次のバイトが特定の値であるかによって分岐します。これにより、オペコードの最初のバイトに基づいて異なるデコードパスに進みます。
    • x86_xCondSlashR: ModR/Mバイトのregopフィールドの値に基づいて分岐します。これは、同じオペコードでもregフィールドによって異なる命令になる場合(例: INC, DEC)に用いられます。
    • x86_xCondPrefix: どのプレフィックスが存在するかによって分岐します。これは、F2/F3/66hなどのプレフィックスが命令の意味を変化させる場合に重要です。特に、Intel XEDとGNU libopcodesの動作の違いを吸収するための複雑なロジックが含まれています。
    • x86_xCondIs64, x86_xCondDataSize, x86_xCondAddrSize, x86_xCondIsMem: CPUモード、オペランドサイズ、アドレスサイズ、オペランドがメモリ参照かレジスタ参照かによって分岐します。
    • x86_xReadSlashR: ModR/Mバイトを読み込み、mod, regop, rmを解析します。メモリ参照の場合、SIBバイトやディスプレースメントも読み込み、x86_Mem構造体に格納します。
    • x86_xReadIb, x86_xReadIw, x86_xReadId, x86_xReadIo, x86_xReadCb, x86_xReadCw, x86_xReadCd, x86_xReadCp, x86_xReadCm: 各種サイズのイミディエイト値やオフセット値を命令ストリームから読み込みます。
    • x86_xSetOp: デコードされた命令のオペコード(inst.Op)を設定します。
    • x86_xArg...: デコードされたオペランド(レジスタ、メモリ、イミディエイト、相対アドレスなど)をinst.Args配列に追加します。
  4. 後処理と正規化:

    • デコードループが終了した後、inst.Op0の場合は無効な命令としてエラーを返します。
    • XCHG EAX, EAXのような特殊なNOP命令の処理。
    • INSB, MOVSBなどの文字列操作命令やMONITOR, MWAITなどの命令に対する暗黙的なオペランドの追加。
    • アドレスサイズプレフィックスやセグメントプレフィックスが、命令のオペランドによって暗黙的に使用される場合にx86_PrefixImplicitフラグを設定し、ディスアセンブル出力時に表示されないようにします。
    • 条件分岐命令に対するブランチ予測プレフィックス(0x2E, 0x3E)の変換。
    • Intel MPX (Memory Protection Extensions) のBNDプレフィックスや、Intel TSX (Transactional Synchronization Extensions) のXACQUIRE/XRELEASEプレフィックスの処理。
    • REPプレフィックスが適用できない命令に対しては、そのプレフィックスを無視するようマークします。
    • REXプレフィックスのビットがすべて使用された場合、そのプレフィックスを暗黙的としてマークします。

x86_GNUSyntax関数の動作

この関数は、デコードされたx86_Inst構造体を受け取り、GNUアセンブラ(AT&T)形式の文字列を生成します。

  1. オペコード名の調整: 一部の浮動小数点命令(FDIV, FSUBなど)やMOVNTSDのように、GNUアセンブラがIntelマニュアルとは異なるニーモニックを使用する場合や、プレフィックスの順序によってニーモニックが変化する場合に対応します。

  2. 暗黙的な引数の追加: MONITOR, MWAITなどの命令に対して、GNUアセンブラの慣習に従って暗黙的なレジスタ引数(例: EAX, ECX, EDX)を追加します。

  3. プレフィックスの表示調整:

    • CRC32命令のように、特定のプレフィックス(例: 0xF2)が複数存在する場合の表示ルールを調整します。
    • MOV命令におけるセグメントレジスタの扱いなど、オペランドサイズプレフィックスの表示に関する特殊なケースを処理します。
    • XACQUIRE/XRELEASEプレフィックスの表示優先順位を調整します。
  4. オペコードサフィックスの決定:

    • GNUアセンブラでは、オペランドのサイズがレジスタから推測できる場合、オペコードのサフィックス(例: b for byte, w for word, l for long, q for quadword)を省略する慣習があります。このロジックは、needSuffixフラグとSuffixLoopによって実装されています。
    • CRC32, LGDT, LIDT, MOVZX, MOVSX, LOOP系命令、CALL, JMP, RET, PUSH, POPなど、特定の命令に対するサフィックスの付与ルールが詳細に定義されています。特に、MOVZXMOVSXのように2つのサフィックスを持つ命令も処理されます。
    • 浮動小数点命令のサフィックス(s, l, tなど)も、メモリオペランドのサイズに基づいて決定されます。

これらのコードは、x86命令セットの複雑な仕様をGo言語で正確にモデル化し、さらにGNUアセンブラの出力慣習に合わせるための細かな調整を行っていることがわかります。これは、低レベルなバイナリ解析ツールを開発する上で非常に重要な側面です。

関連リンク

参考にした情報源リンク

  • コミットメッセージに記載されているGerrit Change-List (CL) リンク:

  • rsc.io/x86/x86asmパッケージ: このコミットでインポートされた元のパッケージ。Goのobjdumpに統合される前のディスアセンブラのソースコードがここにありました。

  • x86命令セットのデコードに関する一般的な情報:

    • Intel Software Developer's Manuals (特にVolume 2A: Instruction Set Reference, A-L)
    • AMD64 Architecture Programmer's Manuals
    • GNU Binutilsのopcodesライブラリのソースコード(特にx86関連の部分)は、x86ディスアセンブラの実装における一般的なアプローチや特殊なケースの処理を理解する上で参考になります。
  • Go言語のobjdumpの進化: このコミットは、Goのobjdumpがより強力なツールへと進化する過程の一部です。その後のGoのバージョンアップで、さらに多くのアーキテクチャや機能が追加されていきました。

これらの情報源は、このコミットがGo言語のツールチェインにおいてどのような位置づけであり、x86ディスアセンブラがどのように機能するかを深く理解するために役立ちます。

[インデックス 19356] ファイルの概要

このコミットは、Go言語のcmd/objdumpツールにx86アーキテクチャ用のディスアセンブラを導入するものです。これにより、objdumpツールがx86バイナリの機械語命令を人間が読めるアセンブリコードに逆アセンブルする機能を持つようになります。

コミット

cmd/objdump: import x86 disassembler

The x86 disassembler lives in rsc.io/x86/x86asm for now.
We need to figure out what should live where in the long term,
but not before the 1.3 release.

The completed code reviews for the disassembler are at:
https://golang.org/cl/95350044
https://golang.org/cl/95300044
https://golang.org/cl/97100047
https://golang.org/cl/93110044
https://golang.org/cl/99000043
https://golang.org/cl/98990043

LGTM=crawshaw
R=crawshaw, jacek.masiulaniec
CC=golang-codereviews
https://golang.org/cl/92360043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e7ad1ebeac01ebfbc36b9848b05aa45712b3e2a2

元コミット内容

commit e7ad1ebeac01ebfbc36b9848b05aa45712b3e2a2
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 14 19:46:53 2014 -0400

    cmd/objdump: import x86 disassembler
    
    The x86 disassembler lives in rsc.io/x86/x86asm for now.
    We need to figure out what should live where in the long term,
    but not before the 1.3 release.
    
    The completed code reviews for the disassembler are at:
    https://golang.org/cl/95350044
    https://golang.org/cl/95300044
    https://golang.org/cl/97100047
    https://golang.org/cl/93110044
    https://golang.org/cl/99000043
    https://golang.org/cl/98990043
    
    LGTM=crawshaw
    R=crawshaw, jacek.masiulaniec
    CC=golang-codereviews
    https://golang.org/cl/92360043

変更の背景

このコミットの主な背景は、Go言語の標準ツールであるobjdumpにx86アーキテクチャのディスアセンブル機能を追加することです。Go言語はクロスコンパイルをサポートしており、様々なアーキテクチャ向けのバイナリを生成できます。objdumpはこれらのバイナリを解析し、機械語命令をアセンブリコードに変換して表示するための重要なツールです。

以前は、Goのobjdumpは特定のアーキテクチャ(例えばGoが動作するホストアーキテクチャ)に限定されたディスアセンブル機能しか持っていなかったか、あるいはx86に対する十分なサポートがありませんでした。このコミットにより、objdumpはx86バイナリ(Goプログラムだけでなく、一般的なx86バイナリも含む)を正確にディスアセンブルできるようになります。

コミットメッセージには「The x86 disassembler lives in rsc.io/x86/x86asm for now. We need to figure out what should live where in the long term, but not before the 1.3 release.」とあります。これは、このディスアセンブラが一時的にrsc.io/x86/x86asmという外部パッケージとして開発され、Go 1.3リリース前にGo本体のcmd/objdumpに取り込まれたことを示唆しています。長期的な配置については、Go 1.3リリース後に検討される予定でした。

この機能追加は、Go開発者が生成されたバイナリの低レベルな動作を理解したり、デバッグしたりする上で非常に役立ちます。特に、パフォーマンスの最適化や、コンパイラが生成するコードの検証において不可欠なツールとなります。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. ディスアセンブラ (Disassembler): 機械語(バイナリコード)を人間が読めるアセンブリ言語に変換するツールです。CPUが直接実行する命令列を、対応するニーモニック(例: MOV, ADD, JMP)とオペランドに変換します。デバッグ、リバースエンジニアリング、セキュリティ分析などに用いられます。

  2. x86アーキテクチャ: Intel 8086プロセッサから続く、広く普及している命令セットアーキテクチャ(ISA)です。デスクトップPCやサーバーで主に使用されます。x86命令セットは非常に複雑で、可変長の命令、多数のプレフィックス、多様なアドレッシングモード(ModR/M、SIBバイトなど)が特徴です。

  3. x86命令の構造: x86命令は、一般的に以下の要素で構成されます。

    • プレフィックス (Prefixes): 命令の動作を変更するオプションのバイト列です。
      • REXプレフィックス (REX Prefix): 64ビットモードでのみ使用され、レジスタの拡張(R8-R15など)、オペランドサイズの指定(REX.W)、SIBバイトやModR/Mバイトの拡張などに使われます。
      • セグメントオーバーライドプレフィックス (Segment Override Prefixes): メモリ参照の際に使用するセグメントレジスタ(CS, DS, ES, FS, GS, SS)を明示的に指定します。
      • オペランドサイズオーバーライドプレフィックス (Operand Size Override Prefix - 0x66): デフォルトのオペランドサイズ(例: 32ビットモードでの32ビット)を切り替えます(例: 16ビット)。
      • アドレスサイズオーバーライドプレフィックス (Address Size Override Prefix - 0x67): デフォルトのアドレスサイズ(例: 32ビットモードでの32ビット)を切り替えます(例: 16ビット)。
      • 繰り返しプレフィックス (Repeat Prefixes - 0xF2, 0xF3): 文字列操作命令などで繰り返し動作を指定します(例: REP, REPE, REPNE)。
      • ロックプレフィックス (LOCK Prefix - 0xF0): メモリ操作をアトミックに行うことを保証します。
    • オペコード (Opcode): 命令の種類を示すバイトです。1バイトから3バイト以上になることもあります。
    • ModR/Mバイト (ModR/M Byte): オペランドがレジスタかメモリか、どのアドレッシングモードを使用するか、どのレジスタが関与するかなどを指定します。
      • Mod (2ビット): オペランドがレジスタか、またはメモリ参照の場合のディスプレースメントの有無とサイズ。
      • Reg/Opcode (3ビット): レジスタオペランド、またはオペコードの拡張。
      • R/M (3ビット): レジスタオペランド、またはメモリ参照のベースレジスタ/SIBバイトの有無。
    • SIBバイト (Scale-Index-Base Byte): ModR/Mバイトが特定のメモリ参照(ベースレジスタ + インデックスレジスタ * スケール + ディスプレースメント)を示す場合に存在します。
      • Scale (2ビット): インデックスレジスタに乗算するスケールファクタ(1, 2, 4, 8)。
      • Index (3ビット): インデックスレジスタ。
      • Base (3ビット): ベースレジスタ。
    • ディスプレースメント (Displacement): メモリ参照のオフセット値。1, 2, 4バイトのいずれか。
    • イミディエイト (Immediate): 命令に直接埋め込まれる定数値。1, 2, 4, 8バイトのいずれか。
  4. Go言語のcmdパッケージ: Goの標準ツール群(コンパイラ、リンカ、アセンブラ、デバッガなど)が格納されているパッケージです。cmd/objdumpもその一つです。

  5. Goのコード生成ツール (bundle): このコミットではbundleというツールが使われています。これは、Goのソースコードを別のGoソースコードに埋め込む(バンドルする)ためのツールです。外部パッケージのコードをGo本体のリポジトリに直接取り込む際に利用されます。これにより、依存関係の管理が簡素化され、ビルドプロセスが安定します。

これらの知識は、x86.goファイル内のコードがどのようにx86命令を解析し、各バイトの意味を解釈しているかを理解する上で不可欠です。

技術的詳細

このコミットの技術的詳細の核心は、x86命令の複雑なデコードロジックをGo言語で実装している点にあります。特に注目すべきは、可変長命令、多数のプレフィックス、ModR/MおよびSIBバイトの解釈、そして異なるCPUモード(16/32/64ビット)への対応です。

src/cmd/objdump/x86.goファイルは、主に以下の要素で構成されています。

  1. 命令デコードのバイトコードプログラム: x86_decodeOp型とそれに対応する定数(x86_xFail, x86_xMatch, x86_xJump, x86_xCondByteなど)は、x86命令のデコードプロセスを記述するためのカスタムバイトコード命令セットを定義しています。このバイトコードは、命令ストリームからバイトを読み込み、条件分岐を行い、オペランドを解釈するためのものです。 x86_decoderというグローバル変数がこのバイトコードプログラムを保持しており、x86_Decode関数がこのプログラムを実行して命令をデコードします。このアプローチは、複雑な命令セットのデコードロジックを、より構造化された、テーブル駆動型またはバイトコード駆動型で表現するための一般的な手法です。

  2. プレフィックスの処理: x86命令のデコードにおいて、プレフィックスの処理は最も複雑な部分の一つです。

    • x86_Decode関数は、まず命令の先頭からプレフィックスを読み込みます。
    • 0xF0 (LOCK), 0xF2 (REPN), 0xF3 (REP), 0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65 (セグメントオーバーライド), 0x66 (オペランドサイズオーバーライド), 0x67 (アドレスサイズオーバーライド) などのプレフィックスを識別し、inst.Prefix配列に格納します。
    • 64ビットモードでは、REXプレフィックス(0x40から0x4F)も処理されます。REXプレフィックスは、レジスタの拡張や64ビットオペランドサイズ(REX.W)の指定に用いられます。
    • 複数のプレフィックスが存在する場合の優先順位や、意味を持たないプレフィックスの扱い(無視されるか、例外を発生させるか)に関する複雑なロジックが実装されています。特に、x86_xCondPrefixのセクションでは、F2/F3プレフィックスと66hプレフィックスの競合に関する詳細なコメントがあり、Intel XEDとGNU libopcodesの動作の違いが考慮されています。
  3. ModR/MおよびSIBバイトの解析:

    • x86_xCondSlashRx86_xReadSlashRなどのデコードオペレーションがModR/Mバイトの読み込みと解析をトリガーします。
    • ModR/Mバイトからmod, regop, rmフィールドが抽出されます。
    • modrmの値に基づいて、オペランドがレジスタか、あるいはメモリ参照なのかが判断されます。
    • メモリ参照の場合、アドレスモード(16/32/64ビット)に応じて、ディスプレースメントの読み込みや、SIBバイトの有無がチェックされます。
    • SIBバイトが存在する場合、scale, index, baseフィールドが抽出され、メモリのアドレッシング計算に利用されます。
  4. イミディエイト値の読み込み: x86_xReadIb, x86_xReadIw, x86_xReadId, x86_xReadIoなどのオペレーションにより、命令に埋め込まれたイミディエイト値(即値)がバイトストリームから読み込まれます。リトルエンディアン形式で読み込まれることが明示されています。

  5. 命令の正規化と特殊ケースの処理:

    • デコードされた命令はx86_Inst構造体に格納されます。
    • XCHG EAX, EAXNOPとして扱われるなど、特定の命令の特殊な振る舞いが後処理で調整されます。
    • 文字列操作命令(MOVSB, LODSBなど)や入出力命令(INS, OUTS)のように、暗黙的なオペランドを持つ命令に対して、それらのオペランドが明示的に追加されます。
    • 条件分岐命令に対するブランチ予測プレフィックス(0x2E, 0x3E)の解釈や、Intel TSX (Transactional Synchronization Extensions) 関連のXACQUIRE/XRELEASEプレフィックスの処理も含まれています。
  6. GNUアセンブラ構文の生成 (gnu.go部分): x86_GNUSyntax関数は、デコードされた命令をGNUアセンブラ(AT&T構文)形式の文字列に変換します。

    • オペコード名の調整(例: FDIVFDIVRの入れ替え、MOVNTSDの優先順位)。
    • MONITOR, MWAITなどの命令に対する暗黙的な引数の追加。
    • プレフィックスの表示ルール調整。特に、CRC32MOV命令におけるプレフィックスの表示に関する複雑なロジックが含まれています。
    • オペランドサイズを示すサフィックス(例: b, w, l, q)の付与ロジック。レジスタオペランドからサイズが推測できる場合はサフィックスを省略するなど、GNUアセンブラの慣習に従っています。

このディスアセンブラは、x86命令セットの膨大な複雑さを、Go言語の型システムとバイトコード駆動のアプローチで効率的に処理しようと試みていることがわかります。特に、プレフィックスの競合解決や、GNUアセンブラの出力形式に合わせるための調整は、x86ディスアセンブラ開発における一般的な課題であり、このコードがそれらに対応していることを示しています。

コアとなるコードの変更箇所

このコミットで追加された主要なファイルは以下の2つです。

  1. src/cmd/objdump/Makefile:

    --- /dev/null
    +++ b/src/cmd/objdump/Makefile
    @@ -0,0 +1,5 @@
    +x86.go: bundle
    +	./bundle -p main -x x86_ rsc.io/x86/x86asm >x86.go
    +
    +bundle:
    +	go build -o bundle code.google.com/p/rsc/cmd/bundle
    

    このMakefileは、x86.goファイルを生成するためのルールを定義しています。rsc.io/x86/x86asmパッケージのコードをbundleツールを使ってx86.goにバンドルしています。

  2. src/cmd/objdump/x86.go: このファイルは新規追加され、x86ディスアセンブラの主要なロジックを含んでいます。非常に大規模なファイルであり、その内容は以下の主要なセクションに分けられます。

    • decode.goセクション:

      • x86_decodeOp型と関連する定数(x86_xFail, x86_xMatch, x86_xJump, x86_xCondByteなど)の定義。これらはディスアセンブラのバイトコード命令です。
      • x86_Inst構造体(デコードされた命令を表す)の定義。
      • x86_Decode関数: 実際の命令デコードロジック。プレフィックスの解析、ModR/MおよびSIBバイトの処理、イミディエイト値の読み込み、そしてバイトコードプログラムの実行が含まれます。
      • x86_truncated, x86_instPrefixなどのヘルパー関数。
      • x86_ErrInvalidMode, x86_ErrTruncated, x86_ErrUnrecognizedなどのエラー定義。
      • x86_addr16, x86_baseReg, x86_prefixToSegment, x86_fixedArg, x86_memBytes, x86_isCondJmp, x86_isLoopなどの補助的なデータテーブルと関数。
    • gnu.goセクション:

      • x86_GNUSyntax関数: デコードされた命令をGNUアセンブラ(AT&T)構文でフォーマットするロジック。
      • GNUアセンブラの出力における特殊なケース(例: FDIVFDIVRの入れ替え、MOVNTSSの優先順位、暗黙的な引数の追加、プレフィックスの表示ルール、オペランドサイズサフィックスの付与)を処理します。

このコミットは、実質的にrsc.io/x86/x86asmパッケージの大部分をcmd/objdumpに統合したものです。

コアとなるコードの解説

src/cmd/objdump/x86.goのコアとなる部分は、x86_Decode関数と、それを駆動するx86_decoderバイトコードプログラムです。

x86_Decode関数の動作フロー

  1. 初期化:

    • 入力バイト列srcとCPUモード(16, 32, 64ビット)を受け取ります。
    • 最大命令長(15バイト)を超える場合はsrcを切り詰めます。
    • プレフィックス、ModR/M、SIB、イミディエイト値などを格納するための変数を初期化します。
  2. プレフィックスの読み込み (ReadPrefixesループ):

    • 命令の先頭からバイトを順に読み込み、それがプレフィックスであるかを判定します。
    • LOCK (0xF0), REP/REPN (0xF2, 0xF3), セグメントオーバーライド (0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65), オペランドサイズオーバーライド (0x66), アドレスサイズオーバーライド (0x67) などのプレフィックスを識別し、inst.Prefix配列に格納します。
    • 64ビットモードでは、REXプレフィックスもここで処理され、dataMode(オペランドサイズ)がREX.Wビットによって64ビットに設定される場合があります。
    • 複数の同じ種類のプレフィックスが存在する場合(例: 複数のREPプレフィックス)、後続のプレフィックスが以前のものを上書きする(または無視する)ロジックが実装されています。
  3. デコードループ (Decodeループ):

    • x86_decoderというバイトコード配列をプログラムカウンタpcで順に実行します。
    • 各バイトコード命令(x86_decodeOp)は、命令のデコードプロセスにおける特定のステップを表します。
    • x86_xCondByte: 次のバイトが特定の値であるかによって分岐します。これにより、オペコードの最初のバイトに基づいて異なるデコードパスに進みます。
    • x86_xCondSlashR: ModR/Mバイトのregopフィールドの値に基づいて分岐します。これは、同じオペコードでもregフィールドによって異なる命令になる場合(例: INC, DEC)に用いられます。
    • x86_xCondPrefix: どのプレフィックスが存在するかによって分岐します。これは、F2/F3/66hなどのプレフィックスが命令の意味を変化させる場合に重要です。特に、Intel XEDとGNU libopcodesの動作の違いを吸収するための複雑なロジックが含まれています。
    • x86_xCondIs64, x86_xCondDataSize, x86_xCondAddrSize, x86_xCondIsMem: CPUモード、オペランドサイズ、アドレスサイズ、オペランドがメモリ参照かレジスタ参照かによって分岐します。
    • x86_xReadSlashR: ModR/Mバイトを読み込み、mod, regop, rmを解析します。メモリ参照の場合、SIBバイトやディスプレースメントも読み込み、x86_Mem構造体に格納します。
    • x86_xReadIb, x86_xReadIw, x86_xReadId, x86_xReadIo, x86_xReadCb, x86_xReadCw, x86_xReadCd, x86_xReadCp, x86_xReadCm: 各種サイズのイミディエイト値やオフセット値を命令ストリームから読み込みます。
    • x86_xSetOp: デコードされた命令のオペコード(inst.Op)を設定します。
    • x86_xArg...: デコードされたオペランド(レジスタ、メモリ、イミディエイト、相対アドレスなど)をinst.Args配列に追加します。
  4. 後処理と正規化:

    • デコードループが終了した後、inst.Op0の場合は無効な命令としてエラーを返します。
    • XCHG EAX, EAXのような特殊なNOP命令の処理。
    • INSB, MOVSBなどの文字列操作命令やMONITOR, MWAITなどの命令に対する暗黙的なオペランドの追加。
    • アドレスサイズプレフィックスやセグメントプレフィックスが、命令のオペランドによって暗黙的に使用される場合にx86_PrefixImplicitフラグを設定し、ディスアセンブル出力時に表示されないようにします。
    • 条件分岐命令に対するブランチ予測プレフィックス(0x2E, 0x3E)の変換。
    • Intel MPX (Memory Protection Extensions) のBNDプレフィックスや、Intel TSX (Transactional Synchronization Extensions) のXACQUIRE/XRELEASEプレフィックスの処理。
    • REPプレフィックスが適用できない命令に対しては、そのプレフィックスを無視するようマークします。
    • REXプレフィックスのビットがすべて使用された場合、そのプレフィックスを暗黙的としてマークします。

x86_GNUSyntax関数の動作

この関数は、デコードされたx86_Inst構造体を受け取り、GNUアセンブラ(AT&T)形式の文字列を生成します。

  1. オペコード名の調整: 一部の浮動小数点命令(FDIV, FSUBなど)やMOVNTSDのように、GNUアセンブラがIntelマニュアルとは異なるニーモニックを使用する場合や、プレフィックスの順序によってニーモニックが変化する場合に対応します。

  2. 暗黙的な引数の追加: MONITOR, MWAITなどの命令に対して、GNUアセンブラの慣習に従って暗黙的なレジスタ引数(例: EAX, ECX, EDX)を追加します。

  3. プレフィックスの表示調整:

    • CRC32命令のように、特定のプレフィックス(例: 0xF2)が複数存在する場合の表示ルールを調整します。
    • MOV命令におけるセグメントレジスタの扱いなど、オペランドサイズプレフィックスの表示に関する特殊なケースを処理します。
    • XACQUIRE/XRELEASEプレフィックスの表示優先順位を調整します。
  4. オペコードサフィックスの決定:

    • GNUアセンブラでは、オペランドのサイズがレジスタから推測できる場合、オペコードのサフィックス(例: b for byte, w for word, l for long, q for quadword)を省略する慣習があります。このロジックは、needSuffixフラグとSuffixLoopによって実装されています。
    • CRC32, LGDT, LIDT, MOVZX, MOVSX, LOOP系命令、CALL, JMP, RET, PUSH, POPなど、特定の命令に対するサフィックスの付与ルールが詳細に定義されています。特に、MOVZXMOVSXのように2つのサフィックスを持つ命令も処理されます。
    • 浮動小数点命令のサフィックス(s, l, tなど)も、メモリオペランドのサイズに基づいて決定されます。

これらのコードは、x86命令セットの複雑な仕様をGo言語で正確にモデル化し、さらにGNUアセンブラの出力慣習に合わせるための細かな調整を行っていることがわかります。これは、低レベルなバイナリ解析ツールを開発する上で非常に重要な側面です。

関連リンク

参考にした情報源リンク

  • コミットメッセージに記載されているGerrit Change-List (CL) リンク:

  • rsc.io/x86/x86asmパッケージ: このコミットでインポートされた元のパッケージ。Goのobjdumpに統合される前のディスアセンブラのソースコードがここにありました。

  • x86命令セットのデコードに関する一般的な情報:

    • Intel Software Developer's Manuals (特にVolume 2A: Instruction Set Reference, A-L)
    • AMD64 Architecture Programmer's Manuals
    • GNU Binutilsのopcodesライブラリのソースコード(特にx86関連の部分)は、x86ディスアセンブラの実装における一般的なアプローチや特殊なケースの処理を理解する上で参考になります。
  • Go言語のobjdumpの進化: このコミットは、Goのobjdumpがより強力なツールへと進化する過程の一部です。その後のGoのバージョンアップで、さらに多くのアーキテクチャや機能が追加されていきました。

これらの情報源は、このコミットがGo言語のツールチェインにおいてどのような位置づけであり、x86ディスアセンブラがどのように機能するかを深く理解するために役立ちます。