2009年7月30日木曜日

startup_32

プロテクトモードに設定するとアドレス0x100000へジャンプしています。ここがプロテクトモードにおけるカーネルのエントリポイントとなります。プロテクトモードにおけるカーネルはいわばカーネル本体なのであって、ここは圧縮された状態で読み込まれます。ソースのパスとしては「arch/x86/boot/compressed」以下のソースに当たります。ここのリンカスクリプトを見てみると(ここでは32bitマシンを想定して「vmlinux_32.lds」を見てみます)、エントリポイントとして「startup_32」を指定していることが分かります。
それではこのstartup_32を見てみましょう。

arch/x86/boot/compressed/head_32.S
=============
startup_32:
cld /* 割り込み禁止 */
/* KEEP_SEGMENTSフラグが立っていたらセグメントを書き換えない */
testb $(1<<6), BP_loadflags(%esi)
jnz 1f

cli /* 割り込み許可 */

/* __BOOT_DSでセグメントレジスタを更新する */
movl $(__BOOT_DS),%eax
movl %eax,%ds
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
movl %eax,%ss
1:

/* スタックポインタを再計算。よく分かりません */
leal (0x1e4+4)(%esi), %esp
call 1f

1: popl %ebp
subl $1b, %ebp

/* EBPにはロードされた場所が入っている。EBXには圧縮したカーネルイメージを
 安全に解凍すべきアドレスを入れてやる。*
#ifdef CONFIG_RELOCATABLE
movl %ebp, %ebx
addl $(CONFIG_PHYSICAL_ALIGN - 1), %ebx
andl $(~(CONFIG_PHYSICAL_ALIGN - 1)), %ebx
#else
movl $LOAD_PHYSICAL_ADDR, %ebx
#endif

/* 圧縮データサイズと解凍したデータサイズを入れ替える */
/* あとはアライメントやら何やらの設定。よく分かりません */
subl input_len(%ebp), %ebx
movl output_len(%ebp), %eax
addl %eax, %ebx
/* Add 8 bytes for every 32K input block */
shrl $12, %eax
addl %eax, %ebx
/* Add 32K + 18 bytes of extra slack */
addl $(32768 + 18), %ebx
/* Align on a 4K boundary */
addl $4095, %ebx
andl $~4095, %ebx

/* 解凍したカーネルをコピーする */
pushl %esi
leal _end(%ebp), %esi
leal _end(%ebx), %edi
movl $(_end - startup_32), %ecx
std
rep
movsb
cld
popl %esi

/* カーネルのスタートアドレスを算出してEBPへ格納 */
#ifdef CONFIG_RELOCATABLE
addl $(CONFIG_PHYSICAL_ALIGN - 1), %ebp
andl $(~(CONFIG_PHYSICAL_ALIGN - 1)), %ebp
#else
movl $LOAD_PHYSICAL_ADDR, %ebp
#endif

/*
* カーネルを再配置する
*/
leal relocated(%ebx), %eax
jmp *%eax

=============

ブートしていた頃のアセンブラよりも複雑でなかなか読み進められません。思いっきり要約してしまうと、セグメントレジスタを初期化して、圧縮されているカーネルの展開先アドレスを
決めて、最終的にジャンプしています。最後のジャンプ先は「leal relocated(%ebx), %ebx」となっていて、これは「EBX=relocated + EBX」ということ。そしてその「relocated」というのはすぐ下にあります。
こちらは次回。

2009年7月23日木曜日

protected_mode_jump

go_to_protected_mode関数で最後に呼んでいるのは「protected_mode_jump」です。
こちらはアセンブラになります。

arch/x86/boot/pmjump.S
=====
protected_mode_jump:
movl %edx, %esi # ブートパラメータ構造体のアドレスをESIにセット

xorl %ebx, %ebx #EBXをクリア
movw %cs, %bx  #CSの値をBXへ
shll $4, %ebx   #EBXの値を4ビット左シフト
addl %ebx, 2f   #EBXの値にラベル「2」のアドレスを加算
jmp 1f # ラベル「1」へジャンプ
1:

movw $__BOOT_DS, %cx #CXに__BOOT_DSの値(24)をセット
movw $__BOOT_TSS, %di #DIに__BOOT_TSSの値(32)をセット

movl %cr0, %edx #CR0の値をEDXにセット
orb $X86_CR0_PE, %dl # OR演算でプロテクトモードフラグを立てる
movl %edx, %cr0   #更新したEDXの値をCR0にセットする

# Transition to 32-bit mode
.byte 0x66, 0xea # キャッシュをクリアするためジャンプして次へ

#ラベル「2」は値の定義です
2: .long in_pm32 # offset
.word __BOOT_CS # segment

.size protected_mode_jump, .-protected_mode_jump

.code32
.type in_pm32, @function

in_pm32:
# セグメントレジスタをECXの値で初期化
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
# スタックはEDXで初期化
addl %ebx, %esp

# TRレジスタDIの値をセット
ltr %di

# 汎用レジスタをクリア
xorl %ecx, %ecx
xorl %edx, %edx
xorl %ebx, %ebx
xorl %ebp, %ebp
xorl %edi, %edi

# 割り込みディスクリプタテーブル(ここではダミー)をロード
lldt %cx

 #プロテクトモードでのエントリポイントへジャンプ
jmpl *%eax # Jump to the 32-bit entrypoint

.size in_pm32, .-in_pm32
=====
最後にジャンプしているエントリポイントは「protected_mode_jump」関数の第1パラメータで指定しています。
呼び出しもとの「go_to_protected_mode」を見てみると

protected_mode_jump(boot_params.hdr.code32_start,
    (u32)&boot_params + (ds() << 4));
}


となっています。
ブートパラメータのcode32_startメンバはheader.Sで定義した値を読み込んだものでs。

======
code32_start: # here loaders can put a different
# start address for 32-bit code.
#ifndef __BIG_KERNEL__
.long 0x1000 # 0x1000 = default for zImage
#else
.long 0x100000 # 0x100000 = default for big kernel
#endif

======
となっていて、通常のbzImageであれば0x100000へジャンプしています。
で、それはどこなのだ?というのは次回。

2009年7月19日日曜日

setup_idt/setup_gdt

go_to_protected_mode関数のつづきを見ていきましょう。
まずは割り込みハンドラを登録するためのInterrupt Description Tableをセットするsetup_idtです。

arch/x86/boot/pm.c
=====

static void setup_idt(void)
{
static const struct gdt_ptr null_idt = {0, 0};
asm volatile("lidtl %0" : : "m" (null_idt));
}

=====
ここでは何も定義していないダミーのIDTを用意して、lidtl命令でロードしています。
ちゃんとしたものを後で登録するのだと思います。
次はセグメントを登録するためのGlobal Description Tableをセットするsetup_gdtです。

=====

static void setup_gdt(void)
{
static const u64 boot_gdt[] __attribute__((aligned(16))) = {
/* CS: code, read/execute, 4 GB, base 0 */
[GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
/* DS: data, read/write, 4 GB, base 0 */
[GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
/* TSS: 32-bit tss, 104 bytes, base 4096 */
/* We only have a TSS here to keep Intel VT happy;
we don't actually use it for anything. */
[GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
};
/* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead
of the gdt_ptr contents. Thus, make it static so it will
stay in memory, at least long enough that we switch to the
proper kernel GDT. */
static struct gdt_ptr gdt;

gdt.len = sizeof(boot_gdt)-1;
gdt.ptr = (u32)&boot_gdt + (ds() << 4);

asm volatile("lgdtl %0" : : "m" (gdt));
}

=====
最初にGDTを定義して最後にlgdtl命令でGDTをロードしています。
ここで設定しているGDTは以下のような設定になっています。

CS:コードセグメント、読み込み/実行用、0開始で4GB
DS:データセグメント、読み込み/書き込み、0開始で4GB
TSS:4096開始で104バイト。

TSSはタスクスイッチの際に実行中のタスク情報を退避させておく領域です。
ここで設定しているGDTもおそらくIDTと同じように暫定的な設定だと思います。

2009年7月18日土曜日

reset_coprocessor/mask_all_interrupts

前回見たenable_a20関数で無事A20ゲートを有効に出来たらgo_to_protected_mode関数では次にコプロセッサをリセットするために「reset_coprocessor」関数をコールします。

arch/x86/boot/pm.c
======
static void reset_coprocessor(void)
{
outb(0, 0xf0);
io_delay();
outb(0, 0xf1);
io_delay();
}
======
outbを使ってコマンドを出力しています。
これできっとリセットしているんでしょう。。

次に呼んでいるのは「mask_all_interrupts」です。

=====
static void mask_all_interrupts(void)
{
outb(0xff, 0xa1); /* Mask all interrupts on the secondary PIC */
io_delay();
outb(0xfb, 0x21); /* Mask all but cascade on the primary PIC */
io_delay();
}
=====

似たようなコードですが、プライマリPIC、セカンダリPICそれぞれの割り込みに対してマスクをかけて割り込みが通知されないようにしています。


2009年7月17日金曜日

enable_a20

go_to_protected_mode関数にて「realmode_switch_hook」の次に呼んでいるのは「move_kernel_around」ですがzImage用の処理なのでここではパス。その次はプロテクトモードへ移行する本体ともいえる「enable_a20」関数になります。

arch/x86/boot/a20.c
======
int enable_a20(void)
{
/*~省略~*/

int loops = A20_ENABLE_LOOPS;
while (loops--) {
/* 既にA20が有効になっていないかチェックする */
if (a20_test_short())
return 0;

/* BIOSを使って有効にしてみる */
enable_a20_bios();
/* なったかな? */
if (a20_test_short())
return 0;

/* キーボードコントローラを読み取ってチェックしてみる */
empty_8042();
/* なったかな?*/
if (a20_test_short())
return 0; /* BIOS worked, but with delayed reaction */

/* キーボードコントローラから有効にしてみる */
enable_a20_kbc();
/* なったかな?(長めのチェック) */
if (a20_test_long())
return 0;

/* ポート92から自力で有効にしてみる */
enable_a20_fast();
/* なったかな?(長めのチェック) */
if (a20_test_long())
return 0;
}

return -1;
#endif
}

======

このように色な手段を使ってA20ゲートを有効にしています。ちなみに、なぜこんなところでキーボードコントローラが出てくるのかというとA20ゲートがキーボードコントローラとつながっているからです。

2009年7月13日月曜日

go_to_protected_mode

main関数最後の「go_to_protected_mode」関数を見ていきましょう。

arch/x86/boot/pm.c
======
void go_to_protected_mode(void)
{
/* プロテクトモードに移行する前にフック関数を呼んでおく */
realmode_switch_hook();

/* セグメントを設定 */
move_kernel_around();

/* A20ゲートを有効にする */
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}

/* コプロセッサをリセット */
reset_coprocessor();

/* PICの割り込み設定をすべてマスクする */
mask_all_interrupts();

/* プロテクトモードへの移行*/
setup_idt();
setup_gdt();
protected_mode_jump(boot_params.hdr.code32_start,
    (u32)&boot_params + (ds() << 4));
}

======

今回からはこの中の関数を順に見ていきます。
まずプロテクトモード移行前にフック関数をコールしておく「realmode_switch_hook」です。
======
static void realmode_switch_hook(void)
{
/* ブートパラメータのヘッダ情報にフック指定あれば、それを呼び出す */
if (boot_params.hdr.realmode_swtch) {
asm volatile("lcallw *%0"
     : : "m" (boot_params.hdr.realmode_swtch)
     : "eax", "ebx", "ecx", "edx");
} else {
/* フック指定なければ割り込みを禁止する */
asm volatile("cli");
outb(0x80, 0x70); /* Disable NMI */
io_delay();
}
}

======
ちなみにブートパラメータのヘッダ情報(header.S)のrealmode_swtchの項は
realmode_swtch: .word 0, 0 # default_switch, SETUPSEG
と何も指定していないので、変更しなければelse句に入って割り込み禁止します。

2009年7月7日火曜日

main関数おさらい

長いこと見てきたmain関数ですが、ついに最後の関数「go_to_protected_mode」を残すのみとなりました。
go_to_protected_modeでプロテクトモードに移行します。
その前に、ここで一度main関数をまとめておきましょう。

arch/x86/boot/main.c
======
void main(void)
{
/* ブートパラメータのヘッダ情報を取得する */
copy_boot_params();

/* ヒープの末尾位置をセット */
init_heap();

/* CPU情報を取得してチェック */
if (validate_cpu()) {
~省略~
}
~省略~

/* メモリマップとメモリサイズを取得 */
detect_memory();

/* キーボードのリピートレートをMAXに設定 */
keyboard_set_repeat();

/* ROMテーブルを取得 */
query_mca();

~省略~

/* IST(Intel Speed step Technology)情報を取得 */
query_ist();

/* APM(Advanced Power Management)情報を取得 */
query_apm_bios();

/* EDD(Enhanced Disk Drive)情報を取得 */
query_edd();

/* ビデオカード情報、ディスプレイ情報を取得 */
set_video();

~省略~

/* プロテクトモードへ移行する */
go_to_protected_mode();
}

======

と、ハード関係の情報をブートパラメータ構造体に格納してからプロテクトモードに移行しています。

2009年7月6日月曜日

set_video

今回は「set_video」を見ていきます。

arch/x86/boot/video.c
=====
void set_video(void)
{
  /* ここにはSVGAモードが入る(header.Sより) */
  u16 mode = boot_params.hdr.vid_mode;

  /* ヒープ位置をリセット */
  RESET_HEAP();
  /* ビデオ関連のパラメータセット(後述) */
  store_mode_params();
  /* set_mode_paramsで設定したパラメータを別変数に退避 */
  save_screen();
  /* ビデオカードのプローブ */
  probe_cards(0);

  /*〜省略〜 */

  /* EDIDを読み込みブートパラメータ構造体にセット */
  vesa_store_edid();
  /* ビデオ関連のパラメータセット#2回目 */
  store_mode_params();

  /* 必要ならsave_screen()で退避させた情報を使ってスクリーン情報を復元する */
  if (do_restore)
  restore_screen();
 }
=====

まずboot_params.hdr.vid_modeの値をローカル変数に代入して、このローカル変数を関数内で参照していますが
この値はheader.Sにて定義されています。


そしてその後は各関数をコールしてビデオ関連の情報をブートパラメータにセットします。
まずは「store_mode_params」。

=====
static void store_mode_params(void)
{
  /*〜省略〜*/

  /* 16色テキストモード(80*25)に設定してカーソル位置を */
  /* boot_params.screen_info.orig_x/y に格納 */
  store_cursor_position();
  /* ビデオモードを取得して、                             */
  /* boot_params.screen_info.orig_video_modeと */
  /* boot_params.screen_info.orig_video_page に格納 */
  store_video_mode();

  /* ビデオモードに合わせてビデオセグメントをセット */
  if (boot_params.screen_info.orig_video_mode == 0x07) {
   /* 〜省略〜 */
 }

  /* フォントサイズをセット */
 set_fs(0);
 font_size = rdfs16(0x485); /* Font size, BIOS area */
 boot_params.screen_info.orig_video_points = font_size;

  /* 〜省略〜*/

 /* ビデオの縦横をセット */
  boot_params.screen_info.orig_video_cols  = x;
 boot_params.screen_info.orig_video_lines = y;
}
======
実際にBIOS関数を使ってビデオモードを取得しているのは「store_vide_mode」関数になります。レジスタに値をセットしてソフトウェア割り込みを起こすいつもの手順なので詳細はスキップします。次にビデオカードをプローブする「probe_cards」です。

arch/x86/boot/video-mode.c
======
void probe_cards(int unsafe)
{
  /*〜省略〜 */

  /* .videocardsセクションの情報をチェック */
  for (card = video_cards; card < video_cards_end; card++) {
   /* unsafeの値がパラメータと一致したらプローブ関数をコールする */
   if (card->unsafe == unsafe) {
   if (card->probe)
     card->nmodes = card->probe();
   else
    card->nmodes = 0;
   }
  }
}
======
card_info構造体にプローブ時に呼び出すためのコールバック関数が指定されていればそのプローブ関数をコールして戻り値をnmodesメンバに格納します。プローブ対象の領域はvideocardsセクションで指定された領域になります。

ビデオカードの情報をセットした次は接続しているディスプレイの情報をとりにいきます。

arch/x86/boot/video-vesa.c
======
void vesa_store_edid(void)
{
  /* 〜省略〜 */

  /* EDIDに未対応のバージョンなら何もしない */
  if (vginfo.version < 0x0200)
   return; /* EDID requires VBE 2.0+ */
 
  ax = 0x4f15; /* VBE DDC */
  bx = 0x0000; /* Report DDC capabilities */
  cx = 0; /* Controller 0 */
  di = 0; /* ES:DI must be 0 by spec */

  /*  DDCが使えるか問い合わせ */
  asm("pushw %%es; movw %2,%%es; "INT10"; popw %%es"
    : "+a" (ax), "+b" (bx), "+c" (cx), "+D" (di)
    : : "esi", "edx");

  /* 〜省略〜 */

  /* EDIDを読み込んで、boot_params.edid_infoにセット */
  ax = 0x4f15; /* VBE DDC */
  bx = 0x0001; /* Read EDID */
  cx = 0; /* Controller 0 */
  dx = 0; /* EDID block number */
  di =(size_t) &boot_params.edid_info; /* (ES:)Pointer to block */
  asm(INT10
    : "+a" (ax), "+b" (bx), "+d" (dx), "=m" (boot_params.edid_info),
      "+c" (cx), "+D" (di)
    : : "esi");
}

======
EDIDとは接続しているディスプレイのIDのことです。このIDをもとに解像度を設定します。DDCとはDisplay Data Channelのことでディスプレイの情報を通知する方式です。ここではEDIDをとってきてブートパラメータにセットしているだけです。

2009年7月3日金曜日

GCC拡張インラインアセンブラ

ちょっと今回は番外編。
ここまで見てきたソースの中でよく出くわすのがインラインアセンブラ。基本的にはasm("~アセンブラ~")という記述なのですが、カーネルで出てくるのはGCCの拡張インラインアセンブラといわれるもので、ごく普通のGAS形式のアセンブラともまたちょっと違うので読みにくさが増しています。

そこで参考にしたのが下記のサイトです。
GCC Inline Assembler

例えば前回見た「get_edd_info」には下記のようなインラインアセンブラがあります。


asm("pushfl; stc; int $0x13; setc %%al; popfl"
    : "+a" (ax), "+b" (bx), "=c" (cx), "+d" (dx)
    : : "esi", "edi");



これを整理してみると、、、

(アセンブラコード)
pushfl   フラグ退避
stc    キャリーフラグセット
int 0x13  BIOS interrupt call 
setc AL  キャリーフラグをALに格納
popfl   フラグをポップ

(出力オペランド)
ax 入出力で使う
bx 入出力で使う
cx 出力で使う
dx 入出力で使う

(入力オペランド)
指定なし

(壊れるレジスタ)
esi
edi

関数の先頭でax=0x4100、bx=0x55aa、dx=ドライブ番号と指定しています。この3つはいずれも「入出力」として指定されていることに注目。ax, bx, dxにこれらの値を入れてint 0x13を実行するとdxで指定したドライブのEDD情報を取得する、という命令になります。

query_edd

main関数もようやく終盤、今回は「query_edd」を見ていきます。
EDDというのは「Enhanced Disk Drive」の略で、4GBを超えたメモリアクセスを可能にする仕組みです。

arch/x86/boor/edd.c
======
void query_edd(void)
{
/* ~省略(変数宣言)~*/

/* 起動オプションでeddのON/OFFが指定されているか? */
if (cmdline_find_option("edd", eddarg, sizeof eddarg) > 0) {
/* ~省略(チェック結果で変数更新)~*/
}

/* "quiet"オプションが指定されているか? */
be_quiet = cmdline_find_option_bool("quiet");

edp    = boot_params.eddbuf;
mbrptr = boot_params.edd_mbr_sig_buffer;

/* EDD拡張いらないならおしまい */
if (!do_edd)
return;

/* "quiet"指定されていなければメッセージ出力 */
if (!be_quiet)
printf("Probing EDD (edd=off to disable)... ");

/* デバイスをチェックする */
for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) { /* EDD情報を取得 */ if (!get_edd_info(devno, &ei)     && boot_params.eddbuf_entries < EDDMAXNR) { memcpy(edp, &ei, sizeof ei); edp++; /* ここでboot_params.eddbufにEDD情報が格納される */ boot_params.eddbuf_entries++; } if (do_mbr && !read_mbr_sig(devno, &ei, mbrptr++)) boot_params.edd_mbr_sig_buf_entries = devno-0x80+1; } if (!be_quiet) printf("ok\n"); }

======

query_eddの中で呼んでいる「get_edd_info」「read_mbr_sig」を見てみます。
まずは「get_edd_info」から。

======
static int get_edd_info(u8 devno, struct edd_info *ei)
{
/*~省略~*/

/* EDD情報を取得 */
ax = 0x4100;
bx = EDDMAGIC1;
dx = devno;
asm("pushfl; stc; int $0x13; setc %%al; popfl"
    : "+a" (ax), "+b" (bx), "=c" (cx), "+d" (dx)
    : : "esi", "edi");

/*~省略~*/

/* EDDバージョン、機能セットの格納しておく */
ei->device  = devno;
ei->version = ax >> 8; /* EDD version number */
ei->interface_support = cx; /* EDD functionality subsets */

/* 拡張デバイスパラメータを取得してei->paramsに格納する */
ei->params.length = sizeof(ei->params);
ax = 0x4800;
dx = devno;
asm("pushfl; int $0x13; popfl"
    : "+a" (ax), "+d" (dx), "=m" (ei->params)
    : "S" (&ei->params)
    : "ebx", "ecx", "edi");

/* レガシーなCHS(Cylinder, Head, Sector)情報を取得 */
ax = 0x0800;
dx = devno;
di = 0;
asm("pushw %%es; "
    "movw %%di,%%es; "
    "pushfl; stc; int $0x13; setc %%al; popfl; "
    "popw %%es"
    : "+a" (ax), "=b" (bx), "=c" (cx), "+d" (dx), "+D" (di)
    : : "esi");

if ((u8)ax == 0) {
ei->legacy_max_cylinder = (cx >> 8) + ((cx & 0xc0) << 2); ei->legacy_max_head = dx >> 8;
ei->legacy_sectors_per_track = cx & 0x3f;
}

return 0;
}

======

次に「read_mbr_sig」。

======
static u32 read_mbr_sig(u8 devno, struct edd_info *ei, u32 *mbrsig)
{
/*~省略~*/

/* セクタサイズが未設定なら512にしておく */
sector_size = ei->params.bytes_per_sector;
if (!sector_size)
sector_size = 512; /* Best available guess */

/* ヒープ上のアラインをセット */
buf_base = (ds() << 4) + (u32)&_end; mbr_base = (buf_base+sector_size-1) & ~(sector_size-1); mbrbuf_ptr = _end + (mbr_base-buf_base); mbrbuf_end = mbrbuf_ptr + sector_size; /*~省略~*/ /* MBRを読み込む */ if (read_mbr(devno, mbrbuf_ptr)) return -1; *mbrsig = *(u32 *)&mbrbuf_ptr[EDD_MBR_SIG_OFFSET]; return 0; }

======

というわけで、「query_edd」とそのサブ関数を見てきたわけですが中心となっているのは「get_edd_info」でBIOS関数を使ってEDD情報を取ってきてブートパラメータ構造体に格納する、といういつもの流れです。