xv6(x86 UNIX v6)カーネルのIP対応化

xv6(x86 UNIX v6)カーネルのIP対応化
Last Update: 2015/09/27
Contact: [email protected]
Author: gucchan (B2, Keio University)
Jun Murai Lab., ARCH(Internet Architecture Research Team) @ Delta North.
目的
コンピュータアーキテクチャを理解するために今回、ファミコンエミュレータの次の目標として有名なUNIX version6 (UNIX v6)のソースコード
を読む事によって理解を深めようと考えていたがUNIX version6はPDP-11上で動作するようにコードが記述されているため現代のコンピュータ
にそぐわないような内容も多く含まれている(e.g. pre K&R-Cによる記述やPDP-11用のアセンブリなど)
そこで、米マサチューセッツ工科大学(MIT)においてオペレーティングシステムの講義(6.828 Operating System Computer Science Enginnering)
で利用されているxv6(x86 UNIX v6)というUNIXライクなOSを採用する事にした。これはUNIX v6ライクなカーネルのソースコードのままでx86
アーキテクチャ(IA-32)上で動作させるように開発された教育向けのOSであり、OSの基本的な仕組みを網羅しているため今回のアーキテクチャ
の把握のためには最適だと判断した。
xv6にはシステムコールやUNIXライクと呼べるだけの必要最低限の機構が実装されており、ネットワーク機能は搭載されていない。従って今回
の最終的な目標としてNE2000互換のデバイスドライバ/NICをxv6向けに開発し、カーネルにTCP/IPプロトコルスタックを実装する事で
Ethernet/DHCP/ARP/IP/UDPでの通信を実現する事した。
ブートローダ
(画像: POST中のBIOS)
起動するとBIOSが起動し、BIOSはPOST(Power On Self Test)と呼ばれるデバイスの動作チェックを行う。
POSTが完了するとBIOSとしての動作が開始される。BIOSはまず起動デバイスの1セクタ分を読み出す。1セクタはフロッピーディスクだと
512byteに相当する。
この512byteはアドレスエントリポイント0x7c00にコピーされるようになっており、
ここの最初の1セクタ目がブートローダーになる。ブートローダー起動時はリアルモードと呼ばれる下位互換性を保持するために16bitのモード
(Intel 8088互換モード)で起動する。
そこから、ある特定の処理を行う事によって32bitで動作するいわゆるプロテクトモードに移行する処理を行い、初期化作業やカーネルイメージ
の読み込みを担当するのがブートローダーだ。 Appendix BのThe Boot loaderによるとxv6のブートローダーは bootasm.S と bootmain.c の二
つのアセンブリとC言語のファイルより構成されている。
.code16
# Assemble for 16-bit mode
.globl start
start:
cli
# BIOS enabled interrupts; disable
# 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
# 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
outb
$0xd1,%al
%al,$0x64
seta20.2:
inb
$0x64,%al
testb
$0x2,%al
jnz
seta20.2
movb
outb
$0xdf,%al
%al,$0x60
# 0xd1 -> port 0x64
# Wait for not busy
# 0xdf -> port 0x60
まず、最初にリアルモードで起動したら各セグメントレジスタの設定を行う。通常リアルモードではメモリには seg * 16 + offset の形式で
アクセスする。この後、32bitモードに移行する(A20以上のバスを有効化する)ためにIntel 4082キーボードコントローラに指令を出してA20有効
化処理を行う。
そして、その次にldgtという命令があるが32bitではセグメントの管理はGDT(Global Descriptor Table)というテーブルに敷き詰められたレジスタ
にセグメント情報を管理させて利用するため、リアルモードで使われたセグメントレジスタはセグメントレジスタの使われ方をせず、セグメン
トセレクタとプロテクトモードでは呼ばれGDTのレジスタを選ぶ役割が新たに与えられる。lgdt命令はgdtの内容のアドレスをオペランドとして
受け取る。
lgdt
movl
gdtdesc
%cr0, %eax
orl
movl
$CR0_PE, %eax
%eax, %cr0
# Bootstrap GDT
.p2align 2
# force 4 byte alignment
gdt:
SEG_NULLASM
# null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)
SEG_ASM(STA_W, 0x0, 0xffffffff)
gdtdesc:
.word
.long
# code seg
# data seg
(gdtdesc - gdt - 1)
# sizeof(gdt) - 1
gdt
# address gdt
gdtには最初にgdtテーブルのサイズを記述した上でgdbテーブルを記述する。最初のgdtセグメントはIntelの仕様によりnullセグメントとなってい
る。(これはSEG_NULLASMを参照すると .word 0,0;.byte 0,0,0,0 となっている事からも分かる)
そして、コードセグメントとデータセグメントに対してセグメントの設定を行っている。
GDTレジスタは1つにつき64bitのビット幅を持っており、以下のような構造になっている。
上のコードで登場してきたSEG_NULLASM, SEG_ASMなどのマクロはasm.hの中に記述されている。
セグメントは領域が重なっていても問題はないため、コードセグメントとデータセグメントの領域を同じセグメントにしている。
#define SEG_NULLASM
\
.word 0, 0;
.byte 0, 0, 0, 0
\
// The 0xC0 means the limit is in 4096-byte units
// and (for executable segments) 32-bit mode.
#define SEG_ASM(type,base,lim)
\
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff);
\
.byte (((base) >> 16) & 0xff), (0x90 | (type)),
\
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
#define STA_X
0x8
// Executable segment
#define STA_E
#define STA_C
0x4
0x4
// Expand down (non-executable segments)
// Conforming code segment (executable only)
#define STA_W
#define STA_R
#define STA_A
0x2
0x2
0x1
// Writeable (non-executable segments)
// Readable (executable segments)
// Accessed
GDTへの設定が終わり、 %cr0 レジスタのbit1番目( CR0_PE )を1にするとプロテクトモードが有効になる
jmp命令を用いてCPUパイプラインの内容を無効にした上で、適切な値をコードセグメントセレクタ( %cs )に与えるためには ljmp cs,
jmp_addr を利用してjmp_addrにはプロテクトモードで動作するアセンブリコードの先頭アドレスを与える。
命令の各ステージは1クロックで原則として動作するようになっている。しかし、このステージを処理する部分はそれぞれ別のためこのステー
ジを独立したものと考えて、例えばIF(Instruction Fetch)部は命令1のステージが完了するとすぐ命令2のステージに移行する。これによって逐次
命令実行方式だと5クロックで1命令を処理できるのに対して効率的に多くの命令を各ステージ処理部を利用して命令を実行できる。しかし、分
岐やその命令が終わらないと次の命令の内容を決まらない場合はパイプラインハザードと呼ばれる現象が起き、CPUパイプラインは効率的なも
のとして機能しなくなる。ここでは、ljmp命令を利用してIntelアーキテクチャではどうやらパイプライン内容をクリアするようなので、プロテ
クトモード以降のためにこの仕組みを利用しているようだ。
GDTのディスクリプタは1つにつき64bitであり、最初のディスクリプタはNULLディスクリプタなのでGDTには0x08を持つ%csでセレクトでき
ればいい事になる。
その次にデータセグメント( %ds, %es, %ss )を初期化する。この初期化は movw 命令を用いて行われている。GDT上ではデータセグメントの
ディスクリプタは16byte目ににあるため、 movw $(SEG_KDATA<<3), %ax という命令がそれに対応しておりSEG_KDATAは2と定義されてい
る。従って16が %es, %es, %ss に格納される事になる。
(16bit real-mode)
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
movw
%ax, %ds
%ax, %es
# -> DS: Data Segment
# -> ES: Extra Segment
movw
%ax, %ss
# -> SS: Stack Segment
movw
movw
$0, %ax
%ax, %fs
# Zero segments not ready for use
# -> FS
movw
%ax, %gs
# -> GS
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain
以下はmmu.hのSEG_KCODE, SEG_KDATAの定義だ。
#define SEG_KCODE 1 // kernel code
#define SEG_KDATA 2 // kernel data+stack
そして、その後にbootmain.cの中にあるbootmain関数を呼び出している。このbootmainの中では、カーネルをメモリにロードするための処理を
行っている。カーネルイメージはelfフォーマットになっている。
#define SECTSIZE 512
#define ELF_MAGIC 0x464c457fU;
void readseg(uchar *, uint, uint);
void bootmain(void) {
struct elfhdr *elf;
struct proghdr *ph, *eph;
void (*entry)(void);
uchar* pa;
elf = (struct elfhdr *)0x10000;
readseg((uchar *)elf, 4096, 0);
if(elf->magic != ELF_MAGIC) return;
ph = (struct proghdr *)((uchar *)elf + elf->phoff);
eph = ph + elf->phnum;
for(; ph < eph; ph++) {
pa = (uchar *)ph->paddr;
readseg(pa, ph->filesz, ph->off);
if(ph->memsz > ph->filesz) stosb(pa + ph->filesz, 0, ph->memsz - ph->filsz);
}
entry = (void(*)(void))(elf->entry);
entry();
}
MBRが正当である事の署名が最後の2byte分( 0x55aa )が入るはずなのだがソースコードにどこにも記述がなかったのでMakefileを見てみると、
sign.plというPerlスクリプトにブートローダーのバイナリを渡している事がわかった。このsign.plは出来上がったブートローダーが510byte以内
に収まっているかをチェックして収まっていたら最後の511,512byte目にMBRブート署名 0x55aa を付加するような作りになっている。
open(SIG, $ARGV[0]) || die "open $ARGV[0]: $!";
$n = sysread(SIG, $buf, 1000);
if($n > 510){
print STDERR "boot block too large: $n bytes (max 510)\n";
exit 1;
}
print STDERR "boot block is $n bytes (max 510)\n";
$buf .= "\0" x (510-$n);
$buf .= "\x55\xAA";
open(SIG, ">$ARGV[0]") || die "open >$ARGV[0]: $!";
print SIG $buf;
close SIG;
ページング機構とMMU(Memory Management Unit)
ページングについての話になる。起動時にはxv6はページングハードウェアを有効化しておらずそのまま仮想アドレスと物理アドレスが対応付
けられている状態になっていた。
仮想メモリを導入する事によって、プロセスは実行中に全てのメモリ空間の一部しか必要としないため(参照の局所性)、プロセスの実行に必要
な一部だけをメモリに置き、残りをディスクに退避させるなど(スワッピング)として実メモリと論理的なアドレス空間を分離する事によって、
セキュリティの向上やパフォーマンスの向上が見込む事が可能になる。
そのうちの一つとしてページングが存在している。
セグメンテーションのように開始アドレスやサイズは自由に決定できるものではなく、サイズは4096byte(4KB)固定でアドレスも4096の倍数に
なっていなければならない。
ページテーブルは2^20個のページテーブルエントリ(PTE)の配列であり、PTEは物理ページ番号(PPN)とページフラグで構成されている。この
時、仮想アドレスの下位12bitはそのまま物理アドレスにコピーされる。12bitは2^12=4096のため、そのままコピーする事によって4KB刻みの塊
(ページ)を作る事ができる。
ページングハードウェアにはページテーブルともう一つ、ページディレクトリというものが存在している。これはページテーブルのテーブルで
あり、1024個のページテーブルを格納する事ができる。
仮想アドレスは、MSBから数えて10bitがページディレクトリ、もう10bitがページテーブル用、残りの12bitがオフセットとして割り当てられて
おり、まず最初にページディレクトリでページテーブルを特定した後、テーブル用ビットの10bitでPTEを特定、そのPTEとオフセット12bitを組
み合わせて物理アドレスへと変換する仕組みとなっている。
# By convention, the _start symbol specifies the ELF entry point.
# Since we haven't set up virtual memory yet, our entry point is
# the physical address of 'entry'.
.globl _start
_start = V2P_WO(entry)
# Entering xv6 on boot processor, with paging off.
.globl entry
entry:
# Turn on page size extension for 4Mbyte pages
movl
%cr4, %eax
orl
$(CR4_PSE), %eax
movl
%eax, %cr4
# Set page directory
movl
$(V2P_WO(entrypgdir)), %eax
movl
%eax, %cr3
# Turn on paging.
movl
%cr0, %eax
orl
$(CR0_PG|CR0_WP), %eax
movl
%eax, %cr0
以前見たブートローダーはbootmain.cのbootmain関数に制御を写し、カーネルをロードした。カーネルをロードした後にelfファイルのエントリ
を実行していたが上に記述されているentry.Sのこの部分の内容に相当する。
この部分では、4MBページ拡張、ページディレクトリ、ページングハードウェアの有効化処理をおこなっている。
4MBページ拡張
CR0 PG
CR4 PAE
CR4 PSE
PDE PS
Page Size
PHY ADDRESS
0
-
-
-
-
Paging disabled
1
0
0
-
4Kbyte
32bit
1
0
1
0
4Kbyte
32bit
1
0
1
1
4Mbyte
32bit
1
1
-
0
4Kbyte
36bit
1
1
-
1
2Mbyte
36bit
4MBページ拡張(PSE)を使うとページサイズを4MBへと拡張する事が可能になる。4KB単位でのページへのアクセスと違い、4MBページでは
ページディレクトリの中にあるPDEが直接4MBページを指す事になる。
// Boot page table used in entry.S and entryother.S.
// Page directories (and page tables), must start on a page boundary,
// hence the "__aligned__" attribute.
// Use PTE_PS in page directory entry to enable 4Mbyte pages.
// defined in mmu.h
#define PGSIZE 4096 //
#define NPDENTRIES 1024 // directory entries per page directory
// defined in memlayout.h
#define KERNBASE 0x80000000
__attribute__((__aligned__(PGSIZE))) pde_t entrypgdir[NPDENTRIES] = {
// Map VA's [0, 4MB) to PA's [0, 4MB)
[0] = (0) | PTE_P | PTE_W | PTE_PS,
// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};
ブートローダーはxv6カーネルを物理アドレス0x00100000に読み込んでいる
(0x00000000に読み込みたい所だが古いI/Oデバイス用のアドレス空間との重複を防ぐため、逆に仮想ではKERNBASEは0x80000000となってい
るが物理マシンにそのような余裕がなかった場合動かない可能性があるためこのアドレスになっている。OSDev.orgでのx86 Memory Mapを参
照)。
ページディレクトリで0番目と512番目を指定してPTE_PS(superpage)の属性情報を付加している。これは物理0x00000000 ~ 0x00400000を仮
想0x00000000 ~ 0x00400000に、物理0x00000000 ~ 0x00400000を仮想KERNBASE(0x80000000) ~ KERNBASE+0x00400000-1まで対応付け
る。二つスーパーページを設定しているが、これはページングハードウェアを有効にした後、KERNBASE~KERNBASE+0x00400000-1の範囲
ではない低いアドレスで実行された時コンピュータがクラッシュするのを防ぐためだ。
この対応付けを行う事でカーネルのコードとデータを4MBに制限する事になる。
ページディレクトリテーブルの設定は %cr3 レジスタへアドレスを設定し、 %cr0 にページングを有効化するビットを立ててページングハード
ウェアを有効化した上でスタックを調整しmain.cのmain関数へ移行する。
// 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) {
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kvmalloc();
mpinit();
// kernel page table
// collect info about this machine
lapicinit();
seginit();
// set up segments
cprintf("\ncpu%d: starting xv6\n\n", cpu->id);
// to be continued...
// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void kvmalloc(void) {
kpgdir = setupkvm();
switchkvm();
}
xv6では、どのプロセスもページテーブルを持っている。
プロセスユーザーメモリは0x00000000 ~ 0x80000000(KERNBASE)まで拡張できるようになっている。 各プロセスにはカーネルが動作するた
めに必要なマッピングも行われる。これはカーネルが自身の命令やデータを実行するための措置であり、具体的には物理0 ~ PHYSTOPを仮想
KERNBASE ~ KERNBASE+PHYSTOP(0xe000000)にマッピングする。 つまり、1プロセスあたり最大2GBまでのユーザーメモリを利用する事
ができる。(2GB以上のメモリを利用する事ができない)
static struct kmap {
void *virt;
uint phys_start;
uint phys_end;
int perm;
} kmap[] = {
{ (void*)KERNBASE, 0,
EXTMEM,
PTE_W}, // I/O space
{ (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0},
// kern text+rodata
{ (void*)data,
V2P(data),
PHYSTOP,
PTE_W}, // kern data+memory
{ (void*)DEVSPACE, DEVSPACE,
};
0,
PTE_W}, // more devices
// Set up kernel part of a page table.
pde_t* setupkvm(void) {
pde_t *pgdir;
struct kmap *k;
if((pgdir = (pde_t*)kalloc()) == 0)
return 0;
memset(pgdir, 0, PGSIZE);
if (p2v(PHYSTOP) > (void*)DEVSPACE)
panic("PHYSTOP too high");
for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
if(mappages(pgdir, k->virt, k->phys_end - k->phys_start,(uint)k->phys_start, k->perm) < 0)
return 0;
return pgdir;
}
kmapでは各プロセスに配置されるカーネルマッピングを表しており、main時に kvmalloc() が呼び出されるので、これらのマッピングを一
回 mappages() を用いてマッピングを行う。mappagesは walkpgdir() を用いて渡された仮想アドレスと物理アドレスをPTEを作成する事に
よって対応付ける。
// Return the address of the PTE in page table pgdir
// that corresponds to virtual address va. If alloc!=0,
// create any required page table pages.
static pte_t * walkpgdir(pde_t *pgdir, const void *va, int alloc) {
pde_t *pde;
pte_t *pgtab;
// defined in mmu.h
// #define PTE_ADDR(pte) ((uint)(pte) & ~0xfff)
// #define PTXSHIFT 12
// #define PDXSHIFT 22
// #define PDX(va) (((uint)(va) >> PDXSHIFT) & 0x3ff)
// #define PTX(va) (((uint)(va) >> PTXSHIFT) & 0x3ff)
pde = &pgdir[PDX(va)];
if(*pde & PTE_P){
pgtab = (pte_t*)p2v(PTE_ADDR(*pde));
} else {
if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
return 0;
// Make sure all those PTE_P bits are zero.
memset(pgtab, 0, PGSIZE);
// The permissions here are overly generous, but they can
// be further restricted by the permissions in the page table
// entries, if necessary.
*pde = v2p(pgtab) | PTE_P | PTE_W | PTE_U;
}
return &pgtab[PTX(va)];
}
// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
static int mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm) {
char *a, *last;
pte_t *pte;
// defined in mmu.h
// #define PGROUNDDOWN(a) (((a)) & ~(PGSIZE - 1))
// #define PGROUNDUP(sz) (((sz) + PGSIZE - 1) & ~(PGSIZE - 1))
a = (char*)PGROUNDDOWN((uint)va);
last = (char*)PGROUNDDOWN(((uint)va) + size - 1);
for(;;){
if((pte = walkpgdir(pgdir, a, 1)) == 0)
return -1;
if(*pte & PTE_P)
panic("remap");
*pte = pa | perm | PTE_P;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
最終的に確保したカーネルページテーブルをcr3にセットして切り替える事になる。
static inline void lcr3(uint v) { asm volatile("movl %0,%%cr3" : : "r" (v)); }
// Switch h/w page table register to the kernel-only page table,
// for when no process is running.
void switchkvm(void) {
lcr3(v2p(kpgdir)); // switch to the kernel page table.
}
void kinit1(void *vstart, void *vend) {
initlock(&kmem.lock, "kmem");
kmem.use_lock = 0;
freerange(vstart, vend);
}
void freerange(void *vstart, void *vend) {
char *p;
p = (char *)PGROUNDUP((uint)vstart);
for(; p + PGSIZE <= (char *)vend; p += PGSIZE) kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when initializing the allocator;)
void kfree(char *v) {
struct run *r;
if((uint)v % PGSIZE || v < end || v2p(v) >= PHYSTOP) panic("kfree");
memset(v, 1, PGSIZE);
if(kmem.use_lock) {
acquire(&kmem.lock);
}
r = (struct run *)v;
r->next = kmem.freelist;
kmem.freelist = r;
if(kmem.use_lock) release(&kmem.lock);
}
割り込みとトラップ
x86は4つのリングを持っており、0が一番高い権限を持っていて(Ring-0)、より大きな数字になればなるほどコンピュータにアクセスできる権限
は少なくなる。
しかし、最近のOSではプロテクションリングのうち2つ(Ring-0, Ring-3)しか利用していない。
これらがカーネルモード(Ring-0)、ユーザーモード(Ring-3)と呼ばれている。
IDT (Interrupt Descriptor Table)
割り込みとISRを関連付けるためのテーブルで、エントリは8byteのディスクリプタを持っており、256個のIDTエントリを保持している。
Task Gate Descriptor
Interrupt Gate Descriptor
Trap Gate Descriptor
int命令実行時の処理
int命令を実行する時、 %cs と %eip の情報を利用する。 CPLがCPUが実行しているプロセスの特権レベル、DPLがセグメントディスクリプタに
書かれているそのセグメントへのアクセスに必要とされる特権レベルとなっている。
1. int命令に渡されたオペランドがIDTのどのエントリを参照するかのインデックスになる
2. %cs の中にある CPL(Current Priviledge Level) <= DPL(Descriptor Priviledge Level) を満たしているかをチェック(DPLは
ディスクリプタの中の特権レベルを表す)
3. セグメントセレクタの PL が PL < CPL だった時 %esp 、 %ss のCPU内部レジスタを退避
4.
5.
6.
7.
タスクセグメントレジスタから %ss と %esp を読み込む
%ss を退避
%esp, %eflags, %cs, %eip をスタックに退避
%eflags のフラグをクリアsite %cs , %eip にディスクリプタの中の値をセットする
ネットワークの仕組みと全体像
大まかな通信の流れ
ハード(NIC)をOS側からデバイスドライバを経由し制御、パケットを送信する。
インターネットモジュールが機能する前提として、ハードウェアで直接接続されたホストやルータ間で通信が可能である事。 IP伝送が機能する
ためには経路上のすべてのノードにおいてインターネットモジュールが動作する必要がある。
トランスポートモジュールはインターネットモジュールが正常に動作するという保証の元動作しており(TCP,UDPなど)、end-to-endで仮想通信
路を確立するためL3で中継を行うルーターはトランスポートモジュールは必要ない。
カーネルから send() や recv() などのネットワークシステムコールが発行されるとカーネルはユーザーモードからカーネルモードへと制御を
移し、送信するメッセージをカーネル空間へコピーする。コピーした後それぞれのモジュールでやるべき事を行った上で下位のキューバッファ
に渡す事になる。この時キューバッファが れてしまうとメッセージは破棄されてしまうため、空きがある必要がある。
パケットが到着するとNICはデバイスドライバに対してIRQを発行し、上層のモジュールへとキューバッファを利用して渡されて最終的にソ
ケットバッファから recv() システムコールを用いてそのバッファからバッファされたメッセージを読み取る。
Ethernet(10Base-T)
今回のRTL8019ASはEthernet 10Base-T規格での速度で通信が可能になっている。
10Base-Tはベースバンド方式でケーブルにUTPを用いる。コード変換を行わない(100Base-TXでは4B5B変換を行って同期を取る工夫がされて
いる)ため、データ速度と信号速度は同じ10Mbpsとなっている。
何を持って0か1かする意味付けにおいて、最も単純な方法として知られているGNDからの電位差によって0か1かを判断する(NRZ)方式が知られ
ているが、Ethernetでは通信データの共有が目的となるため、データの流れていない情報をNRZでは判断できないという性質がEthernetが
CSMA/CD方式を利用している点から問題になってくる。
つまり、情報が存在しているのかしていないのかを判断できる明確な基準が存在しなければならない。
そこで、10Base-T Ethernetではマンチェスター符号化と呼ばれる信号変化によって情報を伝える符号化を行う事によって情報を識別してい
る。(100Base-TXでは4B5B+MLT-3が用いられている)
受信時にEthernetフレームを受信したか否かを判断するためにEthernetプリアンブル(オルタネートデータ)を受信し、チェックする事で判断でき
る。ちなみに、プリアンブルでオルタネートデータがなぜ利用されているかというと、マンチェスタ符号においてビット境界に対して電位変化
は発生せず、情報ビットの中央で必ず電位が変化するというマンチェスタ符号の性質よりオルタネートデータで示される変化点のタイミングで
以降の情報を見ればよいため。 1bitを2bit化して1を(0,1)、0を(1,0)に変換しているものなので伝送レートとしては20Mbpsで処理しなければなら
ない。10MHzでは100nsで1HzなのでCAT3(16MHzで転送可能)以上のケーブルを使用しなければならない。
基本的にSFDでEthernetかどうかを判断している(機器のクロック差などで生じる情報の損失への対処)ためプリアンブル56bit(つまりプリアンブ
ルからSFDを除外した場合)は計測する必要がないように設計されており、読み捨てても構わない。
SFDより最終2bit(1octet: 0b10101011)が検出されるとそれ移行の情報を計測しながらバッファリングを同時に行い、信号変化が見られなくなっ
た場合受信が完了したものと見なし、フレーム長を確認する。この時、64byte未満または1518byte以上だった場合のフレームは廃棄対象とな
り、廃棄対象にならなかったフレームはL2側へ引き渡される事になる。
EthernetとCSMA/CD
CSMA/CDとはそれぞれに意味があり、CS=(Carrier Sense, 通信パケットの検出), MA=(Multiple Access, 検出を行った後で一本の伝送媒体で複
数台の通信を可能にする)、CD=(Collision Detection, 衝突検知)である。
つまり、通信の衝突の検知したらジャム信号を送る事によってランダムな時間で通信を見送る事によって一本の伝送媒体で複数の通信を行う。
かつて10Base5でイエローケーブル(同軸ケーブル)を利用していた時代では、一つの伝送媒体を利用して通信を行っていたためCSMA/CD方式に
よる伝送制御が必須だった。同様にリピータハブ(Shared-hub)を利用する場合でもCSMA/CDにによる制御は必須である。
しかし、L2スイッチを利用して全二重の通信ができるようになった事により、端末はCollision Senseを行っただけでも検出が行われず回線とコ
リジョンドメインを独占できる状態になっている。 1000Base-Tでは、なんとかCSMA/CDはサポートされているもののコリジョンの検出が難し
くなりpaddingを入れる事でCSMA/CDをサポートした経緯もあり、10Gbps Ethernetでは完全にCSMA/CDは仕様から削除される事になった。
Ethernetとクロックの関係
10Base-Tでは自走方式による発振を行っている。ギガビットEthernetでは従属方式といって送信と受信側でMaster-slave構成のクロックで動作
する。これらの方式の使い分けとしては、伝送路上のデータの密度によって使い分けられる。高速のインタフェースでも伝送対象の密度が低け
れば自走方式でも問題ないという事になり、もともとLAN用に利用されたEthernetインタフェースを利用する事によってクロックの要因による
データの破壊や取りこぼしが発生する可能性が十分に考えられる。 これはシンプルな構成のリピーターなどで見られる現象で受信タイミングと
送信タイミングの差によってバッファが れてしまい結果的にビットの取りこぼしが起きてしまうような状態になる。(最終的にFCSエラーでフ
レームは破棄される)
これらの事から指定された精度内の利用(10MHz±0.01% = 10MHz ± 1kHz)が推奨される。
このクロック精度はかなり大雑把であり、市販の大手メーカーの水晶発振器を利用すればこのようなビットの取りこぼしなどの問題が起きる可
能性は限りなく低い。
Ethernetフレーム
ARP (Address Resolution Protocol)
Last login: Tue Jul
[Rk@17:35:25]~% arp
? (169.254.255.255)
? (203.178.139.129)
? (203.178.139.166)
7 15:37:13 on ttys002
-an
at 0:0:5e:0:1:6b on en0 [ethernet]
at 0:0:5e:0:1:6b on en0 ifscope [ethernet]
at f4:81:39:86:35:da on en0 ifscope [ethernet]
?
?
?
?
?
?
at
at
at
at
at
at
(203.178.139.193)
(203.178.139.221)
(203.178.139.235)
(203.178.139.238)
(203.178.139.252)
(203.178.139.255)
0:23:15:b:53:a8 on en0 ifscope [ethernet]
e8:b1:fc:5:2c:bf on en0 ifscope [ethernet]
b8:8d:12:d:7c:32 on en0 ifscope [ethernet]
28:cf:e9:4b:f6:6d on en0 ifscope [ethernet]
0:23:15:4f:79:9c on en0 ifscope [ethernet]
ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
ICMP (Internet Control Message Protocol)
IP (Internet Protocol)
IPルーティング
[Rk@17:38:34]~% netstat -rn
Routing tables
Internet:
Destination
Gateway
Flags
Refs
Use
default
127
127.0.0.1
169.254
169.254.255.255
203.178.139.129
127.0.0.1
127.0.0.1
link#4
0:0:5e:0:1:6b
UGSc
UCS
UH
UCS
UHLSWi
44
0
1
1
0
0
0
38
0
0
en0
lo0
lo0
en0
en0
203.178.139.128/25
203.178.139.129
203.178.139.165
203.178.139.193
203.178.139.255
link#4
0:0:5e:0:1:6b
127.0.0.1
0:23:15:b:53:a8
ff:ff:ff:ff:ff:ff
UCS
UHLWIir
UHS
UHLWIi
UHLWbI
4
45
0
1
0
0
0
0
42
3
en0
en0
lo0
en0
en0
Netif Expire
186
813
1187
経路MTU探索(Path MTU Discovery, PMTUD)
物理層(Ethernet, PPP over Ethernet, etc)の関係によりIPパケットのサイズはそれぞれ変化する。例えばEthernetだと1500octectだが、PPP over
Ethernetなら1492octectといった具合だ。
IPパケットの理論上のサイズはヘッダに格納されるバイト数にビット幅が16bitである事より65536通りだが実際には物理層の影響を受ける。 そ
のため、MTUというものが存在しルータは転送する前に相手とMTU値を交換して小さいMTU値を採用してパケットを分割して送信する。
しかしこれは経路上のすべてのルーターに対して行われるため、IP分割が行われすぎるとスループットの低下を招き、大きなMTUで送信できた
としてもその先の経路上のMTUが小さければ分割をする必要があるため、ルーターに負荷をかけてしまう。
そこで考えだされたのが経路MTU探索であり、経路上のルーターのMTUからできるだけ分割せずにすむ最大サイズを決定するための処理だ。こ
れはICMPパケットを分割禁止フラグを立てて相手側に送信して相手のMTU値がそれよりも小さい場合ICMPエラーメッセージが返ってくるため
それを相手側のホストに到達するまで繰り返す事によって最適なMTU値を見つけ出す事ができる。
UDP (User Datagram Protocol)
DHCP (Dynamic Host Configuration Protocol)
lwIP (lightweight TCP/IP Protocol Stack)
Realtek RTL8019AS (NE2000 Compatible)
秋月(秋月電子通商)では、10Base-T対応のRealTek社のEthernetコントローラをレギュラーピンで接続できるように変換基板を搭載した
RTL8019ASを700円で販売している。今回はこのEthernetコントローラを利用してNICを作成していく事にする。
これだけではNICは作れないのでローパスフィルタ(LPF)やRJ-45のコネクタ(DIP変換基板付き)も同時に購入した。
RTL8019ASはNE2000互換であり、レジスタなどには互換性がある。さらに、NE2000から回路の集積度が上がった結果、ISAバスへの接続が考
慮されバッファRAMまで搭載されている。そのため、工作としてはクロックとEEPROM, トランスとコネクタ程度でEthernetインタフェースを
構築できる。
NE2000メモリマップ
メモリマップは上のようになっており、ベースアドレスから10h離れた所にデータポートが存在している。
このbase+10h ~ base+17hの間の8byteがデータポートになっており、データバスを8bitで利用する時はこのデータポートには8bitでアクセスす
るが、16bitでバスを利用する場合はbase+10hから16bit幅でアクセスする。しかし、バスを16bitで利用していたとしても必ずデータポート以外
のポートは8bit幅でアクセスしなければならない。
リセットポートは一般的にはbase+18hを利用する。
NE2000には40個もの制御レジスタが先頭base+0h ~ base+10hまでの間にマップされているがレジスタは4つのページに別れており、システム
側からは16個のレジスタがあるようにしか見えない。このページの切り替えはCRレジスタによって操作できる。ページ1はEthernetインタ
フェースの自己診断用、ページ2は将来のために予約されているので使用禁止になっており、ページ3はEPROMやPnPに関するレジスタが配置
されている。
データ送受信の流れ
DP8390はバッファRAMを持っており、その先頭にはMACアドレスを保持する目的で使われるEEPROMの内容がマップされる。一般的には16K
バイト(0x4000 ~ 0x7fff)にバッファRAMがマップされている。バッファRAMは送信バッファと受信バッファに別れており、256byteが1ページに
なっている。これはおそらくリングバッファとして扱うためのレジスタ群が8bitでありそのレジスタに16bit幅アドレスのうちの上位8bitのみを
指定するためだと考えられる。送信バッファはEthernetフレーム(Max:1518octet,FCSなし)を1つだけ格納するだけの1536byteが確保されてお
り、これは6ページに相当している。送信バッファはそのために58ページが利用され、レジスタを利用することによってリングバッファのよう
に利用される。
初期化
基本的にNE2000互換NICの初期化は次のような処理過程を る。
1: リセット/レジスタクリア
2: MACアドレスの取得と設定
3: Tx/Rxバッファの設定
4: 割り込み関連の初期化
リセット/レジスタクリア
ISA_write(CR, (CR_PAGE0 | CR_NODMA | CR_STOP)); // CR <- 21h
_delay_ms(2);
まず、リセットポートに書き込み動作を行ってCRレジスタのSTPビットを立てることによってDP8390を停止状態にさせる。リセット解除の操
作は必要ないが、この動作を行った後数msばかりwaitする必要がある。
MACアドレスの取得と設定
リモートDMA経由でのバッファRAMの読み書き
バッファRAMはDP8390が管理しており、直接CPU側からはアクセスできないようになっている。そこで、リモートDMAとローカルDMAと呼
ばれる方法が存在し、リモートDMAを利用する。以下にTxバッファにデータを書き込む時のコードを例示する。
// Remote Byte Count Register(RBCR)へアクセスバイト数を指定
ISA_write(RBCR0, len & 0xff);
ISA_write(RBCR1, len >> 8);
// Remote Start Address Register(RSAR)へバッファRAMのアクセス開始アドレスを指定
ISA_write(RSAR0, 0x00);
ISA_write(RSAR1, TXSTART);
ISA_write(CR, CR_PAGE0 | CR_START | CR_DMAWRITE);
for(i = 0; i < len; i++) {
ISA_write(RDMA, pkt[i]);
}
基本的、RBCRとRSARへの指定を済ませた後にCRレジスタのモード変更を行うことによってRDMAレジスタ経由でデータを読み書きするよう
になってくる。
AT93C46 EEPROM
固有のMACアドレスが書き込まれているEEPROMチップはMicrowireというNational Semiconductorが提唱しているシリアルインタフェースを
用意している。秋月では重複しないMACアドレスを書き込んだEEPROMを200円で販売している。データシートから以下のようにピンアサイン
を確認。
これをたまたま前回ファミコンのカートリッジダンパを作るために利用したATMega328P-PUとMAX232Cを利用してEEPROMのシリアルイン
タフェースのためのドライバをAVR側に書き込みUART経由でPCに128Byte分を転送する事によってMACアドレスを抽出した。
これをUSBシリアルのttyをscreenでボーレート9600、データ8bitで転送する。
しかしなぜか抽出できず... Microwireインタフェースのドライバの実装が悪いのか....
NICの製作
RTL8019ASはそれ単体でISAを利用する事ができるため、今回ISAバスを利用する。そのためにはISAバススロットを搭載した古いPCが必要に
なってくるが、運良く研究室にISAバスを搭載したPCがあったので今回はそれを利用する。
その次に両面ユニバーサル基板をISAバスの形にPCBカッターで加工する。
この辺りは自分には難しかったためmacchanさんにお願いしてやってもらった。サイズを計測してきちんとスロットに刺さる形にしていく。
無事ISAバスにユニバーサル基板が接続された。次はこの基板にLANコントローラ等を基板上に実装していく。
参考資料
6.828: Xv6, a simple Unix-like teaching operating system
http://pdos.csail.mit.edu/6.828/2014/xv6.html
MainPage - OSDev.org Wiki
http://wiki.osdev.org/Main_Page
Multiboot Specification version 0.6.96 - GNU.org
https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Multiboot Specification - Intel
http://www.intel.com/design/pentium/datashts/24201606.pdf
Protected Mode Memory Addressing - University of New Mexico
http://www.ece.unm.edu/~jimp/310/slides/micro_arch2.html
RTL8019AS: SA-Full Duplex Ethernet Controller with Plug and Play Function
http://www.realtek.com.tw/products/productsView.aspx?Langid=1&PFid=15&Level=4&Conn=3&ProdID=22
オペレーティングシステム 設計と実装第3版 ~The MINIX book: OPERATING SYSTEMS Design and Implementation~
出版:Education Japan, Andrew S. Tanenbaum
モダンオペレーティングシステム 原著第2版
出版:Education Japan, Andrew S. Tanenbaum
Ethernetのしくみとハードウェア設計技法∼プロトコルの詳細からネットワークの対応機器の作成まで∼
出版: CQ出版社, ISBN4789833437
基礎からわかる TCP/IPネットワーク実験プログラミング(Linux/FreeBSD対応) 第二版
出版: オーム社, ISBN4-274-06584-7