私たちが初めてプログラミングを始めたとき、最初に完成させた小さなプロジェクトは「hello world」で、すぐにこの言語で hello world を書くことができました。しかし、それはほんの数文字なので見ないでください。しかし、ほとんどの人は、この単純なプログラム hello world の内部動作メカニズムをまだ説明できません。そこで、今日はプログラムの動作メカニズムについて説明します。
hello world これらのメッセージはモニターにどのように表示されますか? CPU によって実行されるコードは、プログラムで記述されるコードとは明らかに異なります。私たちが書いたコードは、CPU が実行できるコードにどのように変化するのでしょうか?プログラムが実行されているとき、コードはどこにあるのでしょうか?それらはどのように組織されていますか?プログラム内の変数はどこに保存されますか?関数呼び出しはどのように表示されるのでしょうか?この記事では、プログラムがどのように機能するかについて簡単に説明します。
開発プラットフォームの隠されたプロセス
すべての言語には独自の開発プラットフォームがあり、ほとんどのプログラムはここで生まれます。プログラムのソース コードから実行可能ファイルへの変換プロセスは、実際には多くのステップに分かれており、非常に複雑です。しかし、現在の開発プラットフォームはこれらすべてを単独で実行するため、利便性がもたらされるだけでなく、多くの実装が隠蔽されます。詳細。したがって、ほとんどのプログラマーはコードを記述することのみを担当し、他の複雑な変換作業は開発プラットフォームによって黙って完了されます。
私の理解によれば、ソースコードから実行可能ファイルまでのプロセスは、簡単に言うと次の段階に分けることができます:
1. ソースコードから機械語まで、そして生成された機械語を一定の規則に従って整理します。とりあえずファイルAと呼びましょう。
2. ファイル A を実行するために必要なファイル B (ライブラリ関数など) をリンクして、ファイル A+ を作成します
3. ファイル A+ をメモリにロードし、ファイルを実行します
(実際、参考書を読んだり、その他の情報 これらの手順以外にも手順があるかもしれませんが、ここでは簡略化するために 3 つの手順にまとめます)
これらは実行可能ファイルを形成するための重要な手順であり、どれも欠けることはできません。これで、開発プラットフォームに「盲目」になっていることがわかります。次のセクションでは霧を晴らし、開発プラットフォームの本当の姿を明らかにします。
オブジェクト ファイル
コンピューター分野には古典的な格言があります。
たとえば、A から B に変換するには、まず A をファイル A+ に変換し、次にファイル A+ を必要なファイル B に変換します。 (実はこの方法はPolyaの「愛し方」にも記載されています。問題を解く際に中間層を追加することで問題を単純化できます)
するとソースコードから実行ファイルまでのプロセスは次のように理解できます。ソース コードから実行可能ファイルに移行する場合も同様で、問題を解決するためにそれらの間に中間層を (継続的に) 追加します。
上で述べたように、まずソースプログラムを中間ファイルAに変換し、次に中間ファイルを必要なターゲットファイルに変換します。
私はファイルを処理するときにこの考えに従います。
実際、上記のファイル A のより専門的な用語は、「ターゲット ファイル」です。これは実行可能プログラムではないため、実行する前に他のターゲット ファイルにリンクしてロードする必要があります。ソース プログラムの場合、開発プラットフォームが最初に行う必要があるのは、ソース プログラムを機械語に翻訳することです。その中で非常に重要な部分はコンパイルです。ソースコードを機械語(実際にはバイナリコードの束)に翻訳することであることはご存知の方も多いと思います。コンパイルに関する知識は非常に重要ですが、この記事の焦点ではありません。興味がある場合は、自分でグーグルで調べてください。
ターゲット ファイル形式:
次に、上記のターゲット ファイルがどのように構成されているか (つまり、ストレージ構造) を見てみましょう。
出典:
もしあなたがバイナリ コードを設計した場合、これらのバイナリ コードをどのように整理するか想像してみてください。管理を容易にするために、机上の物品を分類して整然と配置する必要があるのと同じように、変換されたバイナリ コードも、コードを表すものとデータを表すものをまとめてカテゴリに分けて保存する必要があります。このようにして、バイナリ コードは異なるブロックに分割されて保存されます。このような領域をセグメントと呼びます。
標準:
コンピューターサイエンスの多くのものと同様、人々のコミュニケーション、プログラムの互換性、その他の問題を促進するためのもの。このバイナリ保存方法の標準も開発され、COFF (共通オブジェクト ファイル フォーマット) が誕生しました。 Windows や Linux などの現在の主流のオペレーティング システムでのターゲット ファイル形式は COFF に似ており、その変形と考えることができます。
a.out:
a.out は、ターゲット ファイルのデフォルト名です。つまり、ファイルをコンパイルするときに、コンパイル対象ファイルの名前を変更しないと、コンパイル後に a.out という名前のファイルが生成されます。
この名前が使用されている具体的な理由についてはここでは触れません。興味があれば、自分でグーグルで調べてみてください。
下の図は、ターゲット ファイルをより直観的に理解することができます:
上の図は、ターゲット ファイルの典型的な構造です。実際の状況は異なる場合がありますが、それらはすべて で派生しています。この基礎。ELF ファイルヘッダー: 上の図の最初のセグメント。ヘッダーはターゲット ファイルのヘッダーであり、ターゲット ファイルに関するいくつかの基本情報が含まれています。ファイルのバージョン、ターゲット マシンのモデル、プログラム エントリ アドレスなど。
テキストセグメント: 内部のデータは主にプログラムのコード部分です。
データセグメント: プログラム内の変数などのデータ部分。
再配置セグメント:
再配置セグメントには、テキストの再配置と、再配置情報を含むデータの再配置が含まれます。一般に、コード内には外部関数または変数への参照があります。リファレンスなので対象ファイルにはこれらの関数や変数は存在しません。これらを使用する場合は、実際のアドレスを指定する必要があります (このプロセスはリンク中に発生します)。これらの実際のアドレスを見つけるための情報を提供するのは、これらの再配置テーブルです。上記を理解すれば、テキストの再配置とデータの再配置を理解することは難しくありません。
シンボル テーブル: シンボル テーブルには、ソース コード内のすべてのシンボル情報が含まれます。すべての変数名、関数名などを含めます。各シンボルの情報が記録されています。例えば、コード内に「学生」というシンボルがあれば、そのシンボルに対応する情報がシンボルテーブルに含まれます。このシンボルが配置されているセグメント、その属性 (読み取りおよび書き込み許可)、およびその他の関連情報が含まれます。
実際、記号テーブルの元のソースは、編集の字句解析段階にあると言えます。字句解析を行う場合、コード内の各シンボルとその属性がシンボル テーブルに記録されます。
文字列テーブル: シンボルテーブルと同様の機能があり、いくつかの文字列情報を保存します。
もう 1 つ言っておきたいのは、ターゲット ファイルはすべてバイナリで保存されており、それ自体がバイナリ ファイルであるということです。
実際のターゲット ファイルはこのモデルよりも複雑になりますが、考え方は同じで、タイプに応じて保存することに加え、ターゲット ファイルの情報とリンクに必要な情報を説明するいくつかのセクションが追加されます。
a.out セグメンテーション
Hello World
は空の話です。ここで C で説明されている、Hello World のコンパイル後に形成されるオブジェクト ファイルを見てみましょう。
簡単な hello world ソースコード:
データセグメントに入れるデータを持たせるために、ここに「int a=5」を追加します。
VC を使用している場合は、[実行] をクリックして結果を確認します。
内部でどのように処理されるかを明確に確認するために、GCC を使用してコンパイルします。
gcc hello.c を実行します
ディレクトリを見ると、追加のターゲット ファイル a.out があります。
さて、私たちがやりたいのは、a.out の内容を確認することです。当時、私がそれを表示するために vim テキストを使用していたことを思い出した人もいるかもしれません。しかし、a.out とはどのようなもので、どうしてそう簡単に暴露されるのでしょうか。はい、vim は動作しません。 「私たちが遭遇した問題のほとんどは、先人たちによって解決されてきました。」 はい、objdump という非常に強力なツールがあります。もちろん、後で紹介する readelf という非常に便利なものもあります。
これら 2 つのツールは通常 Linux に付属しており、自分でググることができます
注: ここでのコードは主に Linux で GCC を使用してコンパイルされており、ターゲット ファイルを表示するために Objdump と readelf が使用されます。ただし、すべての実行結果を画像に載せておきます。これまで Linux に触れたことがない場合は、次の内容を読んでも問題ありません。私はubuntuを使用していますが、とてもいい感じです~
以下はa.outの組織構造です: (各セグメントの開始アドレス、サイズなど)
対象ファイルを表示するコマンドはobjdump -h a.outです。
対象ファイルの形式は上記と同じで、カテゴリに格納されていることがわかります。対象ファイルは6つのセクションに分かれています。
左から右に、最初の列 (Idx Name) はセグメントの名前、2 番目の列 (Size) はサイズ、VMA は仮想アドレス、LMA は物理アドレス、File off はセグメント内のオフセットです。ファイル。つまり、段落内の参照 (通常は段落の先頭) に対するこの段落の距離です。最後の Algn はセグメント属性の説明です。今は無視してください
"テキスト" セグメント: コード セグメント。
「データ」セグメント: これは前述のデータ セグメントであり、ソース コード内のデータ (通常は初期化されたデータ) を保存します。
「bss」セグメント: これは、初期化されていないデータを格納するデータ セグメントでもあります。これらのデータはまだ領域が割り当てられていないため、個別に格納されます。
「rodata」セグメント: 読み取り専用データセグメント。そこに保存されているデータは読み取り専用です。
「cmment」にはコンパイラのバージョン情報が格納されます。
残りの 2 つの段落は、私たちの議論にとって実際的な重要性を持たないため、再度紹介することはありません。これらには、リンク、コンパイル、インストールに関する情報が含まれていると考えてください。
注:
ここでの対象ファイル形式は、実際の状況の主要な部分のみをリストしています。表に記載されていない実際の状況もございます。 Linux も使用している場合は、objdump -X を使用して、より詳細なセグメントの内容を一覧表示できます。
a.out の詳細
上記の部分では、例を通じてターゲット ファイル内の一般的なセグメント、主にサイズやその他の関連属性などのセグメント情報について説明します。
では、これらのセグメントには正確に何が含まれているのでしょうか? objdump を使用してみましょう。
objdump -s a.out -s オプションを使用すると、ターゲット ファイルの 16 進形式を表示できます。
次のように結果を表示します:
上の図に示すように、各セグメントの 16 進表現がリストされています。画像が 2 つの列に分かれていることがわかります。左側の列は 16 進数表現で、右側の列は対応する情報を表示します。
より明白なものは、「rodata」読み取り専用データセグメントの「hello world」です。 。ため息、プログラム内の「hello」の入力が間違っているようで、最後に余分な「w」が追加されます。スクリーンショットを撮るのが面倒です。私を許して。
「hello world」の ASCII 値を確認することもでき、対応する 16 進値が内部の内容です。
上記の「コメント」段落には、コンパイラのバージョン情報が含まれています。この段落の後の内容は、GCC コンパイラとそれに続くバージョン番号です。
a.out 逆アセンブリ
コンパイル プロセスでは、常に最初にソース テキストがアセンブリ形式に変換され、次にそれが機械語に翻訳されます。 (中間層を追加します) たくさんの a.out を見た後、そのアセンブリ形式を調べる必要があります。 objdump -da a.out はファイルのアセンブリ形式をリストできます。ただし、ここにはメイン部分、つまり main 関数部分のみがリストされています。実際には、main 関数の実行の開始時と、main 関数の実行後に行うべき作業がまだたくさんあります。
つまり、関数の実行環境の初期化や、関数が占有していた領域の解放などです。
上の図では、左側がコードの 16 進形式、左側がアセンブリ形式です。組み立てに慣れているお子様であれば、ほとんどのことを理解できるはずですので、ここでは詳しく説明しません。
a.out ヘッダー ファイル
ターゲット ファイル形式を紹介するときに、ターゲット ファイルの基本情報が含まれるヘッダー ファイルの概念について説明しました。ファイルのバージョン、ターゲット マシンのモデル、プログラム エントリ アドレスなど。
下の図はファイルヘッダーの形式です:
readelf -h を使用して表示できます。 (下の図で表示されているのは、hello.o です。これは、コンパイルされていますが、ソース ファイル hello.c によってリンクされていないファイルです。これは、a.out を表示するのとほぼ同じです)
は 2 つの列に分かれており、左側の列は属性を表し、右側の列は属性値を表します。最初の行はマジックナンバーと呼ばれることがよくあります。以下は一連の数字です。その具体的な意味については詳しく説明しませんので、ご自身で調べてください。
以下は対象ファイルに関する情報です。私たちが議論したい問題とは密接な関係がないため、ここでは議論しません。
上記の内容は、ターゲット ファイルの内部構成形式を具体的な例を使用して説明しています。ターゲット ファイルは、実行可能ファイルを生成するプロセスの単なる中間プロセスです。プログラムがどのように実行されるか、およびターゲット ファイルがどのように作成されるかについては説明しません。実行可能ファイルと実行可能ファイルの実行方法については、次のセクションで説明します
リンクの簡単な理解
平たく言えば、リンクは複数の実行可能ファイルです。
プログラムAがファイルBに定義された関数を参照する場合、Aの関数が正常に実行されるためには、Bの関数部分をAのソースコードに配置し、AとBを1つのファイルにマージする必要があります。リンクしています。
プログラムをリンクするには、リンカーと呼ばれる特別なプロセスがあります。彼はいくつかの入力ターゲット ファイルを処理し、それらを出力ファイルに合成します。これらのターゲット ファイルには、多くの場合、相互データおよび関数参照が含まれます。
上記で hello world の逆アセンブリ形式を見てきましたが、これはリンクされていないファイルであり、外部関数を参照するときにそのアドレスが不明であることを意味します。
以下に示すように:
上の図では、cal 命令が printf() 関数を呼び出しています。この時点では printf() 関数がファイル内にないため、そのアドレスを 16 進数で表すのに「ff ff ff」を使用します。住所。関数はリンク後にファイルにロードされているため、リンク後はこのアドレスが関数の実際のアドレスになります。
プログラムを実行する前にリンク作業を完了してください。つまり、リンクが完了するまでファイルは実行できません。ただし、これにはライブラリ関数などの明らかな欠点があります。ファイル A とファイル B の両方で特定のライブラリ関数を使用する必要がある場合、リンクが完了すると、リンクされたファイルにはそのライブラリ関数が含まれます。 A と B が同時に実行されると、メモリ内にライブラリ関数のコピーが 2 つ存在することになり、間違いなくストレージ領域を無駄にします。この無駄は、規模が大きくなると特に顕著になります。静的リンクには、アップグレードが難しいという欠点もあります。これらの問題を解決するために、今日の多くのプログラムでは動的リンクが使用されています。
動的リンク: 静的リンクとは異なり、動的リンクはプログラムの実行時に実行されます。このとき、プログラムがロードされて実行されます。上記の例でも、A と B の両方がライブラリ関数 Fun() を使用する場合、A と B の実行時にメモリ内にあれば Fun() のコピーが 1 つだけ必要になります。
リンクについてはまだ多くの知識があり、将来的には専用の記事で説明します。ここでは詳細には触れません。
ロードの簡単な説明
プログラムを実行するには、プログラムをメモリにロードする必要があることはわかっています。以前は、マシンはプログラム全体を物理メモリにロードしていました。現在では、仮想ストレージ メカニズムが一般的に使用されています。つまり、各プロセスがメモリを使用できるという印象を与えます。次に、メモリ マネージャは仮想アドレスを実際の物理メモリ アドレスにマップします。
上記の説明によれば、プログラムのアドレスは仮想アドレスと実アドレスに分けることができます。仮想アドレスは仮想メモリ空間内のアドレスであり、物理アドレスは彼女がロードされる実際のアドレスです。
上のセグメントを見て、おそらくファイルがリンク解除されアンロードされているため、各セグメントの仮想アドレスと物理アドレスは 0 であることに気づいたでしょう。
ロードプロセス このように理解できます。プログラムの各部分に仮想アドレスを割り当て、仮想アドレスから物理アドレスへのマッピングを確立します。実際、重要な部分は、仮想アドレスから物理アドレスへのマッピング プロセスです。プログラムがインストールされると、CPU のプログラム カウンタ pc がファイル内のコードの開始位置を指し、プログラムが順番に実行されます。
この記事を書く目的は、プログラムの動作の仕組みと、実行ファイルの実行の背後に何が隠されているかを整理することです。通常、ソース コードから実行可能ファイルまでは多くの中間ステップを経て、各中間ステップで中間ファイルが生成されます。現在の統合開発環境では、これらの手順が隠されているだけで、統合開発環境に慣れている私たちは、これらの重要な技術内部関係者を徐々に無視してきました。この記事では、このプロセスの主要な部分のみを紹介します。それぞれの詳細については記事で説明します。
この記事を読んで、皆さんが「hello world」をただの実験だと思わないことを願っています。また、この記事を通して、プログラムがどのような動作をするのか、どのように動作するのかを理解していただければ幸いです。
関連する推奨事項:
以上がhello worldからプログラム動作の仕組みについて語るの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。