it-swarm.dev

アセンブリから機械コードへの移行方法(コード生成)

コードをマシンコードにアセンブルする間のステップを視覚化する簡単な方法はありますか?

たとえば、バイナリファイルについてメモ帳で開くと、マシンコードのテキスト形式の表現が表示されます。私はあなたが見る各バイト(記号)がそのバイナリ値に対応するASCII文字であると思いますか?

しかし、どのようにしてアセンブリからバイナリに移行しますか?舞台裏では何が起こっていますか?

16
user12979

命令セットのドキュメントを見ると、このようなエントリが各命令の pic microcontroller から見つかります。

example addlw instruction

"encoding"行は、その命令がバイナリでどのように見えるかを示しています。この場合、それは常に5つの1で始まり、次にドントケアビット(1または0のいずれかになります)、次に「k」は追加するリテラルを表します。

最初の数ビットは「オペコード」と呼ばれ、命令ごとに固有です。 CPUは基本的にはオペコードを見てそれがどの命令であるかを確認し、次に「k」を追加する数値としてデコードすることを認識します。

面倒ですが、エンコードとデコードはそれほど難しくありません。私は試験で手で行わなければならない学部生のクラスを持っていました。

実際に完全な実行可能ファイルを作成するには、メモリの割り当て、ブランチオフセットの計算、および [〜#〜] elf [〜#〜] のような形式に置く必要もあります。オペレーティングシステム。

28
Karl Bielefeldt

アセンブリオペコードは、ほとんどの場合、基になる機械命令と1対1で対応しています。したがって、必要なのは、アセンブリ言語で各オペコードを識別し、それを対応する機械命令にマップし、対応するパラメーター(存在する場合)とともに機械命令をファイルに書き込むことだけです。次に、ソースファイル内の追加のオペコードごとにこのプロセスを繰り返します。

もちろん、オペレーティングシステムで適切にロードして実行する実行可能ファイルを作成するには、それ以上の時間がかかります。まともなアセンブラーには、オペコードからマシン命令(たとえば、マクロ)への単純なマッピング以外にもいくつかの追加機能があります。

10
Robert Harvey

最初に必要なのは このファイル のようなものです。これは、NASMアセンブラによって使用されるx86プロセッサの命令データベースです(実際に命令を変換する部分ではありませんが、私が作成を支援しました)。データベースから任意の行を選びましょう:

ADD   rm32,imm8    [mi:    hle o32 83 /0 ib,s]      386,LOCK

これが意味することは、それがADD命令を記述するということです。この命令には複数のバリアントがあり、ここで説明されているのは、32ビットレジスタまたはメモリアドレスのいずれかを取り、8ビットの即値(つまり、命令に直接含まれる定数)を追加するバリアントです。このバージョンを使用するアセンブリ命令の例は次のとおりです。

add eax, 42

次に、テキスト入力を受け取り、それを個々の命令とオペランドに解析する必要があります。上記の命令の場合、これはおそらく、命令ADDとオペランドの配列(レジスタEAXおよび値42への参照)を含む構造になります。 。この構造を作成したら、命令データベースを実行して、命令名とオペランドのタイプの両方に一致する行を見つけます。一致が見つからない場合、それはユーザーに提示する必要があるエラーです(「オペコードとオペランドの違法な組み合わせ」などは通常のテキストです)。

データベースから行を取得したら、3番目の列を確認します。この命令では、次のようになります。

[mi:    hle o32 83 /0 ib,s] 

これは、必要なマシンコード命令を生成する方法を説明する一連の命令です。

  • miは、オペランドの説明です。1つはmodr/m(レジスタまたはメモリ)オペランドです(つまり、命令の最後にmodr/mバイトを追加する必要があります。後で説明します)と1つの即時命令(命令の説明で使用されます)。
  • 次はhleです。これは、「ロック」接頭辞の処理方法を識別します。 「ロック」を使用していないため、無視します。
  • 次はo32です。これは、16ビット出力形式のコードをアセンブルする場合、命令にはオペランドサイズのオーバーライドプレフィックスが必要であることを示しています。 16ビット出力を生成している場合は、ここでプレフィックス(0x66)を生成しますが、そうではないと想定して続行します。
  • 次は83です。これは、16進数のリテラルバイトです。出力します。
  • 次は/0です。これはmodr/mバイテムで必要となるいくつかの追加ビットを指定し、それを生成させます。 modr/mバイトは、レジスタまたは間接メモリ参照をエンコードするために使用されます。そのようなオペランドが1つ、レジスタがあります。レジスタには番号があり、これは 別のデータファイル で指定されています。

    eax     REG_EAX         reg32           0
    
  • reg32が元のデータベースからの命令の必要なサイズと一致することを確認します(一致します)。 0はレジスタの番号です。 modr/mバイトは、プロセッサによって指定されたデータ構造で、次のようになります。

     (most significant bit)
     2 bits       mod    - 00 => indirect, e.g. [eax]
                           01 => indirect plus byte offset
                           10 => indirect plus Word offset
                           11 => register
     3 bits       reg    - identifies register
     3 bits       rm     - identifies second register or additional data
     (least significant bit)
    
  • ここではレジスタを使用しているため、modフィールドは0b11です。

  • regフィールドは、使用しているレジスタの番号です0b000
  • この命令にはレジスタが1つしかないため、rmフィールドに何かを入力する必要があります。それが/0で指定された追加データの目的でした。そのため、それをrmフィールド0b000に入れました。
  • したがって、modr/mバイトは0b11000000または0xC0です。これを出力します。
  • 次はib,sです。これは、符号付き即値バイトを指定します。オペランドを見て、すぐに利用できる値があることに注意してください。これを符号付きバイトに変換して出力します(42 => 0x2A)。

したがって、完全なアセンブルされた命令は0x83 0xC0 0x2Aです。それを出力モジュールに送信します。どのバイトもメモリ参照を構成しないことに注意してください(出力モジュールがそれらを構成するかどうかを知る必要がある場合があります)。

すべての命令について繰り返します。ラベルを追跡して、参照されたときに何を挿入するかを把握します。オブジェクトファイル出力モジュールに渡されるマクロとディレクティブの機能を追加します。そして、これは基本的にアセンブラの仕組みです。

7
Jules

実際には、 アセンブラ は通常を直接生成しません一部のバイナリ 実行可能 を生成しますが、一部は生成しません- オブジェクトファイル (後で リンカー に供給されます)。ただし、例外があります(一部のアセンブラを使用して、バイナリ実行可能ファイルを直接生成できます。これらは一般的ではありません)。

まず、今日の多くのアセンブラが フリーソフトウェア プログラムであることに注意してください。だから、あなたのコンピューターに GNU asbinutils の一部)と nasm のソースコードをダウンロードしてコンパイルしてください。次に、ソースコードを調べます。ちなみに、私はその目的でLinuxを使用することをお勧めします(これは非常に開発者フレンドリーで、フリーソフトウェアに優しいOSです)。

アセンブラによって生成されたオブジェクトファイルには、特に コードセグメント および 再配置 命令が含まれています。オペレーティングシステムによって異なりますが、十分に文書化されたファイル形式で構成されています。 Linuxでは、その形式(オブジェクトファイル、共有ライブラリ、コアダンプ、実行可能ファイルに使用)は [〜#〜] elf [〜#〜] です。そのオブジェクトファイルは後で linker に入力されます(最終的に実行可能ファイルが生成されます)。再配置は [〜#〜] abi [〜#〜] で指定されます(例 x86-64 ABI )。詳細については、Levineの本 Linkers and Loaders を参照してください。

このようなオブジェクトファイルのコードセグメントには、穴のあるマシンコードが含まれています(リンカーによって、再配置情報を使用して埋められます)。アセンブラによって生成された(再配置可能な)マシンコードは、明らかに 命令セットアーキテクチャ に固有です。 x86 または x86-64 (ほとんどのラップトップまたはデスクトッププロセッサで使用されます)ISAは、その詳細が非常に複雑です。しかし、y86またはy86-64と呼ばれる簡略化されたサブセットが、教育目的で発明されました。それらについて slides をお読みください。この質問に対する他の回答も、その少しを説明しています。良い コンピュータアーキテクチャに関する本 を読むことをお勧めします。

ほとんどのアセンブラは 2つのパス で動作しており、2番目のアセンブラは再配置を発行するか、最初のパスの出力の一部を修正しています。彼らは今、通常の 解析 テクニックを使用しています(多分 The Dragon Book を読んでください)。

OSが実行可能ファイルを起動する方法 kernel (Linuxでのexecveシステムコールの動作など)は、別の(そして複雑な)問題です。通常は virtual address space をセットアップします -(process を実行する execve(2) ...を実行して)プロセスの内部状態を再初期化します( ser-mode レジスタを含む)。 ダイナミックリンカー -Linuxの ld-linux.so(8) などが実行時に含まれる可能性があります。 Operating System:Three Easy Pieces などの優れた本を読んでください。 [〜#〜] osdev [〜#〜] wikiも有用な情報を提供しています。

PS。あなたの質問は非常に幅広いため、それについていくつかの本を読む必要があります。私はいくつかの(非常に不完全な)参照を与えました。あなたはそれらのより多くを見つけるべきです。