技術

[xv6 #18] Chapter 1 – The first process – Code: exec

テキストの27〜29ページ

本文

システムコールが到達したとき(第2章でどうやってそれが起きるか説明する)、syscallはsyscallテーブルを参照しsys_execを呼び出す。
sys_execはシステムコールの引数を解釈し、execを呼び出す。

syscall.cのsyscall関数とテーブル

static int (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
};

void
syscall(void)
{
  int num;

  num = proc->tf->eax;
  if(num >= 0 && num < SYS_open && syscalls&#91;num&#93;) {
    proc->tf->eax = syscalls[num]();
  } else if (num >= SYS_open && num < NELEM(syscalls) && syscalls&#91;num&#93;) {
    proc->tf->eax = syscalls[num]();
  } else {
    cprintf("%d %s: unknown sys call %d\n",
            proc->pid, proc->name, num);
    proc->tf->eax = -1;
  }
}

sysfile.cのsys_exec関数

int
sys_exec(void)
{
  char *path, *argv[MAXARG];
  int i;
  uint uargv, uarg;

  if(argstr(0, &path) < 0 || argint(1, (int*)&uargv) < 0){
    return -1;
  }
  memset(argv, 0, sizeof(argv));
  for(i=0;; i++){
    if(i >= NELEM(argv))
      return -1;
    if(fetchint(proc, uargv+4*i, (int*)&uarg) < 0)
      return -1;
    if(uarg == 0){
      argv&#91;i&#93; = 0;
      break;
    }
    if(fetchstr(proc, uarg, &argv&#91;i&#93;) < 0)
      return -1;
  }
  return exec(path, argv);
}&#91;/sourcecode&#93;

execはnameiを使ってpathで名付けられたバイナリを開き(その辺の詳細は第5章で説明する)、そしてそのELFヘッダを読む。
xv6のアプリケーションは、広く使われてるELFフォーマットで表現されていて、それはelf.hで定義されている。
ELFバイナリは、ELFヘッダ(elfhdr構造体)から成り、その後にプログラムセクションヘッダの連なり(proghdr構造体)が続く。
どのproghdrも、メモリに読み込まれるべきアプリケーションの一部を記述している。
xv6のプログラムは一つだけしかプログラムセクションヘッダを持たないが、他のシステムでは命令とデータのための分割された一部を持つだろう。

exec.cの全て
&#91;sourcecode language="c"&#93;#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "defs.h"
#include "x86.h"
#include "elf.h"

int
exec(char *path, char **argv)
{
  char *s, *last;
  int i, off;
  uint argc, sz, sp, ustack&#91;3+MAXARG+1&#93;;
  struct elfhdr elf;
  struct inode *ip;
  struct proghdr ph;
  pde_t *pgdir, *oldpgdir;

  if((ip = namei(path)) == 0)
    return -1;
  ilock(ip);
  pgdir = 0;

  // Check ELF header
  if(readi(ip, (char*)&elf, 0, sizeof(elf)) < sizeof(elf))
    goto bad;
  if(elf.magic != ELF_MAGIC)
    goto bad;

  if((pgdir = setupkvm(kalloc)) == 0)
    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((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
      goto bad;
  }
  iunlockput(ip);
  ip = 0;

  // Allocate two pages at the next page boundary.
  // Make the first inaccessible.  Use the second as the user stack.
  sz = PGROUNDUP(sz);
  if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
  clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
  sp = sz;

  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv&#91;argc&#93;; argc++) {
    if(argc >= MAXARG)
      goto bad;
    sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
    if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
      goto bad;
    ustack&#91;3+argc&#93; = sp;
  }
  ustack&#91;3+argc&#93; = 0;

  ustack&#91;0&#93; = 0xffffffff;  // fake return PC
  ustack&#91;1&#93; = argc;
  ustack&#91;2&#93; = sp - (argc+1)*4;  // argv pointer

  sp -= (3+argc+1) * 4;
  if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
    goto bad;

  // Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1;
  safestrcpy(proc->name, last, sizeof(proc->name));

  // Commit to the user image.
  oldpgdir = proc->pgdir;
  proc->pgdir = pgdir;
  proc->sz = sz;
  proc->tf->eip = elf.entry;  // main
  proc->tf->esp = sp;
  switchuvm(proc);
  freevm(oldpgdir);
  return 0;

 bad:
  if(pgdir)
    freevm(pgdir);
  if(ip)
    iunlockput(ip);
  return -1;
}

elf.hの全て

// Format of an ELF executable file

#define ELF_MAGIC 0x464C457FU  // "\x7FELF" in little endian

// File header
struct elfhdr {
  uint magic;  // must equal ELF_MAGIC
  uchar elf[12];
  ushort type;
  ushort machine;
  uint version;
  uint entry;
  uint phoff;
  uint shoff;
  uint flags;
  ushort ehsize;
  ushort phentsize;
  ushort phnum;
  ushort shentsize;
  ushort shnum;
  ushort shstrndx;
};

// Program section header
struct proghdr {
  uint type;
  uint off;
  uint vaddr;
  uint paddr;
  uint filesz;
  uint memsz;
  uint flags;
  uint align;
};

// Values for Proghdr type
#define ELF_PROG_LOAD           1

// Flag bits for Proghdr flags
#define ELF_PROG_FLAG_EXEC      1
#define ELF_PROG_FLAG_WRITE     2
#define ELF_PROG_FLAG_READ      4

最初の一歩は、ファイルがELFバイナリを含むかどうかの簡易的なチェックである。
ELFバイナリは、4バイトのマジックナンバー0x7F, ‘E’, ‘L’, ‘F’(elf.hに定義されてるELF_MAGIC)で始まる。
もしELFヘッダが正しいマジックナンバーを持つなら、execはそのバイナリを正しいという事にする。

execが、setupkvmでユーザ用のマッピング無しで新しいページテーブルを割り当てると、allocuvmでそれぞれのELFセグメントのためのメモリを割り当て、そしてloaduvmを使ってそれぞれのセグメントをメモリに読み込む。
/initのためのプログラムセクションヘッダは次のようになる。

# objdump -p _init
_init:     file format elf32-i386
Program Header:
LOAD off 0x00000054 vaddr 0x00000000 paddr 0x00000000 align 2**2
              filesz 0x000008c0 memsz 0x000008cc flags rwx

allocuvmは要求された仮想アドレスがKERNBASE未満かどうかをチェックする。
loaduvmはELFセグメントのページを書き込むための割り当て済み物理メモリをwalkpgdirを使って探し、readiを使ってファイルから読み込む。

vm.cのallocuvmとloaduvm

// Load a program segment into pgdir.  addr must be page-aligned
// and the pages from addr to addr+sz must already be mapped.
int
loaduvm(pde_t *pgdir, char *addr, struct inode *ip, uint offset, uint sz)
{
  uint i, pa, n;
  pte_t *pte;

  if((uint) addr % PGSIZE != 0)
    panic("loaduvm: addr must be page aligned");
  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walkpgdir(pgdir, addr+i, 0)) == 0)
      panic("loaduvm: address should exist");
    pa = PTE_ADDR(*pte);
    if(sz - i < PGSIZE)
      n = sz - i;
    else
      n = PGSIZE;
    if(readi(ip, p2v(pa), offset+i, n) != n)
      return -1;
  }
  return 0;
}

// Allocate page tables and physical memory to grow process from oldsz to
// newsz, which need not be page aligned.  Returns new size or 0 on error.
int
allocuvm(pde_t *pgdir, uint oldsz, uint newsz)
{
  char *mem;
  uint a;

  if(newsz >= KERNBASE)
    return 0;
  if(newsz < oldsz)
    return oldsz;

  a = PGROUNDUP(oldsz);
  for(; a < newsz; a += PGSIZE){
    mem = kalloc();
    if(mem == 0){
      cprintf("allocuvm out of memory\n");
      deallocuvm(pgdir, newsz, oldsz);
      return 0;
    }
    memset(mem, 0, PGSIZE);
    mappages(pgdir, (char*)a, PGSIZE, v2p(mem), PTE_W|PTE_U, kalloc);
  }
  return newsz;
}&#91;/sourcecode&#93;

プログラムセクションヘッダのfileszはmemszより多分小さくなり、その差はファイルから読み込まれる代わりにゼロで埋められていなければならない(Cのグローバル変数のため)。
/initの場合、fileszは2240バイトでmemszは2252バイトであり、allocuvmは2252バイトを保持するのに十分な物理メモリを割り当てるが、/initファイルから読み込まれるのは2240バイトのみである。
(fileszやmemszというのはELFバイナリの中のプログラムセクションヘッダごとに定義されてる値)

そしたら、execは1ページ分だけユーザスタックを割り当て初期化する。
そして、プログラムが1ページより多く使おうとしたら失敗するようにするために、そのスタック用のページの下位にアクセス出来ないページを一つ配置する。
このアクセス出来ないページは、長すぎる引数にexecが対処することもまた可能にする。
そういうとき(長すぎる引数が渡されたとき)は、execが引数をスタックにコピーするために使ってるcopyout関数が、コピー先のページがアクセス不能である事を検知し、-1が返る。

execは、一度に一つずつスタックのトップに引数の文字列をコピーし、そこに対するポインタをustackに記録する。
そして、mainに渡されるargvのリストになるであろうものの最後にヌルポインタを置く。
最初の3つのustackの項目は、argvとargcのポインタとPCへの偽のリターン(fake return PC)である。

新しいメモリイー目地を用意している間、もしexecがプログラムセグメントの異常のようなエラーを検出したら、badラベルにジャンプし、その新しいイメージを解放し、-1を返す。
execは、古いイメージを解放するのをそれが確実になるまで待たなければならない。
もし古いイメージの解放が完了したら、-1を返す事はない。
execにおけるエラーは、イメージの生成をしてる間だけ起きる。
一度イメージが完了設楽、execは新しいイメージをインスールし古いイメージを解放することが出来る。
(exec関数のbadラベルの直前の部分。switchuvmとfreevm)
最後に、execは0を返す。
成功だ!

これで、initcodeは完了した。
execは、それを/initバイナリで置き換え、loaded out of the file system.(ここ分からない)
initは、必要なら新しいコンソールデバイスファイルを生成し、そしてそれをファイルディスクリプタ0,1,2として開く。
そしてループし、コンソールシェルを開始し、シェルが終了するまで親のないゾンビを制御し、繰り返す。
これでシステムは起動を完了した事になる。

init.cの全て
&#91;sourcecode language="c"&#93;// init: The initial user-level program

#include "types.h"
#include "stat.h"
#include "user.h"
#include "fcntl.h"

char *argv&#91;&#93; = { "sh", 0 };

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", 1, 1);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

  for(;;){
    printf(1, "init: starting sh\n");
    pid = fork();
    if(pid < 0){
      printf(1, "init: fork failed\n");
      exit();
    }
    if(pid == 0){
      exec("sh", argv);
      printf(1, "init: exec sh failed\n");
      exit();
    }
    while((wpid=wait()) >= 0 && wpid != pid)
      printf(1, "zombie!\n");
  }
}

システムが起動完了したとはいえ、xv6の重要なサブシステムをいくつか飛ばして説明してきた。
次の章では、int $T_SYSCALLによって引き起こされる割り込みシステムコールを制御させるためにx86ハードウェアをどうやってxv6が設定するかについて説明する。
残りの章では、プロセスの管理とファイルシステムについて説明する。

感想

exec中心の話です。
最初のプロセスの起動にもexecを使うんですね。
もちろんこれまでの節で、最初のプロセスといえどなるべく通常のプロセスと同じように標準のシステムコールを利用しているという感じの説明があったのでそう驚くようなことではないかもしれません。

途中の説明で古いイメージの解放の部分に一瞬おや?となりましたが、そもそもexecはファイルディスクリプタ部分などを残して元のイメージを新しいイメージで置き換えるのが通常の動作です。
(なので通常使うときはその前にforkする必要がありましたね)
解放の話が入ってくるのも当然ですね。

この章の説明部分は終わりですが、あとReal worldという節とExercisesという節が残ってます。
一応全部やるつもりですがExercisesは、やろうと思えばいくらでも掘り下げれそうな気がするので、サラッと程度にしとくつもりです。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください