技術

[xv6 #25] Chapter 2 – Traps, interrupts, and drivers – Code: Assembly trap handlers

テキストの34〜36ページ

本文

xv6は、プロセッサにトラップを引き起こさせるようなint命令に遭遇したとき、目的に叶った何らかの処理を実行するために、x86のハードウェアをセットアップしなければならない。
x86は256個の個別の割り込みを扱うことが出来る。
割り込み0-31は、除算エラーやおかしなメモリアドレスにアクセスを試みたときのような、ソフトウェア例外のために定義されている。
xv6は、32個のハードウェア割り込みを32-63の領域に割り当てていて、64をシステムコール用の割り込みに割り当てている。

tvinit関数は、main関数から呼ばれ、idtテーブルの中の256個のエントリをセットアップする。
割り込みiは、vectors[i]の中のアドレスにあるコードによって制御される。
エントリポイントは全て違う。
なぜならxv6は割り込みハンドラに対してトラップ番号を提供しないからである。
(ここ訳があやしい。because the x86 provides does not provide the trap number to the interrupt handler.)
256個の個別のハンドラを使うということは、256個の場合を見分ける唯一の方法である。
(実際に割り込みハンドラが実行されるときにはそのハンドラからはトラップ番号を得る方法がないということかな。)

trap.cのtvinit関数付近

// Interrupt descriptor table (shared by all CPUs).
struct gatedesc idt[256];
extern uint vectors[];  // in vectors.S: array of 256 entry pointers
struct spinlock tickslock;
uint ticks;

void
tvinit(void)
{
  int i;

  for(i = 0; i < 256; i++)
    SETGATE(idt&#91;i&#93;, 0, SEG_KCODE<<3, vectors&#91;i&#93;, 0);
  SETGATE(idt&#91;T_SYSCALL&#93;, 1, SEG_KCODE<<3, vectors&#91;T_SYSCALL&#93;, DPL_USER);
  
  initlock(&tickslock, "time");
}&#91;/sourcecode&#93;

main.cのmain関数
&#91;sourcecode language="c"&#93;// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
main(void)
{
  kvmalloc();      // kernel page table
  mpinit();        // collect info about this machine
  lapicinit(mpbcpu());
  seginit();       // set up segments
  cprintf("\ncpu%d: starting xv6\n\n", cpu->id);
  picinit();       // interrupt controller
  ioapicinit();    // another interrupt controller
  consoleinit();   // I/O devices & their interrupts
  uartinit();      // serial port
  pinit();         // process table
  tvinit();        // trap vectors
  binit();         // buffer cache
  fileinit();      // file table
  iinit();         // inode cache
  ideinit();       // disk
  if(!ismp)
    timerinit();   // uniprocessor timer
  startothers();    // start other processors (must come before kinit)
  kinit();         // initialize memory allocator
  userinit();      // first user process  (must come after kinit)
  // Finish setting up this processor in mpmain.
  mpmain();
}

tvinitは、ユーザのシステムコールをトラップするためにだけにT_SYSCALLを制御する。
それは、2番目の引数に1という値を渡す事によって”trap”タイプのゲートを指定する。
トラップゲートはFLフラグをクリアせず、システムコールハンドラ中に他の割り込みを許可する。

カーネルもまた、ユーザプログラムが直接的なint命令によてトラップを生成できるよう、システムコールゲートの権限をDPL_USERにセットする。
xv6は、プロセスにint命令を使って他の割り込み(例えば、デバイスの割り込み)を引き起こすことを許可しない。
もしプロセスがそれを試みたら、一般保護違反に遭遇するだろう。
(そしたらvectors[13]に処理が移る)

ユーザモードからカーネルモードに保護レベルを変更したとき、ユーザプロセスのスタックを使わないようにすべきである。
なぜなら、その内容がおかしくなってしまうかもしれないからである。
ユーザプロセスは、そのプロセスのユーザメモリじゃない部分を指すようなアドレスを%espにセットするようなバグや悪意を持ってるかもしれない。
xv6は、ハードウェアがスタックセグメントセレクタと%espのための新しい値を読み込む事を通して、タスクセグメントディスクリプタをセットアップする事によって、トラップ中にスタック切り替えを実行させるよう、x86ハードウェアをプログラムする。
switchuvm関数は、ユーザプロセスのカーネルスタックのトップのアドレスを、タスクセグメントディスクリプタに保存する。

vm.cのswitchuvm関数

// Switch TSS and h/w page table to correspond to process p.
void
switchuvm(struct proc *p)
{
  pushcli();
  cpu->gdt[SEG_TSS] = SEG16(STS_T32A, &cpu->ts, sizeof(cpu->ts)-1, 0);
  cpu->gdt[SEG_TSS].s = 0;
  cpu->ts.ss0 = SEG_KDATA << 3;
  cpu->ts.esp0 = (uint)proc->kstack + KSTACKSIZE;
  ltr(SEG_TSS << 3);
  if(p->pgdir == 0)
    panic("switchuvm: no pgdir");
  lcr3(v2p(p->pgdir));  // switch to new address space
  popcli();
}

トラップが発生したとき、プロセッサのハードウェアは以下の事を行う。
もしプロセッサがユーザモードで実行中だったら、タスクセグメントディスクリプタから%espと%ssをロードし、ユーザの%ssと%espを新しいスタックにプッシュする。
もしプロセッサがカーネルモードで実行中だったら、上記のような事は何も行わない。
そしたらプロセッサは%eflagsと%csと%eipレジスタをプッシュする。
いくつかのトラップでは、プロセッサはエラー語(error word)もプッシュする。
そしたら、プロセッサは適切なIDTエントリから%eipと%csを読み込む。

xv6は、IDTエントリポイントへのエントリポイントを生成するためにPerlスクリプトを使う。
それぞれのエントリは、プロセッサがやらなかったらエラーコードをプッシュし、割り込み番号をプッシュし、そしてalltrapsにジャンプする。

vectors.pl

#!/usr/bin/perl -w

# Generate vectors.S, the trap/interrupt entry points.
# There has to be one entry point per interrupt number
# since otherwise there's no way for trap() to discover
# the interrupt number.

print "# generated by vectors.pl - do not edit\n";
print "# handlers\n";
print ".globl alltraps\n";
for(my $i = 0; $i < 256; $i++){
    print ".globl vector$i\n";
    print "vector$i:\n";
    if(!($i == 8 || ($i >= 10 && $i <= 14) || $i == 17)){
        print "  pushl \$0\n";
    }
    print "  pushl \$$i\n";
    print "  jmp alltraps\n";
}

print "\n# vector table\n";
print ".data\n";
print ".globl vectors\n";
print "vectors:\n";
for(my $i = 0; $i < 256; $i++){
    print "  .long vector$i\n";
}

# sample output:
#   # handlers
#   .globl alltraps
#   .globl vector0
#   vector0:
#     pushl $0
#     pushl $0
#     jmp alltraps
#   ...
#   
#   # vector table
#   .data
#   .globl vectors
#   vectors:
#     .long vector0
#     .long vector1
#     .long vector2
#   ...&#91;/sourcecode&#93;

alltrapsは、プロセッサのレジスタの保存を続ける。
%ds, %es, %fs, %gs、それと汎用的なレジスタをプッシュする。(alltrapsのpushl %dsからpushalの部分)
この努力の結果、カーネルスタックに、トラップの瞬間のプロセッサのレジスタを含むtrapframe構造体が含まれる。(図2−2を参照)
プロセッサは、%ss, %esp, %eflags, %cs, %eipをプッシュする。
プロセッサもしくはトラップベクタはエラー番号をプッシュし、alltrapsは残りをプッシュする。
トラップフレームは、カーネルから現在のプロセスに戻ったときにユーザモードのプロセッサのレジスタを復元するのに十分な全ての情報を含む。
その結果、プロセッサはトラップが開始した時点とちょうど同じ状態で続行することができる。
第1章を思い出すと、userinitはこのゴールを達成するために、手動でトラップフレームを構築している。
(図1-3参照)

図2-2 カーネルスタック上のトラップフレーム
<a href="https://peta.okechan.net/blog/wp-content/uploads/2012/03/スクリーンショット-2012-03-01-7.47.48.png"><img src="https://peta.okechan.net/blog/wp-content/uploads/2012/03/スクリーンショット-2012-03-01-7.47.48-271x300.png" alt="" title="スクリーンショット 2012-03-01 7.47.48" width="271" height="300" class="aligncenter size-medium wp-image-1384" /></a>

図1-3 新しいカーネルスタックのセットアップ(再掲)
<a href="https://peta.okechan.net/blog/wp-content/uploads/2012/02/スクリーンショット-2012-02-19-9.26.23.png"><img src="https://peta.okechan.net/blog/wp-content/uploads/2012/02/スクリーンショット-2012-02-19-9.26.23-294x300.png" alt="" title="スクリーンショット 2012-02-19 9.26.23" width="294" height="300" class="aligncenter size-medium wp-image-1288" /></a>

trapasm.S
#include "mmu.h"

  # vectors.S sends all traps here.
.globl alltraps
alltraps:
  # Build trap frame.
  pushl %ds
  pushl %es
  pushl %fs
  pushl %gs
  pushal
  
  # Set up data and per-cpu segments.
  movw $(SEG_KDATA<<3), %ax
  movw %ax, %ds
  movw %ax, %es
  movw $(SEG_KCPU<<3), %ax
  movw %ax, %fs
  movw %ax, %gs

  # Call trap(tf), where tf=%esp
  pushl %esp
  call trap
  addl $4, %esp

  # Return falls through to trapret...
.globl trapret
trapret:
  popal
  popl %gs
  popl %fs
  popl %es
  popl %ds
  addl $0x8, %esp  # trapno and errcode
  iret&#91;/sourcecode&#93;

x86.hのtrapframe構造体
&#91;sourcecode language="c"&#93;// Layout of the trap frame built on the stack by the
// hardware and by trapasm.S, and passed to trap().
struct trapframe {
  // registers as pushed by pusha
  uint edi;
  uint esi;
  uint ebp;
  uint oesp;      // useless & ignored
  uint ebx;
  uint edx;
  uint ecx;
  uint eax;

  // rest of trap frame
  ushort gs;
  ushort padding1;
  ushort fs;
  ushort padding2;
  ushort es;
  ushort padding3;
  ushort ds;
  ushort padding4;
  uint trapno;

  // below here defined by x86 hardware
  uint err;
  uint eip;
  ushort cs;
  ushort padding5;
  uint eflags;

  // below here only when crossing rings, such as from user to kernel
  uint esp;
  ushort ss;
  ushort padding6;
};&#91;/sourcecode&#93;

最初のシステムコールの場合、保存された%eipは、int命令の直後の命令のアドレスである。
%csはユーザコードのセグメントセレクタである。
%eflagsはint命令を実行した瞬間のeflagsレジスタの内容である。
保存された汎用レジスタの一部のように、alltrapsは、カーネルが後で調べるためのシステムコール番号を含む%eaxもまた保存する。

今、ユーザモードのプロセッサのレジスタは保存され、alltrapsはプロセッサにカーネルのCコードを実行させるためのセットアップを完了することが出来る。
プロセッサは、ハンドラに入る前にセレクタ%cs, %ssをセットする。
alltrapsは%dsと%esをセットする。(alltrapsのmovw $(SEG_KDATA<<3), %axからmovw %ax, %esの部分)
それから、CPUごとのデータセグメントであるSEG_KCPUを指すために%fsと%gsをセットする。(alltrapsのmovw $(SEG_KCPU<<3), %axからmovw %ax, %gsの部分)

一度セグメントが適切にセットされたら、alltrapsは、Cで書かれたトラップハンドラであるtrap関数(trap.c)を呼ぶことが出来る。
alltrapsは、構築されたばかりのトラップフレームを指す%espを、trapへの引数としてスタックにプッシュする。(alltrapsのpushl %espの部分)
そしてtrapを呼ぶ。(alltrapsのcall trapの部分)
trap関数から返ってきた後、alltrapsは、スタックポインタに加算することによってスタックの引数を取り除き、そして、trapretとラベルづけされたコードの実行を開始する。
我々は、第1章で、最初のユーザプロセスがユーザ空間に抜けるためにtrapretを実行したとき、このコードを追った。
同じ処理がここで起きた。
トラップフレームを通じてポップする事は、ユーザモードのレジスタを復元し、そしてiretはユーザ空間に戻す。

trap.cのtrap関数
&#91;sourcecode language="c"&#93;void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_SYSCALL){
    if(proc->killed)
      exit();
    proc->tf = tf;
    syscall();
    if(proc->killed)
      exit();
    return;
  }

  switch(tf->trapno){
  case T_IRQ0 + IRQ_TIMER:
    if(cpu->id == 0){
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
    }
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_IDE:
    ideintr();
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_IDE+1:
    // Bochs generates spurious IDE1 interrupts.
    break;
  case T_IRQ0 + IRQ_KBD:
    kbdintr();
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_COM1:
    uartintr();
    lapiceoi();
    break;
  case T_IRQ0 + 7:
  case T_IRQ0 + IRQ_SPURIOUS:
    cprintf("cpu%d: spurious interrupt at %x:%x\n",
            cpu->id, tf->cs, tf->eip);
    lapiceoi();
    break;
   
  //PAGEBREAK: 13
  default:
    if(proc == 0 || (tf->cs&3) == 0){
      // In kernel, it must be our mistake.
      cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n",
              tf->trapno, cpu->id, tf->eip, rcr2());
      panic("trap");
    }
    // In user space, assume process misbehaved.
    cprintf("pid %d %s: trap %d err %d on cpu %d "
            "eip 0x%x addr 0x%x--kill proc\n",
            proc->pid, proc->name, tf->trapno, tf->err, cpu->id, tf->eip, 
            rcr2());
    proc->killed = 1;
  }

  // Force process exit if it has been killed and is in user space.
  // (If it is still executing in the kernel, let it keep running 
  // until it gets to the regular system call return.)
  if(proc && proc->killed && (tf->cs&3) == DPL_USER)
    exit();

  // Force process to give up CPU on clock tick.
  // If interrupts were on while locks held, would need to check nlock.
  if(proc && proc->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER)
    yield();

  // Check if the process has been killed since we yielded
  if(proc && proc->killed && (tf->cs&3) == DPL_USER)
    exit();
}

ここまでの解説は、ユーザモードから発生したトラップについて話してきた。
しかしトラップはカーネルを実行してるときでも発生する。
そのような場合、ハードウェアはスタックを切り替えたり、スタックポインタやスタックセグメントセレクタを保存したりはしない。
しかしながら、ユーザモードからのトラップと同じ手順が発生し、そして同じxv6のトラップを制御するコードを実行する。
iretの後、カーネルモードの%csを復元し、プロセッサはカーネルモードでの実行を続ける。

感想

ユーザモードでシステムコールしたときに発生するトラップで、どうやってカーネルモードに移行してどうやって戻るかの話ですね。

本文にも書いてあるけど、vectors.Sはvectors.plから生成するようになってるので、一度makeとかしないと存在しません。

タスクセグメントディスクリプタ周りの話がよくわかりません。
プロテクトモード – Wikipediaを読めば少しは分かるかな。

コメントを残す

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



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