Closed34

ゼロからOS自作入門 6章

ackyacky

準備

$ unlink MikanLoaderPkg # 前に残っていたシンボリックリンクを削除
$ ln -s /mnt/30DayOS/workspace/ch06/MikanLoaderPkg/ ./
$ source edksetup.sh
$ build
ackyacky

マウスカーソル

フォントと同じ

ackyacky

テンプレートは勉強しないといけないな

付録か

この実装自体はテンプレートの形が珍しいだけで難しくない

ackyacky

とりあえず絵を書くだけだから特に苦労する所なし

あとでリファクタリング対象だけどまずはOK

extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
  PixelWriter* writer = GetWriter(frame_buffer_config);

  // Base Screen
  const int kFrameWidth =
      static_cast<int>(frame_buffer_config.horizontal_resolution);
  const int kFrameHeight =
      static_cast<int>(frame_buffer_config.vertical_resolution);
  // 上の青い部分
  FillRectangle(*writer, {0, 0}, {kFrameWidth, kFrameHeight - 50},
                kDesktopBGColor);
  // 下のバー(とりあえず全体を塗りつぶす)
  FillRectangle(*writer, {0, kFrameHeight - 50}, {kFrameWidth, 50},
                kDesktopBlackColor);
  // 左下のグレーの部分(下のバーを上書き)
  FillRectangle(*writer, {0, kFrameHeight - 50}, {kFrameWidth / 5, 50},
                kDesktopDarkGrayColor);
  // 左下の四角枠(更に上書き)
  DrawRectangle(*writer, {10, kFrameHeight - 40}, {30, 30},
                kDesktopLightGrayColor);

  console = new (console_buf) Console{*writer, {0, 0, 0}, {255, 255, 255}};
  printk("Welcome to MikanOS!\n");

  // Write Mouse Cursol
  for (int y = 0; y < kMouseCursorHeight; ++y) {
    for (int x = 0; x < kMouseCursorWidth; ++x) {
      if (mouse_cursor_shape[y][x] == '@') {
        writer->Write(200 + x, 100 + y, kMouseEdgeColor);
      } else if (mouse_cursor_shape[y][x] == '.') {
        writer->Write(200 + x, 100 + y, kMouseInsideColor);
      }
    }
  }

  while (1) __asm__("hlt");
}
ackyacky

  • USBホストコントローラ
    • USB機器とOSの橋渡し
    • 通常はチップが搭載される
  • ドライバ
    • ハードウェアを制御するソフトウェア
説明
ホストコントローラドライバ ホストコントローラを制御するファームウェア
USBバスドライバ ホストコントローラの詳細を隠してUSB規格で決められたAPIを提供
クラスドライバ USBターゲット毎に用意するファームウェア
ackyacky

ホストコントローラドライバは規格毎に作る必要がある

図のようにEHCI/OHCI/UHCI/xHCHIがある

ackyacky

PCIデバイスの探索

やることリスト

  • PCIバスに接続されたPCIデバイスを全て列挙する
  • 列挙されたデバイス一覧からxHCを見つける
  • xHCを初期化する
  • USBバス上でマウスを探す
  • マウスを初期化する
  • マウスからデータを受信する

xHC(Extensible Host Controller)
米インテル(Intel)社が仕様を策定・公開しており、様々な企業がこの仕様に準拠したコントローラICを開発している。USB 3.0で導入されたSuperSpeed USB(5Gbps)での高速な通信に対応する

https://qiita.com/kento_study/items/9cb63107849dde4bef0e

ackyacky
  • PCIコンフィギュレーション空間を読むためにCONFIG_ADDRESSレジスタとCONFIG_DATAレジスタを使う
    • CONFIG_ADDRESSレジスタ(0x0cf8、32bit、Read/Write可)
      • bit0-1:0に固定
      • bit2-7:レジスタアドレス
      • bit8-10:機能番号
      • bit11-15:デバイス番号
      • bit16-23:バス番号
      • bit24-30:リザーブで、0に固定
      • bit31:イネーブルビットで、1に固定
    • CONFIG_DATAレジスタ(0x0cfc~0x0cff、任意のサイズ、Read/Write可)
      • CONFIG_ADDRESSレジスタのイネーブルビットが0の場合は、ここはCONFIG_DATAレジスタにはならない

http://oswiki.osask.jp/?PCI

  1. CONFIG_ADDRESSレジスタを設定する
    • デバイスのバス番号・デバイス番号・機能番号をせっていする
    • アクセスしたい領域のオフセット値を設定する
    • イネーブルビットを1に設定する
  2. CONFIG_DATAレジスタに対して読み込みまたは書き込みを行う

読み書きが終わった後にはイネーブルビットは0にする

ackyacky

ラムダ式も勉強しないといけないけど、ここは簡単

ackyacky

IOポートの書き込みはアセンブラを使うのか

bits 64
section .text

global IoOut32  ; void IoOut32(uint16_t addr, uint32_t data);
IoOut32:
    mov dx, di    ; dx = addr
    mov eax, esi  ; eax = data
    out dx, eax
    ret

global IoIn32  ; uint32_t IoIn32(uint16_t addr);
IoIn32:
    mov dx, di    ; dx = addr
    in eax, dx
    ret

IOアドレス空間はメモリアドレス空間とは完全に別のアドレス空間である。メモリアドレス空間はメインメモリ用で、IOアドレス空間は周辺機器用に用意されている。

この2つの操作を分離する必要がある。また、C++ではIOアドレス空間への書き込みは出来ないのでアセンブリ言語で表現する

X86アセンブラ/x86アーキテクチャを見てもDXレジスタは16bit何だよな

うまいとビット演算をしているんだな。凄いな。

  • eax、esiについて
レジスタの名前 主な用途
EAX 計算の途中の値を覚えておくのに使います
HSPのシステム変数STATに値を返したりするのにも使います
EBX 計算の途中の値を覚えておく場所がEAXの他にも必要なときに使います
ECX ある処理を繰り返したい時、回数をカウントするのに使います
EDX 計算の途中の値を覚えておく場所がEAXの他にも必要なときに使います
ESI メモリのアドレスを覚えておくのに使います
EDI メモリのアドレスを覚えておくのに使います
ESP スタックポインタのアドレスを覚えておくのに使います
EBP スタックフレームのアドレスを覚えておくのに使います
EFLAGS CPUの状態を表すフラグが入っています
計算用には使用できません
  • dx/diについて
64 ビット 32 ビット 16 ビット 8 ビット
RAX EAX AX AH, AL
RBX EBX BX BH, BL
RCX ECX CX CH, CL
RDX EDX DX DH, DL
RSI ESI SI
RDI EDI DI
RBP EBP BP
RIP EIP IP
RSP ESP SP

http://ext-web.edu.sgu.ac.jp/koike/CA14/assembler_content.html
http://hp.vector.co.jp/authors/VA014520/asmhsp/chap2.html

ackyacky
void WriteAddress(uint32_t address) { IoOut32(kConfigAddress, address); }

void WriteData(uint32_t value) { IoOut32(kConfigData, value); }

uint32_t ReadData() { return IoIn32(kConfigData); }

uint16_t ReadVendorId(uint8_t bus, uint8_t device, uint8_t function) {
  WriteAddress(MakeAddress(bus, device, function, 0x00));
  return ReadData() & 0xffffu;
}

uint16_t ReadDeviceId(uint8_t bus, uint8_t device, uint8_t function) {
  WriteAddress(MakeAddress(bus, device, function, 0x00));
  return ReadData() >> 16;
}

uint8_t ReadHeaderType(uint8_t bus, uint8_t device, uint8_t function) {
  WriteAddress(MakeAddress(bus, device, function, 0x0c));
  return (ReadData() >> 16) & 0xffu;
}

uint32_t ReadClassCode(uint8_t bus, uint8_t device, uint8_t function) {
  WriteAddress(MakeAddress(bus, device, function, 0x08));
  return ReadData();
}

uint32_t ReadBusNumbers(uint8_t bus, uint8_t device, uint8_t function) {
  WriteAddress(MakeAddress(bus, device, function, 0x18));
  return ReadData();
}

このあたりを深堀りする

ackyacky

PCI Configuration Space Headerをそのまま読んでいるのか

uint16_t ReadVendorId(uint8_t bus, uint8_t device, uint8_t function) {
  WriteAddress(MakeAddress(bus, device, function, 0x00));
  return ReadData() & 0xffffu;
}
ackyacky
/** @brief PCI デバイスを操作するための基礎データを格納する
 *
 * バス番号,デバイス番号,ファンクション番号はデバイスを特定するのに必須.
 * その他の情報は単に利便性のために加えてある.
 * */
struct Device {
  uint8_t bus, device, function, header_type;
};

/** @brief ScanAllBus() により発見された PCI デバイスの一覧 */
inline std::array<Device, 32> devices;
/** @brief devices の有効な要素の数 */
inline int num_device;
/** @brief PCI デバイスをすべて探索し devices に格納する
 *
 * バス 0 から再帰的に PCI デバイスを探索し,devices の先頭から詰めて書き込む.
 * 発見したデバイスの数を num_devices に設定する.
 */
Error ScanAllBus();

これでdevice一覧を確保するため目の領域を確保するのか。

で、ScanAllBusで全てのデバイスを登録するのか。接続可能なデバイス数が16(bit11-15:デバイス番号)だからそんなに大きくならないのか

Error ScanAllBus() {
  num_device = 0;

  auto header_type = ReadHeaderType(0, 0, 0);
  if (IsSingleFunctionDevice(header_type)) {
    return ScanBus(0);
  }

  for (uint8_t function = 1; function < 8; ++function) {
    if (ReadVendorId(0, 0, function) == 0xffffu) {
      continue;
    }
    if (auto err = ScanBus(function)) {
      return err;
    }
  }
  return Error::kSuccess;
}
ackyacky
  • 機能番号は、0~7の値を取り、一つのデバイスに複数の機能が搭載されている場合に、それらを区別して扱うための番号
  • デバイスが存在するなら、まず機能番号0に何らかの機能が割り振られる
  • 機能番号0のデバイスにマルチファンクションであることを示すビットが1になっている
    • これを見付けたら機能番号1~7を検索する
    • 二番目のファンクションの機能番号が1であるとは限らない

つまり、CONFIG_DATAのオフセット0x0cの23bit目が1ならマルチファンクションデバイスなので、1〜7のファンクション番号を調べる

bool IsSingleFunctionDevice(uint8_t header_type) {
  return (header_type & 0x80u) == 0;
}
ackyacky
  • マルチファンクションデバイス
    • 機能番号の最上位ビットが1である
    • 複数の機能を持っているデバイスである
    • ホストブリッジは複数存在する
      • ファンクション0はバス0、ファンクション1はバス1を担当する
  • シングルファンクションデバイス
    • 機能番号の最上位ビットが0である
    • ホストブリッジはバス0を担当するホストブリッジである

このため、バスのスキャンは次の関数で実現できる

/** @brief 指定のバス番号の各デバイスをスキャンする.
 * 有効なデバイスを見つけたら ScanDevice を実行する.
 */
Error ScanBus(uint8_t bus) {
  for (uint8_t device = 0; device < 32; ++device) {
    if (ReadVendorId(bus, device, 0) == 0xffffu) {
      continue;
    }
    if (auto err = ScanDevice(bus, device)) {
      return err;
    }
  }
  return Error::kSuccess;
}
ackyacky

ベンダ番号が無効値「0xffffu」の場合は処理をしていない。有効値の場合にはデバイスを探索する

/** @brief 指定のデバイス番号の各ファンクションをスキャンする.
 * 有効なファンクションを見つけたら ScanFunction を実行する.
 */
Error ScanDevice(uint8_t bus, uint8_t device) {
  if (auto err = ScanFunction(bus, device, 0)) {
    return err;
  }
  if (IsSingleFunctionDevice(ReadHeaderType(bus, device, 0))) {
    return Error::kSuccess;
  }

  for (uint8_t function = 1; function < 8; ++function) {
    if (ReadVendorId(bus, device, function) == 0xffffu) {
      continue;
    }
    if (auto err = ScanFunction(bus, device, function)) {
      return err;
    }
  }
  return Error::kSuccess;
}
ackyacky

ここでもベンダ番号が有効値の場合には機能をスキャンしている

Error ScanFunction(uint8_t bus, uint8_t device, uint8_t function) {
  auto header_type = ReadHeaderType(bus, device, function);
  if (auto err = AddDevice(bus, device, function, header_type)) {
    return err;
  }

  auto class_code = ReadClassCode(bus, device, function);
  uint8_t base = (class_code >> 24) & 0xffu;
  uint8_t sub = (class_code >> 16) & 0xffu;

  if (base == 0x06u && sub == 0x04u) {
    // standard PCI-PCI bridge
    auto bus_numbers = ReadBusNumbers(bus, device, function);
    uint8_t secondary_bus = (bus_numbers >> 8) & 0xffu;
    return ScanBus(secondary_bus);
  }

  return Error::kSuccess;
}

おお。PCI-PCIブリッジがある場合には再帰的に処理をするのか

  • 060400 PCI to AGP bridge
  • 060401 PCI to PCI bridge(support subtractive decode) ?

http://oswiki.osask.jp/?(PCI)class

ackyacky

最後にインクリメントか

定義時点で32個まで定義しているのでこれ以上は対応しないってやつか

/** @brief devices[num_device] に情報を書き込み num_deviceをインクリメントする.
 */
Error AddDevice(uint8_t bus, uint8_t device, uint8_t function,
                uint8_t header_type) {
  if (num_device == devices.size()) {
    return Error::kFull;
  }

  devices[num_device] = Device{bus, device, function, header_type};
  ++num_device;
  return Error::kSuccess;
}
ackyacky

ポーリングでマウス入力

  • 列挙したPCIデバイスからxHCIを探して初期化してUSBマウスを使えるようにする
  • マウスの動きに合わせてデバイスを動かす
ackyacky
// Intel 製を優先して xHC を探す
  pci::Device* xhc_dev = nullptr;
  for (int i = 0; i < pci::num_device; ++i) {
    if (pci::devices[i].class_code.Match(0x0cu, 0x03u, 0x30u)) {
      xhc_dev = &pci::devices[i];

      if (0x8086 == pci::ReadVendorId(*xhc_dev)) {
        break;
      }
    }
  }

  if (xhc_dev) {
    Log(kInfo, "xHC has been found: %d.%d.%d\n",
        xhc_dev->bus, xhc_dev->device, xhc_dev->function);
  }

クラスコードはいかを指す

  • 0c:シリアルバスのコントローラ
  • 03:USBコントローラ
  • 30:xHCI
ackyacky
-device nec-usb-xhci,id=xhci

QEMUにUSB xHCIを渡す run_image.sh

ackyacky

ちょっと直す。main.cppのPCIデバイス一覧の所

  // show_devices
  auto err = pci::ScanAllBus();
  printk("ScanAllBus: %s\n", err.Name());

  for (int i = 0; i < pci::num_device; ++i) {
    const auto& dev = pci::devices[i];
    auto vendor_id = pci::ReadVendorId(dev.bus, dev.device, dev.function);
    auto class_code = pci::ReadClassCode(dev.bus, dev.device, dev.function);
    printk(
        "%d.%d.%d: vend %04x, class %08x, head %02x / base[%02x], sub[%02x], "
        "interface[%02x]\n",
        dev.bus, dev.device, dev.function, vendor_id, class_code,
        dev.header_type, class_code.base, class_code.sub, class_code.interface);
  }

ackyacky
  /** @brief PCI デバイスのクラスコード */
  struct ClassCode {
    uint8_t base, sub, interface;

    /** @brief ベースクラスが等しい場合に真を返す */
    bool Match(uint8_t b) { return b == base; }
    /** @brief ベースクラスとサブクラスが等しい場合に真を返す */
    bool Match(uint8_t b, uint8_t s) { return Match(b) && s == sub; }
    /** @brief ベース,サブ,インターフェースが等しい場合に真を返す */
    bool Match(uint8_t b, uint8_t s, uint8_t i) {
      return Match(b, s) && i == interface;
    }
  };

ふむ。同一性の確認を関数化しているのか

ackyacky

とりあえず諸々エラーが出たので修正

  • Makefile

    • オプションが増えたりしているので内容を確認
  • ld.lld: error: undefined symbol: __cxa_pure_virtual

main.cppに以下の関数を定義

extern "C" void __cxa_pure_virtual() {
  while (1) __asm__("hlt");
}
  • ld.lld: error: undefined symbol: _exit
  • ld.lld: error: undefined symbol: kill
  • ld.lld: error: undefined symbol: getpid

newlib_support.cに追加

#include <errno.h>
#include <sys/types.h>

void _exit(void) {
  while (1) __asm__("hlt");
}

caddr_t sbrk(int incr) {
  errno = ENOMEM;
  return (caddr_t)-1;
}

int getpid(void) {
  return 1;
}

int kill(int pid, int sig) {
  errno = EINVAL;
  return -1;
}
ackyacky

あとはコードをそのまま流用でOK

このスクラップは2021/08/25にクローズされました