ビデオモード初期化とプロテクトモードへの移行
カーネル起動処理シリーズのパート3です。前回のパートでは、set_videoルーチンをmain.c.から呼び出す直前までを扱いました。今回は、次の内容を見ていきます。
- カーネルセットアップコードにおけるビデオモードの初期化
- プロテクトモードに切り替える前の準備
- プロテクトモードへの移行
注 プロテクトモードについてよく知らない場合は、前回のパートの内容を見てください。また、参考になるリンクも同ページに掲載しています。
上にも書いたように、arch/x86/boot/video.cソースコードファイルに定義されたset_video関数から始めましょう。内容を見ると、まずビデオモードをboot_params.hdr構造体から取得することから始まるのがわかります。
u16 mode = boot_params.hdr.vid_mode;
これはcopy_boot_params関数内で値を定義したものになります(前回の投稿で詳しく説明しています)。vid_modeは、ブートローダによって入力される必須フィールドです。関連する情報はカーネル起動プロトコルにあります。
Offset Proto Name Meaning /Size 01FA/2 ALL vid_mode Video mode control
linuxカーネル起動プロトコルには、このように記述されています。
vga=<mode>
<mode> here is either an integer (in C notation, either
decimal, octal, or hexadecimal) or one of the strings
"normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask"
(meaning 0xFFFD). This value should be entered into the
vid_mode field, as it is used by the kernel before the command
line is parsed.
訳:
ここには1つの整数型(C言語記法では、10進数、8進数、16進数のいずれか)または、”normal”(0xFFFFの意)、”ext” (0xFFFEの意)、”ask” (0xFFFDの意)のどれかの文字列が入ります。この値は、コマンドライン分析の前にカーネルが使うため、vid_modeフィールドに入力します。
そこで、GRUBまたはその他のブートローダ設定ファイルにvgaオプションを追加して、それをカーネルコマンドラインに渡します。このオプションは上記の説明通り、様々な値を持つことができます。例えば、整数0xFFFDやaskなどです。askをvgaに渡すと、下図のようなメニューが表示されます。
ここでビデオモードを選択しなければなりません。これからその実装に移りますが、その前に他に確認すべき点が幾つかあります。
カーネルデータタイプ
以前、カーネルセットアップコードにおけるu16などの様々なデータタイプの定義について述べました。カーネルが提供するデータタイプを見てみましょう。
| Type | char | short | int | long | u8 | u16 | u32 | u64 |
|---|---|---|---|---|---|---|---|---|
| Size | 1 | 2 | 4 | 8 | 1 | 2 | 4 | 8 |
カーネルのソースコードを読んでいると、これらを目にする機会は非常に多いので、覚えておくとよいでしょう。
ヒープ API
set_video関数で、vid_modeをboot_params.hdrから取得した後は、RESET_HEAP関数の呼び出しを見ていきます。RESET_HEAPは、boot.hに次のように定義されているマクロです。
#define RESET_HEAP() ((void *)( HEAP = _end ))
パート2を読んだ人は、ヒープをinit_heap関数で初期化したのを覚えているでしょう。幾つか、ヒープのためのユーティリティ関数がboot.hに定義されています。
#define RESET_HEAP()
上記のコードで示したように、これはHEAP変数を_endと等しい値に設定することによってヒープをリセットします。ここでは、_endはextern char _end[];です。
次はGET_HEAPマクロです。
#define GET_HEAP(type, n) \
((type *)__get_heap(sizeof(type),__alignof__(type),(n)))
このマクロは、ヒープを割り当てるためのものです。内部関数__get_heapを次の3つのパラメータで呼び出します。
- 割り当てる必要のある、タイプのサイズをバイト数で表した値
- このタイプの変数がどう配置されるかを表示する
__alignof__(type) - 割り当てられるアイテム数を意味する
n
__get_heapの実装は以下のとおりです。
static inline char *__get_heap(size_t s, size_t a, size_t n)
{
char *tmp;
HEAP = (char *)(((size_t)HEAP+(a-1)) & ~(a-1));
tmp = HEAP;
HEAP += s*n;
return tmp;
}
また、使用例を以下に示します。
saved.data = GET_HEAP(u16, saved.x * saved.y);
それでは、__get_heapの動きを説明します。ここにあるHEAP( RESET_HEAP()の後の_endと同等です)は、aパラメータによって配置されたメモリのアドレスです。この後、メモリアドレスをHEAPからtmp変数に保存し、HEAPを割り当てられたブロックの末尾に移動させ、割り当てメモリの開始アドレスであるtmpを返します。
そしてこれが、最後の関数です。
static inline bool heap_free(size_t n)
{
return (int)(heap_end - HEAP) >= (int)n;
}
HEAPの値をheap_endから減算し(前回のパートで計算しました)、n個だけ保存するのに十分なメモリがあれば、1を返します。
以上です。このシンプルなヒープ用APIで、ビデオモードをセットアップします。
ビデオモードのセットアップ
さて、ビデオモードを初期化できるようになりました。前回の投稿ではset_video関数内のRESET_HEAP()の呼び出しのところまで説明しました。次は、store_mode_paramsの呼び出しです。この関数は、include/uapi/linux/screen_info.hに定義されているboot_params.screen_info構造体にビデオモードパラメータを格納します。
store_mode_params関数を見ると、store_cursor_position関数の呼び出しから始まっています。関数の名称から分かるように、カーソルの情報を取得して格納します。
最初にstore_cursor_positionは、biosregs型の2つの変数をAH = 0x3で初期化し、0x10BIOS割り込みを呼び出します。割り込みが成功すると、DLとDHレジスタに行と列を返します。行と列は、boot_params.screen_info構造体のorig_xとorig_yフィールドに格納されます。
store_cursor_positionを実行すると、store_video_mode関数が呼び出されます。 単純に進行中のビデオモードを取得し、boot_params.screen_info.orig_video_modeに格納します。
この後、この関数は進行中のビデオモードをチェックし、video_segmentを設定します。BIOSがブートセクタに制御を移した後、次のアドレスをビデオメモリに使います。
0xB000:0x0000 32 Kb Monochrome Text Video Memory 0xB800:0x0000 32 Kb Color Text Video Memory
現在のビデオモードがMDA、HGC、VGAのモノクロモードの場合は、video_segment変数を0xB000に設定し、カラーモードの場合は、0xB800に設定します。ビデオセグメントのアドレスをセットアップした後は、次のようにしてフォントサイズをboot_params.screen_info.orig_video_pointsに格納する必要があります。
set_fs(0); font_size = rdfs16(0x485); boot_params.screen_info.orig_video_points = font_size;
最初に、set_fs関数で、FSレジスタに0を代入します。set_fsのような関数は前回のパートで既に見てきました。これらは全てboot.hに定義されています。それから、アドレス0x485(このメモリ位置はフォントサイズの取得に使います)に存在する値を読み、フォントサイズをboot_params.screen_info.orig_video_pointsに保存します。
x = rdfs16(0x44a); y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484)+1;
次に、アドレス0x44aで、まとまった量の列を、アドレス0x484で行を取得し、boot_params.screen_info.orig_video_colsとboot_params.screen_info.orig_video_linesに格納します。これで、store_mode_paramsの実装は完了です。
続いて、スクリーンコンテンツをヒープに保存するsave_screen関数です。この関数は、行や列数など、先の関数で取得した全てのデータを収集し、saved_screen構造体に格納します。定義は次の通りです。
static struct saved_screen {
int x, y;
int curx, cury;
u16 *data;
} saved;
また次の通り、ヒープがそのデータのための空きスペースを持っているかどうかをチェックします。
if (!heap_free(saved.x*saved.y*sizeof(u16)+512))
return;
ヒープ内に十分な空きがあれば、スペースを割り当て、その中にsaved_screen を格納します。
次に、arch/x86/boot/video-mode.cからprobe_cards(0) が呼び出されます。この関数は全てのvideo_cardsを巡回し、カードが提供するモードの数を集めます。ここで面白いのは、ループがあることです。
for (card = video_cards; card < video_cards_end; card++) {
/* collecting number of modes here */
}
しかし、video_cardsはどこにも宣言されていません。答えはシンプルです。x86カーネルセットアップコードにおける全てのビデオモードは次のように定義されています。
static __videocard video_vga = {
.card_name = "VGA",
.probe = vga_probe,
.set_mode = vga_set_mode,
};
ここで、__videocardはマクロです。
#define __videocard struct card_info __attribute__((used,section(".videocards")))
つまり、以下のcard_info構造体
struct card_info {
const char *card_name;
int (*set_mode)(struct mode_info *mode);
int (*probe)(void);
struct mode_info *modes;
int nmodes;
int unsafe;
u16 xmode_first;
u16 xmode_n;
};
が、.videocardsセグメントに含まれます。それではarch/x86/boot/setup.ldリンカファイルの内部を見てみましょう。
.videocards : {
video_cards = .;
*(.videocards)
video_cards_end = .;
}
つまり、video_cardsは単なるメモリのアドレスで、全てのcard_info構造体はこのセグメントに置かれているわけです。また、全てのcard_info構造体がvideo_cardsとvideo_cards_endの間にあるということも意味するので、これを使ってループすることで全ての構造体を得られます。probe_cardsの実行の後には、nmodes(ビデオモードの数)が代入済みのstatic __videocard video_vgaのような構造体全てを得ることができます
probe_cardsの実行が済むと、set_video関数内のメインループに移ります。そこには、無限ループがあり、set_mode関数でビデオモードをセットアップしようとします。あるいは、vid_mode=askをカーネルコマンドラインに渡すか、ビデオモードが定義されていない場合は、メニューを表示します。
set_mode関数はvideo-mode.cに定義されており、ただ1つのパラメータ、modeを取得します。これはビデオモードの数です(この値は、メニューから取得するか、setup_videoの起動時にカーネルセットアップヘッダから取得します)。
set_mode関数はmodeを確認し、raw_set_mode関数を呼び出します。、この関数には、card_info構造体からアクセスできます。各ビデオモードはこの構造体をビデオモードによって入力された値(例えば、vgaには、video_vga.set_mode。上の例、vgaのためのcard_info構造体を参照)で定義します。video_vga.set_modeは、vga_set_modeで、vgaモードをチェックし、それぞれの関数を呼び出します。
static int vga_set_mode(struct mode_info *mode)
{
vga_set_basic_mode();
force_x = mode->x;
force_y = mode->y;
switch (mode->mode) {
case VIDEO_80x25:
break;
case VIDEO_8POINT:
vga_set_8font();
break;
case VIDEO_80x43:
vga_set_80x43();
break;
case VIDEO_80x28:
vga_set_14font();
break;
case VIDEO_80x30:
vga_set_80x30();
break;
case VIDEO_80x34:
vga_set_80x34();
break;
case VIDEO_80x60:
vga_set_80x60();
break;
}
return 0;
}
ビデオモードをセットアップする各関数は、ただ0x10BIOS割り込みを、AHレジスタの値で呼び出すだけです。
ビデオモードを設定した後、それをboot_params.hdr.vid_modeに渡します。
次にvesa_store_edidを呼び出します。この関数は単純に、カーネルが使用するEDID情報を格納します。この後、store_mode_paramsが再び呼び出されます。最後に、do_restoreが設定されていれば、スクリーンは前の状態に復元されます。
この後、ビデオモードを設定し、プロテクトモードに切り替えられるようになります。
プロテクトモード移行前の最終準備
では、main.cの末尾にあるgo_to_protected_modeの呼び出しについて説明します。コメントにDo the last things and invoke protected modeとあるように、これらの最後の作業を確認し、プロテクトモードに切り替えましょう。
go_to_protected_modeはarch/x86/boot/pm.cに定義されています。この関数には、プロテクトモードに飛び込む前の最終準備を行う幾つかの関数が含まれています。では、コードを見て、何を行うのか、どう機能するのかを理解しましょう。
初めに、go_to_protected_mode内のrealmode_switch_hook関数の呼び出しです。この関数はリアルモードスイッチフックがあればそれを動かし、NMIを無効化します。フックはブートローダが標準以外の環境で稼動するときに使われます。フックについてもっと知りたい人は、boot protocol のADVANCED BOOT LOADER HOOKSの項を読んでください。
realmode_switchフックは16ビットリアルモードへのポインタを提示し、マスク不可割り込みを無効化するサブルーチンを呼び出します。realmode_switchフック(私のために存在するわけではありません)が確認されると、マスク不可割り込み(NMI)の無効化が起こります。
asm volatile("cli");
outb(0x80, 0x70); /* Disable NMI */
io_delay();
まず、割り込みフラグ(IF)をクリアにするcli命令を伴う、インラインアセンブリ命令があります。その後、外部割り込みが無効化されます。次の行がNMIを無効にします。
割り込みは、ハードウェアやソフトウェアから出されるCPUへのシグナルです。シグナルを取得すると、CPUは実行中の命令シークエンスを中断し、その状態を保存してコントロールを割り込みハンドラに移します。割り込みハンドラが処理を終えると、コントロールを割り込み前の命令に移します。NMIは、常に許可なしで独自に進行する割り込みです。無視することはできず、復旧不可能なハードウェアエラーのためのシグナルに使われるのが普通です。割り込みについてここで詳細を述べるのは控えますが、次の投稿で取り上げます。
コードに戻りましょう。2行目で0x80(無効化されたビット)バイトから0x70(CMOSアドレスレジスタ)バイトまでの範囲に書き込みを実行していることが分かります。その後、io_delay関数の呼び出しが起こります。io_delayは小さな遅延を作り出すもので、次のように記述されます。
static inline void io_delay(void)
{
const u16 DELAY_PORT = 0x80;
asm volatile("outb %%al,%0" : : "dN" (DELAY_PORT));
}
何らかのバイトをポート0x80に出力すると、ちょうど1マイクロ秒遅延します。そこで0x80ポートに、どのような値(この場合は、ALレジスタの値)でも書くことができます。この遅延の後、realmode_switch_hook関数の実行が完了すると、次の関数に進むことができます。
次の関数はA20ラインを有効にするenable_a20です。この関数はarch/x86/boot/a20.cに定義されており、様々なメソッドでA20ゲートを有効化しようとします。1つ目はa20_test_short関数で、a20_test関数によってA20がすでに有効化されているか否かをチェックします。
static int a20_test(int loops)
{
int ok = 0;
int saved, ctr;
set_fs(0x0000);
set_gs(0xffff);
saved = ctr = rdfs32(A20_TEST_ADDR);
while (loops--) {
wrfs32(++ctr, A20_TEST_ADDR);
io_delay(); /* Serialize and make delay constant */
ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr;
if (ok)
break;
}
wrfs32(saved, A20_TEST_ADDR);
return ok;
}
最初にFSレジスタに0x0000を入力し、GSレジスタに0xffffを入力します。次に、アドレスA20_TEST_ADDR(0x200です)の値を読み、この値をsaved変数とctrに入力します。
それから、更新したctrの値をwrfs32関数を使ってfs:gsに書き込みます。1マイクロ秒の遅延が出た後、アドレスA20_TEST_ADDR+0x10によってGSレジスタから値を読みます。もしゼロでない場合は、既にA20ラインが有効になっています。A20が無効だった場合は、a20.cにある別のメソッドで有効化を試みます。例えば、AH=0x2041などで、0x15BIOS割り込みを呼び出します。
enabled_a20関数が失敗に終わった場合は、エラーメッセージを表示させ、関数dieを呼び出します。今回の作業を開始した最初のソースコードファイル – arch/x86/boot/header.Sを思い出す人もいるでしょう。
die:
hlt
jmp die
.size die, .-die
A20ゲートの有効化に成功すると、reset_coprocessor関数が呼び出されます。
outb(0, 0xf0); outb(0, 0xf1);
この関数は、0xf0に0を記述して、数値演算コプロセッサのデータを消去し、0xf1に0を記述してリセットさせます。
続いて、mask_all_interrupts関数が呼び出されます。
outb(0xff, 0xa1); /* Mask all interrupts on the secondary PIC */ outb(0xfb, 0x21); /* Mask all but cascade on the primary PIC */
これは、主要PICのIRQ2を除く 補助PIC(Programmable Interrupt Controller、プログラム可の割り込みコントローラ)と主要PICにおける全ての割り込みをマスクします。
これらの準備が全て終わると、実際にプロテクトモードに移行できます。
割り込みディスクリプタテーブル(IDT)のセットアップ
ではいよいよ、割り込みディスクリプタテーブル(IDT)のセットアップに入ります。setup_idtを以下に示します。
setup_idt:
static setup_idt()
{
static const struct gdt_ptr null_idt = {, };
volatile(lidtl 0 ::(null_idt));
}
これでIDT(割り込みハンドラなどを示すもの)がセットアップされました。この時点ではIDTをまだインストールしていませんが(後でやります)、IDTをlidtl命令で読み込みました。null_idtにはIDTのアドレスとサイズを格納しますが、今のところは0が入っています。null_idt はgdt_ptr型の構造体です。定義を以下に示します。
struct gdt_ptr {
u16 len;
u32 ptr;
} __attribute__((packed));
上記のコードは、IDTの長さ(len)は16ビット、IDTへのポインタは32ビットでそれぞれ表すことを示しています(IDTと割り込みの詳細は、次回の投稿で詳しく説明します)。また、__attribute__((packed))は、gdt_ptrのサイズが必要最小限であることを示しています。従って、gdt_ptrのサイズは6バイト、即ち48ビットです。(以下で、 GDTRレジスタに gdt_ptrへのポインタを読み込む処理を説明します。前回の投稿でgdt_ptrのサイズは48ビットだと説明したことをここで思い出した方もいるでしょう。)
グローバルディスクリプタテーブル(GDT)のセットアップ
続いて、グローバル記述子テーブル(GDT)もセットアップします。GDTをセットアップするのはsetup_gdt関数です(詳細はカーネル起動処理 パート2を参照してください)。この関数には boot_gdt 配列の定義が含まれますが、配列には3つのセグメントの定義が含まれます。
static const u64 boot_gdt[] __attribute__((aligned())) = {
[GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, , 0xfffff),
[GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, , 0xfffff),
[GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, , ),
};
3つのセグメントとは、コード、データ、タスクステートセグメント(TSS)です。セグメントのうち、TSSは今回使いません。しかしTSSがここに追加されているのは、コメント行に書かれている通り、Intel VTを満足させるためです(Intel VTに興味がある方は、この記事に詳細が書かれているので参照してください)。ではboot_gdtを見ていきましょう。まず、ここに__attribute__((aligned(16)))属性が含まれていることに注意してください。つまり、この構造体は16バイト単位で整列されるということです。簡単な例を以下に示します。
#include stdio.h
struct aligned {
a;
}__attribute__((aligned()));
struct nonaligned {
b;
};
()
{
struct aligned a;
struct nonaligned na;
printf(Not aligned - , sizeof(na));
printf(Aligned - , sizeof(a));
return ;
}
技術的な観点からいえば、 intフィールドを一つ含む構造体は4バイトでなければなりませんが、ここで示したaligned構造体は16バイトです。
$ gcc test.c -o test && test Not aligned - 4 Aligned - 16
GDT_ENTRY_BOOT_CSのインデックスの値はここでは- 2で、GDT_ENTRY_BOOT_DSにはGDT_ENTRY_BOOT_CS + 1などの値が格納されます。初期値は2です。先頭はnullディスクリプタ(index – 0)で、これは必須です。また、2番目は未使用(index – 1)です。
GDT_ENTRYは、フラグ、ベース、上限の値を取ってGDTエントリを構築するマクロです。例えば、コードセグメントのエントリを見てみましょう。GDT_ENTRYには以下の値が格納されています。
- ベース – 0
- 上限 – 0xfffff
- フラグ – 0xc09b
これは何を意味するのでしょう。セグメントのベースアドレスは0、上限(セグメントのサイズ)は0xffff(1 MB)です。ここでフラグを見てみましょう。フラグの値は0xc09bです。これをバイナリで表すと、
1100 0000 1001 1011
となります。各ビットの意味は以下の通りです。左から右に向かって、順番に示します。
- 1 -(G)精度を示すビット
- 1 -(D)16ビットセグメントの場合は0、32ビットセグメントの場合は1
- 0 -(L)1の場合は64ビットモードで実行する
- 0 -(AVL)システムソフトから利用可能
- 0000 – ディスクリプタ内の19:16の4ビット
- 1 -(P)セグメントがメモリに読み込まれているかどうか
- 00 – (DPL) – 特権レベル、0が最高
- 1 -(S)コードまたはデータセグメント、システムセグメントではない
- 101 – セグメントの種類、実行/読み取り
- 1 – アクセス済みのビット
各ビットの意味の詳細は、前回の 投稿またはIntel® 64 and IA-32 Architectures Software Developer’s Manuals 3Aを参照してください。
以下のコードを実行すると、GDTの長さを取得できます。
gdt.len = sizeof(boot_gdt)-;
boot_gdtのサイズを取得して、そこから1(GDTの末尾の有効なアドレス)を引きます。
次に以下のコードで、GDTへのポインタを取得します。
gdt.ptr = (u32)&boot_gdt + (ds() << );
ここでboot_gdtのアドレスを取得して、データセグメントのアドレスを左に4ビットシフトしたものに、このアドレスを加えます(現在はリアルモードであることを思い出してください)。
最後にlgdtl命令を実行して、GDTをGDTRレジスタに読み込みます。
volatile(lgdtl 0 ::(gdt));
プロテクトモードへの実際の移行
これでgo_to_protected_mode関数は終わりです。ここまでにIDTとGDTを読み込み、割り込みを無効にしたので、次はCPUをプロテクトモードに切り替えます。最後のステップは、以下の2つのパラメータを指定してprotected_mode_jump関数を呼び出すことです。
protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));
この関数はarch/x86/boot/pmjump.Sで定義されています。2つのパラメータの意味は次のとおりです。
- プロテクトモードのエントリポイントのアドレス
boot_paramsのアドレス
ではここから、protected_mode_jumpの内部を詳しく見ていきます。上記の通り、これはarch/x86/boot/pmjump.Sファイル内にあります。先頭のパラメータはeax レジスタに、次のパラメータはedx.レジスタに格納されます。
まず、esiレジスタ内にboot_paramsのアドレスを、コードセグメントのレジスタcsのアドレス(0x1000)をbxに書き込みます。次に bxの内容を4ビットシフトさせて、そこにラベル2のアドレスを加算し(この処理の後、bx にあるラベル2の物理アドレスを取得します)、ラベル1にジャンプします。続けて、データセグメントとタスクステートセグメントを、以下の通り csレジスタとdiレジスタにそれぞれ置きます。
$__BOOT_DS, % $__BOOT_TSS, %
上記から分かる通り、GDT_ENTRY_BOOT_CSのインデックス値は2なので、GDTの各エントリは8バイト、従ってCSは2 * 8 = 16、__BOOT_DSは24、のようになります。
次に、CR0コントロールレジスタ内のProtection Enable(PE)ビットを次の通りセットして、
%, % $X86_CR0_PE, % %, %
プロテクトモードへ大きくジャンプします。
.byte , :.long in_pm32 .word __BOOT_CS
ここで
0x66は、16ビットと32ビットのコードを併用できるオペランドサイズプリフィクス、0xeaはジャンプのオペコード、in_pm32はセグメントのオフセット、__BOOT_CSはコードセグメントを、それぞれ示します。
これでプロテクトモードに移行しました。
code32 .section .text32,
プロテクトモードの最初のステップを詳しく見ていきます。まず、データセグメントを以下のようにセットアップします。
movl %ecx, %ds movl %ecx, %es movl %ecx, %fs movl %ecx, %gs movl %ecx, %ss
ここで、 cxレジスタに$__BOOT_DSを格納したことを思い出しましょう。これで、cs 以外の全てのセグメントレジスタに値が格納されました(csには既に__BOOT_CSが格納されています)。次に、eax以外の汎用レジスタ全てに0を格納します。
xorl %ecx, %ecx xorl %edx, %edx xorl %ebx, %ebx xorl %ebp, %ebp xorl %edi, %edi
最後に、32ビットのエントリポイントにジャンプします。
jmpl *%eax
eaxには32ビットエントリのアドレスが格納されていることに注意してください(この値を最初のパラメータとして protected_mode_jumpに渡したためです)。
以上です。プロテクトモードに入って、そのエントリポイントまでを説明しました。この続きは次の投稿で説明します。
結論
Linuxカーネル内部の解説、パート3は以上です。次回の投稿では、プロテクトモードの最初のステップを詳しく解説してから、 ロングモードへの移行についても触れる予定です。
この記事について質問や提案があれば、どうぞ遠慮なくこちらのTwitterアカウントにコメントまたはメッセージを送ってください。
ただし英語は私の母語ではないので、コミュニケーションに多少不便があるかもしれませんが、どうぞご理解ください。記事に誤りを見つけた場合は、訂正内容を添えて linux-internalsにプルリクエストを送ってください。