xv6の力を借りてPCIデバイスの情報を取得しよう


コンピュータには周辺機器とやり取りするためにPCIという仕組みがあります。

OSはこの仕組みでデバイスの初期化や設定をしてusbやネットワークデバイスなどを利用できるように環境を整備します。

PCIについての情報は以下のページに載っています。
https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/ug/ug_a10_pcie_avmm.pdf

https://wiki.osdev.org/PCI

今回はxv6に処理を追加してqemuで実行します。
qemuがエミュレートするPCIデバイスの情報を取得するということです。

xv6の用意


ますはxv6を手に入れます。
https://github.com/mit-pdos/xv6-public

今更言う必要はないと思いますが、xv6はunixv6をx86アーキテクチャ用に書き直したものです。

xv6への処理の追加


PCIへはout命令とin命令を使ってアクセスします。
使うI/OPortは0xcf8と0xcfcです。


Bus Number,Device Number,Function Number, Offsetを指定してポート0xcf8に書き込み、
in命令でポート0xcfcからデータを取得します。
どちらも32bitのデータを扱います。

これらをソースコードにするとこのようになりました。

uint readpcidata(uint bus, uint device, uint func, uint offset) {
  uint address = (uint)(
                  (bus << 16) |
                  (device << 11) |
                  (func << 8) |
                  offset |
                  (uint)0x80000000
                );

  outl(0xCF8, address);

  return inl(0xcfc);
}


この関数自体はどのファイルに追加しても問題ないと思いますが、main.cにでも追加しておきましょうか。

xv6には32bitのデータをやり取りするためのout命令とin命令の記述が見つからなかったのでこれも実装しておきました。
追加したファイルはx86.hです

static inline int
inl(ushort port)
{
  int data;

  asm volatile("in %1,%0" : "=a" (data) : "d" (port));
  return data;
}

static inline void
outl(ushort port, int data)
{
  asm volatile("out %0,%1" : : "a" (data), "d" (port));
}


これらの関数を呼び出してpciデバイスを出力する処理です。
この関数をmain.cのmain関数内などで呼び出します。
とりあえず重要そうな情報を取得して出力しています。

void lsallpcidevice() {

  for (uint b=0; b<255; b++) {
    for (uint d=0; d<32; d++) {
        uint data = readpcidata(b, d, 0, 0);
        if (data != 0xffffffff) {

          ushort vendor = data & 0xffff;
          ushort device = (data >> 16) & 0xffff;
          cprintf("-------------------------------------------------\n");
          cprintf("vendor id => %x device id %x\n", vendor, device);

          data = readpcidata(b, d, 0, 0x8);
          uchar class_code = (data >> 24) & 0xff;
          uchar sub_class = (data >> 16) & 0xff;

          data = readpcidata(b,d, 0, 0xc);
          uchar header_type = (data >> 16) & 0xff;
          cprintf("class => %x sub_class => %x header type %x\n", class_code, sub_class, header_type);

          data = readpcidata(b, d, 0, 0x3c);
          uchar i_pin = (data >> 8) & 0xff;
          uchar i_line = data & 0xff;

          uint bar0 = readpcidata(b, d, 0, 0x10);
          cprintf("i_pin => %x i_line => %x bar => %x \n\n", i_pin, i_line, bar0);

        }
    }
  }
}

実行


実行するとこうなりました。

-------------------------------------------------
vendor id => 8086 device id 1237
class => 6 sub_class => 0 header type 0
i_pin => 0 i_line => 0 bar => 0 

-------------------------------------------------
vendor id => 8086 device id 7000
class => 6 sub_class => 1 header type 80
i_pin => 0 i_line => 0 bar => 0 

-------------------------------------------------
vendor id => 1234 device id 1111
class => 3 sub_class => 0 header type 0
i_pin => 0 i_line => 0 bar => fd000008 

-------------------------------------------------
vendor id => 8086 device id 100e
class => 2 sub_class => 0 header type 0
i_pin => 1 i_line => b bar => febc0000 


vendor idが8086のものが3つありますがこれはintelのことを指しているそうです。

そして一番下にdeviceが100eのものがありますがこれは82540EM Gigabit Ethernet Controllerという名前のデバイスを指しています。
以下のページにその記述がありました。
https://pci-ids.ucw.cz/read/PC/8086/100e

classコードが2、sub classコードが0となっているため、このデバイスがNetworkControllerというクラスに属するEthernetControllerであるということがわかります。
https://wiki.osdev.org/PCI#The_PCI_Bus

他にもclassが6となっているものはBridgeDevice、3となっているものはDisplayControllerを表しているみたいですね。

これらの情報はすべてインターネットから集めたものなので間違いがないとは言えませんが、
vendor idやdevice idなど実際に該当するものもあるので大丈夫でしょう。

今回実装した方法以外にもPCIへアクセスするためのメカニズムはいろいろあるみたいです。
I/OPortは使わずにメモリを通してアクセスもできるみたいです。

ちなみに初めはOSを作れるところまで作ろうと思ったのですが、
僕にはもうそんな体力はないようでxv6を少しいじるのが関の山でした。