Skip to content

Instantly share code, notes, and snippets.

@kaishuu0123
Last active April 23, 2022 15:56
Show Gist options
  • Save kaishuu0123/5235498 to your computer and use it in GitHub Desktop.
Save kaishuu0123/5235498 to your computer and use it in GitHub Desktop.
xv6 を読む

xv6 を学ぶ上での準備

xv6 って何?

なんでxv6を読むのか

  • Linux からとっつくのは正直つらい
  • シンプルに x86 アーキテクチャとカーネルを知りたい
  • OSを学習していくにあたって、どうやって攻めたらいいのかをxv6でつかみたい

実行環境

gas

  • GNU as。gcc ではアセンブラにGNU asを利用している
  • xv6 では gas を利用しているため、簡単にでも gas の理解が必須
  • 代入は向かって左辺から右辺に代入される
  • レジスタなどはレジスタ名の前に「%」が入る
    • %eax, %ebx
  • 定数などは定数の前に「$」が入る
    • $0x40, $word
  • 公開したいシンボルを指定する場合には .global ディレクティブを利用する
.global main # main シンボルを公開する
  • データ領域の宣言方法は .data を利用して行う
.data
msg: .ascii "Hello, World\n"
  • $msg で参照する

x86 拡張インラインアセンブリ

  • gcc の機能で x86 拡張アセンブリを利用することが可能 (gcc がよしなにやってるのか元々x86が持っている機能なのかは要調査)
  • 例として、以下のインラインアセンブリを
asm(
  "movl $0x01, $eax;"
  "addl $0x05, $eax"
);
  • 以下のように書き換えることができる
int result;
asm(
  "movl $0x01, %%eax;"
  "addl $0x05, %%eax"
  : "=a" (result)
);
  • 上記のコードはresultに addl した結果を格納するもの

  • 前半のアセンブリっぽい記法のところを「アセンブリテンプレート」と呼ぶ

    • レジスタの指定方法に違いあり。%% と2つ指定する
  • : 以降が出力オペランドの指定

    • =a (result)となっている箇所は「eax に設定された値を result に書き出す」という意味
  • また、拡張アセンブリには以下のように入力値にC言語の変数を使うことができる

int x = 1, y = 5;
int result;
asm(
  "movl %2, %%eax;"
  "addl %1, %%eax"
  : "=a" (result)
  : "m" (x), "m" (y)
);
  • %0 が result, %1 が x, %2 が y になる。"m" で入力オペランドの指定を行っている

x86 に由来する用語集

  • A20 ゲート

    • アドレスバスの20本目の信号線のことで A0, A1, A2 ... というように信号線に名前がつけてある。
    • 過去8086 CPUを使っていたいときのCPUのアドレスラインが0番から19番目まで20個存在していたため、レジスタは16ビットだった。
    • 過去との互換性を維持するために、起動直後はA0-A19までのアドレスラインしか利用できないため、その制限をA20ゲートと呼ぶ
    • 1MiB バイト以上のメモリを利用するには、このA20ゲートの制限を解除して利用する必要がある
  • リアルモード

    • TODO: 後で書く
  • プロテクトモード

    • TODO: 後で書く
  • ディスクリプタ

    • 直訳すれば「記述子」直感的には分かりにくいが、情報を格納しておく構造体のようなものと理解しておく。
    • セグメントディスクリプタ、ゲートディスクリプタなどいろいろとある
  • GDT

    • Global Descripter Table の略
    • TODO: あとで書く
  • LDT

    • Local Descripter Table の略
    • TODO: あとで書く
  • IDT

    • Interrupt Descripter Table の略
    • TODO: あとで書く

起動処理を読む(main.c の main() が呼ばれるまで)

  • BIOSが起動して初源のコードは bootasm.S
  • まずは最初の数行を読んでみる
# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli                         # BIOS enabled interrupts; disable
  • 以下コメント文直訳

    • 32 bit プロテクトモードに移行して、Cのコードにジャンプする
    • BIOS はハードディスクの最初のセクタからメモリの物理アドレス 0x7c00 に読み込み、リアルモードで開始する。
    • そのときの cs, ip レジスタの値は 0x00, 0x7c00
  • まずは 16 bit mode であることを明示しつつ、startが呼ばれる

  • cli という命令は x86 CPU の命令で IF(Interrput enable Flag)というシステムフラグがクリアされて、外部からのハードウェア割り込みに応答しなくなる。

  • というわけで、ハードウェア割り込みを禁止している

    • XXX: 割り込みがおきると困ることがあるの?
  • 次にレジスタの初期化

  # Zero data segment registers DS, ES, and SS.
  xorw    %ax,%ax             # Set %ax to zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment
  • 明らかだが、xorを取って ax レジスタのクリアをした後に as レジスタの値を使って ds, es, ss の各セグメントをクリアしている

  • A20ゲートの解放

# Physical address line A20 is tied to zero so that the first PCs                                                     
# with 2 MB would run software that assumed 1 MB.  Undo that.
seta20.1: 
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1

  movb    $0xd1,%al               # 0xd1 -> port 0x64
  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60

To this day, modern PCs continue this backwards compatibility dance, and low-level software probably continues to depend on 8088 behavior at boot. The boot sector must enable the A20 line using I/O to the keyboard controller on ports 0x64 and 0x60.

  • とあるのだがなんで、0x64、0x60なのかがまだ分からない。

  • XXX: あとで詳しく調べる

  • とはいえ、これで1MiB以上のメモリを使えるようになる、ということだ

  • 32bit protected mode への移行(GDT の設定、32bit protected mode になったときの jmp について)

# Switch from real to protected mode.  Use a bootstrap GDT that makes
# virtual addresses map directly to physical addresses so that the
# effective memory map doesn't change during the transition.
lgdt    gdtdesc
movl    %cr0, %eax
orl     $CR0_PE, %eax
movl    %eax, %cr0
  • 32bit protected mode セグメントレジスタのセットアップと C コードへの jump
//PAGEBREAK!
# Complete transition to 32-bit protected mode by using long jmp
# to reload %cs and %eip.  The segment descriptors are set up with no
# translation, so that the mapping is still the identity mapping.
ljmp    $(SEG_KCODE<<3), $start32
.code32  # Tell assembler to generate 32-bit code now.
start32:
# Set up the protected-mode data segment registers
movw    $(SEG_KDATA<<3), %ax    # Our data segment selector
movw    %ax, %ds                # -> DS: Data Segment
movw    %ax, %es                # -> ES: Extra Segment
movw    %ax, %ss                # -> SS: Stack Segment
movw    $0, %ax                 # Zero segments not ready for use
movw    %ax, %fs                # -> FS
movw    %ax, %gs                # -> GS

# Set up the stack pointer and call into C.
movl    $start, %esp
call    bootmain
  • イヤー長い。。。じっくり後で読む

  • bootmain.c への jump 実行後

    • bootmain.c の bootmain() が呼ばれる
    • kernel の実体を読み込みつつ、entry.S に飛ぶ
    • entry.S からやっと main.c の main() が呼ばれる

main() からmpmain()が呼ばれるまで

  • あくまで俯瞰

  • 詳しい説明は違うページに分けて読むことにする

    • 分け方としてはテキストに準じて以下のような感じで
      • OS のインタフェースについて(Operating system interfaces)
      • 初源の処理について(The first process)
      • トラップ, 割り込み、ドライバについて(Traps, interrupts, and drivers)
      • ロック処理について(Locking)
      • スケジューリングについて(Scheduling)
      • ファイルシステム(File system)
  • main() が呼ばれた後に何が実行されるかソースコードをじっくり見ていく

int
main(void)
{
  kinit1(end, P2V(4*1024*1024)); // phys page allocator
  kvmalloc();      // kernel page table
  mpinit();        // collect info about this machine
  lapicinit();
  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
  kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
  userinit();      // first user process
  // Finish setting up this processor in mpmain.
  mpmain();
}
  • それぞれの初期化について淡々と見ていく

  • kinit1() (kalloc.c)

    • kernel initialize 1 ?
    • メモリアロケータで物理メモリの割り当てを行う
    • kinit2() と深い関連があるのだが、これについては The first process で深めていくことにする
  • kvmalloc() (vm.c)

    • kernel virtual memory allocation ?
    • kinit1() で何となく割り当てしてるっぽいのに、なんでまたallocation してるのかはまだわからない
  • mpinit() (mp.c)

    • multi processor initialize ?
    • マルチプロセッサに対応するコード周辺の初期化か
    • APを初期化する、とのことだけど、APってなんざんしょ
    • Application Processor の略だそうな
    • システム起動時に動作する CPU はBSP(Bootstrap Processor)
    • 他のCPUをAP(Application Processor)と呼ぶそうだ
    • どのCPUがBSPになるかはハードウェアが決定する
    • じゃぁBSPかAPか判別するロジックって必要なん?
  • lapicinit() (lapic.c)

    • local apic(Advanced Programmable Interrupt Controller)
    • CPU に内蔵されているからLocal APIC なのかね
    • IO APIC と協調して動く
  • seginit() (vm.c)

    • GDT に関連する設定を実行している
    • CPU の数だけ実行されている
  • picinit() (main.c)

    • PIC を初期化する
    • APIC との関連はおそらく trap, interrupt and driver で説明されるか
  • ioapicinit() (ioapic.c)

    • IO APIC の初期化
    • IO APIC とは I/O でバイスから受け取った割り込みを CPU へ通知するためのリダイレクションテーブルを持っていて、そのテーブルを参照して、CPUに割り込みの通知をする
    • リダイレクションテーブルには、エッジ/レベルトリガーの種別、割り込みベクタ(優先度)、割り込み先CPUの設定が可能
      • とはいえ、まだボケてる感じがするので後々で学ぶことにする
  • consoleinit() (console.c)

    • コンソールの初期化
    • Wikipedia ではコンピュータの制御卓とのことだけど、制御卓といってもパッとせず。。。とりあえず操作するための端末を初期化しているものと思っておこう
  • uartinit() (uart.c)

    • UART(Universal Asynchronous Receiver Transmitter)
    • シリアルポートの初期化
  • pinit() (proc.c)

    • プロセステーブルの初期化を行っているようだ
    • でもなんだか initlock() してるだけなように見える
  • tvinit() (trap.c)

    • trap vector table ?
    • 割り込みベクタの初期化を全般的にしているっぽい。
  • binit() (bio.c)

    • バッファキャッシュの初期化。バッファキャッシュって何の?
    • なるほど、ディスク周りか
  • fileinit() (file.c)

    • ファイルシステム周りの初期化と思ったらロック変数の初期化だけだった
  • iinit() (fs.c)

    • inode init ?
  • ideinit (ide.c)

    • IDE(Integrated Drive Electonics) の初期化
    • IDEの割り込みを有効にして、IDEディスクの起動を待つ
  • if (!ismp) timerinit() (timer.c)

    • 1つのプロセッサ(ユニプロセッサ)で動作しているときに大麻割り込みを受けるための準備
    • マルチプロセッサの場合にはどうなる?
  • startothers() (main.c)

    • mpinit() で初期化したAPの起動処理
    • lapicstartap() を実際に起動しているが、まだ謎
  • kinit2( .... ) (kalloc.c)

    • kinit1 に引き続きメモリアロケータの処理
  • userinit() (proc.c)

    • ユーザプロセスの init が呼ばれる
  • mpmain() (main.c)

    • ん〜まだ分からん。
    • idtinit, scheduler() がよばれている
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment