loader
まだ雪が降る。雪が止むと散歩に出るんだけど、農道についたわだち。その間は処女雪。気持ちよく散歩の足跡を残す。
振り返ってみたら、随分とがに股歩きになってるな。これは矯正しなければ。。。出来れば、モデル歩きぐらいしたいぞ。(って、どういう歩きなんだろう)
モナコ在住のあの人に聞いてみればいいのかな? それより
【正しい歩き方・後ろ歩き体操と土踏まずのアーチを作る体操 】
かかと着地はダメなんか。陸王の足袋靴の話を思い出せ。それと、大股歩きだな。
boot loader
前回からやってるxv6の勉強。boot loaderは分かった積りになってた。補強のために、下記も 見ている。
OSの起動に必要な「ブートローダー」を自作してみよう (3/3)
Linux Insides : カーネル起動プロセス part1
でも、まあ少しは手を動かせよって事で、loader部分だけを抜き出して、どんな風にデータが メモリーに配置されるか表示するようにしてみた。ええ、bochsなりでアセンブルコードを 追いかけるって手はありますけどね。出来たら避けたい糞石コードです。
// Boot loader. typedef unsigned int uint; typedef unsigned short ushort; typedef unsigned char uchar; #include "elf.h" #include <stdio.h> void stosb(void *addr, int data, int cnt){ // padding data at addr count cnt printf(" adr=%x cnt=%x\n", addr, cnt); } void bootmain(FILE* fd) { struct elfhdr *elf; struct proghdr *ph, *eph; uchar* pa; uchar buf[4096]; elf = (struct elfhdr*)buf; // readseg((uchar*)elf, 4096, 0); fread((uchar*)elf, 1, 4096, fd); // Is this an ELF executable? if(elf->magic != ELF_MAGIC) return; // let bootasm.S handle error // Load each program segment (ignores ph flags). ph = (struct proghdr*)((uchar*)elf + elf->phoff); eph = ph + elf->phnum; for(; ph < eph; ph++){ pa = (uchar*)ph->paddr; // readseg(pa, ph->filesz, ph->off); printf("adr=%x size=%x offset=%x\n", pa, ph->filesz, ph->off); if(ph->memsz > ph->filesz) stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz); } printf("start=%x\n", elf->entry); } int main(){ FILE* fd; fd = fopen("xv6.img", "rb"); fseek(fd, 512, SEEK_SET); // skip MBR bootmain(fd); fclose(fd); }
bootmain.cを土台にちょっとDISKに読みに行くコードは、読んだ積りでprintfにしました。 stobsって糞石のアセンブルになってるんですけど、比較的よく使われる文字移動用スペシャル命令なのかな。CISC丸出しだと思うぞ。このstobsでZEROクリアしてるエリアって、BSS領域なんだろうね。
新たに追加したのはmain()。特徴的なのは、seekしてMBR相当をすっ飛ばすようにしてる事 ぐらいかなあ。それとelf.hは既存のやつを流用。教育用コードって事なのに、何もコメントが 入っていない。
しょうがないので、OpenBSDから、/sys/sys/exec_elf.hを引っ張ってきて載せておくか。
/* ELF Header */ typedef struct elfhdr { unsigned char e_ident[EI_NIDENT]; /* ELF Identification */ Elf32_Half e_type; /* object file type */ Elf32_Half e_machine; /* machine */ Elf32_Word e_version; /* object file version */ Elf32_Addr e_entry; /* virtual entry point */ Elf32_Off e_phoff; /* program header table offset */ Elf32_Off e_shoff; /* section header table offset */ Elf32_Word e_flags; /* processor-specific flags */ Elf32_Half e_ehsize; /* ELF header size */ Elf32_Half e_phentsize; /* program header entry size */ Elf32_Half e_phnum; /* number of program header entries */ Elf32_Half e_shentsize; /* section header entry size */ Elf32_Half e_shnum; /* number of section header entries */ Elf32_Half e_shstrndx; /* section header table's "section header string table" entry offset */ } Elf32_Ehdr; /* Program Header */ typedef struct { Elf32_Word p_type; /* segment type */ Elf32_Off p_offset; /* segment offset */ Elf32_Addr p_vaddr; /* virtual address of segment */ Elf32_Addr p_paddr; /* physical address - ignored? */ Elf32_Word p_filesz; /* number of bytes in file for seg. */ Elf32_Word p_memsz; /* number of bytes in mem. for seg. */ Elf32_Word p_flags; /* flags */ Elf32_Word p_align; /* memory alignment */ } Elf32_Phdr;
64Bit用も有るけど、取り合えず32Bit用の方ね。
講釈はこれぐらいにして、動かしてみる。まずはちゃんと動くdebianのやつ。
$ ./a.out adr=100000 size=a516 offset=1000 adr=10a516 cnt=af92 adr=0 size=0 offset=0 start=10000c
こちらは、openbsdのやつ。
$ ./a.out adr=0 size=e0 offset=34 adr=801080c8 size=13 offset=90c8 adr=100000 size=7769 offset=1000 adr=80107780 size=4060 offset=8780 adr=8010b7e0 cnt=af88 adr=80116768 size=9f0 offset=d768 adr=8010b610 size=80 offset=c610 adr=0 size=0 offset=0 start=10000c
明らかに違うねぇ。正解をreadelfを使って確かめてみる。xv6.img = MBR + kernel って図式になってるんで、kernelなら素直にreadelfにかけられる。
$ readelf -l debian/kernel Elf file type is EXEC (Executable file) Entry point 0x10000c There are 2 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x001000 0x80100000 0x00100000 0x0a516 0x154a8 RWE 0x1000 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10 Section to Segment mapping: Segment Sections... 00 .text .rodata .stab .stabstr .data .bss 01
$ readelf -l openbsd/kernel Elf file type is DYN (Shared object file) Entry point 0x10000c There are 7 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00001034 0x00000000 0x000e0 0x000e0 R E 0x4 INTERP 0x0090c8 0x801080c8 0x801080c8 0x00013 0x00013 R 0x1 [Requesting program interpreter: /usr/lib/libc.so.1] LOAD 0x001000 0x80100000 0x00100000 0x07769 0x07769 R E 0x1000 LOAD 0x008780 0x80107780 0x80107780 0x04060 0x0efe8 RW 0x1000 LOAD 0x00d768 0x80116768 0x80116768 0x009f0 0x009f0 R 0x1000 DYNAMIC 0x00c610 0x8010b610 0x8010b610 0x00080 0x00080 RW 0x4 NULL 0x000000 0x00000000 0x00000000 0x00000 0x00000 0 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .text 03 .rodata .interp .dynsym .dynstr .hash .stab .stabstr .data .dynamic .got .got.plt .data.rel.ro.local .data.rel.ro .data.rel .bss 04 .rel.dyn 05 .dynamic 06
Index02にtextが有って、Index03にdataが配置されてたな。そしてそのアドレスは、80107780って、とんでもない所になってる。これじゃ初期PDEが読めないわな。こんな配置になるのは、OpenBSD特有の仕様かしらん?
汎用化
パクッて来たコードをちょいと変更して、汎用化してみる。コマンドラインから、ファイル名を 受け付けるようにしただけだれど。readelfのプログラム・セグメントを表示出来るものだな。
int main(int argc, char *argv[]){ FILE* fd; fd = fopen(argv[1], "rb"); bootmain(fd); fclose(fd); }
deb9:debian$ ./a.out /bin/ls start=5430
ばぐってるな。3秒考えて原因が分かった。elf.hが32Bit用になってるからだろう。それを64Bitのアプリに適用しようとしても無理が有るんだな。ならば、逆に32Bitの環境なら動くんじゃねぇ。32Bit時代のウブが有るんで、そこで試してみる。
sakae@ub:/tmp$ ./a.out /bin/ls adr=8048034 size=120 offset=34 adr=8048154 size=13 offset=154 adr=8048000 size=1e40c offset=0 adr=8067f00 size=444 offset=1ef00 adr=8068344 cnt=c34 adr=8067f0c size=f0 offset=1ef0c adr=8048168 size=44 offset=168 adr=8060b74 size=814 offset=18b74 adr=0 size=0 offset=0 adr=8067f00 size=100 offset=1ef00 start=804bee9
sakae@ub:/tmp$ readelf -l /bin/ls Elf file type is EXEC (Executable file) Entry point 0x804bee9 There are 9 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x1e40c 0x1e40c R E 0x1000 LOAD 0x01ef00 0x08067f00 0x08067f00 0x00444 0x01078 RW 0x1000 DYNAMIC 0x01ef0c 0x08067f0c 0x08067f0c 0x000f0 0x000f0 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x018b74 0x08060b74 0x08060b74 0x00814 0x00814 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x01ef00 0x08067f00 0x08067f00 0x00100 0x00100 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got
もう一つの loader
上でみたのは、カーネルをメモリーに配置するためのローダーだ。OSと言うかカーネルが動き出すと、今度は色々なアプリ(大きい所では、firefoxとか、小さいやつだとgrepとか)を呼び出す事になる。 その機能はカーネルが持っている。
シェルを通じてgrepしたり。そのためにはgrepのアプリと言うかコマンドを、メモリーに載せる 事が必要。勿論、システムコールを通じてね。
exec.cがそれを担当する。
// Check ELF header if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf)) goto bad; if(elf.magic != ELF_MAGIC) goto bad; : // Load program into memory. sz = 0; for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){ if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph)) goto bad; if(ph.type != ELF_PROG_LOAD) continue; if(ph.memsz < ph.filesz) goto bad; if(ph.vaddr + ph.memsz < ph.vaddr) goto bad; if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0) goto bad; if(ph.vaddr % PGSIZE != 0) goto bad; if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0) goto bad; }
簡単そうなって事で、echoをやってみる。xv6上でのバイナリーは、_echo なんで、まずは素性の調査から。
deb9:debian$ ./a.out _echo adr=0 size=9d4 offset=80 adr=9d4 cnt=c adr=0 size=0 offset=0 start=0 deb9:debian$ readelf -l _echo Elf file type is EXEC (Executable file) Entry point 0x0 There are 2 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000080 0x00000000 0x00000000 0x009d4 0x009e0 RWE 0x10 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10 Section to Segment mapping: Segment Sections... 00 .text .rodata .eh_frame .bss 01 deb9:debian$ ls -l _echo -rwxr-xr-x 1 sakae sakae 12528 Feb 20 15:31 _echo deb9:debian$ size _echo text data bss dec hex filename 2511 0 12 2523 9db _echo
簡単なんで、グローバル変数は極少ない。
適当にBPを貼って、走らせてみる。
(gdb) bt #0 allocuvm (pgdir=0x8deeb000, oldsz=0, newsz=2528) at vm.c:223 #1 0x80100af9 in exec (path=0x1840 "echo", argv=0x8dfc6ed0) at exec.c:52 #2 0x80105380 in sys_exec () at sysfile.c:420 #3 0x80104837 in syscall () at syscall.c:139 #4 0x801058b9 in trap (tf=0x8dfc6fb4) at trap.c:43 #5 0x8010561f in alltraps () at trapasm.S:20 #6 0x8dfc6fb4 in ?? ()
メモリー頂戴のおねだり。boot loaderの時は、そんなおねだりもなく(メモリーは十分に有るという前提ですから)、我が物顔にメモリーを使ってたけど、今度はそうはいかない。
(gdb) bt #0 loaduvm (pgdir=0x8deeb000, addr=0x0, ip=0x80110b34 <icache+340>, offset=128, sz=2516) at vm.c:199 #1 0x80100b2f in exec (path=0x1840 "echo", argv=0x8dfc6ed0) at exec.c:56 #2 0x80105380 in sys_exec () at sysfile.c:420 :
メモリーが貰えたら、そこにバイナリーコードを埋め込むとな。簡略のチェックプログラムでは、バイナリーコードをポイントする変数としてファイルポインターだった。今回は、DISKから バイナリーがキャッシュに読み込まれているので、キャッシュ上のポインター ip が、その任を負っている。
本物のOSのやつ(loader)
最初にFreeBSDのカーネルを見たんだけど、ごちゃごちゃし過ぎです。ソースを見るならOpenBSDに限るって事で、/sys/kern/kern_exec.cあたりを見る。
execでは、いわゆるシェバングやバイナリーコードも実行する必要があったり、ロードの失敗に 備えて、巻き戻し(実行されなかった事にする)の準備も必要って事で、専用のエミュレーターが用意されてる。sys_execveの中。
/* create the new process's VM space by running the vmcmds */ #ifdef DIAGNOSTIC if (pack.ep_vmcmds.evs_used == 0) panic("execve: no vmcmds"); #endif error = exec_process_vmcmds(p, &pack); /* if an error happened, deallocate and punt */ if (error) goto exec_abort;
実行の主体は /sys/kern/exec_elf.cの中だ(バイナリーな場合)。この中に、elf_load_file関数がある。 この中で、ごちゃごちゃやってるのは分かるんだけど、候補が沢山あって絞り込めないな。
こういう時は実際に走らせて、追ってみるのが一番だろうね。 OpenBSD on QEMUあたりを思い出して、再び環境を作ってみるかな。