linuxカーネル読書がしたい(ブート編3)
前回に引き続き読書を続けて行く.main.cの読書の続きから
main.c(リアルモードでのカーネルコードモジュール)
・0ページ目にブートヘッダをコピー
・early-bootコンソールの初期化(コンソールの準備)
・ヒープの初期化
・CPUの検証
↓このページはここから
・BIOSをどのCPUモードで使うかの設定
・メモリレイアウトの検出
・キーボードの初期化
・(もし定義されていれば)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とは何かとなった.
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)
まずはセクタサイズを確認する.対象の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を返す.
===== read_mbr_sig ここまで =====
read_mbr_sigから-1が返ったら,boot_params.edd_mbr_sig_buf_entries = devno-0x80+1
===== query_edd ここまで =====
参考
http://dev.ariel-networks.com/Members/ohyama/30fc30c830ed30fc30fc-305d306e4/
http://www.yoshinobrain.com/200008-bootup.pdf
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はデジタルデータをアナログデータに変換し,アナログ回線でシリアル転送する際に用いられる単位
自分でビルドした任意のライブラリにリンクさせる
今回はお試しで短期的に使うレベルの話なので長期的に使用することは考慮していない.
デフォルトでインストールされているライブラリではなく,自分でソースコードからビルドしたライブラリにリンクさせたい.
振り返ると大したことはしていないが,すぐに忘れそうなので備忘録.
きっかけは,/usr/lib64に入っているライブラリのバージョンが古かったので新しいバージョンのライブラリをちょっと使いたかったというもの.そしてついでにいろんなバージョンも試そうと思った.
対象はGNU MPライブラリ.
自分のワークディレクトリに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
6
セットアップシグネチャ(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
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へジャンプ
その後ろで記述されているコードはエラー用のコードだったりなので今回は省略とした.
参考
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度目の正直をしたところ、
起動したーーーー!!これでArchlinuxでも静的バイナリ解析ができるようになった!
IDAの導入というよりかは、lddとpkgfileが想像以上に活躍したので忘れないでおきたいところ