Cプログラムの実行時に、「int」や「char」などのデータ型宣言子はRAMに保存されますか?


74

Cプログラムの実行中、データはヒープまたはスタックに保存されます。値はRAMアドレスに保存されます。しかし、タイプインジケーター(intまたは、char)はどうでしょうか。それらも保存されていますか?

次のコードを検討してください。

char a = 'A';
int x = 4;

ここで、Aと4がRAMアドレスに保存されていることを読みました。しかし、何についてax?最も紛らわしいのは、実行がacharとxintであることをどのように認識するのでしょうか?とは、RAMのどこかにintあり、char言及されていますか?

値がRAMのどこかに10011001として保存されているとします。私がコードを実行するプログラムである場合、この10011001がaであるかどうかをどのように知ることがcharできintますか?

私は理解していないことは、それはそれがあるかどうか、アドレスなどの10001から変数の値を読み取ると、コンピュータが、知っている方法ですintchar。というプログラムをクリックすると想像してくださいanyprog.exe。コードはすぐに実行を開始します。この実行可能ファイルには、格納されている変数がタイプであるかどうかに関する情報が含まれていますintchar


24
この情報は、実行時に完全に失われます。ユーザー(およびコンパイラー)は、メモリが正しく解釈されることを事前に確認する必要があります。これはあなたが求めていた答えですか?
5gon12eder

4
そうではありません。自分が何をしているのかを知っていると仮定しているため、指定したメモリアドレスで見つかったものをすべて取得し、stdoutに書き込みます。書き込まれたものが読み取り可能な文字に対応する場合、最終的に誰かのコンソールに読み取り可能な文字として表示されます。一致しない場合は、意味不明な文字、またはランダムに読み取れる文字として表示されます。
ロバートハーヴェイ

22
@ user16307簡単な答えは、静的に型付けされた言語では、charを出力するたびに、コンパイラはintを出力する場合とは異なるコード生成するということです。実行時にはx、char という知識はなくなりましたが、実行されるのはchar-printingコードです。これはコンパイラが選択したものだからです。
Ixrec

13
@ user16307常に数値65のバイナリ表現として格納されます。65として出力されるかAとして出力されるかは、コンパイラが出力するために生成したコードによって異なります。65の横には、実際にはcharまたはint(少なくとも、Cなどの静的に型付けされた言語ではない)であるというメタデータはありません。
Ixrec

2
ここで尋ねる概念を完全に理解し、自分でそれらを実装するために、例えばCourseraの
-mucaho

回答:


122

いくつかのコメントで投稿した質問に対処するには(投稿に編集する必要があると思います):

私が理解していないのは、intまたはcharの場合、10001などのアドレスから変数の値を読み取るときにコンピューターがどのように許可するかを知ることです。anyprog.exeというプログラムをクリックすると想像してください。コードはすぐに実行を開始します。このexeファイルには、変数がinまたはcharとして保存されているかどうかに関する情報が含まれていますか?

それで、それにいくつかのコードを入れましょう。あなたが書いたとしましょう:

int x = 4;

そして、それがRAMに保存されると仮定しましょう:

0x00010004: 0x00000004

最初の部分は住所、2番目の部分は値です。プログラム(マシンコードとして実行される)が実行される0x00010004と、その値はvalueのみ0x000000004です。このデータのタイプを「認識」せず、どのように「想定」されるのかを知りません。

それで、あなたのプログラムは正しいことをどのように理解していますか?次のコードを検討してください。

int x = 4;
x = x + 5;

ここに読み取りと書き込みがあります。プログラムxがメモリから読み込むと、そこを見つけ0x00000004ます。そして、あなたのプログラム0x00000005はそれに追加することを知っています。そして、プログラムがこれが有効な操作であることを「知っている」理由は、コンパイラが型安全性によって操作が有効であることを保証するためです。コンパイラーは、追加4して5一緒にできることを既に検証しています。そのため、バイナリコードを実行するとき(exe)、その検証を行う必要はありません。すべてがOKであると仮定して、各ステップを盲目的に実行します(実際にはOKではなく実際に悪いことが起こります)。

別の考え方はこのようなものです。この情報を提供します。

0x00000004: 0x12345678

前と同じ形式-左側のアドレス、右側の値。値はどのタイプですか?この時点で、コンピューターがコードを実行しているときと同じように、その値に関する情報を把握できます。その値に12743を追加するように指示した場合は、実行できます。その操作がシステム全体にどのような影響を与えるかはわかりませんが、2つの数字を追加することは本当に得意なことなので、それを行うことができます。それは値を作りますintか?必ずしもではありません-表示されるのは、2つの32ビット値と加算演算子だけです。

おそらく、混乱の一部はデータを取り戻すことです。私たちが持っている場合:

char A = 'a';

コンピューターはどのようにaコンソールに表示するのを知っていますか?まあ、それには多くのステップがあります。1つ目はA、メモリ内のsの場所に移動して読み取ることです。

0x00000004: 0x00000061

aASCII の16進値は0x61であるため、上記はメモリに表示されるものです。したがって、マシンコードは整数値を認識します。整数値を文字に変換して表示する方法はどのようにわかりますか?簡単に言えば、コンパイラーは、その移行を行うために必要なすべてのステップを確実に入れるようにしました。しかし、コンピューター自体(またはプログラム/ exe)には、そのデータの種類がわかりません。その32ビット値は何であってもよい- intcharの、半分double、ポインタ、アレイの一部の部分string、命令の一部、等


プログラム(exe)がコンピューター/オペレーティングシステムと行う可能性のある簡単な対話を次に示します。

プログラム:起動したい。20 MBのメモリが必要です。

オペレーティングシステム:使用されていない20 MBの空きメモリを見つけて、引き渡します

(重要な注意点は、これが返すことができることである任意のメモリの20 MB自由に、彼らも、連続している必要はありません。この時点で、プログラムは、今ではOSに話をせずに持っているメモリ内で動作することができます)

プログラム:メモリの最初のスポットは32ビット整数変数であると仮定しますx

(コンパイラは、他の変数へのアクセスがメモリ内のこのスポットに決して接触しないようにします。システムには、最初のバイトがvariableであるx、またはその変数xが整数であると言うものはありません。誰かが後でバッグから何かを取り出すと、青いものや立方体を引き抜くのは衝撃的です-何かが恐ろしく間違っています。プログラムは現在、最初のメモリスポットが変数xであり、整数であると仮定しています。このバイトのメモリ上に何か他のものが書き込まれた場合、または何か他のものであると仮定された場合-恐ろしいことが発生しました。起こらない)

プログラム:私は今2私がいると仮定している最初の4バイトに書き込みますx

プログラム:に5を追加しxます。

  • Xの値を一時レジスタに読み込みます

  • 一時レジスタに5を追加します

  • 一時レジスターの値を最初のバイトに保存しますx

プログラム:次に使用可能なバイトはchar変数であると想定しますy

プログラム:avariableに書き込みますy

  • ライブラリは、バイト値を見つけるために使用されます a

  • バイトは、プログラムが想定しているアドレスに書き込まれますy

プログラム:の内容を表示したい y

  • 2番目のメモリスポットの値を読み取ります

  • ライブラリを使用して、バイトから文字に変換します

  • グラフィックライブラリを使用して、コンソール画面を変更します(ピクセルを黒から白に設定、1行スクロールなど)

(そして、それはここから続きます)

おそらくあなたが夢中になっているのは-メモリの最初のスポットがなくなったときに何が起こるのxでしょうか?または2番目はもうありませんyか?誰かがポインターxとして、charまたはyポインターとして読み取るとどうなりますか?つまり、悪いことが起こります。これらのものの中には、明確に定義された動作を持つものと、未定義の動作を持つものがあります。未定義の動作とは、まさにそれです-何も起こらず、プログラムやオペレーティングシステムがクラッシュすることもあります。明確に定義された動作でさえ、悪意がある場合があります。xプログラムへのポインターに変更し、プログラムにポインターとして使用させることができる場合、プログラムにプログラムの実行を開始させることができます-これはまさにハッカーの仕事です。コンパイラは、私たちが使用していないことを確認助けるためにそこにあるint xようstring、およびその性質のもの。マシンコード自体は型を認識せず、命令が指示することを行うだけです。実行時に発見される大量の情報もあります。プログラムが使用できるメモリのバイトはどれですか?ないx最初のバイトまたは12日で開始?

しかし、このようなプログラムを実際に書くことはどれほど恐ろしいことか想像できます(アセンブリ言語でできます)。あなたは、変数を「宣言」でオフを開始-あなたはそのバイト1は自分自身を教えxバイト2は、yとあなたは、コード、読み込みと保存のレジスタのそれぞれの行を書いて、あなた(ヒトなど)がある1忘れてはいけないxとされ1つはy、システムにわからないためです。そして、あなたは(人間として)どのタイプxで何であるかを覚えておく必要がありyます。これもまた、システムにはわからないからです。


素晴らしい説明。「整数値を文字に変換して表示する方法はどのようにわかりますか?簡単に言えば、コンパイラーは、その遷移を行うために必要なすべてのステップを確実に入れるようにしました。」私にとってはまだ霧です。CPUがRAMレジスタから0x00000061をフェッチしたとしましょう。この時点から、画面に表示されるものに移行する他の指示(exeファイル内)があると言っていますか?
user16307

2
@ user16307はい、追加の指示があります。記述するコードの各行は、多くの命令に変換される可能性があります。使用する文字を決定する指示、変更するピクセルと変更する色などの指示があります。実際には表示されないコードもあります。たとえば、std :: coutを使用すると、ライブラリを使用することになります。コンソールに書き込むコードは1行のみですが、呼び出す関数はより多くの行になり、各行は多くの機械語命令に変換できます。
シャズ

8
@ user16307 Otherwise how can console or text file outputs a character instead of int メモリ位置の内容を整数または英数字として出力するための一連の命令が異なるためです。コンパイラは変数の種類を認識しており、コンパイル時に適切な命令のシーケンスを選択し、EXEに記録します。
チャールズE.グラント

2
バイトコード(またはバイトコード)は通常、Java BytecodeやMSILなどの中間言語を指し、ランタイムが活用するためにこのデータを実際に保存するため、「バイトコード自体」には別のフレーズがあります。さらに、そのコンテキストで「バイトコード」が何を参照するのかが完全に明確ではありません。そうでなければ、いい答えです。
jpmc26

6
@ user16307 C ++とC#を心配しないようにしてください。これらの人々が言っ​​ていることは、コンピューターとコンパイラーがどのように機能するかについての現在の理解をはるかに超えています。あなたが理解しようとしている目的のために、ハードウェアは型、char、int、その他について何も知りません。コンパイラーに変数がintであると伝えると、intである場合にメモリー位置を処理する実行可能コードを生成しました。メモリの場所自体には型に関する情報は含まれていません。プログラムがintとして扱うことを決定しただけです。ランタイム型情報について聞いた他のすべてを忘れてください。
アンドレスF.

43

あなたの主な質問は、「型がコンパイル時に消去され、実行時に保持されない場合、コンピュータは、それを解釈するコードを実行するか、それを解釈するコードを実行するかどうかをどのように知るintcharでしょうか? 」

そして答えは…コンピューターはそうではありません。ただし、コンパイラー知っているので、そもそもバイナリーに正しいコードを入れるだけです。変数がとして入力されたchar場合、コンパイラはそれをintプログラム内でとして扱うためのコードを入れず、それを扱うためのコードを入れますchar

あります、実行時にタイプを保持する理由は:

  • 動的型付け:動的型付けでは、実行時に型チェックが行われるため、明らかに型は実行時に認識されなければなりません。ただし、Cは動的に型指定されないため、型は安全に消去できます。(ただし、これは非常に異なるシナリオであることに注意してください。動的型と静的型は実際には同じものではなく、混合型言語では、静的型を消去して動的型のみを保持できます。)
  • ダイナミックポリモーフィズム:ランタイムタイプに基づいて異なるコードを実行する場合、ランタイムタイプを維持する必要があります。Cには動的なポリモーフィズムはありません(実際には、+演算子などのハードコードされた特別な場合を除いて、ポリモーフィズムはまったくありません)。そのため、ランタイムタイプは必要ありません。ただし、やはりJavaでは、ランタイムタイプは静的タイプとは異なるものであり、理論的には静的タイプを消去しても、ポリモーフィズムのためにランタイムタイプを保持できます。また、タイプルックアップコードを分散して特殊化し、オブジェクト(またはクラス)内に配置する場合、C ++ vtablesなどのランタイムタイプも必ずしも必要ではないことに注意してください。
  • 実行時の反映:プログラムが実行時にその型を反映できるようにする場合、実行時に型を保持する必要があることは明らかです。これはJavaで簡単に確認できます。Javaは実行時に1次型を保持しますが、コンパイル時に型引数を汎用型に消去するため、型引数ではなく型コンストラクター(「生の型」)にのみ反映できます。繰り返しますが、Cには実行時リフレクションがないため、実行時に型を保持する必要はありません。

Cで実行時に型を保持する唯一の理由はデバッグのためです。ただし、デバッグは通常、使用可能なソースで行われ、ソースファイルで型を検索するだけです。

タイプ消去は非常に正常です。型の安全性には影響しません。型はコンパイル時にチェックされ、プログラムが型安全であることをコンパイラが確認すると、型は不要になります(そのため)。静的なポリモーフィズム(オーバーロードとも呼ばれます)には影響しません。オーバーロード解決が完了し、コンパイラーが適切なオーバーロードを選択すると、型は必要なくなります。型も最適化を導くことができますが、繰り返しますが、オプティマイザーが型に基づいて最適化を選択すると、それらはもはや必要なくなります。

実行時に型を保持する必要があるのは、実行時に型を使用して何かをしたい場合のみです。

Haskellは、最も厳密で厳密な、タイプセーフな静的型付け言語の1つであり、Haskellコンパイラは通常すべての型を消去します。(例外は、型クラスのメソッド辞書を渡すことです。)


3
番号!どうして?その情報は何のために必要ですか?コンパイラcharは、コンパイルされたバイナリにaを読み込むためのコードを出力します。これは、のコードを出力せず、のコードをint出力byteせず、ポインターのコードを出力せず、単にのコードのみを出力しますchar。タイプに基づいた実行時の決定はありません。タイプは必要ありません。それは完全にまったく無関係です。関連するすべての決定は、コンパイル時にすでに行われています。
ヨルグWミッター

2
ありません。コンパイラは、バイナリにcharを出力するためのコードを単に配置します。期間。コンパイラは、そのメモリアドレスにcharがあることを知っているため、charを印刷するためのコードをバイナリに入れます。何らかの奇妙な理由でそのメモリアドレスの値が偶然charでない場合、すべての地獄が壊れます。これが基本的に、セキュリティエクスプロイトのクラス全体が機能する方法です。
ヨルグWミットタグ

2
考えてみてください。CPUが何らかの形でプログラムのデータ型を知っていた場合、地球上の誰もが誰かが新しい型を発明するたびに新しいCPUを購入する必要があります。public class JoergsAwesomeNewType {};見る?新しい型を発明しました!新しいCPUを購入する必要があります!
ヨルグWミッター

9
いいえ。そうではありません。コンパイラは、バイナリにどのコードを入れる必要があるかを知っています。この情報を保持する意味はありません。intを印刷する場合、コンパイラはintを印刷するためのコードを配置します。文字を印刷する場合、コンパイラは文字を印刷するためのコードを配置します。期間。しかし、それはほんの少しのパターンです。charを印刷するコードは特定の方法でビットパターンを解釈し、intを印刷するコードは別の方法でビットを解釈しますが、intであるビットパターンとビットパターンを区別する方法はありません。は文字で、ビットの文字列です。
ヨルグWミッター

2
@ user16307: "exeファイルには、どのタイプのデータがどのアドレスであるかに関する情報が含まれていますか?" 多分。デバッグデータを使用してコンパイルする場合、デバッグデータには変数名、アドレス、およびタイプに関する情報が含まれます。また、デバッグデータは.exeファイル内に(バイナリストリームとして)格納される場合があります。ただし、実行可能コードの一部ではなく、アプリケーション自体では使用されず、デバッガーによってのみ使用されます。
ベンフォークト

12

コンピューターは、どのアドレスが何であるかを「認識」しませんが、何が何であるかに関する知識はプログラムの指示に組み込まれます。

char変数を読み書きするCプログラムを作成すると、コンパイラはそのデータをcharとして書き込むアセンブリコードを作成し、メモリアドレスを読み取り、charとして解釈する他のコードがどこかにあります。これら2つの操作を結び付ける唯一のものは、そのメモリアドレスの場所です。

読む時が来ると、命令は「そこにどんなデータ型があるかを見る」と言うのではなく、「そのメモリをフロートとしてロードする」というようなことを言っているだけです。読み込むアドレスが変更された場合、または何かがそのメモリをフロート以外のもので上書きした場合、CPUはとにかくそのメモリをフロートとして幸福にロードし、結果としてあらゆる種類の奇妙なことが起こります。

悪い例え時間:倉庫がメモリで、物を選ぶ人がCPUである複雑な配送倉庫を想像してください。倉庫の「プログラム」の一部は、さまざまなアイテムを棚に置きます。別のプログラムは、倉庫からアイテムを取り出して箱に入れます。彼らが引き出されるとき、彼らはチェックされません、彼らはただビンに入ります。ウェアハウス全体は、すべてが同期して動作し、適切なアイテムが適切なタイミングで適切な場所に常に存在することで機能します。そうでなければ、実際のプログラムのようにすべてがクラッシュします。


CPUがレジスタで0x00000061を見つけてフェッチする場合、どのように説明しますか。コンソールプログラムがこれをintではなく文字として出力することになっていると想像してください。そのexeファイルには、0x00000061のアドレスがcharであることを認識し、ASCIIテーブルを使用して文字に変換するいくつかの命令コードがありますか?
user16307

7
「すべてがクラッシュする」というのは、実際には最良のシナリオであることに注意してください。「奇妙なことが起こる」は2番目に良いシナリオであり、「微妙に奇妙なことが起こる」はさらに悪いことであり、最悪のケースは「あなたの背中の後ろで起こることです。別名セキュリティエクスプロイト。
ヨルグWミッター

@ user16307:プログラム内のコードは、使用されているエンコードに応じて、そのアドレスを取得して表示するようにコンピューターに指示します。メモリの場所にあるそのデータがASCII文字であっても完全なゴミであっても、コンピューターは気にしません。予想される値を格納するために、そのメモリアドレスを設定するのは他の何かが原因でした。アセンブリプログラミングを試してみると有益だと思います。
whatsisname

1
@JörgWMittag:確かに。例としてバッファオーバーフローに言及することを考えましたが、それが単に物事をより混乱させるだけであると決めました。
-whatsisname

@ user16307:画面にデータを表示するのはプログラムです。従来のunixenでは、ターミナル(DEC VT100シリアルターミナルをエミュレートするソフトウェアの一部-モニターに入力されたものをすべてモニターに表示し、キーボードに入力したものをモデムに送信するモニターとキーボードを備えたハードウェアデバイス)。DOSではDOS(実際にはVGAカードのテキストモードですが、無視してください)およびWindowsではcommand.comです。プログラムは、実際に文字列を出力していることを知りません。単にバイト(数字)のシーケンスを出力しているだけです。
スリーブマン

8

そうではありません。Cがマシンコードにコンパイルされると、マシンはほんの一群のビットを見ます。これらのビットがどのように解釈されるかは、追加のメタデータとは対照的に、それらで実行されている操作によって異なります。

ソースコードに入力する型は、コンパイラ専用です。データがどのタイプであると想定されるかを取り、その能力を最大限に活用して、そのデータが意味のある方法でのみ使用されるようにします。コンパイラは、ソースコードのロジックをチェックするのと同じくらい良い仕事をしたら、マシンコードに変換し、型データを破棄します。マシンコードにはそれを表現する方法がないためです(少なくともほとんどのマシンで) 。


私が理解していないのは、intまたはcharの場合、10001などのアドレスから変数の値を読み取るときにコンピューターがどのように許可するかを知ることです。anyprog.exeというプログラムをクリックすると想像してください。コードはすぐに実行を開始します。このexeファイルには、変数がinまたはcharとして保存されているかどうかに関する情報が含まれていますか?–
user16307

@ user16307いいえ、何かがintまたはcharであるかどうかについての追加情報はありません。他に誰も私に勝てないと仮定して、後でいくつかの例を追加します。
8bittree

1
@ user16307:exeファイルには間接的にその情報が含まれています。プログラムを実行するプロセッサは、プログラムの作成時に使用される型を気にしませんが、その多くは、さまざまなメモリ位置にアクセスするために使用される命令から推測できます。
バートヴァンインゲンシェナウ

@ user16307実際には少し余分な情報があります。exeファイルは整数が4バイトであることを知っているので、「int a」と記述すると、コンパイラは変数に4バイトを予約し、aおよび他の変数のアドレスを計算できます。
エスベンスコフペダーセン

1
@ user16307実用的な差は、差(タイプのサイズの横)が存在しないint a = 65char b = 'A'、コードがコンパイルされるとは。

6

ほとんどのプロセッサは、さまざまなタイプのデータを操作するためのさまざまな命令を提供するため、通常、タイプ情報は生成されたマシンコードに「組み込まれ」ます。追加のタイプメタデータを保存する必要はありません。

いくつかの具体的な例が役立つかもしれません。以下のマシンコードは、SuSE Linux Enterprise Server(SLES)10を実行しているx86_64システムでgcc 4.1.2を使用して生成されました。

次のソースコードを想定します。

int main( void )
{
  int x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

上記のソース(を使用gcc -S)に対応する生成されたアセンブリコードの中身を、コメントを追加します:

main:
.LFB2:
        pushq   %rbp               ;; save the current frame pointer value
.LCFI0:
        movq    %rsp, %rbp         ;; make the current stack pointer value the new frame pointer value
.LCFI1:                            
        movl    $1, -12(%rbp)      ;; x = 1
        movl    $2, -8(%rbp)       ;; y = 2
        movl    -8(%rbp), %eax     ;; copy the value of y to the eax register
        addl    -12(%rbp), %eax    ;; add the value of x to the eax register
        movl    %eax, -4(%rbp)     ;; copy the value in eax to z
        movl    $0, %eax           ;; eax gets the return value of the function
        leave                      ;; exit and restore the stack
        ret

あとに続くものがいくつかありますretが、それは議論には関係ありません。

%eax32ビットの汎用データレジスタです。 %rspは、スタックポインタを保存するために予約されている64ビットのレジスタで、スタックに最後にプッシュされたもののアドレスが含まれています。 %rbpは、現在のスタックフレームのアドレスを含むフレームポインターを保存するために予約されている64ビットのレジスタです。スタックフレームは、関数を入力するとスタック上に作成され、関数の引数とローカル変数用のスペースを確保します。引数と変数には、フレームポインターからのオフセットを使用してアクセスします。この場合、変数のメモリはに格納されているアドレスの「下」に12バイトです。 x%rbp

上記のコードでは、32ビットワードをある場所から別の場所にコピーするために使用される命令を使用してx、(1に格納された-12(%rbp))整数値をレジスタにコピーします。我々は、呼の整数値を付加する、(に格納されているが、既にの値に)。私たちはその結果に保存しています、。 %eaxmovladdly-8(%rbp)%eax-4(%rbp)z

double値を変更するので、値ではなく値を処理しintます。

int main( void )
{
  double x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

gcc -S再度実行すると、次のことがわかります。

main:
.LFB2:
        pushq   %rbp                              
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movabsq $4607182418800017408, %rax ;; copy literal 64-bit floating-point representation of 1.00 to rax
        movq    %rax, -24(%rbp)            ;; save rax to x
        movabsq $4611686018427387904, %rax ;; copy literal 64-bit floating-point representation of 2.00 to rax
        movq    %rax, -16(%rbp)            ;; save rax to y
        movsd   -24(%rbp), %xmm0           ;; copy value of x to xmm0 register
        addsd   -16(%rbp), %xmm0           ;; add value of y to xmm0 register
        movsd   %xmm0, -8(%rbp)            ;; save result to z
        movl    $0, %eax                   ;; eax gets return value of function
        leave                              ;; exit and restore the stack
        ret

いくつかの違い。movlandの代わりにand addlを使用movsdしますaddsd(倍精度浮動小数点数を割り当てて追加します)。中間値を保存する代わりに%eax、を使用します%xmm0

これは、型がマシンコードに「焼き付けられた」と言うときの意味です。コンパイラは、特定のタイプを処理するための適切なマシンコードを生成するだけです。


4

歴史的に、Cはメモリをタイプの番号付きスロットの多数のグループで構成されていると見なしていました。unsigned char(「バイト」とも呼ばれますが、常に8ビットである必要はありません)。メモリに格納されたものを使用するコードは、情報が格納されたスロットを知る必要があり、そこにある情報で何をすべきかを知る必要があります[たとえば、アドレス123:456から始まる4バイトを32ビットとして解釈する浮動小数点値」または「最後に計算された数量の下位16ビットをアドレス345:678から始まる2バイトに格納します。」メモリ自体は、メモリスロットに格納された値が「意味」を知らないか気にしません。コードは、あるタイプを使用してメモリを書き込み、別のタイプとしてそれを読み取ろうとすると、書き込みによって保存されたビットパターンは、結果が生じる可能性のある2番目のタイプのルールに従って解釈されます。

たとえば、コードが0x1234567832ビットに保存され、unsigned intその後2つの連続した16ビットを読み取ろうとした場合unsigned intそのアドレスと上記のものから値unsigned int場合、コードの半分がどこに格納されたかによって、コードが値を読み取る可能性があります0x1234と0x5678、または0x5678と0x1234。

ただし、C99標準では、メモリがビットパターンが何を表すかについて何も知らない番号付きスロットの束として動作する必要がなくなりました。コンパイラは、メモリスロットに格納されているデータのタイプを認識しているかのように動作し、unsigned charいずれかのタイプを使用して読み取られる以外のタイプを使用して書き込まれたデータのみを許可しますunsigned charまたは書き込まれたのと同じタイプと; コンパイラはさらに、メモリスロットが、これらの規則に反してメモリにアクセスしようとするプログラムの動作を任意に破壊する能力と傾向を持っているかのように動作することを許可されます。

与えられた:

unsigned int a = 0x12345678;
unsigned short p = (unsigned short *)&a;
printf("0x%04X",*p);

一部の実装は0x1234を出力し、他の実装は0x5678を出力しますが、C99標準では、実装が「FRINK RULES!」を出力することは合法です。または、保持aしているメモリ位置に、書き込みに使用されたタイプを記録するハードウェアを含めること、およびそのようなハードウェアが無効な読み取り試行に何らかの方法で応答することは合法であるという理論に基づいて、「FRINK RULES!」出力されます。

そのようなハードウェアが実際に存在するかどうかは問題ではないことに注意してください。そのようなハードウェアが合法的に存在する可能性があるという事実は、そのようなシステムで実行されているかのように動作するコードをコンパイラが生成することを合法にします。特定のメモリ位置が特定のタイプとして書き込まれ、別のタイプとして読み取られるとコンパイラが判断できる場合、ハードウェアがそのような判断を行えるシステム上で実行されているふりをすることができ、コンパイラ作成者が適切と考えるあらゆる程度の気まぐれで応答することができます。

このルールの目的は、あるタイプの値を保持するバイトのグループが特定の時点で特定の値を保持し、それ以降同じタイプの値が書き込まれていないことを知っているコンパイラがそのグループを推測できるようにすることでしたバイト数はまだその値を保持します。たとえば、プロセッサがバイトのグループをレジスタに読み込んだ後、レジスタに残っている間に同じ情報を再び使用したい場合、コンパイラはメモリから値を再読み込みせずにレジスタの内容を使用できます。便利な最適化。ルールの最初の約10年間、これに違反することは、一般に、変数の読み取りに使用されるタイプ以外のタイプで変数が書き込まれた場合、その読み取りが値の読み取りに影響する場合としない場合があります。こうした行動は悲惨な場合もありますが、無害な場合もありますが、

ただし、2009年頃、CLANGのような一部のコンパイラの作成者は、メモリが1つのタイプを使用して書き込まれ、別のタイプとして読み取られる場合に標準でコンパイラが好きなことを行うことができるため、コンパイラはプログラムが入力を決して受け取らないと推測する必要があると判断しましたそのようなことが起こるようにします。このような無効な入力が受信された場合、コンパイラは好きなことを行うことが許可されていると規格が述べているため、規格が要件を課していない場合にのみ効果があるコードは省略できます(一部のコンパイラ作成者の見解では)無関係です。これにより、エイリアシング違反の動作が、読み取り要求が与えられたときに、読み取り要求と同じタイプを使用して書き込まれた最後の値、または他のタイプを使用して書き込まれた最新の値を任意に返すメモリのようになります。


1
RTTIが存在しないことを理解していない人にタイププルーニングを行うと、未定義の動作に言及するのは直観に反しているようです
コールジョンソン

@ColeJohnson:2009年以前のコンパイラの99%でサポートされているCの方言の正式な名前や標準がないことは残念です。教育の観点と実用的な観点の両方から、根本的に異なる言語と見なされるためです。35年にわたって多くの予測可能および最適化可能な動作を進化させた方言、最適化の想定目的のためにそのような動作をスローする方言の両方に同じ名前が付けられているため、それらで異なる動作をする場合の混乱を避けることは困難です。
supercat

歴史的に、CはLispマシン上で実行されていましたが、そのようなタイプでのゆるいプレイは許可されませんでした。30年前に見られた「予測可能で最適化可能な動作」の多くは、VAX上のBSD Unix以外では動作しなかったと確信しています。
-prosfilaes

@prosfilaes:おそらく「1999年から2009年に使用されたコンパイラの99%」の方が正確でしょうか?コンパイラがかなり積極的な整数最適化のためのオプションを持っていたとしても、それらはただのオプションでした。1999年以前に、与えられint x,y,z;た式x*y > zが1または0を返す以外のことをしないことを保証しないモードを持たないコンパイラ、またはエイリアス違反が影響するコンパイラを見たことがありませんコンパイラが任意に古い値または新しい値を返すようにすること以外。
supercat

1
...ここで、unsigned char型の構築に使用される値は「から来た」。プログラムがポインターをに分解し、unsigned char[]その16進の内容を画面上に簡単に表示し、その後、ポインターを消去unsigned char[]し、キーボードから16進数を受け入れ、それらをポインターにコピーして、そのポインターを逆参照する場合、入力された番号が表示された番号と一致した場合の動作は明確に定義されます。
supercat

3

Cではそうではありません。他の言語(Lisp、Pythonなど)には動的型がありますが、Cは静的に型付けされています。つまり、プログラムは、データが文字や整数などとして適切に解釈するためのタイプを知る必要があることを意味します。

通常、コンパイラがこれを処理します。何か間違ったことをすると、コンパイル時エラー(または警告)が表示されます。


私が理解していないのは、intまたはcharの場合、10001などのアドレスから変数の値を読み取るときにコンピューターがどのように許可するかを知ることです。anyprog.exeというプログラムをクリックすると想像してください。コードはすぐに実行を開始します。このexeファイルには、変数がinまたはcharとして保存されているかどうかに関する情報が含まれていますか?–
user16307

1
@ user16307基本的にいいえ、その情報はすべて完全に失われます。その情報がなくても適切に機能するように十分に設計されるのは、マシンコード次第です。コンピュータが気にするのは、addressに8ビットが連続していること10001です。マシンまたはアセンブリコードを記述している間、手動でそのようなものに追いつくのは、あなたの仕事またはコンパイラの仕事です。
パンツァー危機

1
型を保持する唯一の理由は動的型付けではないことに注意してください。Javaは静的に型付けされますが、型を動的に反映できるため、型を保持する必要があります。さらに、ランタイムポリモーフィズム、つまり、ランタイムタイプに基づいたメソッドディスパッチがあり、そのためにタイプも必要です。C ++はメソッドディスパッチコードをオブジェクト(またはクラス)自体に配置するため、何らかの意味で型は必要ありません(もちろん、vtableは何らかの意味で型の一部であるため、実際には少なくとも型保持されます)が、Javaでは、メソッドディスパッチコードは集中化されます。
ヨルグWミットタグ

「Cプログラムが実行されるとき」と書いた私の質問を見てください。彼らは間接的に命令コードの中でexeファイルに間接的に保存し、最終的にメモリ内の場所を取る?CPUがレジスタで0x00000061を見つけてフェッチする場合、これを再度作成します。コンソールプログラムがこれをintではなく文字として出力することになっていると想像してください。そのexeファイル(マシン/バイナリコード)には、0x00000061のアドレスがcharであり、ASCIIテーブルを使用して文字に変換するいくつかの命令コードがありますか?もしそうなら、それはchar int識別子が間接的にバイナリにあることを意味しますか???
user16307

値が0x61であり、char(つまり 'a')として宣言されている場合、それを表示するルーチンを呼び出すと、[最終的に]その文字を表示するシステムコールが発生します。intとして宣言し、表示ルーチンを呼び出すと、コンパイラは0x61(10進数の97)をASCIIシーケンス0x39、0x37( '9'、 '7')に変換するコードを生成することを認識します。結論:生成されるコードは異なります。コンパイラはそれらを異なる方法で処理することを知っているからです。
マイクハリス

3

あなたは区別しなければならないcompiletimeruntime片手にcodeしてdata一方。

機械の観点から、それはあなたが呼んでいるものの間に違いはありませんcodeinstructions、何を呼び出しますdata。それはすべて数字に帰着します。しかし、いくつかのシーケンス-私たちが呼ぶものcode-は、私たちが便利だと思うことをしますが、他のシーケンスは単にcrashマシンです。

CPUによって行われる作業は、単純な4ステップループです。

  • 指定されたアドレスから「データ」を取得します
  • 命令をデコードします(つまり、数値をとして解釈しますinstruction
  • 有効な住所を読む
  • 結果を実行して保存する

これは命令サイクルと呼ばれます

ここでAと4がRAMアドレスに保存されていることを読みました。しかし、aとxはどうですか?

aおよびx変数は、アドレスのプレースホルダーであり、プログラムは変数の「コンテンツ」を見つけることができます。そのため、変数aが使用されるときはいつでも、事実上a使用されたコンテンツのアドレスがあります。

最も紛らわしいことに、aがcharでxがintであることを実行がどのように認識するのでしょうか?

実行は何も知りません。導入部で述べられたことから、CPUはデータをフェッチするだけで、このデータを命令として解釈します。

printfその結果のコードは、特殊なメモリ・セグメントに対処する方法を右の指示を与え、つまり-functionは、あなたがそれに入れている入力の種類を、「知っている」ように設計されています。もちろん、無意味な出力を生成することは可能です:文字列が "%s"と共に保存されていないアドレスを使用するとprintf()0\0)が存在するランダムなメモリ位置によってのみ無意味な出力が停止します。

プログラムのエントリポイントについても同じことが言えます。C64では、プログラムを(ほぼ)すべての既知のアドレスに配置することができました。Assembly-Programsは、命令のsys後にアドレスが続いて開始されました。これsys 49152は、アセンブラーコードを配置する一般的な場所でした。しかし49152、例えばにグラフィカルデータをロードすることを妨げるものは何もありません。その結果、この時点から「開始」した後にマシンがクラッシュします。この場合、命令サイクルは「グラフィックデータ」を読み取り、それを「コード」として解釈しようとすることから始まりました(もちろん意味がありません)。効果は驚くべきものでした;)

値がRAMのどこかに10011001として保存されているとします。私がコードを実行するプログラムである場合、この10011001がcharであるかintであるかをどのようにして知ることができますか?

前述のように、「コンテキスト」、つまり前後の指示は、データを希望どおりに処理するのに役立ちます。マシンの観点からは、メモリの場所に違いはありません。intそしてchar、語彙のみであり、それはcompiletime; 中runtime(アセンブリレベル)では、charまたははありませんint

私が理解していないのは、コンピューターが10001などのアドレスから変数の値を読み取るとき、それがintかcharかに関係なく、コンピューターがどのように知っているかです。

コンピューターは知りません。プログラマはありません。コンパイルされたコードは、人間にとって意味のある結果を生成するために必要なコンテキストを生成します

この実行可能ファイルには、格納されている変数がint型またはchar型であるかどうかに関する情報が含まれていますか

はいいいえ。それがあるかどうかの情報、intまたはchar失われています。しかし一方で、コンテキスト(メモリの場所を処理する方法、データが保存されている場所を指示する命令)は保持されます。そう暗黙はい、「情報」である暗黙利用できます。


コンパイル時間と実行時間の素晴らしい区別。
マイケルブラックバーン

2

この議論をC言語のみに絞ってみましょう。

参照しているプログラムはCなどの高水準言語で書かれています。コンピューターは機械語のみを理解します。高レベルの言語により、プログラマーはより人間に優しい方法でロジックを表現できるようになり、その後、マイクロプロセッサがデコードおよび実行できるマシンコードに変換されます。さて、あなたが言及したコードについて議論しましょう。

char a = 'A';
int x = 4;

各部分を分析してみましょう。

char / intはデータ型として知られています。これらは、メモリを割り当てるようコンパイラーに指示します。その場合、char1バイトとint2バイトになります。(このメモリサイズはマイクロプロセッサに依存することに注意してください)。

a / xは識別子と呼ばれます。これで、RAMのメモリ位置に付けられた「ユーザーフレンドリー」な名前を言うことができます。

=は、コンパイラに、「A」をメモリ位置にa、4をメモリ位置に格納するように指示しますx

したがって、int / charデータ型識別子は、プログラムの実行中にマイクロプロセッサではなく、コンパイラによってのみ使用されます。したがって、それらはメモリに保存されません。


ok int / charデータ型識別子は変数としてメモリに直接保存されませんが、命令コード間で間接的にexeファイルに保存され、最終的にメモリで行われますか?CPUがレジスタで0x00000061を見つけてフェッチする場合、これを再度作成します。コンソールプログラムがこれをintではなく文字として出力することになっていると想像してください。そのexeファイル(マシン/バイナリコード)には、0x00000061のアドレスがcharであり、ASCIIテーブルを使用して文字に変換するいくつかの命令コードがありますか?もしそうなら、それはchar int識別子が間接的にバイナリにあることを意味しますか???
user16307

CPUにはそのすべての数字はありません。特定の例では、コンソールでの印刷は、変数がcharまたはintであるかどうかに依存しません。高レベルのプログラムをプログラムが実行されるまでどのように機械語に変換するかの詳細なフローで回答を更新します。
プラサド

2

ここでの私の答えはやや単純化されており、Cのみを参照します。

いいえ、型情報はプログラムに保存されません。

intまたはchar、CPUに対するタイプインジケータではありません。コンパイラーのみ。

コンパイラーによって作成されたexeにintは、変数がint。同様に、変数がとして宣言されたchar場合、exeにはを操作するための指示が含まれますchar

Cで:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

とはRAM内で同じを持つため、このプログラムメッセージを出力します。charint

さて、どうやってan とa printfを出力するのか不思議に思っているなら、それはあなたが "フォーマット文字列"で値をどう扱うべきかを指定しなければならないからです。 (例えば、などの値を治療するための手段、及び、しかし、いずれかの方法を同じ値として整数値を扱うことを意味します。)65intAcharprintf
%cchar%d


2
私は誰かがを使用して例を使用することを望んでいましたprintf。@OP:int a = 65; printf("%c", a)が出力されます'A'。どうして?プロセッサは気にしないからです。それに、それが見るすべてはビットです。プログラムはプロセッサに65(偶然'A'ASCII の値)を保存してからa文字を出力するようにプロセッサに指示しましたが、これは喜んで行われます。どうして?気にしないので。
コールジョンソン

しかし、なぜ一部の人がここでC#の場合に言うのですか、それは話ではありませんか?私はいくつかの他のコメントを読みましたが、C#とC ++ではストーリー(データ型に関する情報)は異なり、CPUでさえ計算を行いません。それについてのアイデアはありますか?
user16307

@ user16307 CPUが計算を行わない場合、プログラムは実行されていません。:) C#についてはわかりませんが、私の答えはそこにも当てはまると思います。C ++に関しては、私の答えがそこに当てはまることを知っています。
BenjiWiebe

0

最下位レベルでは、実際の物理CPUにはタイプがまったくありません(浮動小数点ユニットは無視されます)。ビットのパターン。コンピューターは、ビットのパターンを非常に高速に操作することで機能します。

これがCPUが実行するすべてのことです。intやcharなどはありません。

x = 4 + 5

次のように実行されます。

  1. 00000100をレジスタ1にロードします
  2. 00000101をレジスタ2にロードします
  3. Iレジスタ1をレジスタ2に追加し、レジスタ1に格納します

iadd命令は、レジスタ1と2が整数であるかのように動作するハードウェアをトリガーします。それらが実際に整数を表していない場合、すべての種類のものは後で間違って行くことができます。通常、最良の結果はクラッシュします。

ソースで指定された型に基づいて正しい命令を選択するのはコンパイラーですが、CPUによって実行される実際のマシンコードには、型はどこにもありません。

編集:実際のマシンコードは実際には4、5、または整数をどこにも言及していないことに注意してください。それは、ビットの2つのパターンであり、2つのビットパターンを取り、それらがintであると想定し、それらを加算する命令です。


0

簡単に言えば、型はコンパイラーが生成するCPU命令にエンコードされます。

情報のタイプまたはサイズに関する情報は直接保存されませんが、コンパイラはこれらの変数の値にアクセス、変更、および保存するときにこの情報を追跡します。

実行は、aがcharでxがintであることをどのように知るのですか?

そうではありませんが、コンパイラがマシンコードを生成するとき、それは知っています。intそしてchar、異なるサイズのものとすることができます。charがバイトのサイズでintが4バイトのアーキテクチャでは、変数xはアドレス10001ではなく、10002、10003、および10004にもあります。コードがxCPUレジスタに値をロードする必要がある場合、 4バイトをロードするための命令を使用します。文字をロードする場合、命令を使用して1バイトをロードします。

2つの指示のどちらを選択するのですか?コンパイラはコンパイル中に決定します。メモリ内の値を検査した後、実行時に実行されません。

また、レジスタのサイズは異なる可能性があることに注意してください。Intel x86 CPUでは、EAXは32ビット幅で、その半分はAX(16)であり、AXは両方とも8ビットのAHとALに分割されます。

(x86 CPUで)整数をロードする場合は、整数にMOV命令を使用し、charにMOV命令を使用してcharをロードします。これらは両方ともMOVと呼ばれますが、異なるopコードがあります。事実上、2つの異なる命令である。変数のタイプは、使用する命令にエンコードされます。

他の操作でも同じことが起こります。オペランドのサイズに応じて、およびそれらが符号付きまたは符号なしであっても、加算を実行するための多くの命令があります。https://en.wikipedia.org/wiki/ADD_(x86_instruction)を参照してください。さまざまな追加可能なものがリストされています

値がRAMのどこかに10011001として保存されているとします。私がコードを実行するプログラムである場合、この10011001がcharであるかintであるかをどのようにして知ることができますか

最初に、charは10011001になりますが、intは00000000 00000000 00000000 10011001になります。サイズが異なるためです(上記と同じサイズのコンピューター上)。しかし、signed charvs の場合を考えてみましょうunsigned char

メモリの場所に保存されているものは、とにかく解釈できます。Cコンパイラの責任の一部は、変数に格納され、変数から読み取られるものが一貫した方法で実行されることを保証することです。したがって、プログラムはメモリ位置に保存されているものを知っているのではなく、常に同じ種類のものを読み書きすることを事前に同意しているのです。(キャストタイプなどはカウントされません)。


しかし、なぜ一部の人がここでC#の場合に言うのか、それは物語ではないのですか 他のコメントを読んだところ、C#とC ++ではストーリー(データ型に関する情報)が異なり、CPUでさえ計算を行っていません。それについてのアイデアはありますか?
user16307

0

しかし、なぜ一部の人がここでC#の場合に言うのですか、それは物語ではありませんか?私はいくつかの他のコメントを読みましたが、C#とC ++ではストーリー(データ型に関する情報)は異なり、CPUでさえ計算を行いません。それについてのアイデアはありますか?

C#のような型チェック言語では、型チェックはコンパイラーによって行われます。コードbenjiは次のように書きました:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

単にコンパイルを拒否します。同様に、文字列と整数を乗算しようとした場合(追加すると言っていましたが、演算子 '+'は文字列連結でオーバーロードされており、動作する可能性があります)。

int a = 42;
string b = "Compilers are awesome.";
double[] c = a * b;

コンパイラは、文字列がどれだけキスしても、このC#からマシンコードを生成することを単に拒否します。


-4

他の答えは、あなたが遭遇する本質的にすべての消費者デバイスがタイプ情報を保存しないという点で正しいです。ただし、タグ付きアーキテクチャを使用する過去(および現在の研究環境)には、データと型(および場合によってはその他の情報も)の両方を格納するハードウェア設計がいくつかあります。これらはLispマシンを最も顕著に含むでしょう。

オブジェクト指向プログラミング用に設計された、似たようなハードウェアアーキテクチャについて聞いたことを漠然と思い出しましたが、今は見つかりません。


3
この質問は、C言語(Lispではない)を参照していることを明確に示しており、C言語は可変メタデータを格納していませ。C実装でこれを行うことは確かに可能ですが、標準では禁止されていないため、実際には決して起こりません。質問に関連する例がある場合は、特定の引用を提供し、C言語に関連する参照提供してください。

まあ、Lispマシン用のCコンパイラを書くことはできますが、この日と一般にLispマシンを使用する人はいません。ところで、オブジェクト指向アーキテクチャはRekursivでした
ネイサンリンゴ

2
この答えは役に立たないと思います。OPの現在の理解レベルをはるかに超えて事態を複雑にします。OPはCPU + RAMの基本的な実行モデルを理解しておらず、コンパイラがシンボリックな高レベルのソースを実行可能なバイナリに変換する方法を理解していないことは明らかです。タグ付きメモリ、RTTI、Lispなどは、私の意見では質問者が知る必要があるものをはるかに超えており、彼/彼女を混乱させるだけです。
アンドレスF。15年

しかし、なぜ一部の人がここでC#の場合に言うのか、それは物語ではないのですか 他のコメントを読んだところ、C#とC ++ではストーリー(データ型に関する情報)が異なり、CPUでさえ計算を行っていません。それについてのアイデアはありますか?
user16307
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.