数日前、読者の小さなパートナーが尋ねました:プロセスが作成された後、私が書いた主な機能にどのように入るのですか?
今日、この記事ではこのトピックについて説明します。
まず、この問題の議論の範囲を定義します:C / C ++言語
この記事では主に、オペレーティングシステムレベルでのプロセスとスレッドの作成と初期化、およびインタープリターと仮想マシンに基づくPythonやJavaなどの言語がメイン関数の実行に入る方法について説明します。この背後にあるパスは長くなります。 (インタープリターと仮想マシンの内部実行プロセスを含む)、後で機会があれば説明します。したがって、ここでは、C / C ++などのネイティブ言語の主な機能がどのように入力されるかに焦点を当てます。
この記事では、LinuxとWindowsの2つの主要なプラットフォームでの詳細なプロセスについて説明します。
プロセスの作成
最初のステップは、プロセスを作成することです。
Linuxでは、新しいプロセスを開始する必要があります。これは通常、fork + execシリーズの関数によって実装されます。前者は現在のプロセスをツインチャイルドプロセスに「フォーク」し、後者はこの子プロセスの実行ファイルを置き換えて子を実行します。プロセスの新しいプログラムファイル。
ここでのforkおよびexecシリーズの関数は、オペレーティングシステムからアプリケーションプログラムに提供されるAPI関数です。これらは、最終的にシステム呼び出しを介してオペレーティングシステムカーネルに入り、カーネルのプロセス管理メカニズムを介してプロセスの作成を完了します。
オペレーティングシステムカーネルがプロセスの作成を担当し、主に次のタスクを実行する必要があります。
カーネル内のプロセスを記述するために使用されるデータ構造を作成します。Linuxではtask_struct
新しいプロセスのページディレクトリとページテーブルを作成して、新しいプロセスのメモリアドレススペースを構築します
Linuxカーネルでは、歴史的な理由により、初期のLinuxカーネルにはスレッドの概念がありませんでしたが、task:task_structを使用してプログラムの実行インスタンスを記述しました:process。
カーネルでは、タスクはtask_struct、つまりプロセスに対応し、カーネルのスケジューリング単位もtask_structです。
その後、マルチスレッドの概念が登場しました。Linuxカーネルでマルチスレッドテクノロジをサポートするために、task_structは実際にはスレッドを表し、プロセスは複数のtask_structをグループに結合することによって記述されます(構造内のグループIDフィールドを介して) 。したがって、Linux上のスレッドは軽量プロセスとも呼ばれます。
システムコールフォークの重要な使命は、新しいプロセスのtask_struct構造を作成することです。作成が完了すると、プロセスにはスケジューリングユニットがあります。後で、スケジューリングに参加して、実行する機会を得ることができます。
実行可能ファイルをロードする
プロセスがフォークによって正常に作成された後、この時点での子プロセスと親プロセスは、有糸分裂を起こしている細胞と同等であり、2つのプロセスは「ほぼ」同一です。
子プロセスが新しいプログラムを実行するためには、プロセス実行可能プログラムを置き換えるために、子プロセスに一連のexec関数も必要です。
execシリーズの関数は、システム呼び出しのカプセル化でもあります。これらを呼び出すことにより、実際の作業を実行するためにカーネルsys_execveに入ります。
この作業には多くの詳細があり、重要なタスクの1つは、実行可能ファイルをプロセススペースにロードし、それを分析して実行可能ファイルのエントリアドレスを抽出することです。
CやC ++などの高レベル言語で記述されたコードを使用し、最終的に実行可能ファイルはコンパイラによってコンパイルされます。LinuxではELF形式です。WindowsではPEファイルと呼ばれます。
ELFファイルであるかPEファイルであるかに関係なく、それぞれのファイルヘッダーには、プログラムを実行する場所を示す実行可能ファイルの命令エントリアドレスが記録されます。
このエントリポイントはどこにありますか?それは私たちの主な機能ですか?前の問題を解決するための重要なポイントは次のとおりです。プロセスが作成された後、プロセスはどのようにしてこのエントリアドレスに到達しましたか?
WindowsまたはLinuxに関係なく、アプリケーションスレッドは、ユーザースペースとカーネルスペースの間を行き来することがよくあります。これは、次の状況で発生する可能性があります。
システムコール
割り込み
異常な
カーネルから戻るとき、スレッドはどのようにしてそれがどこから来たのか、そしてどこでアプリケーションスペースに戻って実行を継続するのかを知るのですか?
答えは、カーネルスペースに入ると、スレッドは自動的にコンテキスト(実際には、命令レジスタEIPなどの一部のレジスタの内容)をスレッドのスタックに保存し、それがどこから来たかを記録し、カーネルから戻ってからスタックから戻るまで待機するということです。この情報をロードし、元の場所に戻って実行を続行します。
前述のように、子プロセスはsys_execveシステム呼び出しを介してカーネルに入ります。実行可能ファイルの分析が後で完了した後、ELFファイルのエントリアドレスが取得され、スタックに最初に保存されたコンテキスト情報が変更され、EIPが変更されます。 ELFファイルのエントリアドレスをポイントします。このように、sys_execveシステム呼び出しが終了すると、ユーザースペースに戻った後、新しいプログラムエントリに直接移動して、コードの実行を開始できます。
したがって、非常に重要な機能は次のとおりです。execシリーズの関数は通常の状況では戻りません。入力されると、実行フローはミッションの完了後に新しい実行可能ファイルエントリにシフトします。
もう1つ言及すべきことは、Linuxは、ELFファイルに加えて、MS-DOSやCOFFなどの他の形式の実行可能ファイルもサポートしているということです。
バイナリ実行可能ファイルに加えて、シェルスクリプトもサポートされています。この場合、スクリプトインタープリタープログラムが開始のエントリポイントとして使用されます。
ELFエントリーからメイン機能まで
上記は、実行可能ファイルのエントリアドレスに対して新しいプロセスが実行される方法を説明しています。
同時に、エントリーアドレスは何ですか?それが私たちの主な機能ですか?
これは、実行後にクラシックなhelloワールドを出力する単純なCプログラムです。
#include <stdio.h>
int main() {
printf("hello, world!\n");
return 0;
}
gccでコンパイルした後、ELF実行可能ファイルが生成されます。readelf命令により、ELFファイルの分析を実現できます。ここで、ELFファイルのエントリアドレスが0x400430であることがわかります。
次に、分解アーティファクトを使用し、IDAがこのファイルを開いて分析し、0x400430の入り口にある関数を確認しました。
ご覧のとおり、エントリポイントは_startという 関数であり、メイン関数ではありません。
_startの終わりに、__ libc_start_main 関数が呼び出され 、この関数はlibc.soにあります。
この関数はどこから来たのか疑問に思われるかもしれませんか?コードでは使用していませんか?
実際、main関数に入る前に、実行する必要のあるもう1つの重要な作業があります。それは、C / C ++ランタイムライブラリの初期化です。上記の __libc_start_main がこの作業を行っています。
GCCを使用してコンパイルすると、コンパイラはランタイムライブラリのリンクを自動的に完了し、メイン関数をカプセル化して呼び出します。
glibcはオープンソースです。このプロジェクトのlibc-start.cファイルをGitHubで見つけて 、__ libc_start_mainの素顔を垣間見ること ができます。私たちの主な関数はそれによって呼び出されます。
完全なプロセス
この時点で、フォークの作成プロセスから、execシリーズの関数による実行可能ファイルの置換の完了、実行プロセスへのELFファイルの入力、およびメイン関数の完全なプロセスまでのプロセスを整理しました。
Windowsでのいくつかの違い
以下では、Windowsでのこのプロセスのいくつかの違いを簡単に紹介します。
最初のステップはプロセスを作成することです。Windowsシステムはfork + execの2つのステップを1つのステップにマージします。CreateProcessシリーズ関数は1つのステップで使用され、子プロセスの実行可能ファイルパスはそのパラメーターで指定されます。
Linuxのプロセスとスレッドのあいまいな境界とは異なり、Windowsオペレーティングシステムでは、カーネルにはプロセスとスレッドの概念が明確に定義されています。プロセスはEPROCESS構造で表され、スレッドはETHREAD構造で表されます。
したがって、Windowsでは、プロセス関連の作業の準備ができたら、プロセスの最初のスレッドであるメインスレッドであるカーネルスケジューリングに参加する別の実行ユニットを作成する必要があります。もちろん、この作業はCreateProcessシリーズの関数にもカプセル化されています。
新しいプロセスのメインスレッドが作成されると、システムスケジューリングへの参加が開始されます。メインスレッドはどこから実行を開始しますか?カーネルは作成時に明確に指定されます:nt!KiThreadStartup、これはカーネル関数であり、スレッドはここから実行を開始します。
ここからスレッドを開始した後、Windowsの非同期プロセスを介してAPCメカニズムを呼び出し、事前に挿入したAPCを実行し、アプリケーション層に実行フローを導入して、コアDLLファイル(Kernel32.dll)の読み込みなど、Windowsプロセスアプリケーションの初期化作業を行います。 、Ntdll.dll)など。
次に、再びAPCメカニズムを介して、実行可能ファイルのエントリポイントに移動します。
この背後にあるメカニズムはLinuxのメカニズムと似ています。また、メイン関数に直接アクセスすることもできません。代わりに、C / C ++ランタイムライブラリを最初に初期化してから、ランタイム関数をラップしてから、最終的にメイン関数に到達します。
以下は、Windowsでの作成プロセスからメイン機能までの完全なプロセスです(高解像度の大きな画像:https://bbs.pediy.com/upload/attach/201604/501306_qz5f5hi1n3107kt.png):
これで、プロセスの開始からメイン機能にステップバイステップで到達する方法がわかりましたか?疑問や困惑があれば、交換のためにメッセージを残してください。
過去のTOP5記事
CPUには明らかに8コアがありますが、なぜネットワークカードが必死に1番コアを投げているのですか?