Notes

Do not read codes but patch binary.

xv6 code reading(2.exec編)

前回shellでexecが実行される部分までみたので次はexecの中(https://github.com/mit-pdos/xv6-public/blob/master/exec.c). 普通はforkを先に説明するべきなのかもしれないけど、後に回す。

前提としてexec自体はsystem callでそのざっくりとした処理の流れとしては、

  1. 引数として与えられたobject format(xv6の場合elf format)fileのprogram headerから各section毎の virtual addressのoffset,allocateするmemory size(text/data/bss)を抜き出す。

  2. elfからの情報を仮想メモリに配置した後、heap/stack領域を用意。

  3. 現在process構造体の情報の一部(name/page dir/trap frame/allocateされたsize)を書き換える。

以下詳細を見ていこう。

先ず、前提として、systemcallで与えられた引数のcheckから。linuxのexecveであれば、filename,argumentの他に環境変数へのpointerがregister(%ebx,%ecx,%edx)を経てtrapframeにあるので、それらを元にpath解決を行なっているはず。が、ここでは、環境変数がないので、filename,argumentのみを取得しておく。

elf format file(object format file)は,3つの異なるheaderからなる。それぞれ、

  1. elf header
  2. program header(長いので以下codeに倣ってphと表記)
  3. section header

で実際にexecで使うのは、1,2(3はlinkingで使う). というか1も2のoffsetを参照する為に使うだけで、実質的に2の中に入っている配列状に入っているsegment情報を取得する為に使う。 また、実際には、linkingを経て複数のsection headerがprogram headerに内方されている状態になっている.

因みに、ここは、kernel内なので、通常のsystemcallは使えない。 なので、 引数として与えられた実行fileの名前を解決する為にopenではなく、kernel内のnamei, inodeのアドレスから指定したbyteを読む為にreadではなくkernel内のreadiを使っている。

elfのph自体は固定サイズ(https://github.com/mit-pdos/xv6-public/blob/master/exec.c#L43)みたいで、sizeofで固定sizeのsegment数分だけ移動しながら全segmentを回る。

各segment情報をvalidateする条件分岐が、phが期待通りに固定サイズでしたってことの他に6個ある。

1.phのheaderのtypeがloadである。

if(ph.type != ELF_PROG_LOAD)
      continue;

2.segmentのmemory size > file size (file sizeはメモリ配置後に初期化されるbss領域を含まない為に,bss領域がある場合常にmemory sizeより小さい)

if(ph.memsz < ph.filesz)
      goto bad;

3.memory sizeが0じゃない。

if(ph.vaddr + ph.memsz < ph.vaddr)
      goto bad;

4.個々のprocessから参照されるpage directory entryにあるpage table entry(以下pte)を仮想開始アドレス(virtual address)からmemsize分だけ確保することが可能。

if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;

5.programのvirtual addressがちゃんとpageを確保する際の単位と合致している。

if(ph.vaddr % PGSIZE != 0)
      goto bad;

6.実際にsegment情報の中に書かれている情報を取り出し、先ほど確保したpteにloadする。

if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
      goto bad;

このcycleが1segment毎にあるので、複数segmentあれば、その分、別のpteが確保されload されるという仕組み。 各segmentが含んでいるtext,data,bssなどのsection情報はこの中に暗黙的に含まれているのであろう。

なお、実際にdynamic linkingが必要な場合は、elf header,その解析をするexecのprogramも対応して複雑となる。

ここで面白い点として,

  • elf内のvirtual addressの開始address(%eipのjump先)を変更することで、user空間のどこにでもprogramを置ける。 -> 例えば、一般的には、code/data/bss/heap/stackと配置されるのだけど、codeの配置領域を後ろに持ってきて、heapをvaddressの先頭から確保するみたいなこともできるはず。

  • elf header,program header自身のsizeは実はそれ自体で定義可能? -> 例えば、最小のelfを作りたい場合、普通に考えたら,elf header(52Byte),program header(32byte),そして、少々のcode領域(main関数にexit(1) syscallをlinkしたbinary), 16byte位(?テキト-)の合計100byte位が必要なのかなと思っていた.だけどheader自体ってそれ自身のheader内で見ない領域は結構あるので、サイズ的にexecを変更せずとも50byte以下になる気がする。と思ったのだけど、やはりfileからmemoryにallocateする段階で、固定headerを読み込まないとbinary列の意味(xxxbyte目に何を表すflagがあるか)が分からないので、やはり、基本となるサイズは確保しなければならないのか..ではなぜsize項目が存在?

pagingの話は長いので後に譲るとして、ここではproc構造体がpdeを保持し、それの切り替えにより仮想メモリのswitch(context switch) が実現すると外観だけ捉えて、とりあえずexecの処理をstepout的に進んでいこう。

まず、sz = PGROUNDUP(sz);で現在のallocateされたpteの先頭をpage size単位で四捨五入(切り上げ)して、2ページ程確保している。 1つのpageは、意図的にblankで置かれている。これはよく見るprocess memory layoutの図の内、bssの次に来るheap領域,或いは共有memoryの確保の為に開けているのであろうと推察。当然,stack領域も逆側から使用可能。

もう1つにuser stack(所謂stack領域)が配置されていく. allocateされた先頭のszが常に+されていたが、stack frameの先頭 = stack pointerとして以降、programに与えられる引数を確保した後、-に転じている点に注目。なお、processが保持するstack pointerの値は、stack frameの先頭を常に指し示す。

これは、当然stack pointerがallocateした方向と逆側に伸びていく為。

なお、pagedirはuser processの仮想メモリをpointしており、szは、user仮想メモリstack frame先頭アドレスを指している点に注意。 pageの実態はkernel内に存在しているのだが、少なくともexec.c内では、kernel内においてもあくまでuser側の仮想アドレスを操作している。

(このfileの)最後で、processのname(16byte),trap frameのaddress(4byte),allocated size(4byte),page dirのaddress(4byte),全てを書き換えるが、ここでproc構造体の内、それらの項目は、fork時に暫定的に親からそのまま受け継いでいるからだ。なぜかというと、

processの変更が一通り変更したら、この場合、強制的に、このprocessにcpuを割り当てて実行されるようになっている。