ソフトウェアシステム論でxv6読み会をした時のメモ(2018/04/16) †xv6のdebug † †
Boot Sequence †Boot †Kernel load Page Table(Boot用のpagetableの設定) initialize(memory / IO / process / file) ini process
ls 時の User と System †User Sys fork exec ls (lsを探す) opendir open ls (load ls (loadの後にmemory空間の初期化を行う) page table reset goto User Mode opendir system call (file IO) (VM) (filesystemを読んでまたUserに値を返す)
この4つの Kernel の要素を読んでいく X.v6 の trace †
qemuを立ち上げる 見たいのは Boot Sequence だから qemu-debug を使う make clean; make -f makefile-armclang qemu-debug makefile-armclang の qemu-debug 部分 †qemu-debug : kernel.elf @clear @echo "Press Ctrl-A and then X to terminate QEMU session\n" export QEMU_AUDIO_DRV=none ; $(QEMU) -M versatilepb -m 128 -cpu ${QEMUCPU } -nographic -singlestep -d exec,cpu,guest_errors -D qemu.log -kernel kernel.elf -s -S
** gdb (gdb)b _start # remote 接続 (gdb) target remote :1234 # bt: stackを参照して、stack上のfanction callの履歴を調べる (gdb)bt (gdb)info registarts (初期のレジスタ) r0 ~ r12 (ARMのレジスタは0~12) sp (stack pointer) lr (link regester) pc (program counter) cpsr (cpu statusのレジスタ) (gdb)disass # disassemblerの略 (gdb)x/20x $pc+48+4 0x100030 # entry.Sのr1,=edata_entryやend_entryが$pc+48に変わってる # pcは次の命令を指している 10030->10038だから48からさらに+4 (gdb)p (void*) edata_entry $1 = (void *0) 0x10588 <edata_entry> # 10588がレジスタのr1に入る (gdb)stepi (gdb)info registers r1 0x10588 66952 (gdb)stepi (gdb)info registers r1 0x10588 66952 r2 0x19000 102400 (gdb)define ni >x/1i $pc >x/1i $pc >end # こうするとentry.Sを1命令ずつ実行できる (gdb)si 0x0001000c in _start () => 0x1000c <_start+12>: cmp r1,r2 (gdb)si 0x0001000c in _start () => 0x1014c <_start+16>: cmp r1,{r3} (gdb)disass => 0x00010014 <*20>: blt 0x00010018 <*24>: msr # blt でループしてることがわかる (gdb)tb *0x00010018 (gdb)c # bltのループを抜ける (gdb)disass (gbd)info registers r1 0x19000 102400 r2 0x19000 102400 # r2の値になるまでr1をincrementすることでループを抜けてる事がわかる # 19000までmemoryがある # CPUのコントロール (gdb)si (gdb)info registers cpsr 0x600001d3 # entry.Sの MSRの行を実行 (gdb)si (gdb)info registers cpsr 0xd3 # CPUのコントロールレジスタを指定している # supervisorじゃないとアクセスできない # entry.Sの LDR の行を実行 (gdb) si (gdb) info registers sp 0x12000 # 0x0 -> 0x12000 # stack ponterが設定されたのでサブルーチンコールが使えるようになる # サブルーチンコール 戻り先をstackに覚えておいてそこに飛ぶ # lr(link register)に覚えさせる事で1回だけアクセスせずに飛ぶ事ができる # BL breach & link (gdb)si (gdb)info registers lr 0x10024 (gdb)disass _start 0x00010024 <+36>: b #startまできたのでここ以降はstart.c (gdb)l # cなので next が使える (gdb)next (gdb)next (gdb)s set_bootpgtbl(... lent=1048576 ...) (gdb)p (void*) len $2 = (void *) 0x100000 # 1048576 は16進数で100000
entry.S †_start: LDR r1, =edata_entry LDR r2, =end_entry MOV r3, #0x00 # BLT までをループ 1: CMP r1, r2 STMLTIA r1!, {r3} # {}はレジスタのリスト r1にセーブする # !は セーブした数だけr1を進める(++とかと一緒) # Memclear とかと一緒 BLT 1b # initialize stack pointers for svc modes # CPUのコントロールレジスタを指定 # cpsr 0xd3 MSR CPSR_cxsf, #(SVC_MODE|NO_INT) LDR sp, =svc_stktop BL start B.
start.c †166行目 void start (void) { uint32 vectbl; _puts("starting xv6 for ARM...\n"); // double map the low memory, required to enable paging // we do not map all the physical memory set_bootpgtbl(0, 0, INIT_KERNMAP, 0); set_bootpgtbl(KERNBASE, 0, INIT_KERNMAP, 0); 69行目 for (idx = 0; idx < len; idx++) { pde = (phy << PDE_SHIFT); if (!dev_mem) { // normal memory, make it kernel-only, cachable, bufferable pde |= (AP_KO << 10) |PE_CACHE | PE_BUF | KPDE_TYPE; // |(or) を使って足して行く } else { // device memory, make it non-cachable and non-bufferable pde |= (AP_KO << 10) | KPDE_TYPE; }
ソフトウェアシステム論でxv6読み会をした時のメモ(2018/05/27) †読んだとこ †
exec †
$ /net/open/Linux/arm/gcc-arm-none-eabi-7-2017-q4-major/bin/arm-none-eabi-gdb kernel.elf (gdb) b _start (gdb) target remote :1234 (gdb) l exec (gdb) b exec (gdb) c (gdb) bt #0 exec (path=0x1c "/init", argv=0x87fdeef0) at exec.c:16 #1 0x800272fc in sys_exec () at sysfile.c:473 #2 0x80026310 in syscall () at syscall.c:152 #3 0x800277d4 in swi_handler (r=0x87fdefb8) at trap.c:14 #4 0x80027634 in trap_swi () #5 0x00000010 in ?? () // 最初のexecはinit // trap_swi はソフトウェアインタラクト(armのsyscallの命令) (gdb) l start // l _start は アンセブラ 169 _puts("starting xv6 for ARM...\n"); // OS経由じゃなく直接 uart を叩いて出力している 183 set_bootpgtbl(VEC_TBL, 0, 1 << PDE_SHIFT, 0); 184 set_bootpgtbl(KERNBASE+DEVBASE, DEVBASE, DEV_MEM_SZ, 1); // 一番最初の page table を設定 186 load_pgtlb (kernel_pgtbl, user_pgtbl); // 仮想メモリで動くようになる // ただし、実アドレスと仮想アドレスが同じアドレスで走る 192 kmain (); // void kmain (void) { に飛ぶ (gdb) l kmain 29 init_vmm (); 30 kpt_freerange (align_up(&end, PT_SZ), vectbl); 31 kpt_freerange (vectbl + PT_SZ, P2V_WO(INIT_KERNMAP)); 32 paging_init (INIT_KERNMAP, PHYSTOP); // 仮想メモリマネージャ(vmm) を初期化 // freepage を仮想メモリ(vmm)に登録 // 次のkmem_init までが仮想メモリの初期化 37 trap_init (); // vector table and stacks for models // (ソフト|ハード)インタラクトの飛び先の vector table を初期化 38 pic_init (P2V(VIC_BASE)); // interrupt controller // 割り込みできるようにする 39 uart_enable_rx (); // interrupt for uart // ic(集積回路) を割り込めるようにする // ユーザーが入力した key は割り込みで kernel に伝えられる 40 consoleinit (); // console // uart が初期化されたので console が起動できる 50 sti (); // sti(Set Interrupt Flag) で 割り込みが許可される // uart_enableはic(集積回路)の許可 // cpu の割り込みを許可 52 userinit(); // init のプロセスをロードして入れてるだけ // 実行されるのは scheduler が起動されてから (gdb) l userinit 122 p = allocproc(); // process 構造体を取ってくる // kernel の alloc 125 if((p->pgdir = kpt_alloc()) == NULL) { 126 panic("userinit: out of memory?"); 127 } // kpt_alloc は kernelの page table の allocate // つまり process と process のメモリ空間を allocate 129 inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size ); // user の仮想メモリを allocate 136 p->tf->r14_svc = (uint)error_init; // trap flame(tf) を設定している // trap flame は processの実行に必要 // process の中に current directory 入っている // working directory は shell が持ってればいい 148 p->state = RUNNABLE; // OS の p->state と同じ // userinit() {ではprocess構造体をつくっただけ // process に接続しただけで実行されてない // 実際の実行は scheduler から行われる (gdb) b scheduler (gdb) c (gdb) l // この段階で sti(cpuの割り込み許可) されている swtch †(gdb) l swtchuvm 166 pushcli(); 177 popcli(); // pushcli と popcli の間で割り込みを禁止している 174 asm("MCR p15, 0, %[v], c2, c0, 0": :[v]"r" (val):); // MCR がpagetable を設定するアセンブラ 175 flush_tlb(); // userスペースにだけflush する(systemにはしない) // gcc llvm の syntax 154 asm("MCR p15, 0, %[r], c8, c7, 0" : :[r]"r" (val):); // asm も gdb 使えば追えるが大事なのは switch (gdb) l swtch // swtch はアセンブラなので出て来ない (gdb) l scheduler 346 swtch(&cpu->scheduler, proc->context); // swtch の引数はこの2つ $ grep swtch * // swtch はアセンブラなのでCでかけない
$ vi swtch.S zx swtch: STMFD r13!, {r4-r12, lr} // push svc r4-r12, lr to the stack # switch the stack STR r13, [r0] // save current sp to the old PCB (**old) // store register // r0に格納 // swtch の引数 cpu->scheduler がr0に入る MOV r13, r1 // load the next stack // swtch の引数 proc->context がr1に入る # load the new registers. pc_usr is not restored here because # LDMFD^ will switch mode if pc_usr is loaded. We just simply # pop it out as pc_usr is saved on the stack, and will be loaded # when we return from kernel to user space (swi or interrupt return) LDMFD r13!, {r4-r12, lr} // pop svc r4-r12, lr // ! マークは store を load したあと、r13の値を書き終わったとこに合わせる # return to the caller bx lr // bx が抜けるためのコードかも? // system call から戻るときはここは使わない // 特権モードかはgdbでは確認できないけど、registerの状態を見れば何かわかるかも
(gdb) si (gdb) x/1i $pc => 0x80025fd4 <swtch+16>: bx lr (gdb) info register cpsr 0x200000d3 536871123 // cpsr にcpu の値が入っている (gdb) si (gdb) info register 0x80025c98 in forkret () at proc.c:397 // forkret() に入る (gdb) info register // register の値は変わらない // register の記述は C ではかけない (gdb) l 395 // will swtch here. "Return" to user space. // ここで切り替わる 403 if (first) { // ここでif 文で分岐してるのはバグよけ // 本来は省略できる (gdb) n 0x80027634 in trapret () // n を 繰り返すと trapret に戻る trapret †(gdb) bt #0 0x80027634 in trapret () #1 0x8002077c in popcli () at arm.c:83 #2 0xe59f2024 in ?? () // popcli から trapret に来ている // user スペースに切り替えたから信用はできない
$ grep trapret * // trap_asm.S がある $ less trap_asm.S trapret: LDMFD r13, {sp, lr}^ // restore user mode sp and lr ADD r13, r13, #8 LDMFD r13!, {r14} // restore r14 LDMFD r13!, {r2} // restore spsr MSR spsr_cxsf, r2 // 特権モードを抜ける LDMFD r13!,{r0-r12, pc}^ // restore context and return // pc も r13 から取って来ている // trapret 全体が割り込みから抜けるルーチンになっている // user スペースに戻る
(gdb) b trapret (gdb) c (gdb) p *proc $2 = {sz = 4096, pgdir = 0x800dfc00, kstack = 0x87fde000 "", state = RUNNING, pid = 1, parent = 0x0, tf = 0x87fdefb8, context = 0x87fdef88, chan = 0x0, killed = 0, ofile = { 0x0 <repeats 16 times>}, cwd = 0x800ac508 <icache+52>, name = "initcode\000\000\000\000\000\000\000"} // プロセスの構造体 // アセンブラで書いたinitcode は中でexecされる (gdb) b exec (gdb) si (gdb) x/20i $pc => 0x4: ldr r2, [pc, #36] ; 0x30 0x8: mov r0, #7 0xc: svc 0x00000000 0x10: mov r0, #2 0x14: svc 0x00000000 0x18: b 0x10 0x1c: stmdbvs lr!, {r0, r1, r2, r3, r5, r8, r11, sp, lr}^ 0x20: andeq r0, r0, r4, ror r0 0x24: andeq r0, r0, r12, lsl r0 0x28: andeq r0, r0, r0 0x2c: andeq r0, r0, r12, lsl r0 0x30: andeq r0, r0, r4, lsr #32 0x34: andeq r0, r0, r0 0x38: andeq r0, r0, r0 0x3c: andeq r0, r0, r0 0x40: andeq r0, r0, r0 0x44: andeq r0, r0, r0 0x48: andeq r0, r0, r0 0x4c: andeq r0, r0, r0 0x50: andeq r0, r0, r0 // これが出るまで si と x/20i $pc 繰り返す // svc が software interact
$ vi initcode.S start: LDR r1, =init LDR r2, =argv MOV r0, #SYS_exec SWI 0x00 // これが一番最初に動くコード
(gdb) x/s $r1 0x1c: "/init" // $r1 で init にexecする (gdb) si 0x80027614 in trap_swi () // system call に切り替わった // trap したので interact の vector に入ったはず (gdb) x/20i $pc // user プロセスはスタートした状態でsystem に戻って来てる (gdb) si swi_handler (r=0x0) at trap.c:10 // ここにくるまで si (gdb) l // 割り込みはかかっている状態
(gdb) info register // init 探せない // initcode.asm は user 側には入ってない (gdb) s syscall () at syscall.c:147 147 num = proc->tf->r0; // proc の trapframe に色々入っている (gdb) p *proc->tf $4 = {sp_usr = 4096, lr_usr = 0, r14_svc = 16, spsr = 1610612816, r0 = 7, r1 = 28, r2 = 36, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0, r10 = 0, r11 = 0, r12 = 0, pc = 16} // こうすると register の値がみれる // r1 = 28 に initcode が入っている (gdb) x/s 28 0x1c: "/init" // 確認
9 LDR r1, =init // user 空間で コンパイルされている // kernel 空間はもっと高いアドレスにあるはず
(gdb) s 151 if((num > 0) && (num <= NELEM(syscalls)) && syscalls[num]) { // C のコードに移ってるので s でいける // syscalls[] 配列は ポインタ (gdb) s sys_exec () at sysfile.c:448 448 if(argstr(0, &path) < 0 || argint(1, (int*)&uargv) < 0){ // sys_exec まで来た // argv はポインタ (gdb) l 473 return exec(path, argv); // exec へ リターン fetchint †(gdb) l fetchint // kernel 側から指定されたプロセスのアドレスから integer を取ってくるサブルーチン 36 *pp = (char*)addr; // *だから userプロセスのアドレスを*pp に入れてる (gdb) s 27 if ((ip = namei(path)) == 0) { (gdb) p path $5 = 0x1c "/init" // nameiのパスはuser空間にある // 割り込みは解除してないのでkernelにはいかない (gdb) l 33 // Check ELF header 34 if (readi(ip, (char*) &elf, 0, sizeof(elf)) < sizeof(elf)) { 35 goto bad; 36 } // elf : linux のa.out // namei はファイルシステムだから 書かないとexec 動かない // 長いコードなのでここから先のテストはかなり難しい 78 if ((sz = allocuvm(pgdir, sz, sz + 2 * PTE_SZ)) == 0) { // exec した時の引数をプロセス空間と別のとこにコピってる // 再利用できない // 正しい場所にコピって実行する必要がある 82 clearpteu(pgdir, (char*) (sz - 2 * PTE_SZ)); // allocate してから クリアしていく // allocateuvm で物理アドレスは確保されてるが、page entry が切り替わってない(argvはuser空間に残っている) 94 if (copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0) { // copyoutでkernelにコピーしてる copyout †(gdb) l copyout // current page table に乗ってたら memcopy でできるけど、乗ってないからcopyoutする 414 pa0 = uva2ka(pgdir, (char*) va0); // uva2ka で allocate した物理メモリを計算 426 memmove(pa0 + (va - va0), buf, n); // uva2ka で計算したとこに memmoveする 66 if (loaduvm(pgdir, (char*) ph.vaddr, ip, ph.off, ph.filesz) < 0) { // allocate して読み込んでそれを loaduvmでコピってる 71 iunlockput(ip); // ip は namei から // exec の途中で書き換えられないように lockしてたのを解除 130 freevm(oldpgdir); // oldpgdir exec する前のもの // execで新しいバイナリで古いバイナリをかき潰すことはしない(再利用しない) // 新しく allocate してそこでloadしてる (gdb) b 131 Breakpoint 4 at 0x80022548: file exec.c, line 131. (gdb) c // user 空間に init が load された状態 (gdb) si 0x80026310 in syscall () at syscall.c:152 152 ret = syscalls[num](); // syscall から return される (gdb) l 157 // copyout されてない // さっきcopyout したのは新しいtrap frame に書き込まなきゃ行けなかったから // exec の場合はr0に入れちゃ行けない(argc,argvが入ってる) (gdb) n swi_handler (r=0x87fdefb8) at trap.c:15 15 if (proc->killed) (gdb) n Breakpoint 2, 0x80027634 in trapret () // trapret () はアセンブラで書かれてるので C で breakpoint かけないと行き過ぎる (gdb) si 0x00000004 in ?? () // trapret 抜けるまでsi (gdb) x/20i 0 //ここでようやく最初のinit が動く |