Linuxカーネルブート。 パート1

ブートローダーからカーネルへ



以前の記事を読んだら、低レベルプログラミングのための私の新しい趣味について知っています。 x86_64



Linux向けのx86_64



プログラミングに関する記事をいくつか書いたと同時に、Linuxカーネルのソースコードを掘り下げました。



私は、低レベルのものがどのように機能するかを理解することに非常に興味があります:プログラムが私のコンピューターで実行される方法、それらがメモリに配置される方法、カーネルがプロセスとメモリを管理する方法、ネットワークスタックが低レベルで動作する方法など。 そこで、 x86_64アーキテクチャ用のLinuxカーネルに関する別のシリーズの記事を書くことにしました。



私はプロのカーネル開発者ではなく、職場でカーネルコードを作成しないことに注意してください。 これはただの趣味です。 低レベルのものが好きで、それらを掘り下げることは興味深いです。 そのため、混乱や質問/コメントが表示される場合は、 Twitterメール、またはチケットを作成してください。 ありがたいです。



すべての記事はGitHubリポジトリで公開されています 。英語や記事の内容に問題がある場合は、遠慮なくプルリクエストを送信してください。



これは公式のドキュメントではなく、単にトレーニングと知識の共有であることに注意してください。



必要な知識





いずれにせよ、あなたがそのようなツールを学び始めたばかりなら、この記事と以下の記事で何かを説明しようとします。 さて、紹介が終了したら、Linuxカーネルと低レベルのものに飛び込む時が来ました。



Linux 3.18カーネルの時代にこの本を書き始めましたが、それ以来多くのことが変わりました。 変更がある場合は、それに応じて記事を更新します。



魔法の電源ボタン、次は?



これらはLinuxカーネルに関する記事ですが、少なくともこのセクションではまだ到達していません。 ラップトップまたはデスクトップコンピューターの魔法の電源ボタンを押すと、すぐに機能し始めます。 マザーボードは電源に信号を送ります。 信号を受信した後、コンピューターに必要な電気量を供給します。 マザーボードが「Power OK」信号を受信するとすぐに、CPUを起動しようとします。 彼は、残りのすべてのデータを自分のレジスターにダンプし、それぞれに事前定義された値を設定します。



プロセッサ80386以降のバージョンでは、再起動後にCPUレジスタに次の値が含まれている必要があります。



  IP 0xfff0
 CSセレクター0xf000
 CSベース0xffff0000 


プロセッサはリアルモードで動作し始めます 。 少し戻って、このモードのメモリセグメンテーションを理解しみましょう。 リアルモードは、すべてのx86互換プロセッサ( 8086から最新の64ビットIntelプロセッサまで)でサポートされています。 8086プロセッサは20ビットのアドレスバスを使用します。つまり、 0-0xFFFFF



または1



アドレス空間で0-0xFFFFF



できます。 ただし、最大アドレスが2^16-1



または0xffff



(64キロバイト)の16ビットレジスタしかありません。



使用可能なアドレス空間全体を使用するには、 メモリセグメンテーションが必要です。 すべてのメモリは、 65536



バイト(64 KB)の固定サイズの小さなセグメントに分割されます。 16ビットのレジスタでは64 KBを超えるメモリにアクセスできないため、別の方法が開発されました。



アドレスは2つの部分で構成されます。1)ベースアドレスを持つセグメントセレクタ。 2)ベースアドレスからのオフセット。 リアルモードでは、セグメント * 16



ベースアドレスは * 16



です。 したがって、メモリ内の物理アドレスを取得するには、セグメントセレクターの一部を16倍して、それにオフセットを追加する必要があります。



   =   * 16 + 
      
      





たとえば、 CS:IP



レジスタの値が0x2000:0x0010



場合、対応する物理アドレスは次のようになります。



 >>> hex((0x2000 << 4) + 0x0010) '0x20010'
      
      





ただし、最大セグメントのセレクターとオフセット0xffff:0xffff



を取得すると、アドレスが取得されます。



 >>> hex((0xffff << 4) + 0xffff) '0x10ffef'
      
      





つまり、最初のメガバイトの後の65520



バイトです。 リアルモードでは1メガバイトしか使用できないため、A20回線を無効にする0x00ffef



0x10ffef



なります。



さて、これでリアルモードとこのモードでのメモリアドレス指定について少し理解できました。 リセット後のレジスタ値の説明に戻ります。



CS



レジスタは、可視セグメントセレクターと非表示のベースアドレスの2つの部分で構成されています。 ベースアドレスは通常、セグメントセレクタの値に16を掛けることで形成されますが、ハードウェアリセット中、CSレジスタのセグメントセレクタは0xf000



で、ベースアドレスは0xffff0000



です。 プロセッサは、CSが変更されるまでこの特別なベースアドレスを使用します。



開始アドレスは、EIPレジスタの値にベースアドレスを追加することにより形成されます。



 >>> 0xffff0000 + 0xfff0 '0xfffffff0'
      
      





0xfffffff0



を取得します。これは、4 GB未満の16バイトです。 この点をリセットベクトルと呼びます。 これは、リセット後にCPUが最初の命令の実行を待つメモリ内の場所です。ジャンプ操作( jmp )は、通常BIOSエントリポイントを示します。 たとえば、 corebootのソースコード( src/cpu/x86/16bit/reset16.inc



)を見ると、次のように表示されます。



  .section ".reset", "ax", %progbits .code16 .globl _start _start: .byte 0xe9 .int _start16bit - ( . + 2 ) ...
      
      





ここでは、オペレーションコード( opcodejmp



、つまり0xe9



が表示され、宛先アドレス_start16bit - ( . + 2)



です。



また、 reset



セクションは16バイトであり、アドレス0xfffff0



src/cpu/x86/16bit/reset16.ld



)から実行するようにコンパイルされていることが0xfffff0



ます。



 SECTIONS { /* Trigger an error if I have an unuseable start address */ _bogus = ASSERT(_start16bit >= 0xffff0000, "_start16bit too low. Please report."); _ROMTOP = 0xfffffff0; . = _ROMTOP; .reset . : { *(.reset); . = 15; BYTE(0x00); } }
      
      





BIOSが起動します。 BIOSハードウェアを初期化して確認した後、ブートデバイスを見つける必要があります。 起動順序はBIOS設定に保存されます。 ハードドライブから起動しようとすると、BIOSはブートセクターを見つけようとします。 MBRパーティションディスクでは、ブートセクターは最初のセクターの最初の446バイトに格納されます。各セクターは512バイトです。 最初のセクターの最後の2バイトは0x55



0xaa



です。 BIOSが起動デバイスであることを示しています。



例:



 ; ; :       Intel x86 ; [BITS 16] boot: mov al, '!' mov ah, 0x0e mov bh, 0x00 mov bl, 0x07 int 0x10 jmp $ times 510-($-$$) db 0 db 0x55 db 0xaa
      
      





収集して実行します:



nasm -f bin boot.nasm && qemu-system-x86_64 boot







QEMUは、作成したばかりのboot



バイナリをディスクイメージとして使用するコマンドを受け取りboot



。 上記で生成されたバイナリファイルは、ブートセクタの要件( 0x7c00



から始まり、マジックシーケンスで終わる)を満たすため、QEMUはバイナリをディスクイメージのマスターブートレコード(MBR)と見なします。



表示されます:







この例では、コードが16ビットのリアルモードで0x7c00



れ、メモリのアドレス0x7c00



から開始することが0x7c00



ます。 開始後、 0x10割り込みが発生し、単に文字が出力されます!



; 残りの510バイトをゼロで埋め、2つのマジックバイト0xaa



0x55



終了します。



objdump



ユーティリティを使用してバイナリダンプを表示できます。



nasm -f bin boot.nasm

objdump -D -b binary -mi386 -Maddr16,data16,intel boot








もちろん、実際のブートセクターには、ゼロの束と感嘆符の代わりに、ブートプロセスとパーティションテーブルを継続するコードがあります:)。 この瞬間から、BIOSはブートローダーに制御を渡します。



:上記で説明したように、CPUはリアルモードです。 メモリ内の物理アドレスの計算は次のとおりです。



   =   * 16 + 
      
      





16ビットの汎用レジスタのみがあり、16ビットレジスタの最大値は0xffff



であるため、最大値での結果は次のようになります。



 >>> hex((0xffff * 16) + 0xffff) '0x10ffef'
      
      





ここで、 0x10ffef



1 + 64 - 16



です。 8086プロセッサ (リアルモードを備えた最初のプロセッサ)には、20ビットのアドレス行があります。 2^20 = 1048576



であるため、実際に利用可能なメモリは1 MBです。



一般に、リアルモードメモリのアドレス指定は次のとおりです。



  0x00000000-0x000003FF-リアルモードの割り込みベクターのテーブル
 0x00000400-0x000004FF-BIOSデータ領域
 0x00000500-0x00007BFF-使用されていません
 0x00007C00-0x00007DFF-ブートローダー
 0x00007E00-0x0009FFFF-使用されていません
 0x000A0000-0x000BFFFF-ビデオRAM(VRAM) 
 0x000B0000-0x000B7777-モノクロビデオメモリ
 0x000B8000-0x000BFFFF-カラーモードビデオメモリ
 0x000C0000-0x000C7FFF-ビデオROM BIOS
 0x000C8000-0x000EFFFF-シャドウエリア(BIOSシャドウ)
 0x000F0000-0x000FFFFF-システムBIOS 


記事の冒頭で、プロセッサの最初の命令は0xFFFFFFF0



にあり、これは0xFFFFF



(1 MB)よりもはるかに大きいと書かれています。 CPUはリアルモードでこのアドレスにどのようにアクセスできますか? corebootドキュメントの回答:



0xFFFE_0000 - 0xFFFF_FFFF: 128 ROM







実行の開始時には、BIOSはRAMではなくROMにあります。



ブートローダー



Linuxカーネルは、 GRUB 2syslinuxなどのさまざまなブートローダーでロードできます。 カーネルには、Linuxサポートを実装するためのブートローダー要件を定義するブートプロトコルがあります。 この例では、GRUB 2を使用しています。



ブートプロセスを続行すると、BIOSはブートデバイスを選択し、制御をブートセクターに転送し、実行はboot.imgで始まります。 サイズが限られているため、これは非常に単純なコードです。 メインのGRUB 2イメージに移動するためのポインターが含まれており、 diskboot.imgで始まり、通常は最初のパーティションの前の未使用スペースの最初のセクターの直後に保存されます。 上記のコードは、ファイルシステムを処理するためのGRUB 2カーネルとドライバーを含む残りのイメージをメモリにロードします。 その後、 grub_main関数が実行されます。



grub_main



関数は、コンソールを初期化し、モジュールのベースアドレスを返し、ルートデバイスを設定し、grub構成ファイルをロード/解析し、モジュールをロードします。 実行の最後に、grubを通常モードにします。 grub_normal_execute



関数( grub-core/normal/main.c



ソースファイルから)は、最後の準備を完了し、オペレーティングシステムを選択するためのメニューを表示します。 grubメニュー項目の1つを選択すると、 grub_menu_execute_entry



関数がboot



これにより、grub boot



コマンドが実行され、選択したOSがロードされます。



カーネルブートプロトコルで示されているように、ブートローダーはカーネルインストールヘッダーのいくつかのフィールドを読み取り、入力する必要があります。 0x01f1



は、カーネルインストールコードからのオフセット0x01f1



から始まります。 このオフセットは、 リンカスクリプトに示されています 。 カーネルヘッダーarch / x86 / boot / header.S次で始まります



  .globl hdr hdr: setup_sects: .byte 0 root_flags: .word ROOT_RDONLY syssize: .long 0 ram_size: .word 0 vid_mode: .word SVGA_MODE root_dev: .word 0 boot_flag: .word 0xAA55
      
      





ブートローダーは、このヘッダーと他のヘッダー(この例のようにLinuxブートプロトコルでタイプwrite



としてのみマークされている)をコマンドラインから受け取った値またはブート時に計算した値で埋める必要があります。 ここで、すべてのヘッダーフィールドの説明と説明については説明しません。 カーネルがそれらをどのように使用するかについては後で説明します。 すべてのフィールドの説明については、ダウンロードプロトコルを参照してください。



カーネルブートプロトコルで確認できるように、メモリは次のように表示されます。



  | 保護されたカーネルモード|
 100000 + ------------------------ +
          |  I / Oマッピング|
 0A0000 + ------------------------ +
          | 予備  BIOSの場合| 可能な限り無料で残す
          〜〜
          | コマンドライン|  (X + 10000未満の場合もあります)
 X + 10000 + ------------------------ +
          | スタック/ヒープ| 実際のカーネルモードコードを使用するには
 X + 08000 + ------------------------ +
          | カーネルのインストール| 実際のカーネルモードコード
          | カーネルブートセクター| レガシーカーネルブートセクター
        X + ------------------------ +
          | ローダー|  <-エントリポイント0x7C00ブートセクタ
 001000 + ------------------------ +
          | 予備  MBR / BIOSの場合|
 000800 + ------------------------ +
          | 通常使用  MBR |
 000600 + ------------------------ +
          | 中古  BIOSのみ|
 000000 + ------------------------ +


そのため、ローダーが制御をカーネルに転送するとき、次のアドレスで開始します。



 X + sizeof (KernelBootSector) + 1
      
      





X



はカーネルブートセクタのアドレスです。 この例では、メモリダンプに見られるように、 X



0x10000



です。







ブートローダーはLinuxカーネルをメモリに移動し、ヘッダーフィールドに入力してから、対応するメモリアドレスに移動しました。 これで、カーネルのインストールコードに直接アクセスできます。



カーネルインストールフェーズの開始



最後に、私たちは中心にいます! 技術的にはまだ実行されていません。 まず、カーネルのインストール部分では、解凍プログラムやメモリ管理を含むものなど、何かを構成する必要があります。 このすべての後、彼女は本当のコアを開梱してそこに行きます。 インストールは、 _start文字を使用してarch / x86 / boot / header.Sで開始されます



一見すると、これは少し奇妙に見えるかもしれません。彼の前にはいくつかの指示があります。 しかし、昔、Linuxカーネルには独自のブートローダーがありました。 たとえば、実行すると、



qemu-system-x86_64 vmlinuz-3.18-generic







表示されます:







実際、 header.S



ファイルは、マジックナンバーMZ (上記のダンプのスクリーンショットを参照)、エラーメッセージのテキスト、およびPEヘッダーで始まります。



 #ifdef CONFIG_EFI_STUB # "MZ", MS-DOS header .byte 0x4d .byte 0x5a #endif ... ... ... pe_header: .ascii "PE" .word 0
      
      





UEFIサポートのあるオペレーティングシステムをロードする必要があります。 次の章でそのデバイスを検討します。



カーネルをインストールするための実際のエントリポイント:



 // header.S line 292 .globl _start _start:
      
      





ローダー(grub2など)はこのポイント( MZ



からのオフセット0x200



)を知っており、 header.S



直接header.S



ますが、 .bstext



.bstext



セクションで始まり、エラーメッセージのテキストがあります:



 // // arch/x86/boot/setup.ld // . = 0; // current position .bstext : { *(.bstext) } // put .bstext section to position 0 .bsdata : { *(.bsdata) }
      
      





カーネルインストールエントリポイント:



  .globl _start _start: .byte 0xeb .byte start_of_setup-1f 1: // // rest of the header //
      
      





ここにオペレーションコードjmp



0xeb



)があり、これはポイントstart_of_setup-1f



ます。 たとえば、 Nf



表記では、 2f



はローカルラベル2:



指し2:



この例では、これはラベル1



であり、遷移直後に存在し、セットアップヘッダーの残りを含みます。 インストールヘッダーの直後に、 start_of_setup



ラベルで始まる.entrytext



セクションがあります。



これは、実際に実行される最初のコードです(もちろん、以前のジャンプ命令以外)。 カーネルのインストールの一部がローダーから制御を受け取った後、最初のjmp



命令は、実際のカーネルモードの先頭から、つまり最初の512バイトの後のオフセット0x200



配置されます。 これは、Linuxカーネルブートプロトコルとgrub2ソースコードの両方で確認できます。



 segment = grub_linux_real_target >> 4; state.gs = state.fs = state.es = state.ds = state.ss = segment; state.cs = segment + 0x20;
      
      





この場合、カーネルは0x10000



起動します。 これは、カーネルのインストールを開始した後、セグメントレジスタが次の値を持つことを意味します。



gs = fs = es = ds = ss = 0x10000

cs = 0x10200








start_of_setup



進んだ後start_of_setup



カーネルは次のことを行う必要があります。





これがどのように実装されているか見てみましょう。



セグメントケースアライメント



まず、カーネルはds



およびes



セグメントレジスタが同じアドレスを指していることを確認します。 次に、 cld



を使用して方向フラグをクリアします。



  movw %ds, %ax movw %ax, %es cld
      
      





先ほど書いたように、grub2はデフォルトでカーネルインストールコードを0x10000



に、 cs



0x10200



します。これは、ファイルの先頭からではなく、ここからの遷移から実行が開始されるためです。



 _start: .byte 0xeb .byte start_of_setup-1f
      
      





これは、 4d 5aからの512



バイトのオフセットです。 他のすべてのセグメントレジスタと同様に、 cs



0x10200



から0x10000



に揃える必要もあります。 その後、スタックをインストールします。



  pushw %ds pushw $6f lretw
      
      





この命令は、 ds



値をスタックにプッシュし、その後にラベル6のアドレスとlretw



命令がlretw



ますlretw



命令は、ラベル6



のアドレスをコマンドカウンターのレジスタにロードし、値ds



cs



ロードします。 その後、 ds



cs



は同じ値になります。



スタック設定



このコードのほとんどすべては、リアルモードでC環境を準備するプロセスの一部です。 次の手順は、 ss



レジスタ値を確認し、 ss



値が正しくない場合に正しいスタックを作成することです。



  movw %ss, %dx cmpw %ax, %dx movw %sp, %dx je 2f
      
      





これにより、3つの異なるシナリオがトリガーされます。





すべてのシナリオを順番に検討してください。





 2: andw $~3, %dx jnz 3f movw $0xfffc, %dx 3: movw %ax, %ss movzwl %dx, %esp sti
      
      





ここでは、 dx



レジスタ(ブートローダーによって示されるsp



値を含む)のアライメントを4



バイトに設定し、ゼロをチェックします。 ゼロの場合、値0xfffc



dx



(最大セグメントサイズ64 KBの前に4



バイトで整列されたアドレス)を入力します。 ゼロに等しくない場合、ブートローダーで指定されたsp



値(この場合は0xf7f4



)を引き続き使用します。 次に、 ss



ax



値を入力します。これにより、正しいセグメントアドレス0x1000



が保存され、正しいsp



設定されます。 これで正しいスタックができました。









 #define LOADED_HIGH (1<<0) #define QUIET_FLAG (1<<5) #define KEEP_SEGMENTS (1<<6) #define CAN_USE_HEAP (1<<7)
      
      





そして、ブートプロトコルに示されているように:



: loadflags



.



7 (): CAN_USE_HEAP

1, ,

heap_end_ptr . ,

.








CAN_USE_HEAP



ビットがCAN_USE_HEAP



場合、 dx



で値heap_end_ptr



_end



を指す)を設定し、それにSTACK_SIZE



を追加します(最小スタックサイズは1024



バイトです)。 その後、ラベル2



に移動し(前の場合と同様)、正しいスタックを作成します。













BSSセットアップ



メインCコードに進む前に、さらに2つのステップが必要です。これは、 BSSエリアをセットアップし、「マジック」署名を検証することです。 最初に署名検証:



  cmpl $0x5a5aaa55, setup_sig jne setup_bad
      
      





この命令は、単にsetup_sigとマジック番号0x5a5aaa55を比較します。 それらが等しくない場合、致命的なエラーが報告されます。



マジックナンバーが同じで、正しいセグメントレジスタのセットとスタックがある場合、Cコードに進む前にBSSセクションを構成するためだけに残ります。



BSSセクションは、静的に割り当てられた初期化されていないデータを格納するために使用されます。 Linuxは、このメモリ領域がリセットされていることを慎重にチェックします。



  movw $__bss_start, %di movw $_end+3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl
      
      





まず、開始アドレス__bss_startが di



移動します。 次に、アドレス_end + 3



(4バイトのアライメントで+3)がcx



移動されます。 eax



レジスタがクリアされ( xor



命令を使用)、bss( cx-di



)セクションのサイズが計算され、 cx



配置されます。 次に、 cx



は4つに分割され(「ワード」のサイズ)、 stosl



命令がstosl



使用さstosl



di



指すアドレスに値



(ゼロ)を格納し、自動的にdi



を4増やし、



がゼロになるまでこれを繰り返します。 このコードの最終的な効果は、メモリ内のすべてのワードに__bss_start



から_end



までゼロが書き込まれることです:







メインに移動



それだけです:スタックとBSSがあるので、 main()



C関数に移動できます:



  calll main
      
      





main()



関数はarch / x86 / boot / main.cにあります。 次のパートで彼女について話します。



おわりに



これで、Linuxカーネルデバイスに関する最初のパートは終わりです。質問や提案がある場合は、Twitterメール、またはチケットを作成してください次の部分では、我々は、Linuxカーネル、メモリなどのサブプログラムの実装のインストール中に実行されるCの最初のコード、表示されますmemset



memcpy



earlyprintk



、早期実施と、コンソールの初期化、および多くを。



参照資料






All Articles