linuxカーネル読書がしたい(ブート編2)
前回に引き続きlinux読書を続けていく.header.Sからmainにjmpするので,次はmain.cの読書.
main.c(リアルモードでのカーネルコードモジュール)
これをただただ上から読むと,
↓このページはここから
・early-bootコンソールの初期化(コンソールの準備)
↑ここまで
・BIOSをどのCPUモードで使うかの設定
・メモリレイアウトの検出
・キーボードの初期化
・(もし定義されていれば)APM情報の照会
・(もし定義されていれば)EDD情報の照会
・ビデオモードの設定
・プロテクトモードへの移行
をしているようである.つまりリアルモードでする仕事はこれらであるということ.
copy_boot_params()
コメントを直訳すると,
"0ページ目にブートヘッダをコピーする."
"ブートパラメータブロックにヘッダをコピーする.これは古いスタイルのコマンドラインプロトコルを壊すので,代わりに新しいスタイルのコマンドラインポインタを入力して調整している."
後々boot_params,setup_headerは多く出てくるのでここで定義を載せておく.bootparams.hより.
113 /* The so-called "zeropage" */ 114 struct boot_params { 115 struct screen_info screen_info; /* 0x000 */ 116 struct apm_bios_info apm_bios_info; /* 0x040 */ 117 __u8 _pad2[4]; /* 0x054 */ 118 __u64 tboot_addr; /* 0x058 */ 119 struct ist_info ist_info; /* 0x060 */ 120 __u8 _pad3[16]; /* 0x070 */ 121 __u8 hd0_info[16]; /* obsolete! */ /* 0x080 */ 122 __u8 hd1_info[16]; /* obsolete! */ /* 0x090 */ 123 struct sys_desc_table sys_desc_table; /* obsolete! */ /* 0x0a0 */ 124 struct olpc_ofw_header olpc_ofw_header; /* 0x0b0 */ 125 __u32 ext_ramdisk_image; /* 0x0c0 */ 126 __u32 ext_ramdisk_size; /* 0x0c4 */ 127 __u32 ext_cmd_line_ptr; /* 0x0c8 */ 128 __u8 _pad4[116]; /* 0x0cc */ 129 struct edid_info edid_info; /* 0x140 */ 130 struct efi_info efi_info; /* 0x1c0 */ 131 __u32 alt_mem_k; /* 0x1e0 */ 132 __u32 scratch; /* Scratch field! */ /* 0x1e4 */ 133 __u8 e820_entries; /* 0x1e8 */ 134 __u8 eddbuf_entries; /* 0x1e9 */ 135 __u8 edd_mbr_sig_buf_entries; /* 0x1ea */ 136 __u8 kbd_status; /* 0x1eb */ 137 __u8 _pad5[3]; /* 0x1ec */ 138 __u8 sentinel; /* 0x1ef */ 139 __u8 _pad6[1]; /* 0x1f0 */ 140 struct setup_header hdr; /* setup header */ /* 0x1f1 */ 141 __u8 _pad7[0x290-0x1f1-sizeof(struct setup_header)]; 142 __u32 edd_mbr_sig_buffer[EDD_MBR_SIG_MAX]; /* 0x290 */ 143 struct e820entry e820_map[E820MAX]; /* 0x2d0 */ 144 __u8 _pad8[48]; /* 0xcd0 */ 145 struct edd_info eddbuf[EDDMAXNR]; /* 0xd00 */ 146 __u8 _pad9[276]; /* 0xeec */ 147 } __attribute__((packed));
48 struct setup_header { 49 __u8 setup_sects; 50 __u16 root_flags; 51 __u32 syssize; 52 __u16 ram_size; 53 __u16 vid_mode; 54 __u16 root_dev; 55 __u16 boot_flag; 56 __u16 jump; 57 __u32 header; 58 __u16 version; 59 __u32 realmode_swtch; 60 __u16 start_sys; 61 __u16 kernel_version; 62 __u8 type_of_loader; 63 __u8 loadflags; 64 __u16 setup_move_size; 65 __u32 code32_start; 66 __u32 ramdisk_image; 67 __u32 ramdisk_size; 68 __u32 bootsect_kludge; 69 __u16 heap_end_ptr; 70 __u8 ext_loader_ver; 71 __u8 ext_loader_type; 72 __u32 cmd_line_ptr; 73 __u32 initrd_addr_max; 74 __u32 kernel_alignment; 75 __u8 relocatable_kernel; 76 __u8 min_alignment; 77 __u16 xloadflags; 78 __u32 cmdline_size; 79 __u32 hardware_subarch; 80 __u64 hardware_subarch_data; 81 __u32 payload_offset; 82 __u32 payload_length; 83 __u64 setup_data; 84 __u64 pref_address; 85 __u32 init_size; 86 __u32 handover_offset; 87 } __attribute__((packed));
memcpyでヘッダをブートパラメータのヘッダにコピーしている.
以下は古いスタイルのコマンドラインプロトコルの時に実行される.古いカーネルがコピーした0x9000...までの領域にコマンドラインが含まれているか調べて,コマンドラインのセグメントを設定.
→含まれている:cmdline_seg = ds();
→含まれてない:cmdline_seg = 0x9000;
(ds()はdsレジスタから変数に返す関数.boot.hにあり.)
コマンドラインのポインタを設定.
console_init()
void console_init(void) { parse_earlyprintk(); if (!early_serial_base) parse_console_uart8250(); }
early_serial_console.cより
これだけではなんにもわからないのでparse_earlyprintk()の中身から見ていく.
最初の分岐で"cmdline_find_option("earlyprintk", arg, sizeof arg) > 0"がある.これはoption=argumentを発見するための関数.argumentの長さを返す.
cmdline_find_option
ブートパラメータヘッダのコマンドラインポインタを取得.もしこのアドレスが0x100000以上だとアクセス不可なので-1を返す
この中では4つの状態がある
st_wordstart:ワードの始まりか,空白の後
st_wordcmp:ワードの比較中
st_wordskip:ワード不一致.スキップ
st_bufcpy:バッファにコピー中
whileで1文字ずつ進みつつ状態を見て行く
・st_wordstart
空白でなければ,状態をst_wordcmpにしてopptrにoption("earlyprintk"のポインタ)を代入
・st_wordcmp
→ c == '='かつ*opptrがNULLでなければ,バッファのポインタを設定して状態をst_bufcpyにする.
→ 空白であれば,状態をst_wordstartにする
c != *opptrであれば,状態をst_wordskipにする
・st_wordskip
空白であれば,状態をst_wordstartにする
・st_bufcpy
空白であれば,状態をst_wordstartにする
それ以外であればbufにwordをコピーしていく
今回"earlyprintk"がoptionなので,上の関数を通じてearlyprintk=(文字列)の文字列部分がarg[32]にコピーされる.
arg == serialだったらひとまずportをDEFAULT_SERIAL_PORTとする.argには以下のようなものが来ているはずである.
"serial,0x3f8,115200"
"serial,ttyS0,115200"
"ttyS0,115200"
(serial),port,baudというフォーマット
・"serial,0x"であれば,0x移行の数字をportとする.ただし0だったら0x3f8(デフォルト値)
・"serial,ttyS"または"ttyS"の時,ttyS0ならportを0x3f8とし,ttyS1ならportを0x2f8とする.
そのあとの数字をbaudとするが,0なら9600(デフォルト値).*1
port != 0なら,early_serial_init()
early_serial_init()
#define TXR 0 /* Transmit register (WRITE) */ #define RXR 0 /* Receive register (READ) */ #define IER 1 /* Interrupt Enable */ #define IIR 2 /* Interrupt ID */ #define FCR 2 /* FIFO control */ #define LCR 3 /* Line control */ #define MCR 4 /* Modem control */ #define LSR 5 /* Line Status */ #define MSR 6 /* Modem Status */ #define DLL 0 /* Divisor Latch Low */ #define DLH 1 /* Divisor latch High */ static void early_serial_init(int port, int baud) { unsigned char c; unsigned divisor; outb(0x3, port + LCR); /* 8n1 */ outb(0, port + IER); /* no interrupt */ outb(0, port + FCR); /* no fifo */ outb(0x3, port + MCR); /* DTR + RTS */ divisor = 115200 / baud; c = inb(port + LCR); outb(c | DLAB, port + LCR); outb(divisor & 0xff, port + DLL); outb((divisor >> 8) & 0xff, port + DLH); outb(c & ~DLAB, port + LCR); early_serial_base = port; }
init_heap()
ヒープの初期化をする.ヒープ領域が使えない時はWARNINGが出力される.
boot_params.hdr.loadflagsからヒープ領域が使えるなら,
stack_end = %esp-STACKSIZE(上に伸ばすイメージ)
headend = (size_t)boot_params.hdr.heap_end_ptr + 0x200
この時,heap_end > stack_endとなっているとスタック領域とヒープ領域とが被っている領域があることになるため,heap_end = stack_endとする.
validate_cpu
cpu.cより
カーネルが要求しているレベルよりもCPUレベルが低い時にはvalidationに失敗したことになる.まずはcheck_cpu()にかける.
check_cpu
cpucheck.cより
まずはcpuflags.hからここで使用されているcpu構造体を載せる
struct cpu_features { int level; /* Family, or 64 for x86-64 */ int family; /* Family, always */ int model; u32 flags[NCAPINTS]; };
デフォルトではcpu.levelは3とする.
if ( has_eflag(X86_EFLAGS_AC) ) cpu.level = 4;とあったのでX86_EFLAGS_ACの定義を参照する.
#define X86_EFLAGS_AC_BIT 18 /* Alignment Check/Access Control */ #define X86_EFLAGS_AC _BITUL(X86_EFLAGS_AC_BIT)
include/uapi/asm/processor-flags.hより
EFLAGSの下から18bit目(0bit目もある)のACフラグがセットされていればレベル4になる.ACフラグはメモリチェックの時にアライメントチェックが有効であればセットされる.
次にcpuフラグを入手する(get_cpuflags())
get_cpuflags
・FPUの有無をチェック
・EFLAGSのIDフラグのチェック(なければこのままget_cpuflagsを抜ける)
→ あればCPUID命令が使用できる.
eaxに0を指定してCPUID命令でベンダ情報を入れる.
↑でeaxに返って来た値が0x1以上0xffff以下の時,eaxに1を指定してCPUID命令で機能フラグを入手しcpu.flags[0], cpu.flags[4]に代入される.この時eaxにはモデルやファミリーを示す値(プロセッサ・シグネチャ)が代入される.この時cpu構造体に以下のように代入される
11:8の4bit:cpu.levelとfamily
7:3の4bit:cpu.model [0:3]
(cpu.modelが6以上の時)19:16の4bit:cpu.model [7:4]
次にeaxに0x80000000を指定してeaxに返ってくる最大値を取得.
eaxに返って来た値が0x80000001以上0x8000ffff以下の時,eaxに0x80000001を指定してCPUID命令で拡張機能フラグを入手しcpu.flags[1], cpu.flags[6]に代入される.
===== get_cpuflags ここまで=====
CPUがLong Modeに対応していたらcpu.level = 64(X86_FEATURE_LMはcpufeatures.hに定義されている)
得られたフラグ群から各種チェックしていきerrに代入,その後足りないものはRDMSRとWRMSR命令で補う努力をしてみる.(例えばAMDでSSE+SSE2のフラグが立っていなかった時にMSRの該当部分をセットしてみている)
MSRはIntelやAMDのx86系プロセッサー固有の情報を格納した特殊なレジスタ.
errが0でなければIntel Xeon PhiのKnights Landingに関するチェックを行っている.ここではKnights Landingで64bitモード,もしくはx86のPAEを使用していない場合はエラーが起こるようになっているようだ.puts()にはプロセッサのエラッタによるものだと記載されている.
最終的なcpu.levelやreq_levelをポインタに代入してから,CPUレベルが要求レベルに足りているかの判定を返り値に指定する.
===== check_cpu ここまで =====
このあとでCPUID命令が使用できない場合でCONFIG_M486が使用できるなら,カーネルをそのCPUで走らせるために設定を変えるようにするアラートが出る.
このあとでerr_flagsが0でなければ機能が足りてないとして-1を返すようになっている.また先述のKnights Landingのチェックをここでも行っている.
ここまでくぐり抜けて初めてreturn 0となる.
===== validate_cpu ここまで =====
冗長な記述が散見されてしまったけれど,やはりというか慣れていないので読むのはなかなかしんどい.長文となってしまったので今回はここまでにする.
*1:baudはデジタルデータをアナログデータに変換し,アナログ回線でシリアル転送する際に用いられる単位