chroot("/home/hibari")

備忘録とかに使えそうなノート

linuxカーネル読書がしたい(ブート編3)

前回に引き続き読書を続けて行く.main.cの読書の続きから

 

main.c(リアルモードでのカーネルコードモジュール)

・0ページ目にブートヘッダをコピー

・early-bootコンソールの初期化(コンソールの準備)

・ヒープの初期化

・CPUの検証

↓このページはここから

BIOSをどのCPUモードで使うかの設定

・メモリレイアウトの検出

・キーボードの初期化

Intel SpeedStep (IST) 情報の照会

・(もし定義されていれば)APM情報の照会

・(もし定義されていれば)EDD情報の照会

↑ここまで

・ビデオモードの設定

・プロテクトモードへの移行

 

set_bios_mode()

見てみるとコードそのものは短いようなので以下に載せる.

104 static void set_bios_mode(void)
105 {
106 #ifdef CONFIG_X86_64
107     struct biosregs ireg;
108
109     initregs(&ireg);
110     ireg.ax = 0xec00;
111     ireg.bx = 2;
112     intcall(0x15, &ireg, NULL);
113 #endif
114 }

 x86_64が有効であれば,BIOSレジスタとした構造体を初期化する.中身は各種レジスタ群の構造体の共用体であった.initregs(struct biosregs* reg)はregs.cに記述されている.0クリアしたあとでEFLAGSをセットし,セグメントレジスタの中身のコピーをしている.

次の処理のintcallに来て,ここでax=0xec00, bx=2のint 0x15とは何かとなった.

https://www.kernel.org/pub/linux/kernel/people/akpm/patches/2.5/2.5.75/2.5.75-mm1/broken-out/x86_64-critical-fixes.patch

2003年の7月に↑のパッチから,カーネルにこのint 0x15が新たに追加されたらしい.該当箇所の抜粋を以下に載せると,

- Tell BIOS we run in long mode
(this is a nop currently, but will help with some future boxes)

+# tell BIOS we want to go to long mode
+	movl  $0xec00,%eax	# declare target operating mode
+	movl  $2,%ebx		# long mode
+	int $0x15

 コメントを見ると,BIOSに対してlong modeで実行したいことを伝えているようである.(要調査)

 

===== set_bios_mode ここまで ===== 

 

detect_memory()

122 int detect_memory(void)
123 {
124     int err = -1;
125
126     if (detect_memory_e820() > 0)
127         err = 0;
128
129     if (!detect_memory_e801())
130         err = 0;
131
132     if (!detect_memory_88())
133         err = 0;
134
135     return err;
136 }

eax=0xe820, 0xe801, 0x88の3通りを順次指定し,int 0x15を実行する.

メモリが見つかればerr = 0となり,なければerr = -1のままで返ることになる.各関数を見る前にstruct e820entryの定義を見てみる.e820.hより.

 58 struct e820entry {
 59     __u64 addr; /* start of memory segment */
 60     __u64 size; /* size of memory segment */
 61     __u32 type; /* type of memory segment */
 62 } __attribute__((packed));
detect_memory_e820()

システムメモリマップを取得する関数.

eaxを0xe820に指定してint 0x15を行う.指定したバッファに,メモリ開始アドレス,メモリサイズ,メモリタイプを格納するため,ちょうど↑の構造体と同じ形式に格納される.これを,取得可能な次のアドレスを返すebxが0(これ以上ない時)になるか,EFLAGSのCF=1(失敗)になるかのどちらかが発生するまで探索して情報を収集する.

ただしイレギュラーもあるようで,出力のeaxには"SMAP"(System memory MAP)のシグネチャが返されるはずが,SMAPを返さないこともあるようである.この時にはcountを0としてループから抜け出す.

最終的なcount数(収集できた数)をboot_params.e820_entriesに格納して返る.

格納されるメモリ情報の詳細は下記の参考に記載されている.勉強になります.

 

detect_memory_e801

ビッグメモリサイズ取得の関数.

eaxに0xe801を設定してint 0x15を行う.EFLAGSのCF=0で成功.

ax:1MB〜16MBまでのメモリ領域のメモリサイズ(1KB単位)

bx:16MB以上のメモリ領域のサイズ(64KB単位)

cx:1MB〜16MBまでのマザーボードのジャンパで切り替えた時のメモリ領域のサイズ(1KB単位)

dx:16MB以上のマザーボードのジャンパで切り替えた時のメモリ領域のサイズ(64KB単位)

で返ってくる.ax == cx, bx == dxだと思って問題なさそう.

ax > 15*1024の時は偽の値として失敗.

ax == 15*1024の時はboot_params.alt_mem_k = (oreg.bx << 6) + oreg.ax

ax < 15*1024の時はboot_params.alt_mem_k = oreg.ax

0を返す.

 

detect_memory_88

1MB以上の拡張メモリ領域のサイズを取得する関数.

eaxに0x88を設定してint 0x15を行う.EFLAGSのCF=0で成功.

出力のaxを以下のメンバに代入

boot_params.screen_info.ext_mem_k = oreg.ax

 

keyboard_init()

キーボードのリピートレートの設定.コメントで(why?)と書かれているのは面白い.

ここではint 0x16が呼ばれている.int 0x16はキーボードサービスに関するものである.AHに0x2を指定して実行するとshiftキーやaltキーなどが押されているかのキーボードステータスをALに入れる.例えば左shiftキーが押されていれば00000010bが返ってくる.これをboot_params.kbd_statusに代入.

次にaxに0x305を指定して実行している.これはキーボードのリピートレートのセットをしている....のだが,セットする値を設定するbh, blの値が設定されていない....調べて見ると,0に設定されていると最大値として設定するようである.

 

query_ist()

Intel SpeedStep(IST)情報の照会.ISTは電源の種類やCPUへの負荷状況に応じて動作性能を切り替える技術.

一定以上古いBIOSではISTをサポートしているかの問い合わせのint 0x15でクラッシュを起こすようなので,cpu.levelが6未満の場合は弾くようになっている.

axに0xe980,edxに0x47534943(リクエスト値."CISG"?)を指定してint 0x15を実行.実行したあとで各レジスタに入った値をboot_paramsの各該当メンバに代入.

 95     boot_params.ist_info.signature  = oreg.eax;
 96     boot_params.ist_info.command    = oreg.ebx;
 97     boot_params.ist_info.event      = oreg.ecx;
 98     boot_params.ist_info.perf_level = oreg.edx;

 

query_apm_bios()

APM情報を照会する.APMはAdvanced Power Managementの略で,電源管理インターフェース.

はじめにAHに0x53を指定してint 0x15.EFLAGSのCF=0,bx != "PM",!(cx & 0x02)であればエラーとして返る.

次にalに0x04を指定してint 0x15を実行してAPMのインターフェースを切断する.

切断したらalに0x03を指定してint 0x15を実行して32bitでインターフェースに接続する.CF=1で失敗となる.出力される値の詳細は以下の通りである.

ax:プロテクトモードでの32bitコードセグメントの,リアルモードにおけるベースアドレス

ebx:エントリポイントからのオフセット

cx:axの16bitコードセグメント版

dx:axの16bitデータセグメント版

si:32bit APMコードセグメントの長さ

hsi:16bit APMコードセグメントの長さ

di:32bit APMコードセグメントの長さ

これらをboot_params.apm_bios_infoの各メンバに代入する.

その後alを0x00に指定してint 0x15を実行して32-bit接続でインストールチェックを行う.32bit接続で失敗した時には切断してそのまま返る.成功したらバージョン情報とフラグをboot_params.apm_bios_infoの各メンバに代入する.

 

query_edd()

EDD情報の照会.Enhanced Disk Deviceの略.

まずdo_mbrとdo_eddのフラグが2つある.デフォルトではdo_mbr=1,do_eddはEDDをOFFにする設定ができるなら0,出来ないなら1としている.次にフラグの設定をしている.

・edd=skipmbrもしくはedd=skip → do_edd=1,do_mbr=0

・edd=off → do_edd=0

・edd=on → do_edd=1

ここで初めてcmdline_find_option_boolが出てくるが,これは引数に指定されたオプションが存在するかを判定しているものである.今回はquietが指定されているので,"quiet"があればtrueとなる.

boot_paramsからeddバッファ構造体のポインタと,mbrのsigバッファのポインタを受け取る.

do_edd=0ならここで終了.

be_quiet=0なら"Probing EDD (edd=off to disable)... "を出力.

次のfor文ではBIOSがサポートされているハードディスクをスキャンして,EDD情報を照会する.

 

get_edd_info(u8 devno, struct edd_info *ei)

関数名からしてEDD情報を取ってくるものだろう.

ah=0x41, bx=EDDMAGIC1(0x55AA),dl=ドライブデバイス番号(0x80~0xff)を指定してint 0x13を実行して,EDDの拡張情報があるかのチェックを行う.CF=1あるいはbx != EDDMAGIC2(0xAA55)であれば失敗として-1を返す.

エラーがなければ,EDD情報の構造体にデバイス番号,EDDバージョン,インターフェースのサポート情報(functionality subsets)を格納する.インターフェースのサポート情報は以下のものがある.

0:拡張ディスクアクセス機能

1:リムーバブルドライブコントローラ機能

2:EDD機能

これが有効ならばそれに応じたint 0x13が使用できる.

次にah=0x48,si=&ei->paramsを指定してint 0x13を実行してドライブパラメータを入手する.成功すればah=0,指定したバッファが埋まる.

ついでah=0x08,es=0(BIOSのバグを防ぐために0が推奨されているようだ)を指定してint 0x13を実行してCHS(Cylinder, Head, Sector)情報を得る.返り値としては以下のものがある.

ch:シリンダ番号の最大の下位8bit

cl:最大のセクタ番号[5:0],シリンダ番号の最大の上位2bit[7:6]

dh:最大のヘッド番号

dl:ドライブ数

特にシリンダに関しては少し複雑な返り値であるため,以下の構造体メンバへの代入の際にぱっと見では分かりにくいことをしている.clレジスタに2つの値が混在するためにANDで切り分けている.

 

113     if (!(oreg.eflags & X86_EFLAGS_CF)) {
114         ei->legacy_max_cylinder = oreg.ch + ((oreg.cl & 0xc0) << 2);
115         ei->legacy_max_head = oreg.dh;
116         ei->legacy_sectors_per_track = oreg.cl & 0x3f;
117     }
 

===== get_edd_info ここまで =====

 
read_mbr_sig(u8 devno, struct edd_info *ei, u32 *mbrsig)

関数名的にMBRシグネチャを読みに行く関数?

まずはセクタサイズを確認する.対象のEDD情報からei->params.bytes_per_sectorでセクタサイズを得る.0の時には512とする.

_end変数はsetup.ldに定義されている..bss領域の末尾を指しており,同時にヒープ領域の先頭を指している.

 52     /* Produce a naturally aligned buffer on the heap */
 53     buf_base = (ds() << 4) + (u32)&_end;
 54     mbr_base = (buf_base+sector_size-1) & ~(sector_size-1);
 55     mbrbuf_ptr = _end + (mbr_base-buf_base);
 56     mbrbuf_end = mbrbuf_ptr + sector_size;

buf_base = ヒープ先頭 + ds * 16(リニアアドレス)

mbr_baseはsector_size=512の時には下位8bitが0クリアされる

要するにヒープ領域の先頭でアライメントされているところからセクタサイズ分の領域を計算したことになっているのだろうか.

↑で計算した領域がヒープ領域内にあるのかのチェック.具体的にはヒープの使用の可否と,mbrbuf_endがヒープ領域を超えていないか.

チェックが済んだら↑で計算した領域を0クリア.

MBRを読みに行く.CF=0なら失敗.

 

read_mbr(u8 devno, void *buf) 

MBR(最初のセクタ)を読みに行く.入力は以下の通り

ah:0x02

al:読むセクタ数(0にしないこと)

ch:シリンダ番号の下位8bit

cl:セクタ番号(1〜63で指定)[5:0],シリンダ番号の上位2bit(HDDのみ)[7:6]

dh:先頭の番号?

dl:ドライブ番号(ハードディスクの為に7bit目はセット)

es:bx:データバッファのポインタ

出力としてCF=0なら成功.

今回,al=0x01,cx=0x0001であるから,シリンダ0の第1セクタから1つ分だけ(つまりMBRのこと)を読みに行くようにしている.

 

===== read_mbr ここまで =====

 

read_mbrからmbrbuf_ptrから1セクタ読めなかったらここで-1で返る.

mbrbuf_ptrからEDD_MBR_SIG_OFFSETバイト目から2バイトのシグネチャを取得.

同様にmbrbuf_ptrから510バイト目から2バイトのマジックを取得.

→ 0xAA55であれば0,それ以外は-1を返す.

MBRの末尾2バイトはシグネチャとして指定されている.

 

===== read_mbr_sig ここまで =====

 

read_mbr_sigから-1が返ったら,boot_params.edd_mbr_sig_buf_entries = devno-0x80+1

 

===== query_edd ここまで =====

 

 

 

参考

http://softwaretechnique.jp/OS_Development/Tips/Bios_Services/General_Services/bigmemory_services_E820.html

http://dev.ariel-networks.com/Members/ohyama/30fc30c830ed30fc30fc-305d306e4/

http://www.yoshinobrain.com/200008-bootup.pdf

http://search.luky.org/linux-users.3/msg02165.html

http://www.delorie.com/djgpp/doc/rbinter/ix/15/53.html

linuxカーネル読書がしたい(ブート編2)

前回に引き続きlinux読書を続けていく.header.Sからmainにjmpするので,次はmain.cの読書. 

 

main.c(リアルモードでのカーネルコードモジュール)

これをただただ上から読むと,

↓このページはここから

"0ページ目"にブートヘッダをコピー

early-bootコンソールの初期化(コンソールの準備)

ヒープの初期化

CPUの検証

↑ここまで

BIOSをどのCPUモードで使うかの設定

・メモリレイアウトの検出

・キーボードの初期化

Intel SpeedStep (IST) 情報の照会

・(もし定義されていれば)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;
}

outb(u8 v, u16 port)はportからvを出力する基本I/O.色々入出力して初期化している.

 

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はIntelAMDx86プロセッサー固有の情報を格納した特殊なレジスタ

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はデジタルデータをアナログデータに変換し,アナログ回線でシリアル転送する際に用いられる単位

自分でビルドした任意のライブラリにリンクさせる

 

今回はお試しで短期的に使うレベルの話なので長期的に使用することは考慮していない.

デフォルトでインストールされているライブラリではなく,自分でソースコードからビルドしたライブラリにリンクさせたい.

 

振り返ると大したことはしていないが,すぐに忘れそうなので備忘録.

 

きっかけは,/usr/lib64に入っているライブラリのバージョンが古かったので新しいバージョンのライブラリをちょっと使いたかったというもの.そしてついでにいろんなバージョンも試そうと思った.

 

 

対象はGNU MPライブラリ.

The GNU MP Bignum Library

 

自分のワークディレクトリにver6.1.2ダウンロードして解凍,ビルド,インストール.

/install/dest/dirにインストールすることを仮定.

$ tar -Jxvf gmp-6.1.2.tar.xz; cd gmp-6.1.2

$ ./configure --prefix=/install/dest/dir 

$ make -j8

$ make check -j4

$ make install

makeのjオプションの数に深い意味はない.

以下は/install/dest/dirをlsしたもの.インストールされているのを確認.ここにあるライブラリにリンクさせる.

$ ls
include lib share

 

環境変数LD_PRELOADを変更する.

$ export LD_PRELOAD=/install/dest/dir/lib/libgmp.so

 

ワークディレクトリ内で,ソースコードには通常通り#include <gmp.h>を記述する.

明示的にヘッダファイルを指定するために以下のようにコンパイルする.

パスを記述するオプションはI(大文字アイ)で,使用するライブラリの指定はl(小文字エル)を使用する.

$ gcc -I/install/dest/dir/include -lgmp test.c -o test

 

共有ライブラリの依存関係を調べて指定のライブラリパスに通っているかの確認.

$ ldd test

linux-vdso.so.1 =>  (0x00007ffd727cd000)

/install/dest/dir/lib/libgmp.so (0x00007fce37ba3000)

(中略)

 

libc.so.6 => /lib64/libc.so.6 (0x00007fce37047000)

 

ちなみに,LD_PRELOADで指定せずにコンパイルしたときにはデフォルトでインストールされているライブラリにリンクされる.

$ ldd test

linux-vdso.so.1 =>  (0x00007f8f435ed000)

libgmp.so.10 => /lib64/libgmp.so.10 (0x00007f8f4334d000)

(中略)

libc.so.6 => /lib64/libc.so.6 (0x00007f8f42818000)

 

別でインストールした別バージョンのライブラリを使うときにはLD_PRELOADを書き換えてコンパイルし直すとそこにリンクさせることができるのが確認できた.

今回は自分で手打ちでexportをしているため,ログアウトしたらその環境変数は消える.

 

 

linuxカーネル読書がしたい(ブート編1)

linuxカーネル読書をしようしようって言ってずっとやっていなかったのでついに一歩踏み出そうという気になれた.

全体的な流れとしては,まずはざっと読んでから詳細に調べたりして行く予定なので,書いたらそれっきりではなく随時追記していくつもりである.

とはいえ実際にはメモ書きのためなので可読性には欠けている.

 

まずはブートについて.

Intelプロセッサはリアルモードとプロテクトモードの2種類存在する.

リアルモードでプロテクトモードへの移行の準備をする.

コンピュータの電源を入れた直後はリアルモードでしか動作しない.BIOS手続きはリアルモードでのみ動かすため,プロテクトモード移行後にはBIOSは一切使用しない.

 

はじめはbootのheader.S 

過去のbootsect.Sとsetup.Sをベースに作られたもの.

この時点では64kセグメントのリアルモードであるため,各リニアアドレスを取得するためには"seg_addr * 16"をしなければならない.

例えば,ブートセクタのアドレスが0x7c00から始まるということになっているので,セグメントによる表記ではBOOTSEG = 0x7c0となる.

 

bootsect_start:

まずは上記のBOOTSEGセグメントのstart2オフセットにljmpする.(ljmpはセグメント間ジャンプとも言われる.)

start2:

csレジスタ = dsレジスタ = esレジスタ = ssレジスタに設定し,spレジスタ = 0にする.

sti:EFLAGSの割り込みフラグ(IF)をセットし,次の命令から割り込みへの応答を開始する.

cld:EFLAGSのDFフラグをクリアし,バイト操作する際のインデックスレジスタ(edi, esi)をインクリメント方向に設定する.(DF=1でデクリメント)

siレジスタを"Use a boot loader.  Remove disk an d press any key to reboot ..."の先頭に設定する.

 

...

 

start_of_setup:

%es = %dsをし,cld

%ds == %ss(スタックセグメント)*1

→ 真ならスタックが正常に設定されているとして2へ

→ 偽ならスタック領域の再計算を行う

ロードフラグと比較してヒープ領域が使用できるなら,%dx = heap_end_ptr + STACK_SIZE

(heap_end_ptr = _end + STACK_SIZE - 512なので実質_end - 512)

 2

%dxの下位2bitのalignment(dword align)

0になったら%dx = 0xfffc

3

%ss = %ds

%esp = %dx(上位16bitはクリア)

IFフラグをセットする(この時点でスタック領域は使用可能になっている) 

push %ds

push 6

ret

セットアップシグネチャ(0x5a5aaa55)の照会 

→ 失敗なら最終的にhlt, die()

%eax = 0

%cx = ( ( _end + 3 ) - __bss_start ) >> 2

rep; stosl(%cxが0になるまで0クリアを繰り返す)

 

stosはeax(ax, al)をsi:(e)diにストアする.命令サイズに応じて(e)diをインクリメント(デクリメント)する.

 

call main(Cで書かれたコードへの移行)

 

次回にmainを読んでいく.

 

 

参考

http://d.hatena.ne.jp/outland_karasu/20070509/1178697916

http://softwaretechnique.jp/OS_Development/index.html

http://www.mztn.org/lxasm64/x86_x64_table.html

 

*1:昔のLILOブートローダ)では%ss != %dsでカーネルを呼び出すのだが,これではアクシデントが発生してしまうために再計算を必要とする

grub読書がしたい2

grub読書,前回はMBRに格納されているはずのstage1部分を読んで,stage2にジャンプするところに至った.今回はその続きから.

 

stage2_address(つまり0x8000)はstart.Sから.スタックやレジスタは前回から引き継いでいる状態になる.

 

dxを退避させてドライブ情報をセーブする

"Loading stage2"を出力

di = firstlist - 8(BOOTSEC_LISTSIZE)?(bootloopの準備)

ebp = (di)(第2セクタのセクタ番号をセーブ)

 
bootloop

4(di)を見て,読むべきセクタ数を確認する.0であればこれ以上はないとしてbootitにジャンプ.まだ読むべきセクタがある判定になったらそのまま下に続いていく.

-1(si) != 0ならLBAで読みにいく.(-1(si)はstage1で謎に1をセットしていたところが関与していそう)

 

===== ちょっとどういうことか理解できてないエリア =====

ebx = (di)(論理セクタのスタート位置をロード?)

al = 0x7f(Phoenix EDDにより最大値は0x7fに制限されている)

4(di) > 0x7f であれば ax = 4(di)(全セクタ数と比較している)

4(di) - ax

(di) + eax(論理セクタスタートに足す)

=====  =====

 

bootit

改行を出力し,dxにpush.(dlにはブートするドライブが入っているはず)

stage1.5が定義されてたら0x2200(stage1.5のmain)に,そうでなければ0x8200(stage2のmain)にジャンプ

 

 

main in stage2.asm.Sに記述.

ljmpでcodestartにジャンプ.この時点ではまだリアルモードになっている.割り込みの禁止を行う.

ds = ss = es = 0

ebp = esp = STACKSEG(0x2000 - 0x10)(リアルモード/BIOSスタックの設定)

割り込みの許可

real_to_prot(プロテクトモードへの移行)

 

割り込みの禁止

GDTレジスタの読み込み

cr0 | 0x1でプロテクトモードにする

プロテクトセグメント(protcseg)へljmp

 

protcseg

ds = es = fs = gs = ss = PROT_MODE_DSEG(セグメントレジスタへのリロード)

STACKSEG = (esp)(リターンアドレスをSTACKSEGに設置する)

esp = ebp = protstack

(esp) = STACKOFF(さきほど設定したリターンアドレスを設置する)

ret

 

.code32

bss領域を0クリアする(具体的にはbssの最初と最後を読み込んで差分の分だけrep stosbをする)

 

init_bios_infoへcall(ここからCコードになる)

 

init_bios_info in common.c

mbi.mem_lower = get_memsize(0)

mbi.mem_upper = get_memsize(1)

ここでのmbiはMultiBoot Infoの構造体の略称.get_memsizeはgrub/asmstub.cに記載.

0の時には640 * 1024,それ以外の時には3 * 1024 * 1024(3MB)

 

boot_driveとinstall_partitionを保存

ディスク情報を取りに行き,cdrom_driveをboot_driveにする.(CDROMドライブと書いてあるけれど...)

cmainへ

 

 

grub読書がしたい1

前回までやってきたブート編を読んだ後でgrubも読みたくなったので,寄り道的な意味でgrub読書がしたいを開催.

ざっくりどういう動作をしているのかをメモ書き.まずはgrub-0.97からで.

 

Makefile.amにLDFLAGS = -nostdlib -Wl,-N,-Ttext,7C00と指定されている.
これはstage1のイメージファイルをビルドする時にリンカに与えるオプション.
-Ttext 7c00はリンカにこのコードは0x7c00からロードされ実行されることを伝える.

 

stage1.Sより

_start:は0x7c00にロードされる.いきなりafter_BPBにジャンプする.

というのも,この後で記述されているBPB(BIOS Parameter Block)をスキップするようにしているからである.

各種ヘッダ情報の領域の後でstage2のアドレスを0x8000と記述されている.

cliで割り込み禁止にする.

dlレジスタを検証,0x80であればHDDにインストールされていると認識する.そうでなければdlに0x80を代入している.

real_startにジャンプ

ds, ssを0クリア.stage1のスタックをセットし,stiで割り込みを許可する.

次に強制的にディスクの中身を参照できるかの検証をする.具体的にはboot_driveの中身をalに移し,フラグ検証をする.正しければdlにalを代入する.

もしフロッピーであればCHSモードへジャンプ.そうでなければLBAをサポートしているかの検証.ah=0x41,bx=0x55aaにしてint 0x13.

popしてpushしている.(?)これに失敗したらCHSモードへ移行.

LBAがサポートされていればLBAモードへ.LBA(Logical Block Addressing)は全てのセクタ番号に通し番号を割り振ってそれで指定する方法のことである.

 

 
lba_mode

LBAモードでstage2の準備をするラベル.最初のsiレジスタがもともとなんの値を示しているのかを調査する必要がある...

ecx = 0x10(si) 全セクタ数のセーブ

si = (Disk Address Packet)

Disk Address Packet(DAP)はHDDの読み書きやブロックの情報が入った構造体の名前である.

-1(si) = 1(non-zeroモードにしておく)?

(si) = 0x10(DAPのサイズ.元々0x10か0x18とされているらしい)

2(si) = 1(読み書きするブロック数)

8(si) = stage2_sector(HDD上の開始ブロック.stage2セクタの絶対アドレスの下位32bitを代入)

4(si):6(si) = 0000:STAGE1_BUFFERSEG(読み書き先の指定)

12(si) = 0

ah = 0x42; int 0x13(DAP情報を頼りにディスクからメモリにセクタを読む)

bx = STAGE1_BUFFERSEG

copy_bufferへジャンプ

 

copy_buffer

es = stage2_segment

汎用レジスタ全てとdsをpush(↓でcxとsi,dsを別に使うため退避)

cx = 0x100

ds = STAGE1_BUFFERSEG

si = di = 0

cldでsi, diをインクリメントさせるようにする

rep movswをする(movsってmov seg?)

これにより,ds:si → es:diへのコピーを行う.cx = 0x100(256)でword指定なのでつまり,STAGE1_BUFFERSEGにコピーされた1セクタ分のデータをstage2_segmentにコピーしている.

さきほど退避させたレジスタをpop

 

stage2_addressへジャンプ

 

その後ろで記述されているコードはエラー用のコードだったりなので今回は省略とした.

 

 

参考

http://caspar.hazymoon.jp/OpenBSD/misc/hdd.html

ArchlinuxにIDA Demoを導入する

みんな大好きIDAくん、もともとWindowsで使用していたけれど基本的に解析するバイナリはELFファイルだしLinux環境に入れたほうが手間も省けるだろうというのが事の始まり

単純に入れられるかなと思ったら、主に2つの大きなエラーを抱えてしまったので全然そんなこともなかった為、備忘録を記す

OSディストリビューションはArchlinuxを使用

 

まずは公式サイトから"IDA Demo 6.95 for Linux"をダウンロードして展開

$ md5sum idademo695_linux.tgz

$ tar zxvf idademo695_linux.tgz

 では展開先に移動してさっそくIDA起動してみよう

$ ./idaq
./idaq: error while loading shared libraries: libgobject-2.0.so.0: cannot open shared object file: No such file or directory

エラー発生。ライブラリが足りないというもの。ならばidaqバイナリが必要としているものを導入してやる必要があるため、それらを調べることから。

$ ldd idaq | grep 'not found'
    libgobject-2.0.so.0 => not found
    libglib-2.0.so.0 => not found
    libX11.so.6 => not found
    libz.so.1 => not found
    libz.so.1 => not found
    libglib-2.0.so.0 => not found

結構足りなかった。それぞれ個別であるわけでもないので何に含まれているかも調べてあげる必要があるのでpkgfileで見てみる

と、思っていたがpkgfileが入っていなかったのでこれを導入してから

$ sudo pacman -S pkgfile
$ sudo pkgfile --update
$ pkgfile libgobject-2.0.so.0
core/glib2
multilib/lib32-glib2

IDAくんは32bit環境での話となるのでmultilibの方を参照すれば十分であると判断した

$ sudo pacman -S lib32-glib2 

あとは同様にnot foundがなくなるまで同じことを繰り返せば無事起動できるはず

$ ldd idaq | grep 'not found'
    libX11.so.6 => not found
$ pkgfile libX11.so.6
extra/libx11
multilib/lib32-libx11
archlinuxfr/insync
$ sudo pacman -S lib32-libx11

 

not foundがなくなったので嬉々としてもう一度起動

$ ./idaq
This application failed to start because it could not find or load the Qt platform plugin "xcb"
in "".

Available platform plugins are: linuxfb, minimal, xcb.

Reinstalling the application may fix this problem.
中止 (コアダンプ)

なんてこった...起動しないではないか...今度はQt周りでコケているように見受けられる

今度はQt本体に何か足りないものがあるのか?と思って調べてみた

$ ldd /usr/lib/qt/plugins/platforms/libqxcb.so | grep not
$

なにも起きなかった。ここからしばらく迷走をした後libqxcb.soを試しに探してみたところ

$ sudo find / -name libqxcb.so
/home/User/Downloads/idademo695/plugins/platforms/libqxcb.so
/usr/lib/qt/plugins/platforms/libqxcb.so

 2つ該当することが判明した。

自分が勝手に「同名ダイナミックリンクライブラリは1つしか存在しない」という思い込みをしていたことによるもので...一つ学んだ...

ダウンロードした方のディレクトリにあるlibqxcb.soを調べる

 $ ldd libqxcb.so | grep not
    libSM.so.6 => not found
    libICE.so.6 => not found
    libfontconfig.so.1 => not found
    libfreetype.so.6 => not found

 足りない!足りていないぞ!!とテンションが上がったところで先と同じように補っていく

$ pkgfile libSM.so.6
extra/libsm
multilib/lib32-libsm
archlinuxfr/insync
$ sudo pacman -S lib32-libsm

$ ldd libqxcb.so | grep not
    libfontconfig.so.1 => not found
    libfreetype.so.6 => not found
$ pkgfile libfontconfig.so.1
extra/fontconfig
multilib/lib32-fontconfig
archlinuxfr/insync
$ sudo pacman -S lib32-fontconfig

そしてnot foundがなくなったところで3度目の正直をしたところ、

f:id:lcstmarck:20170212162936p:plain

 

起動したーーーー!!これでArchlinuxでも静的バイナリ解析ができるようになった!

IDAの導入というよりかは、lddとpkgfileが想像以上に活躍したので忘れないでおきたいところ