はじめに
BitVisor Advent Calendar 8 日目の記事になります。
諸事情で ゲスト OS が PCI Configuration Space のレジスタにアクセスするときに BitVisor の処理を追いかける必要があったので、読んだ際のメモを残そうと思います。
今回読んだコードは drivers/pci_core.c の pci_config_data_handler() から始まる処理です。
読み間違いなどあるかと思いますので、間違っている箇所があればご指摘いただけると幸いです。
なお、読んだコードのコミットは c7327e831a3718bb767e5399e602ea4866932d3c です。
参考資料
- 
PCI - OSDev Wiki :: https://wiki.osdev.org/PCI#Configuration_Space_Access_Mechanism_.231 
 PCI デバイスのコンフィギュレーション空間へのアクセス方法について書かれています。
- 
BitVisor 2017年の主な変更点 by 榮樂さん :: https://www.bitvisor.org/summit6/slides/bitvisor-summit-6-3-eiraku.pdf 
 今回読んだコードは、2017 年に大きく変更されています。
 その変更内容について説明がこちらの資料には含まれています。
- 
BitVisor 2017年の主な変更点 by 榮樂さん :: https://www.bitvisor.org/summit6/slides/bitvisor-summit-6-3-eiraku.pdf 
- 
BitVisor 2019年の主な変更点 by 榮樂さん:: https://www.slideshare.net/bitvisor/bitvisor-summit-82-bitvisor-2019 
 こちらには仮想デバイスに関連する変更の説明が含まれています。
pci_config_data_handler()
この関数は PCI Configuration Space のレジスタへアクセスする際に、CONFIG_DATA (Port-Mapped I/O の 0xCFC) にゲスト OS がアクセスした際に実行されるハンドラです。この関数がハンドラが呼ばれる前には、CONFIG_ADDRESS レジスタ (Port-Mapped I/O 0xCF8) にコンフィグレーション空間のアドレスが設定されていることが想定されます。
コードは以下の通りです。
int pci_config_data_handler(core_io_t io, union mem *data, void *arg)
{
	pci_config_address_t caddr;
	u8 offset;
	pci_config_pmio_enter ();
	caddr = current_config_addr;
	if (!caddr.allow) {
		/* Not configuration access */
		pci_config_pmio_do (io.dir != CORE_IO_DIR_IN, caddr,
				    io.port - PCI_CONFIG_DATA_PORT, io.size,
				    data);
		goto leave;
	}
	offset = caddr.reg_no * sizeof(u32) + (io.port - PCI_CONFIG_DATA_PORT);
	pci_config_io_handler (NULL, io.dir != CORE_IO_DIR_IN, caddr.bus_no,
			       caddr.device_no, caddr.func_no, offset, io.size,
			       data);
leave:
	pci_config_pmio_leave ();
	return CORE_IO_RET_DONE;
}
pci_config_pmio_enter (); と pci_config_pmio_leave (); はロック関係の処理です(次の節で中身を説明します)。
if (!caddr.allow) {...} のブロックは、CONFIG_ADDR に有効なアドレスが入っていない場合の処理です。この場合、 PCI コンフィグレーション空間へのアクセスとはみなされないので、パススルーしています。
ハンドラの処理本体は pci_config_io_handler() のようです。
ロック機構について
pci_config_pmio_enter() と pci_config_pmio_leave() は CONFIG_ADDR レジスタ用のロック機構です。榮樂さんの資料にも書かれていますがこれは ゲスト OS と BitVisor の間で排他制御するため のロックです。 BitVisor のスレッド間での排他制御のためのものではないので注意がです。
コードは以下の通り
static inline void
pci_config_pmio_addrlock (enum addrlock_mode mode)
{
	static spinlock_t pmio_addr_lock = SPINLOCK_INITIALIZER;
	switch (mode) {
	case ADDR_LOCK:
		spinlock_lock (&pmio_addr_lock);
		break;
	case ADDR_RESTORE_UNLOCK:
		pci_config_pmio_do (false, current_config_addr, 0, 0, NULL);
		/* Fall through */
	case ADDR_UNLOCK:
		spinlock_unlock (&pmio_addr_lock);
		break;
	}
}
/*
  >0: addrlock if count is 0, then count++
  <0: count--, then restore addr and addrunlock if count is 0
  Note:
    Read current_config_addr while count > 0 or addrlock.
    Modify current_config_addr while count == 0 and addrlock.
    Use pci_config_pmio_do() while count > 0 or addrlock.
    Access addr or data port directly instead of using pci_config_pmio_do()
    while count == 0 and addrlock, and restore addr after modifying addr.
*/
static inline void
pci_config_pmio_count (int add)
{
	static spinlock_t pmio_count_lock = SPINLOCK_INITIALIZER;
	static int count;
	spinlock_lock (&pmio_count_lock);
	if (add > 0) {
		if (!count)
			pci_config_pmio_addrlock (ADDR_LOCK);
		count++;
	} else {
		count--;
		if (!count)
			pci_config_pmio_addrlock (ADDR_RESTORE_UNLOCK);
	}
	spinlock_unlock (&pmio_count_lock);
}
void
pci_config_pmio_enter (void)
{
	pci_config_pmio_count (1);
}
void
pci_config_pmio_leave (void)
{
	pci_config_pmio_count (-1);
}
使い方と仕組みは以下の通りです。
- ゲスト OS による CONFIG_ADDR をハンドルする場合
- 直接ロックを取得および開放 (pci_config_pmio_addrlock()を直接呼び出す)
 
- 直接ロックを取得および開放 (
- 上記以外で BitVisor の処理のために CONFIG_ADDR をロックする場合
- クリティカルセクション開始時 (pci_config_pmio_enter()を呼び出す)- ロックを取っていない場合 ( pci_config_pmio_count()のcountが 0) の時にはロックの取得を試みる。取得出来たらcountをインクリメント
- ロックをすでに取っている状態であれば、countのインクリメント
 
- ロックを取っていない場合 ( 
- クリティカルセクション終了時 (pci_config_pmio_leave()を呼び出す)- 非ネスト時 (countが 1) のときは、countをデクリメントして、ロックを開放
- ネスト時 (countが 2 以上) のときはcountのデクリメントのみ
 
- 非ネスト時 (
 
- クリティカルセクション開始時 (
pci_config_io_handler()
コンフィグレーション空間へのアクセスを扱う処理の本体です。
色々なパターンに対応するために複雑になっていますが、基本的には以下の通りです。
- BitVisor が管理するデバイスリストの中からアクセス先のレジスタに対応するものを探す。
- なければアクセス先のデバイスをデバイスリストに登録する
 
- 見つけたデバイスに紐づくデバイスドライバを見つける
- デバイスドライバに紐づくコンフィグレーション空間アクセスに対応するハンドラを呼び出す
- ドライバの config_readやconfig_writeという名前のフィールドに登録されている関数ポインタ
 
- ドライバの 
というわけでコードは以下の通りです。
static void
pci_config_io_handler (struct pci_config_mmio_data *d, bool wr,
		       uint bus_no, uint device_no, uint func_no,
		       uint offset, uint len, void *buf)
{
	enum core_io_ret ioret;
	struct pci_device *dev;
	int (*func) (struct pci_device *dev, u8 iosize, u16 offset,
		     union mem *data);
	pci_config_address_t new_dev_addr;
	static struct pci_device *last_dev;
	static uint last_bus_no, last_device_no, last_func_no;
	struct pci_virtual_device *virtual_dev;
	void (*virtual_func) (struct pci_virtual_device *dev, u8 iosize,
			      u16 offset, union mem *data);
	spinlock_lock (&pci_config_io_lock);
	if (last_bus_no == bus_no && last_device_no == device_no &&
	    last_func_no == func_no && last_dev &&
	    last_dev->address.bus_no == bus_no &&
	    last_dev->address.device_no == device_no &&
	    last_dev->address.func_no == func_no) {
		/* Fast path */
		dev = last_dev;
		goto dev_found;
	}
	if (!bus_no && pci_virtual_devices[device_no]) {
		virtual_dev = pci_virtual_devices[device_no][func_no];
		virtual_func = NULL;
		if (virtual_dev && virtual_dev->driver)
			virtual_func = wr ? virtual_dev->driver->config_write :
				virtual_dev->driver->config_read;
		if (virtual_func)
			virtual_func (virtual_dev, len, offset, buf);
		else if (!wr)
			memset (buf, 0xFF, len);
		if (!wr && !func_no && pci_virtual_devices[device_no][1] &&
		    offset <= PCI_CONFIG_SPACE_GET_OFFSET (header_type) &&
		    offset + len > PCI_CONFIG_SPACE_GET_OFFSET (header_type)) {
			u8 *p = PCI_CONFIG_SPACE_GET_OFFSET (header_type) -
				offset + buf;
			*p |= 0x80; /* Multifunction */
		}
		goto ret;
	}
	LIST_FOREACH (pci_device_list, dev) {
		if (dev->address.bus_no == bus_no &&
		    dev->address.device_no == device_no &&
		    dev->address.func_no == func_no) {
			last_bus_no = bus_no;
			last_device_no = device_no;
			last_func_no = func_no;
			last_dev = dev;
			goto dev_found;
		}
		if (func_no != 0 &&
		    dev->address.bus_no == bus_no &&
		    dev->address.device_no == device_no &&
		    dev->address.func_no == 0 &&
		    dev->config_space.multi_function == 0) {
			/* The guest OS is trying to access a PCI
			   configuration header of a single-function
			   device with function number 1 to 7. The
			   access will be concealed. */
			if (!wr)
				memset (buf, 0xFF, len);
			goto ret;
		}
	}
	new_dev_addr.value = 0;
	new_dev_addr.bus_no = bus_no;
	new_dev_addr.device_no = device_no;
	new_dev_addr.func_no = func_no;
	dev = pci_possible_new_device (new_dev_addr, d);
new_device:
	if (dev) {
		struct pci_driver *driver;
		printf ("[%02X:%02X.%X] New PCI device found.\n",
			bus_no, device_no, func_no);
		driver = pci_find_driver_for_device (dev);
		if (driver) {
			dev->driver = driver;
			driver->new (dev);
		}
		goto connected_dev_found;
	}
	/* Passthrough accesses to PCI configuration space of
	 * non-existent devices.  This behavior is apparently required
	 * for EFI variable access on iMac (Retina 5K, 27-inch, Late
	 * 2015) and MacBook (Retina, 12-inch, Early 2016). */
	if (d)
		pci_readwrite_config_mmio (d, wr, bus_no, device_no, func_no,
					   offset, len, buf);
	else
		pci_readwrite_config_pmio (wr, bus_no, device_no, func_no,
					   offset, len, buf);
	goto ret;
dev_found:
	if (dev->disconnect && pci_reconnect_device (dev, dev->address, d))
		goto new_device;
	if (dev->disconnect) {
		dev = NULL;
		goto new_device;
	}
connected_dev_found:
	if (dev->bridge.yes && wr)
		pci_handle_bridge_pre_config_write (dev, len, offset, buf);
	if (dev->bridge.yes && wr)
		pci_handle_bridge_config_write (dev, len, offset, buf);
	if (dev->driver == NULL)
		goto def;
	if (dev->driver->options.use_base_address_mask_emulation) {
		if (pci_config_emulate_base_address_mask (dev, offset, wr,
							  buf, len))
			goto ret2;
	}
	func = wr ? dev->driver->config_write : dev->driver->config_read;
	if (func) {
		ioret = func (dev, len, offset, buf);
		if (ioret == CORE_IO_RET_DONE)
			goto ret2;
	}
def:
	if (wr)
		pci_handle_default_config_write (dev, len, offset, buf);
	else
		pci_handle_default_config_read (dev, len, offset, buf);
ret2:
	if (dev->bridge.yes && wr)
		pci_handle_bridge_post_config_write (dev, len, offset, buf);
ret:
	spinlock_unlock (&pci_config_io_lock);
}
Fast path
if (last_bus_no == bus_no && last_device_no == device_no && ...) {/* Fast path */ ... goto dev_found;} のブロックは、前回と同じアドレスにアクセスしてきたときの処理です。read-modify-write の際にデバイス検索のコストを減らすためかなと推測しています。
アクセス先が仮想デバイスだった場合
if (!bus_no && pci_virtual_devices[device_no]) {...	goto ret;} のブロックは仮想デバイスに対するアクセスの際の処理です。
if 文の条件で !bus_no となっているのが不思議に見えますが、 BitVisor では仮想デバイスは必ずバス番号 0 のアドレスに配置するようです。
BitVisor Summit 8 での榮樂さんの資料 にも記載があります。
また、最後の if ブロックは、仮想デバイスが multifunction の時に header_type を読みに来た際に multifunction を示すフラグを立てる処理のようです。仮想デバイスが実装された際の当該コミット(685159cf66e7)の説明文にある以下の箇所が該当するかと思います。
The bit 7 in the header type indicating a multifunction device is automatically set by the pci_config_io_handler() function in the pci_core.c if necessary.
なお、 goto 先の ret: はロックを開放して return するだけです。
デバイスリストからアクセス先のデバイスを探す
LIST_FOREACH (pci_device_list, dev) {...}  は BitVisor が管理するデバイスリストからアクセス先アドレスに対応するデバイスを見つける処理です。前半の if (dev->address.bus_no == bus_no && ...) { last_bus_no = bus_no; ... goto dev_found;} はアクセス先のアドレスと完全一致するデバイス(正確にはデバイスのファンクション)が見つかった際の処理です。 last_*_no 変数にあれこれ入れているのは、先述の通り同じアドレスに対する CONFIG_DATA のアクセスの際の Fast path を実現するためです。
後半の if (func_no != 0 && ... && dev->config_space.multi_function == 0) {...goto ret;} は非マルチファンクションのデバイスのファンクション番号 0 番以外にアクセスしようとした際の処理です。存在しないはずのデバイスですので、存在しないかのような挙動をエミュレートしています(Write は破棄、Read は戻り値をフルビットにして返す)
デバイスリストからアクセス先のデバイスが見つからなかったとき
new_dev_addr.value = 0;... dev = pci_possible_new_device (new_dev_addr, d); はアクセス先のデバイスをデバイスリストに追加する処理です。
この処理に到達するパターンは主に 2 つです。
- hotplug されたデバイスの PCI コンフィグレーションにアクセスしようとした場合
- 存在しないデバイスの PCI コンフィグレーションにアクセスしようとした場合
1 の場合は、 pci_possible_new_device() はデバイスリストに追加したデバイスの情報を非 NULL で返します。2 の場合は NULL を返します。
new_device: ラベル以降の if (dev) {... goto connected_dev_found;} は上記 1 の場合に追加したデバイス情報にドライバを紐づけています。
その下にある if (d) ... else ... goto ret; はコメントにもある通り、存在しないデバイスのコンフィグレーションへのアクセスをちゃんとエミュレートするための処理です。ありもしないデバイスへのアクセスなんかエミュレートする必要なさそうですが、これをやらないと一部の Mac がへそを曲げるようですね...
Device を見つけた時
dev_found: ~ connected_dev_found: は見つけたデバイスが切断されている場合の処理です。Mac の thunderbolt デバイスなんかが切断されたりするそうです。
connected_dev_found: から def: までは、見つけたデバイスが切断されていない場合の処理です。
if (dev->bridge.yes && wr) で始まる if ブロックが 2 つありますが、これらはアクセス対象のデバイスが PCI ブリッジだった場合の処理のようです。
if (dev->driver == NULL) goto def; はデバイスに対応するドライバがない場合で、この時はパススルーの処理(通常の読み書きをエミュレート) します。
if (dev->driver->options.use_base_address_mask_emulation){...} のブロックは、コンフィグレーション空間への読み書きが、Base Address Register のマスクを調べるものだった場合に、BitVisor がこれをエミュレーションするという処理を呼び出しています。はっきりした理由はわかりませんが、おそらくこれをパススルーしてしまうと、ゲストがマスクを調べている間に BitVisor が BAR にアクセスすると処理がおかしくなるためではないかと思います(違ったらごめんなさい)。
以降の func = ... から def: までは、デバイスドライバに登録されているコンフィグレーション空間への読み書きに対するハンドラを見つけて呼び出すというものです。
def: ラベル以下にある処理は、BitVisor が本来のコンフィグレーション空間へのアクセスを肩代わりして行う部分です。上記のハンドラが何も処理しなかった場合 (ioret が CORE_IO_RET_DONE じゃない場合) などに行われます。
ret2: すぐ後ではアクセス先が PCI ブリッジだった場合の処理があるようです。
後片付け
最後に 、ret: ではロックの開放を行っています。
まとめ
というわけで PCI コンフィグレーション空間へのアクセスの際の BitVisor の処理について、コードを読んでみたメモになります。誰かの役に立つかどうかわかりませんが、お役に立てば幸いです。