0


0

CPU命令セットに対するユーザー定義のデータ型/操作

どのプログラミング環境でも、最終的に選択するデータ型が何であれ、CPUは算術演算(加算/論理演算)のみを実行します。

この移行(ユーザー定義のデータ型/操作からCPU命令セットへ)がどのように行われ、このライフサイクルにおけるコンパイラ、インタープリタ、アセンブラ、およびリンカの役割は何ですか

最悪の場合、ほとんどすべてがOOPS(Java言語を意味します)のオブジェクトであるため、OOPSがこのマッピングを処理する方法も。

2 Answer


3


Javaソース→ネイティブコード変換は、実際には2つの異なるステップで発生します。コンパイル時のソースコードからバイトコードへの変換(「javac」の動作)と、実行時のバイトコードからネイティブCPU命令への変換(「java `はあります)。

ソースコードが「コンパイル」されると、フィールドとメソッドがシンボルテーブルのエントリに圧縮されます。 「System.out.println()」と言うと、「javac」は「シンボル#2004で参照される静的フィールドを取得し、シンボル#300で参照されるメソッドを呼び出す」などのように変換します(#2004は「System.out」であり、#300は「void java.io.PrintStream.println()」である可能性があります。 (注意してください、私はあまりにも単純化しすぎています-シンボルはそのようには見えず、さらに分割されています。 ただし、そのような情報は含まれています。)

実行時に、JVMはこれらのシンボルを調べ、それらで参照されるクラスをロードし、メソッドを見つけて実行するために必要なネイティブ命令を実行(またはJITtingの場合は生成)します。 Javaには本当の「リンカー」はありません。すべてのリンクは、参照されるクラスに基づいて実行時に行われます。 これは、WindowsでのDLLの動作によく似ています。

JITは、「アセンブラ」に最も近いものです。 バイトコードを受け取り、同等のネイティブコードをその場で生成します。 ただし、バイトコードは人間が読める形式ではないため、通常、翻訳を「アセンブル」としてカウントしません。

…​

CやC (C / CLIではない)などの言語では、話はまったく異なります。 すべての翻訳(およびリンクの一部)はコンパイル時に行われます。 `struct`のメンバーへのアクセスは、「この特定のバイトの先頭からintを4バイト与えてください」のようなものに変換されます。 柔軟性はありません。構造体のレイアウトが変更された場合、通常はアプリ全体を再コンパイルする必要があります。


1


さまざまなサイズの整数と浮動小数点のみを持つ言語の出発​​点と、それらの型へのポインターを持つことができるメモリを指す型を考えてください。

これとCPUが使用するマシンコードとの相関関係は比較的明確です(実際、それ以上に最適化することもできます)。

コードポイントを何らかのエンコーディングで保存することで追加できる文字、およびそのような文字の配列として作成する文字列。

次に、これを次のような場所に移動したいとします。

class User
{
  int _id;
  char* _username;
  public User(int id, char* username)
  {
    _id = id;
    _username = username;
  }
  public virtaul bool IsDefaultUser()
  {
    return _id == 0;
  }
}

言語に最初に追加する必要があるのは、メンバーを含む何らかの構造体/クラス構成体です。 その後、次のようにすることができます。

class User
{
  int _id;
  char* _username;
}

コンパイルプロセスは、これが整数の後に文字の配列へのポインタを格納することを意味することを知っています。 したがって、_idにアクセスすることは、構造の開始のアドレスにある整数にアクセスすることを意味し、_usernameにアクセスすることは、構造の開始のアドレスから指定されたオフセットにあるcharへのポインターにアクセスすることを意味します。

これを考えると、コンストラクタは次のようなことをする関数として存在できます。

  _ctor_User*(int id, char* username)
  {
    User* toMake = ObtainMemoryForUser();
    toMake._id = id;
    toMake._username = ObtainMemoryAndCopyString(username);
    return toMake;
  }

必要に応じてメモリを取得してクリーンアップするのは複雑です。構造体へのポインタの使用方法と、mallocがこれを行う方法の1つを探す方法については、K&Rのセクションをご覧ください。

この時点から、次のようなIsDefaultUserを実装することもできます。

bool _impl_IsDefaultUser(*User this)
{
  return this._id == 0
}

ただし、これはオーバーライドできません。 オーバーライドできるようにするには、Userを次のように変更します。

class User
{
  UserVTable* _vTable;
  int _id;
  char* _username;
}

次に、_vTableは、関数へのポインターのテーブルを指します。この場合、このテーブルには、上記の関数へのポインターである単一のエントリが含まれます。 次に、仮想メンバーを呼び出すことは、そのテーブルへの正しいオフセットを調べ、見つかった適切な関数を呼び出すことの問題になります。 派生クラスには、オーバーライドされるメソッドの異なる関数ポインターがあることを除いて、同じになる異なる_vTableがあります。

これはひどいことであり、それぞれの場合に可能性があるだけではありません(例: vテーブルは、オーバーライド可能なメソッドを実装する唯一の方法ではありません)が、より多くのプリミティブデータ型でよりプリミティブな操作にコンパイルできるオブジェクト指向言語を構築する方法を示しています。

また、C#をILにコンパイルしてからマシンコードにコンパイルする方法などの可能性についても説明しているため、OO言語と実際に実行されるマシンコードの間に2つのステップがあります。