以前の記事を読んだら、低レベルプログラミングのための私の新しい趣味について知っています。
x86_64
Linux向けの
x86_64
プログラミングに関する記事をいくつか書いたと同時に、Linuxカーネルのソースコードを掘り下げました。
私は、低レベルのものがどのように機能するかを理解することに非常に興味があります:プログラムが私のコンピューターで実行される方法、それらがメモリに配置される方法、カーネルがプロセスとメモリを管理する方法、ネットワークスタックが低レベルで動作する方法など。 そこで、 x86_64アーキテクチャ用のLinuxカーネルに関する別のシリーズの記事を書くことにしました。
私はプロのカーネル開発者ではなく、職場でカーネルコードを作成しないことに注意してください。 これはただの趣味です。 低レベルのものが好きで、それらを掘り下げることは興味深いです。 そのため、混乱や質問/コメントが表示される場合は、 Twitterでメール、またはチケットを作成してください。 ありがたいです。
すべての記事はGitHubリポジトリで公開されています 。英語や記事の内容に問題がある場合は、遠慮なくプルリクエストを送信してください。
これは公式のドキュメントではなく、単にトレーニングと知識の共有であることに注意してください。
必要な知識
- Cコードについて
- アセンブラーコードの理解(AT&T構文)
いずれにせよ、あなたがそのようなツールを学び始めたばかりなら、この記事と以下の記事で何かを説明しようとします。 さて、紹介が終了したら、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 ) ...
ここでは、オペレーションコード( opcode )
jmp
、つまり
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 2やsyslinuxなどのさまざまなブートローダーでロードできます。 カーネルには、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
カーネルは次のことを行う必要があります。
- すべてのセグメントレジスタ値が同じであることを確認してください
- 必要に応じて、正しいスタックを構成します
- bssを構成する
- arch / x86 / boot / main.cの Cコードに移動します
これがどのように実装されているか見てみましょう。
セグメントケースアライメント
まず、カーネルは
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つの異なるシナリオがトリガーされます。
-
ss
有効な値は0x1000
(cs
を除く他のすべてのレジスタと同様) -
ss
無効な値があり、CAN_USE_HEAP
フラグCAN_USE_HEAP
設定されています(以下を参照) -
ss
無効な値があり、CAN_USE_HEAP
フラグCAN_USE_HEAP
設定されていません(以下を参照)
すべてのシナリオを順番に検討してください。
-
ss
有効な値(0x1000
)があります。 この場合、ラベル2に進みます。
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
設定されます。 これで正しいスタックができました。

- 2番目のシナリオでは、
ss != ds
です。 まず、値_end (インストールコードの終了アドレス)をdx
loadflags
、testb
命令を使用してヘッダーフィールドloadflags
をチェックし、ヒープを使用できるかどうかをチェックします。 loadflagsは、次のように定義されるビットマスクヘッダーです。
#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
に移動し(前の場合と同様)、正しいスタックを作成します。

-
CAN_USE_HEAP
設定されていない場合CAN_USE_HEAP
、_end
から_end + STACK_SIZE
までの最小スタックを使用します。

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
、早期実施と、コンソールの初期化、および多くを。