IRC=チャンネルでこの質問をしました。悲しいことに私は輪になって回っています。
Stdio.hからのprintf()などの関数がWindowsオペレーティングシステムに「通信」する方法について、高レベルの概要(ただし、必要に応じていくつかの技術的な詳細を含む)を目指しています。
私はMSVCRT.dllについて少し知っています。ntdll.dllのネイティブAPIに移動するkernel32.dllとの通信方法など、Windows APIの一部です。 VisualStudioはC標準ライブラリにmsvcrt100.dllを使用していると思います...
誰かが内部で何が起こるかを理解するためにオープンソースのC標準ライブラリを使用することを勧めましたVisual Studioプロジェクトでの使用方法がわからないだけでなく、OSとの通信方法もわかりません。
いくつかのステップを逃した私の恐ろしい理解は次のとおりです:
1)printf()の構文はヘッダーstdio.hに対してチェックされます
2)プログラムを実行すると、printfにmsvcrt100が使用されます
3)msvcrt100は、kernel32などの必要なWindowsライブラリをロードします。
4)Kernel32はそれをntdll.dllに渡します
この質問はCに焦点を当てていますが、C++が同じである場合は自由に投稿してください。
Windowsのみ。
具体的な詳細はオペレーティングシステムによって異なりますが、ある程度理解してから開始することをお勧めします。
これらの(ディスクベースの)ファイル形式には、マシンコード、初期プログラムデータ値、 再配置 、およびシンボルテーブルのセクションが含まれています。
コードをハードウェアで実行する前に、記号で記述されたすべての値(つまり、main
、printf
)をメモリロケーションに完全に解決する必要があります。
コンパイラー(およびアセンブラー)は、さまざまなコードおよびデータの最終的なメモリー位置に関するコンパイル時の不完全な情報を持っています。したがって、これらの情報は、最終的なメモリーアドレスとしてではなく、シンボリックにオブジェクトファイル内で共有されます。これは、再配置とシンボルの場所ですテーブルが出てきます。
シンボルテーブルにはインポートとエクスポートの両方があります。エクスポートでは、オブジェクトファイルのセクション(コード/データ)内のオフセットに文字列名を指定します。一方、インポートは文字列名(後で解決するための外部)のみに関連付けられています。
シンボルのメモリアドレスがわかったら、再配置により、オブジェクトファイルのコンシューマにマシンコードとデータを修正する方法と場所を指示します。したがって、再配置は通常、シンボルテーブルのエントリを参照します(オブジェクトモジュールのセクションも参照します...)。
プログラムの.exeファイルには、実行可能ファイルのヘッダーのエントリポイントとしてマークされている1つの特別な場所main
があります。プログラムの.exeでも、すべての外部参照が何らかの方法で解決されます。ただし、実行可能ファイルを生成するリンカには、すべてのシンボルのメモリアドレスに関する完全な情報はありません。ロード時間。そのため、.exeファイルには再配置とシンボルテーブルがまだ残っています。
.exeは、すべての.objファイルを1つのファイルに結合します。コードセクションはデータセクションと同様に連結されます。 .objファイルと比較して、objファイルの再配置の一部を解決して、.exeファイルから削除できます。その他の再配置では、形式を簡略化できます(特定のシンボルを参照するのではなく、コードまたはデータセクションを参照します。これにより、再配置エントリが短くなります)。
通常、オペレーティングシステムローダーが最終的にコードとデータセクションの場所を決定します。これにより、メモリアドレスのシンボルへの割り当てが完了します。つまり、未解決の再配置を実行できるようになります。
DLL(.dll)ファイルは、特定のエクスポートがあることを除いて、.exeファイルに似ています。リンカーによって作成された.exeファイルは、.dllファイルへの参照を持っている可能性があります。これらの参照は、ダイナミックリンクローダー。exeファイルをロードする操作には.dllファイルもロードする必要があり、追加の.dllファイルをロードする必要がある場合もあります。再配置エントリに従って、それらすべてが相互に相互リンクされます。
次に、ハードウェアはcall printf
などのマシンコード命令を実行します。ここで、printf
への参照は、そのメモリの場所によって指定されます。多くのアプローチがあり、ジャンプテーブルまたはいくつかの命令のコードシーケンスを含むアプローチもあります。ただし、マシンコードの実行中、ハードウェアはメモリアドレスのみを認識/認識し、シンボル名は認識しないと言えば十分です。すべてのシンボリック参照は、再配置およびシンボルテーブルエントリのシステムによって、最終的にメモリアドレスに解決されます。
printf
は、適切な場合(たとえば、ローカルバッファーがいっぱいの場合)、ローカルバッファーをディスクまたはデバイスにフラッシュするなど、何らかの システムコール を呼び出して入出力を実行します。システムコールは、ユーザープロセスがオペレーティングシステムの操作を要求する方法です。呼び出し元がユーザープロセス内にあり、呼び出し先がオペレーティングシステムのエントリポイントであることを除いて、通常の呼び出しと同様のシステムコール。システムコールは、共有デバイスへのアクセスを提供するために必要に応じて、特権を(ユーザーからカーネルに)上げる制御された方法を提供します。システムコールはメモリアドレスを使用しませんが、代わりに各システムコールは単純な整数インデックスに関連付けられています。これにより、オペレーティングシステムを呼び出す.exeと.dllがカーネルメモリアドレスを認識できなくなります。
WindowsプログラムがDLLと対話する方法は2つあります。
通常の方法は、プロセスのスタートアップの一部に、dllのコンテンツをプロセスのアドレス空間にマッピングするウィンドウが含まれ、エクスポートされた関数(およびデータ)の(ロードされた)アドレスをプログラムのコード内の(デフォルト)呼び出しターゲットに関連付けることです。 main
が始まる頃には、すべてがdllの関数がexe内にあるかのように動作します。
もう1つの方法は、dllのロードが起動後まで遅延され、dllの関数へのポインターが要求されることです。ここで重要な2つの関数は LoadLibrary
と GetProcAddress
です。
あなたはおそらく WriteConsole
のドキュメントを読みたいでしょう。 printf
に最も密接に対応するのは、ネイティブのWindows関数です。ご覧のとおり、フォーマットされた文字列が必要です-puts
のように機能します。 sprintf
を呼び出す前に、最初にWriteConsole
を呼び出す必要がある場合があります。
C++にも同様の機能分割があります。 std::cout
は最終的にWriteConsole
を呼び出しますが、operator<<
は、個々の引数のフォーマットを行います。