blog

Linux システムノート システムの初期化

CPUと他のデバイスはバスと呼ばれるもので接続されており、実際にはマザーボード上に集積回路が密集しており、CPUと他のデバイスの間に高速チャネルを形成しています。 これらのデバイスの中で最も重要なのは...

Apr 22, 2020 · 15 min. read
シェア

x86

コンピューターの作業モードとは何ですか?

CPUはバスと呼ばれるものによって他のデバイスに接続されています。バスは実際にはマザーボード上の集積回路の密集した配列であり、CPUと他のデバイスの間に高速チャネルを形成しています。

その中でも最も重要なのがメモリです。CPUだけでは計算タスクを完了できないため、多くの複雑な計算タスクでは中間結果を保存し、その中間結果に基づいてさらに計算を行う必要があります。

モニターに接続するグラフィックカード、ハードドライブに接続するディスクコントローラ、キーボードやマウスに接続するUSBコントローラなど、バス上には他にも多くのデバイスがあります。

CPUは1個ではなく、演算ユニット、データユニット、制御ユニットの3つで構成されています。

演算ユニットは足し算や置換などの計算だけを行います。しかし、どのようなデータを計算し、その結果をどこに置くべきかはわかりません。演算ユニットで計算したデータをいちいちバスを経由してメモリから取り出していたら遅すぎるので、データユニットがあります。

データユニットは、CPU内部のキャッシュとレジスタセットで構成され、小さいながらも高速にデータや演算結果を一時的に格納する場所です。データを置く場所があれば、計算する場所もありますが、最終的にその場所の演算をどうするかという命令が必要で、それが制御ユニットです。

制御ユニットは、統一されたコマンドセンターであり、それは次の命令を取得し、この命令を実行することができます。この命令は、データユニットからいくつかのデータを取り出し、結果を計算し、それをデータユニットのどこかに置くように計算ユニットに指示します。

プロセスが実行されると、たとえば、図の2つのプロセスAとBは、互いに分離された別のメモリ空間が存在する、プログラムは、独自のコードセグメントを形成し、それぞれプロセスAとプロセスBのメモリ空間にロードされます。もちろん、実際の状況は、コードとデータセグメントの間の単純な区別に加えて、分離が、連続ではなく、プロセスのメモリは、より詳細に分割されますが、私が言ったよりも複雑にする必要があります。

プログラムの実行中に操作されるデータや計算結果は、データセグメントの中に置かれます。では、CPUはどのようにしてこれらのプログラムを実行し、これらのデータを操作し、何らかの結果を出し、それをメモリに書き戻すのでしょうか?

CPUの制御ユニット内部には命令ポインタ・レジスタがあり、メモリ上の次の命令のアドレスを保持しています。制御ユニットは、コードセグメントから命令を取り込んで、最初に命令レジスタに入れ続けます。現在の命令は2つの部分に分かれています。1つは加算なのか置換なのかといった演算を行う部分、もう1つはどのようなデータを操作するかという部分です。この命令を実行するために、最初の部分は演算ユニットに与えられ、2番目の部分はデータユニットに与えられます。

データ・ユニットはデータ・セグメントからデータのアドレスに従ってデータ・レジスタにデータを読み出し、演算に参加することができます。演算ユニットが演算を行った後、生成された結果はデータ・ユニットのデータ・レジスタに一時的に格納されます。最終的には、命令によってデータがメモリ上のデータ・セグメントに書き戻されます。

CPUには、現在の処理プロセスのコードセグメントの開始アドレス、およびデータセグメントの開始アドレスを保存するために特別に設計された2つのレジスタがあります。これは、プロセスA、プロセスBに切り替えるなど、プロセスAの命令の現在の実装に書き込まれ、それはBの命令を実行します、このプロセスは、プロセスの切り替えと呼ばれます。

CPUとメモリは、バスを頼りにデータをやり取りしています。実は、バス上のデータには大きく分けて2種類あり、1つはアドレスデータ、つまり、どの場所のデータをメモリに取り込みたいかというもので、このタイプのバスをアドレス・バスと呼び、もう1つは実データで、このタイプのバスをデータ・バスと呼びます。

アドレスバスのビット数によって、アクセスできるアドレスの範囲が決まります。たとえば、ビットが2つしかない場合、CPUは00、01、10、11の4つの場所しか認識できず、4つ以上の場所を区別することはできません。ビットが多ければ多いほど、アクセスできる場所が増え、管理できるメモリの範囲が広がります。データバスのビット数によって、一度に取り込めるデータの数が決まります。例えば、ビットが2つしかない場合、CPUが一度にメモリから取り込めるのは2ビットだけです。8ビットを取り込むには、それを4回行わなければなりません。ビットが多ければ多いほど、一度に取り込めるデータ量が増え、アクセス速度も速くなります。

x86 これは何ですか?

CPUのバス上のビット数の標準はありますか?もし標準がなければ、ソフトウェア・レベルで共通の演算ロジックを実装する方法がないため、ソフトウェアとしてのオペレーティング・システムは厳しいものになります。インテルの技術は、業界のオープンなデファクトスタンダードとなりました。このファミリーは8086から始まったので、x86アーキテクチャと呼ばれています。

ビットプロセッサー

BIOSからブートローダへ

BIOS

コンピュータのブートボタンを押すと、マザーボードに電源が入ります。この時、オペレーティングシステムはなく、メモリは空で無一文です。CPUは何をすべきでしょうか?

マザーボード上にはROMと呼ばれるものがあり、通常私たちがメモリRAMと 呼んでいるものとは異なります。ROM は通常購入する RAM とは異なります。 通常購入する RAM は読み書き可能で、計算結果を保存することができます。ROMは読み取り専用で、BIOSと呼ばれる初期化ルーチンが組み込まれています。このスペースは非常に限られており、適切に利用する必要があります。

1K = 1Kb = 1024b = 8*1024 Bit アドレス範囲 0x00~0x3FF
1M = 1Mb = 1024K = 1024Kb = 1024*1024B アドレス範囲 0x00000~0xFFFFF
1G = 1Gb = 1024M = 1024Mb = 1024*1024K B= 10243B アドレス範囲 0x00000~0x3FFFFFFF
1TB = 1024GB = 10242MB = 10243KB = 10244B = 8*10244  

x86システムでは、1M空間の上位64K、0xF0000から0xFFFFFまでがROMにマッピングされており、アドレスのこの部分にアクセスするときは、ROMにアクセスすることになります。

コンピュータは最初に電源を入れると、CSを0xFFFFに、IPを0x0000に設定してリセットを行います。ここで、ROMの初期化作業を行うコードにジャンプするJMPコマンドがあるので、BIOSは初期化作業を開始します。

BIOSがやってくれます:

  1. BIOS システムのハードウェアが正常かどうかを確認します。
  2. 割り込みベクタテーブルと割り込みサービスルーチンを作成します。キーボードやマウスも割り込みで使うことになるからです。

bootloader

オペレーティング・システムはどこにありますか?通常、BIOS画面のハードドライブにインストールされています。ブートディスクのオプションが表示されます。ブートディスクの特徴は何ですか?通常は最初のセクタにあり、512バイトを占め、0xAA55で終わります。この条件が満たされると、ブートディスクであることを意味し、512バイト以内に関連するコードが開始されるという慣例があります。

誰がこのコードをここに置いたのでしょうか?Linuxには、Grub2(Grand UnifiedBootloader Version 2)と呼ばれるツールがあります。

grub2-mkconfig -o /boot/grub2/grub.cfg でシステム起動時のオプションを設定できます。そこにはこのような設定があります。

menuentry 'CentOS Linux (3.10.0-862.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-862.el7.x86_64-advanced-b1aceb95-6b9e-464a-a589-bed66220ebee' {
	load_video
	set gfxpayload=keep
	insmod gzio
	insmod part_msdos
	insmod ext2
	set root='hd0,msdos1'
	if [ x$feature_platform_search_hint = xy ]; then
	 search --no-floppy --fs-uuid --set=root --hint='hd0,msdos1' b1aceb95-6b9e-464a-a589-bed66220ebee
	else
	 search --no-floppy --fs-uuid --set=root b1aceb95-6b9e-464a-a589-bed66220ebee
	fi
	linux16 /boot/vmlinuz-3.10.0-862.el7.x86_64 root=UUID=b1aceb95-6b9e-464a-a589-bed66220ebee ro console=tty0 console=ttyS0,115200 crashkernel=auto net.ifnames=0 biosdevname=0 rhgb quiet 
	initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img
}

ここにあるオプションは、システム起動時にどのシステムから起動するかを選択するためのリストになります。最終的に表示されるのが下のイメージです。を grub2-install /dev/sda使用して、ブートローダを適切な場所にインストールできます。

grub2が最初にインストールするのはboot.imgです。これはboot.Sを512バイトにコンパイルしたもので、ブートディスクの最初のセクタに正式にインストールされます。このセクタはしばしば MBR と呼ばれます。 BIOS がタスクを終了すると、boot.img をハードドライブからメモリの 0x7c00 にロードして実行します。

512 バイトというのは本当に限られているので、boot.img は大したことはできません。ブートセクタは、あなたが見つけたゲートキーパーであり、 アーカイブの扉を見てはいるものの、ほとんど何も知りません。彼はあなたの宝の山がどこにあるかは知りませんが、誰に聞けばいいかは知っています。ドアマンは、アーカイブの入り口に管理事務所があると言って、あなたをドアに案内します。core.imgは管理事務所で、彼らはもう少し多くのことを知っていて、できることがあります。多くのことができます。

boot.img は core.img の最初のセクタを最初にロードします。ハードディスクから起動する場合、このセクタには diskboot.img があり、対応するコードは diskboot です。

boot.imgがdiskboot.imgに制御を渡した後、diskboot.imgはcore.imgの残りをロードするのが仕事です。diskboot.imgは、デコンプレッサのlzma_decompress.imgから始まり、kernel.img、各モジュールのイメージと続きます。これはLinuxカーネルではなく、grubカーネルであることに注意してください。

lzma_decompress.imgに対応するコードはstartup_raw.Sです。もともとkernel.imgは圧縮されていましたが、現在は実行時に解凍する必要があります。

しかし、ロードされるものが大きくなるにつれて、リアルモードの1Mのアドレス空間では収まらなくなってきます。そこで、lzma_decompress.imgは、実際の解凍において、real_to_protを呼び出し、プロテクトモードに切り替えるという重要な決定を下します。を呼び出すことです。

リアルモードからプロテクトモードへの切り替え

プロテクトモードに切り替えることでできることはたくさんありますが、そのほとんどはメモリへのアクセス方法に関係しています。プロテクトモードにすることで、自分の権限と他人に許可する権限を分けることができます。

  1. セグメンテーションの有効化とは、メモリ内部にセグメント記述子テーブルを作成し、レジスタ内部のセグメント・レジスタを、特定のセグメント記述子を指すセグメント・セレクタに変えることで、異なるプロセス間の切り替えを可能にすることです。
  2. ページング開始。管理できるメモリが大きくなり、同じ大きさのチャンクに分割する必要があります。

プロテクト・モードで必要なことの1つは、21番目のアドレス・ラインの制御ラインであるゲートA20をオンにすることです。リアル・モードの8086では、1Mのアドレス空間にアクセスできるアドレス・ラインは20本しかありません。この制限を超えるとどうなるでしょうか?もちろん、ループバックしなければなりません。プロテクト・モードでは、21番目のアドレス・ラインが動作しなければならないので、ゲートA20を開く必要があります。 プロテクト・モードを切り替える関数DATA32 call real_to_protは、21番目のアドレス・ラインの制御ラインであるゲートA20を開きます。

これで十分なスペースができました。次のステップは、圧縮されたkernel.imgを解凍し、kernel.imgにジャンプして実行を開始することです。kernel.imgはstartup.Sとたくさんのcファイルに対応し、startup.Sはgrub_mainを呼び出します。grub_mainはgrubカーネルのメイン関数です。この関数の中で、grub_load_config()が、上に書いたgrub.confファイルから設定情報の解析を始めます。普通に起動している場合、grub_mainはgrub_command_executeを呼び出し、grub_normal_execute()関数を呼び出して終了します。この関数の中で、grub_show_menu()がOSのリストを表示します。

特定のオペレーティングシステムを起動したら、grub_menu_execute_entry() を呼び出して、選択したオペレーティングシステムの解析と実行を開始します。例えば、linux16コマンドは、指定されたカーネルファイルをロードし、カーネルブートパラメータを渡すことを意味します。次にgrub_cmd_linux()関数が呼び出され、まずLinuxカーネルのImageヘッダからいくつかのデータ構造を読み込み、メモリに格納してチェックします。チェックに合格すると、Linuxカーネル・イメージ全体をメモリに読み込みます。設定ファイル内に initrd コマンドがある場合は、起動しようとしているカーネルの init ramdisk パスを渡すために使用されます。その後、grub_cmd_initrd()関数が呼び出され、initramfsをメモリにロードします。これが完了すると、grub_command_executeが実際にカーネルを起動し始めます。

カーネルの初期化

カーネルの起動はエントリ関数 start_kernel() で始まります。init/main.cファイルの中で、start_kernelはカーネルのメイン関数です。この関数を開くと、さまざまな初期化関数 XXXX_init が含まれていることがわかります。

set_task_stack_end_magic(&init_task)オペレーティング・システムの内部では、まず.NET Frameworkの命令行を持つ創設プロセスが存在します。 structtask_struct init_task = INIT_TASK(init_task)これはパラメータinit_taskを持ち、.これはシステムによって最初に生成されるプロセスで、プロセス0と呼ばれます。これはforkやkernel_threadによって生成されない唯一のプロセスです。

ここで対応する関数は trap_init()で、様々な割り込みを処理するために多くの割り込みゲートを設定します。 set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32)そのうちの1つが 、システムコール用の割り込みゲートです。システムコールも割り込みを送ることで行われます。もちろん、64ビット版ではシステムコールの方法が異なります。

mnt_init()->init_rootfs()次に初期化するのは、メモリ管理モジュールです。mm_init()は、これを初期化するための関数です。 vfs_caches_init()は、メモリベースのファイルシステムrootfsを初期化するために使用されます。 この関数の内部では、.register_filesystem(&rootfs_fs_type)というコード行があります。 struct file_system_typerootfs_fs_typeVFS仮想ファイル・システムに型が登録され、.

ために、ファイルシステムの様々な互換性は、関連するデータ構造と統一されたインタフェースの提供の抽象化層を形成するファイルの操作を抽象化する必要性は、この抽象化層は、VFS、仮想ファイルシステムです。

最後に、start_kernel() は rest_init() を呼び出して残りの初期化を行います。

プロセス1の初期化

kernel_thread(kernel_init, NULL, CLONE_FS) rest_initの最初の主要なタスクは、 , で2番目のプロセスを作成することです。これがプロセス#1です。 プロセス#1は、オペレーティング・システムにとって「画期的」なものです。これはユーザープロセスを実行するため、オーナーが独自に行っていたシステムを、他の人でも行えるようにしたことを意味します。ユーザープロセスが存在する以上、会社の運営形態を変え、権利の管理に注意を払う必要があります。

x86は階層的なパーミッション機構を備えており、領域を4つのRingに分け、内側に行くほどパーミッションが高くなり、外側に行くほどパーミッションが低くなります。x86では、クリティカルなリソースにアクセスするコードをカーネル・ステートと呼ばれるRing0に置き、通常のプログラム・コードをユーザー・ステートと呼ばれるRing3に置きます。プロテクト・モードには、より多くのアクセス・スペースがあることに加えて、もうひとつ重要な機能があります。

ユーザーステートプログラムが実行途中で、ネットワークカードにアクセスしてネットワークパケットを送信するなど、コアリソースにアクセスしたい場合、現在の実行を一時停止してシステムコールを呼び出す必要があり、その後、カーネル内のコードが実行する順番が回ってきます。まず、カーネルはシステムコールから渡されたパケットを受け取り、ネットワークカードにキューに入れ、自分の番になったら送信します。送信し終わると、システムコールは終了してユーザー状態に戻り、一時停止していたプログラムは実行を続けることができます。この一時停止の仕組みは?実は、プログラムの実行を途中でセーブしているのです。たとえば、あなたが知っている、メモリは、プログラムの実行時間の中間結果を保存するために使用され、今一時停止するには、これらの中間結果は、再び実行するため、失われることはありませんが、また、これらの中間結果に基づいて継続することができます。もう一つは、コードのどの行が現在実行されている、どこに現在のスタックは、これらはすべてのレジスタにあります。従って、一時停止の瞬間には、CPUのレジスタのすべての値を、プロセス管理システムが簡単にアクセスできる場所に一時的に保存しておく必要があります。この場所にはプロセス管理システムから簡単にアクセスできますが、これについては後でプロセス管理データ構造について説明します。システム・コールが終了して戻ると、レジスタ値はこの場所からリストアされ、プロセスは実行を継続できます。ユーザー状態 - システム・コール - レジスタの保存 - カーネル状態によるシステム・コールの実行 - レジスタのリストア - ユーザー状態に戻り、実行。

カーネル状態からユーザー状態へ

プロセス#1の起動に戻りましょう。現在実行されている関数kernel_threadはまだカーネル状態にあります。どうすればいいでしょうか?カーネルが先で、次にユーザー」というのはあまり聞きません。

kernel_threadの引数はkernel_initと呼ばれる関数で、プロセスが実行するものです。 kernel_init_freeable()kernel_initの内部では、次のようなコードで呼び出します:

if (!ramdisk_execute_command)
	ramdisk_execute_command = "/init";

ramdiskを無視して、kernel_initに戻ってみましょう。そこには次のようなコードのブロックがあります:

if (ramdisk_execute_command) {
	ret = run_init_process(ramdisk_execute_command);
 ......
}
......
if (!try_to_run_init_process("/sbin/init") 
	|| !try_to_run_init_process("/etc/init") 
 || !try_to_run_init_process("/bin/init") 
 || !try_to_run_init_process("/bin/sh"))
	return 0;

これは、プロセス1がファイルを実行していることを意味します。run_init_process関数を開いてみると、do_execveを呼び出していることがわかります。先ほどシステムコールの話をしましたが、execveは実行ファイルを実行するシステムコールです。do_はしばしばカーネルシステムコールの実装です。つまり、ramdiskの"/init "や、通常のファイルシステムの"/sbin/init"、"/etc/init"、"/bin/init"、"/bin/sh"。Linuxのバージョンによって起動するファイルは異なりますが、どれかが起動していれば問題ありません。

ramdisk

なぜ ramdisk が存在するのでしょうか?前節でカーネルが起動するときにこのパラメータを設定したことを思い出してください:

initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img

メモリベースのファイルシステムです。なぜそこに?initプログラムがファイルシステム上にあるからです。ファイルシステムはハードドライブなどのストレージデバイス上になければなりません。Linuxはストレージデバイスにアクセスしますが、アクセスするにはドライバが必要です。ストレージシステムの数が非常に限られている場合、ドライバを直接カーネルに入れることができます。

しかし、ストレージシステムはどんどん増えており、市販されているすべてのストレージシステムのドライバをデフォルトでカーネルに入れると、カーネルが大きくなりすぎてしまいます。どうすればいいでしょうか?まずメモリベースのファイルシステムを手に入れる必要があります。メモリアクセスにはドライバは必要ありません。

その後、ramdisk 上で /init を実行し、終了する頃にはユーザーランドにいるはずです。initプログラムは、ストレージシステムのタイプに応じたドライバをロードし、そのドライバで本当のルートファイルシステムをセットアップします。本当のルートファイルシステムができたら、ramdisk上の/initがファイルシステム上でinitを開始し、続いて様々なシステムの初期化を行います。システムのサービスを開始し、コンソールを起動して、ユーザーがログインできるようにします。あまり興奮しないでください。rest_initが行う最初の大きなことは完了です。ユーザーランドのすべてのプロセスの先祖を形成するだけです。

プロセス#2の作成

ユーザー状態のプロセスにはすべてお兄さんがいるので、カーネル状態のプロセスを管理する人がいるのでしょうか?はい、rest_initの2番目に重要なのは3番目のプロセス、プロセス2です。

kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 繰り返しますが、プロセスの作成にはkernel_thread関数が使用されます。関数名threadがThreadと訳されていることに注意することが重要です。スレッドとプロセスの違いは何ですか?なぜここでプロセスが作成され、関数名が thread なのですか?

ユーザーステートの観点からは、作成プロセスとはまさにプロジェクトのセットアップ、つまりプロジェクトの開始です。このプロジェクトには、会議室やデータベースなど、たくさんのリソースが含まれています。これらのものはすべてプロジェクトに属しますが、プロジェクトにはそれを実行する人が必要です。複数の人が異なる部分を並行して実行することをマルチスレッドと呼びます。もし一人しかいなければ、それがこのプロジェクトのメインスレッドです。

しかし、カーネルの状態から見ると、タスクと総称されるプロセスもスレッドも、同じデータ構造を使用し、同じチェーンテーブルにフラットに配置されています。

ここでの関数kthreaddは、すべてのカーネル状態のスレッドのスケジューリングと管理を担当し、カーネル状態で実行されているすべてのスレッドの祖先です。

プロセス#0 は fork や kernel_thread によって生成されていない唯一のプロセスで、プロセスリストの最初のものです。

プロセス #1 はユーザー状態の祖先プロセスです。

プロセス#2は、カーネルステートで実行されているすべてのスレッドの祖先です。

システムコール

glibcでのシステムコールのラップ

システムコール表

システムコールはすべてシステムコールテーブルで終わります。32ビットのシステムコールテーブルは arch/x86/entry/syscalls/syscall_32.tbl ファイルに定義されています。例えば、openはこのように定義されています:

5 i386 open sys_open compat_sys_open

64ビットシステムコールはarch/x86/entry/syscalls/syscall_64.tblという別のファイルで定義されています。例えば、openはこのように定義されています:

2 common open sys_open

最初の列の数字はシステム・コール番号です。ご覧のように、システムコール番号は32ビットと64ビットで異なります。3列目はシステムコールの名前で、4列目はカーネルにおけるシステムコールの実装関数です。ただし、これらはすべてsys_で始まります。

Read next

セキュリティ・インシデント共有のための OpenIOC のコンセプト

セキュリティインシデント対応中にセキュリティインシデント調査者が直面する課題の1つは、攻撃者の活動、使用されたツール、マルウェア、または IOC と呼ばれるその他の攻撃指標を含む、調査プロセスからのすべての情報を整理する効率的な方法を見つけることです。

Apr 22, 2020 · 3 min read