ソフトウェアシステム論でxv6読み会をした時のメモ(2018/04/16)

xv6のdebug †

  • qemu
  • /mnt/dalmore-home/one/src/xv6-rpi/src
    cd /mnt/dalmore-home/one/src/xv6-rpi/src
    /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
    qemu再起動
    make clean;make -f makefile-armclang qemu-debug
    CtrlA-Xで抜けれる

Boot Sequence

Boot

        Kernel load
Page    Table(Boot用のpagetableの設定)
initialize(memory / IO / process / file)
ini process
  • Kernel load
    • x86ならEFIがある
    • ARMだとVersatile(fimware)
  • pagingを設定しなければ動かない
  • Boot用のpage table の設定ができた後一連の初期化を行う
    • memory
    • I/O
    • process
    • file
  • 初期化が終わった後
    • init process(process番号1番)
    • init process は linux だと rc.d/~ の下にある

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に値を返す)
  • 10行目 VM の説明
    • lsは自分で仮装メモリにアクセスしていくので memory allocation をやる
    • malloc は User library だが memory の要求は System でやらなきゃいけない
    • break とい System --call で memory を増やす
    • memory を増やすと OS は最初に ls のバイナリを生成
    • すると、break で取った領域が別にできる
    • breakで 取った memory が全部 リアル memory に割り当てられるわけではない
    • いくつかは仮想メモリに行く
    • memory にアクセスした時に仮装メモリだったらtrapしてmemoryを割り当てる
    • memoryが割り当てられなければ、他の実メモリを追い出して書き換える
    • VM関係の一連の処理がある
  • fork
    • forkすると process structure ができる
    • process自体は active や waitの状態を持っている
    • 複数のactiveがあると順に実行していく(scheduler)
  • kernel の要素
    • process management
    • scheduler
    • file IO
    • Virtual Memory

この4つの Kernel の要素を読んでいく

X.v6 の trace

  • arm 用の gdb で kernel.elf を開く
  • dalmoreに入って
    cd /mnt/dalmore-home/one/src/xv6-rpi/src
    /net/open/Linux/arm/gcc-arm-none-eabi-7-2017-q4-major/bin/arm-none-eabi-gdb kernel .elf

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
  • -M versatilepb
    • 仮想メモリにこの firmwere を使うという指示
    • versarilepb は Raspi(ARM) のfirmwereの一種
  • -m 128
    • memory の量
  • -cpu ${QEMUCPU}
    • cpu の種類
    • アーキテクチャによって命令違う
    • armのxv6を作る時のcompile時にcpuに教えるcpuの種類と合わせる必要がある
    • 名前がqemu側とcompile側のcpuの名前が違うので試行錯誤して合わせる
  • -s -S
    • Boot時に debugger が接続するまで止めるようにする
** 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
  • memory に直接 load できる値は 長い値だとmemory一旦置かないといけない
  • 一番最初に呼び出すのは C で書かれたファイルじゃなくてアセンブラ
    • entry.S で行なっている

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.
  • B . まで行くとそこを永遠とループする
    • ARM fault持ってるので Bではなくfaultにすべき

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
  • initcode
  • swtch
  • flush_tlb
    • userスペースにだけflush する(systemにはしない)
  • fetchint
    • kernel 側から指定されたプロセスのアドレスから integer を取ってくるサブルーチン
  • trapret
    • 特権モードを抜ける
  • coppyout
    • current page table に乗ってたら memcopy でできるけど、乗ってないからcopyout必要

exec

  • xv6-rpi/src から grep で exec を探す
    • exec.c
    • sysecxec の2つがある sysファイルの中で exec 呼んでるので exec から読む
  • gdb側
$ /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でかけない
  • src 側
$ 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側
(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 スペースに切り替えたから信用はできない
  • src 側
$ 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側
(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   
  • src側
$ vi initcode.S
start:
   LDR     r1, =init
   LDR     r2, =argv
   MOV     r0, #SYS_exec
   SWI     0x00
// これが一番最初に動くコード
  • gdb側
(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"
// 確認
  • src側(initcode.S)
 9   LDR     r1, =init
// user 空間で コンパイルされている
// kernel 空間はもっと高いアドレスにあるはず
  • gdb
(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 が動く

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-06-05 (火) 17:40:36 (2153d)