1) PCI 디바이스 소개
2) PCI 디바이스 에뮬레이션 (pci_qdev_realize)
3) edu 디바이스: BAR 등록과 MMIO 읽기/쓰기
4) Northbridge I/O 에뮬레이션
5) SeaBIOS의 BAR 베이스 주소 설정
6) PCI 디바이스 인터럽트
1) PCI 디바이스 소개
PCI는 외부 디바이스를 연결하는 데 쓰이는 일종의 local 버스인데, 주요 기능은 외부 디바이스를 연결하는 거다.
PCI 버스 규격은 20세기 90년대에 제기된 이후로 점차 다른 여러 버스들을 대체했고, 각종 프로세서에서 다 지원하게 됐다.
지금까지도 PCI 버스랑 거기서 발전된 PCIe 버스가 PC에서 광범위하게 쓰이고 있다. 리눅스 시스템에서 lspci 명령어를 입력하면 현재 시스템의 PCI 디바이스를 볼 수 있다
$ lspci
00:00.0 Host bridge: Intel Corporation 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)
00:01.0 PCI bridge: Intel Corporation 440BX/ZX/DX - 82443BX/ZX/DX AGP bridge (rev 01)
00:07.0 ISA bridge: Intel Corporation 82371AB/EB/MB PIIX4 ISA (rev 08)
00:07.1 IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)
00:07.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 08)
00:07.7 System peripheral: VMware Virtual Machine Communication Interface (rev 10)
00:0f.0 VGA compatible controller: VMware SVGA II Adapter
00:10.0 SCSI storage controller: Broadcom / LSI 53c1030 PCI-X Fusion-MPT Dual Ultra320 SCSI (rev 01)
00:11.0 PCI bridge: VMware PCI bridge (rev 02)
00:15.0 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.1 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.2 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.3 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.4 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.5 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.6 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.7 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.0 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.1 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.2 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.3 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.4 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.5 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.6 PCI bridge: VMware PCI Express Root Port (rev 01)
00:16.7 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.0 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.1 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.2 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.3 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.4 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.5 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.6 PCI bridge: VMware PCI Express Root Port (rev 01)
00:17.7 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.0 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.1 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.2 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.3 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.4 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.5 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.6 PCI bridge: VMware PCI Express Root Port (rev 01)
00:18.7 PCI bridge: VMware PCI Express Root Port (rev 01)
02:00.0 USB controller: VMware USB1.1 UHCI Controller
02:01.0 Ethernet controller: Intel Corporation 82545EM Gigabit Ethernet Controller (Copper) (rev 01)
02:02.0 Multimedia audio controller: Ensoniq ES1371 ES1373 / Creative Labs CT2518 (rev 02)
02:03.0 USB controller: VMware USB2 EHCI Controller
02:04.0 SATA controller: VMware SATA AHCI controller
lspci는 디바이스에 있는 PCIe 디바이스 정보를 보는 명령어다. 이 명령어의 여러 가지 매개변수를 조합해서 PCIe 디바이스를 확인하고 PCIe 문제를 찾을 때 매우 유용하게 쓰인다. PCIe 디바이스의 인터럽트 번호 확인, 설정 공간 내용 확인, 설정 공간 레지스터 수정 같은 작업이 다 포함된다.
| tag | 설명 | 예시 |
| -v | 디바이스에 있는 모든 pcie 디바이스의 일부 정보를 표시한다 | lspci -v |
| -vv | 더 많은 정보를 표시하는데, 거의 모든 유용한 정보가 다 포함된다 | lspci -vv |
| -vvv | 상당히 자세한 정보를 표시하는데, 해석할 수 있는 pcie 정보는 다 표시된다 | lspci -vvv |
| -n | 디바이스에 있는 모든 pcie 디바이스의 vendor id와 device id를 표시한다 | lspci -n |
| -x | 디바이스에 있는 모든 pcie 디바이스 설정 공간의 표준 부분(앞 64바이트 혹은 CardBus 브리지의 128바이트)을 표시한다 | lspci -x |
| -xxx | 디바이스에 있는 pcie 디바이스 설정 공간의 모든 내용을 표시한다 | lspci -xxx |
| -xxxx | PCI-X 2.0과 PCI Express 버스에서 사용 가능한 확장(4096바이트) PCI 설정 공간 내용을 표시한다 | lspci -xxxx |
| -b | 모든 pcie 디바이스의 버스 주소를 표시한다 | lspci -b |
| -t | 트리 구조로 pcie 디바이스를 표시하는데, 디바이스에 있는 모든 pcie 버스, 브리지, pcie 디바이스 사이의 연결 관계를 보여줄 수 있다 | lspci -t |
| tag | 설명 | 예시 |
| -s [[[[domain]:]bus]:][slot][.[func]] | domain bus 번호 등 정보를 근거로 지정된 pcie 디바이스의 정보를 본다. 표1의 임의 매개변수와 조합해서 사용할 수 있다 | lspci -vv -s 00:1f.3 |
| -d []:[] | 지정된 device id와 vendor id의 pcie 디바이스 정보를 본다. 표1의 임의 매개변수와 조합해서 사용할 수 있다 | lspci -vv -d 8086:8c22 |
모든 PCI 디바이스가 시스템 안에서 차지하는 위치는 버스 번호(Bus Number), 디바이스 번호(Device Number), 기능 번호(Function Number)로 유일하게 확정된다.
어떤 디바이스는 여러 개의 기능을 가질 수 있는데, 논리적으로는 단독 디바이스라고 볼 수 있다. PCI 버스 위에 브리지 디바이스 하나를 걸 수도 있고, 그 다음에 그 브리지 위에 또 PCI 버스나 다른 버스를 걸 수도 있다. PCI 디바이스 구조는 다음 그림과 같다.

PC 머신의 3단계 버스 구조

PCI 디바이스는 자기만의 독립적인 주소 공간을 갖고 있는데, 이걸 PCI 주소 공간이라고 부른다.
즉, 디바이스 각도에서 본 주소와 CPU 각도에서 본 주소는 본질적으로 같은 주소 공간이 아니다.
이런 격리는 위 그림의 HOST-PCI(HOST/PCI) 주 브리지가 완성하는 거다.
CPU는 주 브리지를 거쳐야만 PCI 디바이스에 접근할 수 있고, PCI 디바이스도 마찬가지로 주 브리지를 거쳐야만 메인 메모리에 접근할 수 있다.
주 브리지의 중요한 역할 중 하나는 프로세서가 접근하는 메모리 주소를 PCI 버스 주소로 변환하는 거다. x86 아키텍처에서는 메모리 주소 공간과 PCI 주소 공간의 구분이 그렇게 명확하지 않은데, 본질적으로는 두 개의 서로 다른 주소 공간이지만 그 주소가 같고 일대일로 대응되기 때문이다.
모든 PCI 디바이스는 다 설정 공간을 갖고 있는데, 이 공간은 최소 256바이트다. 그 중 앞 64바이트는 표준화돼 있어서 모든 디바이스가 다 이 형식이고, 뒷부분 데이터는 디바이스가 결정한다.
PCI 디바이스의 설정 공간은 다음과 같다.
| 31 — 24 | 23 — 16 | 15 — 8 | 7 — 0 | 오프셋 |
| Device ID | Device ID | Vendor ID | Vendor ID | 0x00 |
| Status | Status | Command | Command | 0x04 |
| Class Code | Class Code | Class Code | Revision ID | 0x08 |
| BIST | Header Type | Latency Timer | Cache Line Size | 0x0C |
| Base Address Register0 | 0x10 | |||
| Base Address Register1 | 0x14 | |||
| Base Address Register2 | 0x18 | |||
| Base Address Register3 | 0x1C | |||
| Base Address Register4 | 0x20 | |||
| Base Address Register5 | 0x24 | |||
| Cardbus CIS Pointer | 0x28 | |||
| Subsystem ID | Subsystem ID | Subsystem Vendor ID | Subsystem Vendor ID | 0x2C |
| Expansion ROM Base Address | 0x30 | |||
| Reserved | Reserved | Reserved | Capabilities Pointer | 0x34 |
| Reserved | 0x38 | |||
| MAX_Lat | Min_Gnt | Interrupt Pin | Interrupt Line | 0x3C |
Vendor ID, Device ID, Class Code 는 디바이스의 신원(정체) 을 나타내는 데 쓰이며, 경우에 따라 Subsystem Vendor ID와 Subsystem Device ID도 함께 설정한다.
6개의 Base Address(BAR) 는 PCI 디바이스의 IO/메모리 주소 공간을 나타낸다. (BAR 레지스터는 PCI 디바이스가 사용하는 주소 공간의 베이스 주소를 저장하며, 이 베이스 주소는 그 디바이스가 PCI 버스 영역에서 갖는 주소다.)
가상머신은 이 주소 공간을 통해 디바이스를 읽고 쓰며 제어할 수 있다. 6개 BAR 외에, Expansion ROM base 라는 ROM용 BAR가 하나 더 있을 수 있는데, 일부 PCI 디바이스는 프로세서가 운영체제를 실행하기 전에 기본 초기화 설정을 마쳐야 하기 때문이다.
인터럽트 관련 값은 두 개다.
- IRQ Line = 그 핀이 실제로 컨트롤러의 몇 번 IRQ로 연결됐는지 (0~15)
- 디바이스가 사용하는 인터럽트 번호를 나타낸다.
- 전통적 인터럽트 컨트롤러는 두 개의 8259A(8259) 칩이 캐스케이드(직렬 연결) 된 것으로, 0~15번 라인이 있다.
- IRQ Line은 그중 어느 라인을 쓰는지를 가리킨다.
- IRQ Pin = 디바이스 입장에서 "나는 INTA#~D# 중 몇 번 핀을 쓴다" (디바이스가 보고하는 값, 보통 INTA#)
- PCI 디바이스가 어느 핀으로 인터럽트 컨트롤러에 연결되는지를 나타낸다.
- PCI 버스의 디바이스는 4개의 인터럽트 핀 INTA#~INTD# 로 인터럽트 요청을 보낼 수 있는데, IRQ Pin은 이 핀 번호를 나타낸다.
- 1~4가 각각 INTA#~INTD# 의 네 핀에 해당하고, 대부분의 디바이스는 INTA# 핀을 사용한다.
- 디바이스가 인터럽트를 지원하지 않으면 이 필드는 0이다.
- 이 둘을 이어주는 게 인터럽트 라우팅 테이블이고, BIOS가 부팅 때 만들어 OS에 알려준다.
PCI 버스는 INTA#~INTD# 4개 신호를 보낼 수 있고, 이 신호들은 인터럽트 컨트롤러의 IRQ 핀과 연결된다.
다만 PCI 버스 규격은 PCI 디바이스의 INTx 신호와 인터럽트 컨트롤러 핀이 어떻게 연결되는지를 규정하지 않는다.
그래서 시스템 소프트웨어(보통 BIOS)가 인터럽트 라우팅 테이블을 만들어, 각 PCI 디바이스의 INTx 신호와 인터럽트 컨트롤러의 연결 관계를 기록해 둬야 한다.
핵심은 "PCI 인터럽트는 연결이 고정돼 있지 않다" 는 점이다. PCI 디바이스에는 INTA#~INTD# 4개의 인터럽트 핀이 있는데, 이게 인터럽트 컨트롤러(8259A 또는 I/O APIC)의 어느 입력에 연결될지는 하드웨어마다 다르고 규격에 정해져 있지 않다.
또한 PCI 버스의 부하를 고르게 분산시키기 위해, 보통 PCI 디바이스의 INTx 신호와 컨트롤러 핀의 연결을 서로 엇갈리게(분산되게) 배치한다.
"엇갈리게 배치(부하 분산)"는, 여러 PCI 슬롯의 INTA#들이 전부 같은 IRQ로 몰리지 않도록, 슬롯1의 INTA→IRQ-X, 슬롯2의 INTA→IRQ-Y 식으로 돌려가며(rotate) 연결하는 걸 말한다. 이렇게 하면 인터럽트가 한 라인에 집중되지 않는다.

슬롯 A 위의 디바이스와 슬롯 B, C 위의 디바이스는 서로 다른 신호선으로 인터럽트 제어기의 IRQY# 핀에 연결되고, 다른 것도 비슷하다. 이런 연결 방식은 또 모든 슬롯의 인터럽트 신호 INTA가 서로 다른 인터럽트 디바이스 핀에 연결되도록 한다.
2) PCI 디바이스의 에뮬레이션 (pci_qdev_realize)
QEMU가 에뮬레이션하는 디바이스는 대부분이 PCI 디바이스다. 모든 디바이스 클래스랑 비슷하게, PCI 디바이스의 부모 디바이스도 TYPE_DEVICE다.
static const TypeInfo pci_device_type_info = {
.name = TYPE_PCI_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(PCIDevice),
.abstract = true,
.class_size = sizeof(PCIDeviceClass),
.class_init = pci_device_class_init,
.class_base_init = pci_device_class_base_init,
};
static void pci_register_types(void)
{
type_register_static(&pci_bus_info);
type_register_static(&pcie_bus_info);
type_register_static(&cxl_bus_info);
type_register_static(&conventional_pci_interface_info);
type_register_static(&cxl_interface_info);
type_register_static(&pcie_interface_info);
type_register_static(&pci_device_type_info);
}
type_init(pci_register_types)
typedef struct TypeInfo TypeInfo;
/**
* struct TypeInfo:
* @name: The name of the type.
* @parent: The name of the parent type.
* @instance_size: The size of the object (derivative of #Object). If
* @instance_size is 0, then the size of the object will be the size of the
* parent object.
* @instance_align: The required alignment of the object. If @instance_align
* is 0, then normal malloc alignment is sufficient; if non-zero, then we
* must use qemu_memalign for allocation.
* @instance_init: This function is called to initialize an object. The parent
* class will have already been initialized so the type is only responsible
* for initializing its own members.
* @instance_post_init: This function is called to finish initialization of
* an object, after all @instance_init functions were called.
* @instance_finalize: This function is called during object destruction. This
* is called before the parent @instance_finalize function has been called.
* An object should only free the members that are unique to its type in this
* function.
* @abstract: If this field is true, then the class is considered abstract and
* cannot be directly instantiated.
* @class_size: The size of the class object (derivative of #ObjectClass)
* for this object. If @class_size is 0, then the size of the class will be
* assumed to be the size of the parent class. This allows a type to avoid
* implementing an explicit class type if they are not adding additional
* virtual functions.
* @class_init: This function is called after all parent class initialization
* has occurred to allow a class to set its default virtual method pointers.
* This is also the function to use to override virtual methods from a parent
* class.
* @class_base_init: This function is called for all base classes after all
* parent class initialization has occurred, but before the class itself
* is initialized. This is the function to use to undo the effects of
* memcpy from the parent class to the descendants.
* @class_data: Data to pass to the @class_init,
* @class_base_init. This can be useful when building dynamic
* classes.
* @interfaces: The list of interfaces associated with this type. This
* should point to a static array that's terminated with a zero filled
* element.
*/
struct TypeInfo
{
const char *name;
const char *parent;
size_t instance_size;
size_t instance_align;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract;
size_t class_size;
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void *class_data;
InterfaceInfo *interfaces;
};
여기, TypeInfo 즉 struct TypeInfo의 객체 pci_device_type_info로 말하자면, 그 class_init(함수 포인터) 멤버는 pci_device_class_init 함수를 가리킨다.
static void pci_device_class_init(ObjectClass *klass, void *data)
{
DeviceClass *k = DEVICE_CLASS(klass);
k->realize = pci_qdev_realize;
k->unrealize = pci_qdev_unrealize;
k->bus_type = TYPE_PCI_BUS;
device_class_set_props(k, pci_props);
}
PCI 클래스 초기화 함수에서 PCIDeviceClass 베이스 클래스 객체 DeviceClass의 realize와 unrealize 함수를 설정한다.
bus_type은 디바이스가 걸리는 버스를 표시하고;
props는 PCI 디바이스에 어떤 속성이 있는지 표시한다.
이런 속성들은 다 명령행으로 지정할 수 있다. 마찬가지로, 단독 PCI 디바이스는 존재하지 않고, PCI 디바이스도 추상 클래스다.
TypeInfo와 class_init 관계도

PCI 디바이스의 realize 함수는 pci_qdev_realize다.
static void pci_qdev_realize(DeviceState *qdev, Error **errp)
{
PCIDevice *pci_dev = (PCIDevice *)qdev;
PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
ObjectClass *klass = OBJECT_CLASS(pc);
Error *local_err = NULL;
bool is_default_rom;
uint16_t class_id;
/*
* capped by systemd (see: udev-builtin-net_id.c)
* as it's the only known sane honor it to avoid users
* misconfigure QEMU and then wonder why acpi-index doesn't work
*/
if (pci_dev->acpi_index > ONBOARD_INDEX_MAX) {
error_setg(errp, "acpi-index should be less or equal to %u",
ONBOARD_INDEX_MAX);
return;
}
/*
* make sure that acpi-index is unique across all present PCI devices
*/
if (pci_dev->acpi_index) {
GSequence *used_indexes = pci_acpi_index_list();
if (g_sequence_lookup(used_indexes,
GINT_TO_POINTER(pci_dev->acpi_index),
g_cmp_uint32, NULL)) {
error_setg(errp, "a PCI device with acpi-index = %" PRIu32
" already exist", pci_dev->acpi_index);
return;
}
g_sequence_insert_sorted(used_indexes,
GINT_TO_POINTER(pci_dev->acpi_index),
g_cmp_uint32, NULL);
}
if (pci_dev->romsize != -1 && !is_power_of_2(pci_dev->romsize)) {
error_setg(errp, "ROM size %u is not a power of two", pci_dev->romsize);
return;
}
/* initialize cap_present for pci_is_express() and pci_config_size(),
* Note that hybrid PCIs are not set automatically and need to manage
* QEMU_PCI_CAP_EXPRESS manually */
if (object_class_dynamic_cast(klass, INTERFACE_PCIE_DEVICE) &&
!object_class_dynamic_cast(klass, INTERFACE_CONVENTIONAL_PCI_DEVICE)) {
pci_dev->cap_present |= QEMU_PCI_CAP_EXPRESS;
}
if (object_class_dynamic_cast(klass, INTERFACE_CXL_DEVICE)) {
pci_dev->cap_present |= QEMU_PCIE_CAP_CXL;
}
pci_dev = do_pci_register_device(pci_dev,
object_get_typename(OBJECT(qdev)),
pci_dev->devfn, errp);
if (pci_dev == NULL)
return;
if (pc->realize) {
pc->realize(pci_dev, &local_err);
if (local_err) {
error_propagate(errp, local_err);
do_pci_unregister_device(pci_dev);
return;
}
}
/*
* A PCIe Downstream Port that do not have ARI Forwarding enabled must
* associate only Device 0 with the device attached to the bus
* representing the Link from the Port (PCIe base spec rev 4.0 ver 0.3,
* sec 7.3.1).
* With ARI, PCI_SLOT() can return non-zero value as the traditional
* 5-bit Device Number and 3-bit Function Number fields in its associated
* Routing IDs, Requester IDs and Completer IDs are interpreted as a
* single 8-bit Function Number. Hence, ignore ARI capable devices.
*/
if (pci_is_express(pci_dev) &&
!pcie_find_capability(pci_dev, PCI_EXT_CAP_ID_ARI) &&
pcie_has_upstream_port(pci_dev) &&
PCI_SLOT(pci_dev->devfn)) {
warn_report("PCI: slot %d is not valid for %s,"
" parent device only allows plugging into slot 0.",
PCI_SLOT(pci_dev->devfn), pci_dev->name);
}
if (pci_dev->failover_pair_id) {
if (!pci_bus_is_express(pci_get_bus(pci_dev))) {
error_setg(errp, "failover primary device must be on "
"PCIExpress bus");
pci_qdev_unrealize(DEVICE(pci_dev));
return;
}
class_id = pci_get_word(pci_dev->config + PCI_CLASS_DEVICE);
if (class_id != PCI_CLASS_NETWORK_ETHERNET) {
error_setg(errp, "failover primary device is not an "
"Ethernet device");
pci_qdev_unrealize(DEVICE(pci_dev));
return;
}
if ((pci_dev->cap_present & QEMU_PCI_CAP_MULTIFUNCTION)
|| (PCI_FUNC(pci_dev->devfn) != 0)) {
error_setg(errp, "failover: primary device must be in its own "
"PCI slot");
pci_qdev_unrealize(DEVICE(pci_dev));
return;
}
qdev->allow_unplug_during_migration = true;
}
/* rom loading */
is_default_rom = false;
if (pci_dev->romfile == NULL && pc->romfile != NULL) {
pci_dev->romfile = g_strdup(pc->romfile);
is_default_rom = true;
}
pci_add_option_rom(pci_dev, is_default_rom, &local_err);
if (local_err) {
error_propagate(errp, local_err);
pci_qdev_unrealize(DEVICE(pci_dev));
return;
}
pci_set_power(pci_dev, true);
pci_dev->msi_trigger = pci_msi_trigger;
}
pci_qdev_realize 함수는 주로 세 가지 방면의 작업을 포함한다.
pci_qdev_realize의 세 가지 작업 개요

PCI 디바이스의 에뮬레이션 - 첫 번째 작업: do_pci_register_device
(1) 먼저 do_pci_register_device 함수를 호출해서 등록을 진행한다.
pci_dev = do_pci_register_device(pci_dev,
object_get_typename(OBJECT(qdev)),
pci_dev->devfn, errp);
if (pci_dev == NULL)
return;
/* -1 for devfn means auto assign */
static PCIDevice *do_pci_register_device(PCIDevice *pci_dev,
const char *name, int devfn,
Error **errp)
{
PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
PCIConfigReadFunc *config_read = pc->config_read;
PCIConfigWriteFunc *config_write = pc->config_write;
Error *local_err = NULL;
DeviceState *dev = DEVICE(pci_dev);
PCIBus *bus = pci_get_bus(pci_dev);
bool is_bridge = IS_PCI_BRIDGE(pci_dev);
/* Only pci bridges can be attached to extra PCI root buses */
if (pci_bus_is_root(bus) && bus->parent_dev && !is_bridge) {
error_setg(errp,
"PCI: Only PCI/PCIe bridges can be plugged into %s",
bus->parent_dev->name);
return NULL;
}
if (devfn < 0) {
for(devfn = bus->devfn_min ; devfn < ARRAY_SIZE(bus->devices);
devfn += PCI_FUNC_MAX) {
if (pci_bus_devfn_available(bus, devfn) &&
!pci_bus_devfn_reserved(bus, devfn)) {
goto found;
}
}
error_setg(errp, "PCI: no slot/function available for %s, all in use "
"or reserved", name);
return NULL;
found: ;
} else if (pci_bus_devfn_reserved(bus, devfn)) {
error_setg(errp, "PCI: slot %d function %d not available for %s,"
" reserved",
PCI_SLOT(devfn), PCI_FUNC(devfn), name);
return NULL;
} else if (!pci_bus_devfn_available(bus, devfn)) {
error_setg(errp, "PCI: slot %d function %d not available for %s,"
" in use by %s,id=%s",
PCI_SLOT(devfn), PCI_FUNC(devfn), name,
bus->devices[devfn]->name, bus->devices[devfn]->qdev.id);
return NULL;
} /*
* Populating function 0 triggers a scan from the guest that
* exposes other non-zero functions. Hence we need to ensure that
* function 0 wasn't added yet.
*/
else if (dev->hotplugged &&
!pci_is_vf(pci_dev) &&
pci_get_function_0(pci_dev)) {
error_setg(errp, "PCI: slot %d function 0 already occupied by %s,"
" new func %s cannot be exposed to guest.",
PCI_SLOT(pci_get_function_0(pci_dev)->devfn),
pci_get_function_0(pci_dev)->name,
name);
return NULL;
}
pci_dev->devfn = devfn;
pci_dev->requester_id_cache = pci_req_id_cache_get(pci_dev);
pstrcpy(pci_dev->name, sizeof(pci_dev->name), name);
memory_region_init(&pci_dev->bus_master_container_region, OBJECT(pci_dev),
"bus master container", UINT64_MAX);
address_space_init(&pci_dev->bus_master_as,
&pci_dev->bus_master_container_region, pci_dev->name);
if (phase_check(PHASE_MACHINE_READY)) {
pci_init_bus_master(pci_dev);
}
pci_dev->irq_state = 0;
pci_config_alloc(pci_dev);
pci_config_set_vendor_id(pci_dev->config, pc->vendor_id);
pci_config_set_device_id(pci_dev->config, pc->device_id);
pci_config_set_revision(pci_dev->config, pc->revision);
pci_config_set_class(pci_dev->config, pc->class_id);
if (!is_bridge) {
if (pc->subsystem_vendor_id || pc->subsystem_id) {
pci_set_word(pci_dev->config + PCI_SUBSYSTEM_VENDOR_ID,
pc->subsystem_vendor_id);
pci_set_word(pci_dev->config + PCI_SUBSYSTEM_ID,
pc->subsystem_id);
} else {
pci_set_default_subsystem_id(pci_dev);
}
} else {
/* subsystem_vendor_id/subsystem_id are only for header type 0 */
assert(!pc->subsystem_vendor_id);
assert(!pc->subsystem_id);
}
pci_init_cmask(pci_dev);
pci_init_wmask(pci_dev);
pci_init_w1cmask(pci_dev);
if (is_bridge) {
pci_init_mask_bridge(pci_dev);
}
pci_init_multifunction(bus, pci_dev, &local_err);
if (local_err) {
error_propagate(errp, local_err);
do_pci_unregister_device(pci_dev);
return NULL;
}
if (!config_read)
config_read = pci_default_read_config;
if (!config_write)
config_write = pci_default_write_config;
pci_dev->config_read = config_read;
pci_dev->config_write = config_write;
bus->devices[devfn] = pci_dev;
pci_dev->version_id = 2; /* Current pci device vmstate version */
return pci_dev;
}
do_pci_register_device 함수는 디바이스랑 그에 대응하는 PCI 버스 위의 일부 초기화 작업을 완성한다.
do_pci_register_device의 다섯 단계

1) 지정된 devfn이 -1이면, 버스 자신이 슬롯을 선택하는 걸 표시한다. 슬롯을 얻은 다음 PCIDevice의 devfn(즉 pci_dev->devfn)에 저장한다; 디바이스 명령행에 addr이 지정돼 있으면, addr이 디바이스의 devfn으로 쓰인다.
if (devfn < 0) {
for(devfn = bus->devfn_min ; devfn < ARRAY_SIZE(bus->devices);
devfn += PCI_FUNC_MAX) {
if (pci_bus_devfn_available(bus, devfn) &&
!pci_bus_devfn_reserved(bus, devfn)) {
goto found;
}
}
error_setg(errp, "PCI: no slot/function available for %s, all in use "
"or reserved", name);
return NULL;
found: ;
} else if (pci_bus_devfn_reserved(bus, devfn)) {
error_setg(errp, "PCI: slot %d function %d not available for %s,"
" reserved",
PCI_SLOT(devfn), PCI_FUNC(devfn), name);
return NULL;
} else if (!pci_bus_devfn_available(bus, devfn)) {
error_setg(errp, "PCI: slot %d function %d not available for %s,"
" in use by %s,id=%s",
PCI_SLOT(devfn), PCI_FUNC(devfn), name,
bus->devices[devfn]->name, bus->devices[devfn]->qdev.id);
return NULL;
} /*
* Populating function 0 triggers a scan from the guest that
* exposes other non-zero functions. Hence we need to ensure that
* function 0 wasn't added yet.
*/
else if (dev->hotplugged &&
!pci_is_vf(pci_dev) &&
pci_get_function_0(pci_dev)) {
error_setg(errp, "PCI: slot %d function 0 already occupied by %s,"
" new func %s cannot be exposed to guest.",
PCI_SLOT(pci_get_function_0(pci_dev)->devfn),
pci_get_function_0(pci_dev)->name,
name);
return NULL;
}
pci_dev->devfn = devfn;
pci_dev->requester_id_cache = pci_req_id_cache_get(pci_dev);
pstrcpy(pci_dev->name, sizeof(pci_dev->name), name);
2) 그 다음으로 PCIDevice 구조체 안의 각 영역을 설정한다. pci_init_bus_master 함수를 호출해서 PCIDevice 안의 Address 멤버 bus_master_as와 그에 대응하는 MR을 초기화하는 것도 포함된다.
memory_region_init(&pci_dev->bus_master_container_region, OBJECT(pci_dev),
"bus master container", UINT64_MAX);
address_space_init(&pci_dev->bus_master_as,
&pci_dev->bus_master_container_region, pci_dev->name);
if (phase_check(PHASE_MACHINE_READY)) {
pci_init_bus_master(pci_dev);
}
3) 그 다음으로, pci_config_alloc 함수를 호출해서 PCI 디바이스의 설정 공간을 할당한다. cmask는 관련된 능력을 검출하는 데 쓰이고, wmask는 제어 읽기 쓰기를 하는 데 쓰이고, w1cmask는 RW1C를 실현하는 데 쓰인다. 이로부터 일부 초기화 설정, 예를 들면 vendor_id 등을 완성한다.
pci_config_alloc(pci_dev);
pci_config_set_vendor_id(pci_dev->config, pc->vendor_id);
pci_config_set_device_id(pci_dev->config, pc->device_id);
pci_config_set_revision(pci_dev->config, pc->revision);
pci_config_set_class(pci_dev->config, pc->class_id);
if (!is_bridge) {
if (pc->subsystem_vendor_id || pc->subsystem_id) {
pci_set_word(pci_dev->config + PCI_SUBSYSTEM_VENDOR_ID,
pc->subsystem_vendor_id);
pci_set_word(pci_dev->config + PCI_SUBSYSTEM_ID,
pc->subsystem_id);
} else {
pci_set_default_subsystem_id(pci_dev);
}
} else {
/* subsystem_vendor_id/subsystem_id are only for header type 0 */
assert(!pc->subsystem_vendor_id);
assert(!pc->subsystem_id);
}
pci_init_cmask(pci_dev);
pci_init_wmask(pci_dev);
pci_init_w1cmask(pci_dev);
if (is_bridge) {
pci_init_mask_bridge(pci_dev);
}
pci_init_multifunction(bus, pci_dev, &local_err);
if (local_err) {
error_propagate(errp, local_err);
do_pci_unregister_device(pci_dev);
return NULL;
}
4) 그 다음으로 디바이스의 config_read와 config_write 함수를 설정한다. 만약 관련된 자식 클래스 자신이 설정하지 않았으면, 그러면 기본값 pci_default_read/write_config 함수를 사용한다.
if (!config_read)
config_read = pci_default_read_config;
if (!config_write)
config_write = pci_default_write_config;
pci_dev->config_read = config_read;
pci_dev->config_write = config_write;
5) 마지막으로, 이 device를 bus->devices 배열 안에 복제한다.
bus->devices[devfn] = pci_dev;
pci_dev->version_id = 2; /* Current pci device vmstate version */
PCI 디바이스의 에뮬레이션 - 두 번째 작업: pc->realize
그 다음으로, pci_qdev_realize 함수는 PCI 디바이스가 속한 class의 realize 함수, 즉 pc->realize 함수를 호출한다.
if (pc->realize) {
pc->realize(pci_dev, &local_err);
if (local_err) {
error_propagate(errp, local_err);
do_pci_unregister_device(pci_dev);
return;
}
}
pc는 PCIDeviceClass 구조체 타입의 포인터다.
#define TYPE_PCI_DEVICE "pci-device"
typedef struct PCIDeviceClass PCIDeviceClass;
DECLARE_OBJ_CHECKERS(PCIDevice, PCIDeviceClass,
PCI_DEVICE, TYPE_PCI_DEVICE)
struct PCIDeviceClass {
DeviceClass parent_class;
void (*realize)(PCIDevice *dev, Error **errp);
PCIUnregisterFunc *exit;
PCIConfigReadFunc *config_read;
PCIConfigWriteFunc *config_write;
uint16_t vendor_id;
uint16_t device_id;
uint8_t revision;
uint16_t class_id;
uint16_t subsystem_vendor_id; /* only for header type = 0 */
uint16_t subsystem_id; /* only for header type = 0 */
const char *romfile; /* rom bar */
};
PCI_DEVICE_GET_CLASS 매크로 추적 흐름

PCI_DEVICE_GET_CLASS를 명확하게 이해하려면, 먼저 DECLARE_OBJ_CHECKERS부터 확인해야한다.
#define TYPE_PCI_DEVICE "pci-device"
typedef struct PCIDeviceClass PCIDeviceClass;
DECLARE_OBJ_CHECKERS(PCIDevice, PCIDeviceClass,
PCI_DEVICE, TYPE_PCI_DEVICE)
/**
* DECLARE_OBJ_CHECKERS:
* @InstanceType: instance struct name
* @ClassType: class struct name
* @OBJ_NAME: the object name in uppercase with underscore separators
* @TYPENAME: type name
*
* Direct usage of this macro should be avoided, and the complete
* OBJECT_DECLARE_TYPE macro is recommended instead.
*
* This macro will provide the three standard type cast functions for a
* QOM type.
*/
#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
\
DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)
실제 값을 대입해서 풀어보면, 다음과 같이 된다.
DECLARE_INSTANCE_CHECKER(PCIDevice, PCI_DEVICE, TYPE_PCI_DEVICE)
DECLARE_CLASS_CHECKERS(PCIDeviceClass, PCI_DEVICE, TYPE_PCI_DEVICE)
/**
* DECLARE_INSTANCE_CHECKER:
* @InstanceType: instance struct name
* @OBJ_NAME: the object name in uppercase with underscore separators
* @TYPENAME: type name
*
* Direct usage of this macro should be avoided, and the complete
* OBJECT_DECLARE_TYPE macro is recommended instead.
*
* This macro will provide the instance type cast functions for a
* QOM type.
*/
#define DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
static inline G_GNUC_UNUSED InstanceType * \
OBJ_NAME(const void *obj) \
{ return OBJECT_CHECK(InstanceType, obj, TYPENAME); }
실제 값을 대입해서 풀어보면
static inline G_GNUC_UNUSED PCIDevice* PCI_DEVICE(const void *obj)
{
return OBJECT_CHECK(PCIDevice, obj, TYPE_PCI_DEVICE);
}
/**
* DECLARE_CLASS_CHECKERS:
* @ClassType: class struct name
* @OBJ_NAME: the object name in uppercase with underscore separators
* @TYPENAME: type name
*
* Direct usage of this macro should be avoided, and the complete
* OBJECT_DECLARE_TYPE macro is recommended instead.
*
* This macro will provide the class type cast functions for a
* QOM type.
*/
#define DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME) \
static inline G_GNUC_UNUSED ClassType * \
OBJ_NAME##_GET_CLASS(const void *obj) \
{ return OBJECT_GET_CLASS(ClassType, obj, TYPENAME); } \
\
static inline G_GNUC_UNUSED ClassType * \
OBJ_NAME##_CLASS(const void *klass) \
{ return OBJECT_CLASS_CHECK(ClassType, klass, TYPENAME); }
실제 값을 대입해서 풀어보면
static inline G_GNUC_UNUSED PCIDeviceClass* PCI_DEVICE_GET_CLASS(const void *obj)
{
return OBJECT_GET_CLASS(PCIDeviceClass, obj, TYPE_PCI_DEVICE);
}
static inline G_GNUC_UNUSED PCIDeviceClass* PCI_DEVICE_CLASS(const void *klass)
{
return OBJECT_CLASS_CHECK(PCIDeviceClass, klass, TYPE_PCI_DEVICE);
}
두 식을 합쳐서, 최종적으로 다음을 얻는다.
static inline G_GNUC_UNUSED PCIDevice* PCI_DEVICE(const void *obj)
{
return OBJECT_CHECK(PCIDevice, obj, TYPE_PCI_DEVICE);
}
static inline G_GNUC_UNUSED PCIDeviceClass* PCI_DEVICE_GET_CLASS(const void *obj)
{
return OBJECT_GET_CLASS(PCIDeviceClass, obj, TYPE_PCI_DEVICE);
}
static inline G_GNUC_UNUSED PCIDeviceClass* PCI_DEVICE_CLASS(const void *klass)
{
return OBJECT_CLASS_CHECK(PCIDeviceClass, klass, TYPE_PCI_DEVICE);
}
이렇게 해서, 최종적으로 PCI_DEVICE_GET_CLASS도 생성된다고 말할 수 있다.
좀 더 깊이 따라가보면, OBJECT_CHECK, OBJECT_GET_CLASS, OBJECT_CLASS_CHECK 세 개의 매크로 정의는 다 include/qom/object.h 안에 있고, 코드는 다음과 같다.
/**
* OBJECT_CHECK:
* @type: The C type to use for the return value.
* @obj: A derivative of @type to cast.
* @name: The QOM typename of @type
*
* A type safe version of @object_dynamic_cast_assert. Typically each class
* will define a macro based on this type to perform type safe dynamic_casts to
* this object type.
*
* If an invalid object is passed to this function, a run time assert will be
* generated.
*/
#define OBJECT_CHECK(type, obj, name) \
((type *)object_dynamic_cast_assert(OBJECT(obj), (name), \
__FILE__, __LINE__, __func__))
/**
* OBJECT_CLASS_CHECK:
* @class_type: The C type to use for the return value.
* @class: A derivative class of @class_type to cast.
* @name: the QOM typename of @class_type.
*
* A type safe version of @object_class_dynamic_cast_assert. This macro is
* typically wrapped by each type to perform type safe casts of a class to a
* specific class type.
*/
#define OBJECT_CLASS_CHECK(class_type, class, name) \
((class_type *)object_class_dynamic_cast_assert(OBJECT_CLASS(class), (name), \
__FILE__, __LINE__, __func__))
/**
* OBJECT_GET_CLASS:
* @class: The C type to use for the return value.
* @obj: The object to obtain the class for.
* @name: The QOM typename of @obj.
*
* This function will return a specific class for a given object. Its generally
* used by each type to provide a type safe macro to get a specific class type
* from an object.
*/
#define OBJECT_GET_CLASS(class, obj, name) \
OBJECT_CLASS_CHECK(class, object_get_class(OBJECT(obj)), name)
PCI 디바이스의 에뮬레이션 - 세 번째 작업: pci_add_option_rom
마지막으로, pci_add_option_rom 함수를 호출해서 PCI 디바이스의 ROM을 로드한다.
/* rom loading */
is_default_rom = false;
if (pci_dev->romfile == NULL && pc->romfile != NULL) {
pci_dev->romfile = g_strdup(pc->romfile);
is_default_rom = true;
}
pci_add_option_rom(pci_dev, is_default_rom, &local_err);
if (local_err) {
error_propagate(errp, local_err);
pci_qdev_unrealize(DEVICE(pci_dev));
return;
}
어떤 디바이스는 자기만의 ROM을 갖고 있는데 (어떤 PCI 디바이스는 프로세서에서 운영체제를 실행하기 전에 기본 초기화 설정을 완료해야 한다, 예를 들면 그래픽카드, 키보드, 하드디스크 같은 디바이스).
이 "사전 실행" 기능을 실현하기 위해서, PCI 디바이스는 ROM 프로그램 한 단락을 제공해야 한다.
만약 QEMU 명령행에 ROM이 지정되지 않았는데, 그 디바이스의 class가 ROM을 지정했으면, 그러면 기본 ROM을 사용한다.
이런 경우에는, pci_add_option_rom 함수의 매개변수 is_default_rom이 true로 설정된다.
그렇지 않으면 QEMU 명령행에서 전달된 romfile 파일을 사용한다.
ROM 로딩 분기 흐름

/* Add an option rom for the device */
static void pci_add_option_rom(PCIDevice *pdev, bool is_default_rom,
Error **errp)
{
int64_t size = 0;
g_autofree char *path = NULL;
char name[32];
const VMStateDescription *vmsd;
/*
* In case of incoming migration ROM will come with migration stream, no
* reason to load the file. Neither we want to fail if local ROM file
* mismatches with specified romsize.
*/
bool load_file = !runstate_check(RUN_STATE_INMIGRATE);
if (!pdev->romfile || !strlen(pdev->romfile)) {
return;
}
if (!pdev->rom_bar) {
/*
* Load rom via fw_cfg instead of creating a rom bar,
* for 0.11 compatibility.
*/
int class = pci_get_word(pdev->config + PCI_CLASS_DEVICE);
/*
* Hot-plugged devices can't use the option ROM
* if the rom bar is disabled.
*/
if (DEVICE(pdev)->hotplugged) {
error_setg(errp, "Hot-plugged device without ROM bar"
" can't have an option ROM");
return;
}
if (class == 0x0300) {
rom_add_vga(pdev->romfile);
} else {
rom_add_option(pdev->romfile, -1);
}
return;
}
if (load_file || pdev->romsize == -1) {
path = qemu_find_file(QEMU_FILE_TYPE_BIOS, pdev->romfile);
if (path == NULL) {
path = g_strdup(pdev->romfile);
}
size = get_image_size(path);
if (size < 0) {
error_setg(errp, "failed to find romfile \"%s\"", pdev->romfile);
return;
} else if (size == 0) {
error_setg(errp, "romfile \"%s\" is empty", pdev->romfile);
return;
} else if (size > 2 * GiB) {
error_setg(errp,
"romfile \"%s\" too large (size cannot exceed 2 GiB)",
pdev->romfile);
return;
}
if (pdev->romsize != -1) {
if (size > pdev->romsize) {
error_setg(errp, "romfile \"%s\" (%u bytes) "
"is too large for ROM size %u",
pdev->romfile, (uint32_t)size, pdev->romsize);
return;
}
} else {
pdev->romsize = pow2ceil(size);
}
}
vmsd = qdev_get_vmsd(DEVICE(pdev));
snprintf(name, sizeof(name), "%s.rom",
vmsd ? vmsd->name : object_get_typename(OBJECT(pdev)));
pdev->has_rom = true;
memory_region_init_rom(&pdev->rom, OBJECT(pdev), name, pdev->romsize,
&error_fatal);
if (load_file) {
void *ptr = memory_region_get_ram_ptr(&pdev->rom);
if (load_image_size(path, ptr, size) < 0) {
error_setg(errp, "failed to load romfile \"%s\"", pdev->romfile);
return;
}
if (is_default_rom) {
/* Only the default rom images will be patched (if needed). */
pci_patch_ids(pdev, ptr, size);
}
}
pci_register_bar(pdev, PCI_ROM_SLOT, 0, &pdev->rom);
}
pci_add_option_rom의 4단계

1) pci_add_option_rom 함수는 pdev->rom_bar가 true인지 아닌지에 따라서, fw_cfg 안의 파일을 사용할지 아니면 rom_bar 하나를 생성할지를 결정한다.
if (!pdev->rom_bar) {
/*
* Load rom via fw_cfg instead of creating a rom bar,
* for 0.11 compatibility.
*/
int class = pci_get_word(pdev->config + PCI_CLASS_DEVICE);
/*
* Hot-plugged devices can't use the option ROM
* if the rom bar is disabled.
*/
if (DEVICE(pdev)->hotplugged) {
error_setg(errp, "Hot-plugged device without ROM bar"
" can't have an option ROM");
return;
}
if (class == 0x0300) {
rom_add_vga(pdev->romfile);
} else {
rom_add_option(pdev->romfile, -1);
}
return;
}
pdev->rom_bar는 기본값이 true다.
2) 그 다음에 ROM 파일의 경로와 크기를 얻는다.
if (load_file || pdev->romsize == -1) {
path = qemu_find_file(QEMU_FILE_TYPE_BIOS, pdev->romfile);
if (path == NULL) {
path = g_strdup(pdev->romfile);
}
size = get_image_size(path);
if (size < 0) {
error_setg(errp, "failed to find romfile \"%s\"", pdev->romfile);
return;
} else if (size == 0) {
error_setg(errp, "romfile \"%s\" is empty", pdev->romfile);
return;
} else if (size > 2 * GiB) {
error_setg(errp,
"romfile \"%s\" too large (size cannot exceed 2 GiB)",
pdev->romfile);
return;
}
if (pdev->romsize != -1) {
if (size > pdev->romsize) {
error_setg(errp, "romfile \"%s\" (%u bytes) "
"is too large for ROM size %u",
pdev->romfile, (uint32_t)size, pdev->romsize);
return;
}
} else {
pdev->romsize = pow2ceil(size);
}
}
3) 그 다음에, load_image_size 함수를 호출해서 파일을 지정된 ROM 안에 로드한다.
if (load_file) {
void *ptr = memory_region_get_ram_ptr(&pdev->rom);
if (load_image_size(path, ptr, size) < 0) {
error_setg(errp, "failed to load romfile \"%s\"", pdev->romfile);
return;
}
if (is_default_rom) {
/* Only the default rom images will be patched (if needed). */
pci_patch_ids(pdev, ptr, size);
}
}
4) 마지막으로, pci_register_bar 함수를 호출해서 이 ROM을 디바이스의 PCI bar 안에 등록한다.
pci_register_bar(pdev, PCI_ROM_SLOT, 0, &pdev->rom);
전체 호출 흐름 정리

핵심 개념 요약

3) edu 디바이스: BAR 등록과 MMIO 읽기/쓰기
QEMU PCI edu 디바이스의 구현 함수 해석
앞에서 PCI 디바이스의 구현 함수 pci_qdev_realize()를 분석했고, 거기서 수행되는 세 가지 작업을 주로 확인했다.
PCI 디바이스의 에뮬레이션에서, 전기와 관련된 부분은 신경 쓸 필요가 없고, 운영체제와의 인터페이스 부분만 신경 쓰면 된다.
디바이스와 운영체제의 인터페이스는 주로 PCI 디바이스의 컨피그 공간 및 PCI 디바이스의 레지스터 베이스를 포함한다.
모든 PCI 디바이스의 베이스 클래스는 TYPE_PCI_DEVICE이고, 모든 PCI 디바이스는 초기화 시 PCI 컨피그 공간 한 블록을 할당받게 되며, 이는 PCIDevice의 config에 저장된다.
do_pci_register_device 함수에서 PCI 컨피그 공간의 기본 데이터를 초기화한다.
구체적인 디바이스가 구현 함수를 진행할 때, PCI 컨피그 공간의 일부 기타 공간을 초기화하기도 하고, 디바이스에 필요한 기타 자원을 할당하기도 한다.
구체적인 디바이스의 구현 함수는 pci_qdev_realize 함수를 호출하는 데 사용되는 부분을 포함한다 (다시 말해, pci_qdev_realize 함수 과정 중의 두 번째 부분, 즉 PCI 디바이스가 속한 class의 realize 함수, 즉 pc->realize 함수다).
아래에서는 EDU 클래스 디바이스에 대응하는 구현 함수 pci_edu_realize()를 예로 들어 관련 설정을 살펴본다.
static void pci_edu_realize(PCIDevice *pdev, Error **errp)
{
EduState *edu = EDU(pdev);
uint8_t *pci_conf = pdev->config;
pci_config_set_interrupt_pin(pci_conf, 1);
if (msi_init(pdev, 0, 1, true, false, errp)) {
return;
}
timer_init_ms(&edu->dma_timer, QEMU_CLOCK_VIRTUAL, edu_dma_timer, edu);
qemu_mutex_init(&edu->thr_mutex);
qemu_cond_init(&edu->thr_cond);
qemu_thread_create(&edu->thread, "edu", edu_fact_thread,
edu, QEMU_THREAD_JOINABLE);
memory_region_init_io(&edu->mmio, OBJECT(edu), &edu_mmio_ops, edu,
"edu-mmio", 1 * MiB);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &edu->mmio);
}
이 함수가 할당되는 곳도 hw/misc/edu.c에 있고, 코드는 다음과 같다.
static void edu_class_init(ObjectClass *class, void *data)
{
DeviceClass *dc = DEVICE_CLASS(class);
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);
k->realize = pci_edu_realize;
k->exit = pci_edu_uninit;
k->vendor_id = PCI_VENDOR_ID_QEMU;
k->device_id = 0x11e8;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}
이제 pci_edu_realize 함수에 대한 분석을 시작한다.
(1) pci_config_set_interrupt_pin
pci_config_set_interrupt_pin은 PCI 디바이스 컨피그 공간의 PCI_INTERRUPT_PIN 바이트를 설정한다.
pci_config_set_interrupt_pin(pci_conf, 1);
static inline void
pci_config_set_interrupt_pin(uint8_t *pci_config, uint8_t val)
{
pci_set_byte(&pci_config[PCI_INTERRUPT_PIN], val);
}
PCI Agent 디바이스의 컨피그 공간 구조
PCI 컨피그 공간의 레이아웃은 다음과 같다. Interrupt Pin은 0x3C 오프셋에 위치한다.

(2) msi_init
msi_init 함수는 PCI 컨피그 공간에서 MSI 인터럽트와 관련된 데이터를 설정한다.
if (msi_init(pdev, 0, 1, true, false, errp)) {
return;
}
/*
* Make PCI device @dev MSI-capable.
* Non-zero @offset puts capability MSI at that offset in PCI config
* space.
* @nr_vectors is the number of MSI vectors (1, 2, 4, 8, 16 or 32).
* If @msi64bit, make the device capable of sending a 64-bit message
* address.
* If @msi_per_vector_mask, make the device support per-vector masking.
* @errp is for returning errors.
* Return 0 on success; set @errp and return -errno on error.
*
* -ENOTSUP means lacking msi support for a msi-capable platform.
* -EINVAL means capability overlap, happens when @offset is non-zero,
* also means a programming error, except device assignment, which can check
* if a real HW is broken.
*/
int msi_init(struct PCIDevice *dev, uint8_t offset,
unsigned int nr_vectors, bool msi64bit,
bool msi_per_vector_mask, Error **errp)
{
unsigned int vectors_order;
uint16_t flags;
uint8_t cap_size;
int config_offset;
if (!msi_nonbroken) {
error_setg(errp, "MSI is not supported by interrupt controller");
return -ENOTSUP;
}
MSI_DEV_PRINTF(dev,
"init offset: 0x%"PRIx8" vector: %"PRId8
" 64bit %d mask %d\n",
offset, nr_vectors, msi64bit, msi_per_vector_mask);
assert(!(nr_vectors & (nr_vectors - 1))); /* power of 2 */
assert(nr_vectors > 0);
assert(nr_vectors <= PCI_MSI_VECTORS_MAX);
/* the nr of MSI vectors is up to 32 */
vectors_order = ctz32(nr_vectors);
flags = vectors_order << ctz32(PCI_MSI_FLAGS_QMASK);
if (msi64bit) {
flags |= PCI_MSI_FLAGS_64BIT;
}
if (msi_per_vector_mask) {
flags |= PCI_MSI_FLAGS_MASKBIT;
}
cap_size = msi_cap_sizeof(flags);
config_offset = pci_add_capability(dev, PCI_CAP_ID_MSI, offset,
cap_size, errp);
if (config_offset < 0) {
return config_offset;
}
dev->msi_cap = config_offset;
dev->cap_present |= QEMU_PCI_CAP_MSI;
pci_set_word(dev->config + msi_flags_off(dev), flags);
pci_set_word(dev->wmask + msi_flags_off(dev),
PCI_MSI_FLAGS_QSIZE | PCI_MSI_FLAGS_ENABLE);
pci_set_long(dev->wmask + msi_address_lo_off(dev),
PCI_MSI_ADDRESS_LO_MASK);
if (msi64bit) {
pci_set_long(dev->wmask + msi_address_hi_off(dev), 0xffffffff);
}
pci_set_word(dev->wmask + msi_data_off(dev, msi64bit), 0xffff);
if (msi_per_vector_mask) {
/* Make mask bits 0 to nr_vectors - 1 writable. */
pci_set_long(dev->wmask + msi_mask_off(dev, msi64bit),
0xffffffff >> (PCI_MSI_VECTORS_MAX - nr_vectors));
}
dev->msi_prepare_message = msi_prepare_message;
return 0;
}
(3) memory_region_init_io
memory_region_init_io 함수는 edu->mmio 하나를 초기화하는데, 이는 해당 디바이스의 MMIO를 나타내며, 그 크기는 1MB다.
memory_region_init_io(&edu->mmio, OBJECT(edu), &edu_mmio_ops, edu,
"edu-mmio", 1 * MiB);
void memory_region_init_io(MemoryRegion *mr,
Object *owner,
const MemoryRegionOps *ops,
void *opaque,
const char *name,
uint64_t size)
{
memory_region_init(mr, owner, name, size);
mr->ops = ops ? ops : &unassigned_mem_ops;
mr->opaque = opaque;
mr->terminates = true;
}
(4) pci_register_bar
마지막으로 pci_register_bar 함수를 호출해서 해당 MMIO를 디바이스의 0번 BAR로 등록한다.
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &edu->mmio);
void pci_register_bar(PCIDevice *pci_dev, int region_num,
uint8_t type, MemoryRegion *memory)
{
PCIIORegion *r;
uint32_t addr; /* offset in pci config space */
uint64_t wmask;
pcibus_t size = memory_region_size(memory);
uint8_t hdr_type;
assert(!pci_is_vf(pci_dev)); /* VFs must use pcie_sriov_vf_register_bar */
assert(region_num >= 0);
assert(region_num < PCI_NUM_REGIONS);
assert(is_power_of_2(size));
/* A PCI bridge device (with Type 1 header) may only have at most 2 BARs */
hdr_type =
pci_dev->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION;
assert(hdr_type != PCI_HEADER_TYPE_BRIDGE || region_num < 2);
r = &pci_dev->io_regions[region_num];
r->addr = PCI_BAR_UNMAPPED;
r->size = size;
r->type = type;
r->memory = memory;
r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
? pci_get_bus(pci_dev)->address_space_io
: pci_get_bus(pci_dev)->address_space_mem;
wmask = ~(size - 1);
if (region_num == PCI_ROM_SLOT) {
/* ROM enable bit is writable */
wmask |= PCI_ROM_ADDRESS_ENABLE;
}
addr = pci_bar(pci_dev, region_num);
pci_set_long(pci_dev->config + addr, type);
if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&
r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
pci_set_quad(pci_dev->wmask + addr, wmask);
pci_set_quad(pci_dev->cmask + addr, ~0ULL);
} else {
pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff);
pci_set_long(pci_dev->cmask + addr, 0xffffffff);
}
}
pci_register_bar 함수 상세 분석
pci_register_bar 호출 흐름

(1) PCIIORegion 찾기
먼저 region_num에 근거해서 PCIDevice->io_regions 배열 중 대응하는 항목을 찾는다. PCI 디바이스의 MMIO는 PCIIORegion 구조체에 저장되며, 구조체에는 MMIO의 주소, 크기, 타입 등 정보가 들어 있다.
r = &pci_dev->io_regions[region_num];
(2) PCIIORegion 초기화
region_num이 나타내는 PCIIORegion을 얻은 후에는 몇 가지 초기화 설정을 진행한다.
r->addr = PCI_BAR_UNMAPPED;
r->size = size;
r->type = type;
r->memory = memory;
r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
? pci_get_bus(pci_dev)->address_space_io
: pci_get_bus(pci_dev)->address_space_mem;
(3) region의 type을 BAR 주소에 쓰기
그 다음에는 이 region의 type을 대응하는 PCI 컨피그 공간의 대응 BAR 주소 위치에 써넣는다.
r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
? pci_get_bus(pci_dev)->address_space_io
: pci_get_bus(pci_dev)->address_space_mem;
(4) wmask와 cmask 설정
마지막으로 PCI Device 중의 wmask와 cmask의 값을 설정한다.
wmask = ~(size - 1);
if (region_num == PCI_ROM_SLOT) {
/* ROM enable bit is writable */
wmask |= PCI_ROM_ADDRESS_ENABLE;
}
addr = pci_bar(pci_dev, region_num);
pci_set_long(pci_dev->config + addr, type);
if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&
r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
pci_set_quad(pci_dev->wmask + addr, wmask);
pci_set_quad(pci_dev->cmask + addr, ~0ULL);
} else {
pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff);
pci_set_long(pci_dev->cmask + addr, 0xffffffff);
}
운영체제가 PCI 디바이스와 상호작용하는 주요 방식은 PIO와 MMIO다.
MMIO는 비록 메모리 한 구간이지만, EPT 매핑이 없기 때문에 가상 머신에서 디바이스의 MMIO에 접근할 때 VM Exit가 발생한다.
KVM은 이 MMIO 접근을 식별하고 그것을 응용 계층의 QEMU로 할당하며, QEMU는 메모리 가상화의 절차에 따라 할당해서 디바이스에 등록된 MMIO 읽기/쓰기 콜백 함수를 찾는다.
디바이스의 MMIO 읽기/쓰기 콜백 함수는 디바이스의 기능에 근거해서 에뮬레이션을 진행하고, 에뮬레이션이 끝난 후에는 인터럽트가 가상 머신으로 보내져서 한 차례의 MMIO 접근이 완료될 수 있다.
게스트의 MMIO 접근이 처리되는 흐름

3) QEMU edu 디바이스의 MMIO 읽기/쓰기 함수 해석
memory_region_init_io 함수는 edu->mmio 하나를 초기화하는데, 이는 해당 디바이스의 MMIO를 나타내며, 그 크기는 1MB다.
memory_region_init_io(&edu->mmio, OBJECT(edu), &edu_mmio_ops, edu,
"edu-mmio", 1 * MiB);
static const MemoryRegionOps edu_mmio_ops = {
.read = edu_mmio_read,
.write = edu_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 8,
},
.impl = {
.min_access_size = 4,
.max_access_size = 8,
},
};
edu_mmio_ops의 타입은 MemoryRegionOps다.
typedef struct MemoryRegionOps MemoryRegionOps;
/*
* Memory region callbacks
*/
struct MemoryRegionOps {
/* Read from the memory region. @addr is relative to @mr; @size is
* in bytes. */
uint64_t (*read)(void *opaque,
hwaddr addr,
unsigned size);
/* Write to the memory region. @addr is relative to @mr; @size is
* in bytes. */
void (*write)(void *opaque,
hwaddr addr,
uint64_t data,
unsigned size);
MemTxResult (*read_with_attrs)(void *opaque,
hwaddr addr,
uint64_t *data,
unsigned size,
MemTxAttrs attrs);
MemTxResult (*write_with_attrs)(void *opaque,
hwaddr addr,
uint64_t data,
unsigned size,
MemTxAttrs attrs);
enum device_endian endianness;
/* Guest-visible constraints: */
struct {
/* If nonzero, specify bounds on access sizes beyond which a machine
* check is thrown.
*/
unsigned min_access_size;
unsigned max_access_size;
/* If true, unaligned accesses are supported. Otherwise unaligned
* accesses throw machine checks.
*/
bool unaligned;
/*
* If present, and returns #false, the transaction is not accepted
* by the device (and results in machine dependent behaviour such
* as a machine check exception).
*/
bool (*accepts)(void *opaque, hwaddr addr,
unsigned size, bool is_write,
MemTxAttrs attrs);
} valid;
/* Internal implementation constraints: */
struct {
/* If nonzero, specifies the minimum size implemented. Smaller sizes
* will be rounded upwards and a partial result will be returned.
*/
unsigned min_access_size;
/* If nonzero, specifies the maximum size implemented. Larger sizes
* will be done as a series of accesses with smaller sizes.
*/
unsigned max_access_size;
/* If true, unaligned accesses are supported. Otherwise all accesses
* are converted to (possibly multiple) naturally aligned accesses.
*/
bool unaligned;
} impl;
};
그중 read와 write 함수는 각각 이 MMIO의 읽기/쓰기 콜백을 나타낸다. endianness는 바이트의 엔디언 모드를 나타낸다.
write 콜백 함수를 예로 들면,
/* Write to the memory region. @addr is relative to @mr; @size is
* in bytes. */
void (*write)(void *opaque,
hwaddr addr,
uint64_t data,
unsigned size);
static void edu_mmio_write(void *opaque, hwaddr addr, uint64_t val,
unsigned size)
그 원형에서 opaque는 디바이스의 객체를 나타내고, addr은 가상 머신이 읽은 주소의 이 MMIO 내에서의 오프셋 주소를 나타내며, data(val)는 써넣으려는 값을 나타내고, size는 써넣는 값의 크기를 나타내는데, 보통은 1바이트, 2바이트, 4바이트 및 8바이트로 구성된다.
static void edu_mmio_write(void *opaque, hwaddr addr, uint64_t val,
unsigned size)
{
EduState *edu = opaque;
if (addr < 0x80 && size != 4) {
return;
}
if (addr >= 0x80 && size != 4 && size != 8) {
return;
}
switch (addr) {
case 0x04:
edu->addr4 = ~val;
break;
case 0x08:
if (qatomic_read(&edu->status) & EDU_STATUS_COMPUTING) {
break;
}
/* EDU_STATUS_COMPUTING cannot go 0->1 concurrently, because it is only
* set in this function and it is under the iothread mutex.
*/
qemu_mutex_lock(&edu->thr_mutex);
edu->fact = val;
qatomic_or(&edu->status, EDU_STATUS_COMPUTING);
qemu_cond_signal(&edu->thr_cond);
qemu_mutex_unlock(&edu->thr_mutex);
break;
case 0x20:
if (val & EDU_STATUS_IRQFACT) {
qatomic_or(&edu->status, EDU_STATUS_IRQFACT);
/* Order check of the COMPUTING flag after setting IRQFACT. */
smp_mb__after_rmw();
} else {
qatomic_and(&edu->status, ~EDU_STATUS_IRQFACT);
}
break;
case 0x60:
edu_raise_irq(edu, val);
break;
case 0x64:
edu_lower_irq(edu, val);
break;
case 0x80:
dma_rw(edu, true, &val, &edu->dma.src, false);
break;
case 0x88:
dma_rw(edu, true, &val, &edu->dma.dst, false);
break;
case 0x90:
dma_rw(edu, true, &val, &edu->dma.cnt, false);
break;
case 0x98:
if (!(val & EDU_DMA_RUN)) {
break;
}
dma_rw(edu, true, &val, &edu->dma.cmd, true);
break;
}
}
edu_mmio_write 함수는 가상 머신이 디바이스의 MMIO 주소에 쓸 때 QEMU 내에서 디바이스 에뮬레이션의 전형적인 동작을 보여준다.
edu MMIO 주소 맵
edu->mmio (1MB) 내에서 각 오프셋이 어떤 기능을 하는지 정리하면 다음과 같다.

(1) 주소 및 크기 범위 검사
먼저, 읽기/쓰기 주소와 크기가 범위 내에 있는지 검사해야 한다.
if (addr < 0x80 && size != 4) {
return;
}
if (addr >= 0x80 && size != 4 && size != 8) {
return;
}
(2) 주소에 따라 적절한 동작 수행
그 다음, 구체적인 주소에 근거해서 적절한 동작을 진행한다.
이러한 동작들은 단순하게 값을 설정하는 것일 수도 있다. 예컨대 여기서 0x04 주소에 쓰는 것처럼.
case 0x04:
edu->addr4 = ~val;
break;
인터럽트를 하이 레벨로 설정하거나 (0x60 주소 쓰기) 로우 레벨로 설정 (0x64 주소 쓰기) 하는 것일 수도 있다.
case 0x60:
edu_raise_irq(edu, val);
break;
case 0x64:
edu_lower_irq(edu, val);
break;
또한 dma를 통해 디바이스 가상 머신의 물리 주소를 읽고 쓰는 것 (0x80 주소 쓰기) 일 수도 있다.
case 0x80:
dma_rw(edu, true, &val, &edu->dma.src, false);
break;
read 콜백 함수에 대해서도 유사한 메커니즘이다. 여기서는 edu_mmio_read 함수의 소스 코드만 제시한다.
static uint64_t edu_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
EduState *edu = opaque;
uint64_t val = ~0ULL;
if (addr < 0x80 && size != 4) {
return val;
}
if (addr >= 0x80 && size != 4 && size != 8) {
return val;
}
switch (addr) {
case 0x00:
val = 0x010000edu;
break;
case 0x04:
val = edu->addr4;
break;
case 0x08:
qemu_mutex_lock(&edu->thr_mutex);
val = edu->fact;
qemu_mutex_unlock(&edu->thr_mutex);
break;
case 0x20:
val = qatomic_read(&edu->status);
break;
case 0x24:
val = edu->irq_status;
break;
case 0x80:
dma_rw(edu, false, &val, &edu->dma.src, false);
break;
case 0x88:
dma_rw(edu, false, &val, &edu->dma.dst, false);
break;
case 0x90:
dma_rw(edu, false, &val, &edu->dma.cnt, false);
break;
case 0x98:
dma_rw(edu, false, &val, &edu->dma.cmd, false);
break;
}
return val;
}
4) Northbridge I/O 에뮬레이션
Northbridge의 PCI 부분은 구조체 PCIHostState로 표현한다.
Northbridge의 PCI 부분은 두 개의 I/O 레지스터가 있다.
그중 하나는 컨피그 주소 레지스터로, CONFGADDR라고 부른다.
그것에 대응하는 MemoryRegion은 PCIHostState의 conf_mem 멤버에 저장되며, 이 레지스터의 역할은 PCI 디바이스를 선택하는 것이다.
또 다른 레지스터는 컨피그 데이터 레지스터로, CONFDATA라고 부른다. 그것에 대응하는 MemoryRegion은 PCIHostState의 data_mem 멤버에 저장된다. 그 CONFADDR 레지스터의 최상위 비트가 1일 때, 이 레지스터는 CONFADDR에서 지정한 디바이스에 대해 컨피그를 진행하는 데 사용된다.
Northbridge의 두 I/O 포트와 PCIHostState

이 두 MMIO 주소는 Northbridge의 instance_init 함수 i440fx_pcihost_initfn에서 초기화된다. i440fx_pcihost_initfn 함수는 hw/pci-host/i440fx.c에 있다.
static void i440fx_pcihost_initfn(Object *obj)
{
I440FXState *s = I440FX_PCI_HOST_BRIDGE(obj);
PCIHostState *phb = PCI_HOST_BRIDGE(obj);
memory_region_init_io(&phb->conf_mem, obj, &pci_host_conf_le_ops, phb,
"pci-conf-idx", 4);
memory_region_init_io(&phb->data_mem, obj, &pci_host_data_le_ops, phb,
"pci-conf-data", 4);
object_property_add_link(obj, PCI_HOST_PROP_RAM_MEM, TYPE_MEMORY_REGION,
(Object **) &s->ram_memory,
qdev_prop_allow_set_link_before_realize, 0);
object_property_add_link(obj, PCI_HOST_PROP_PCI_MEM, TYPE_MEMORY_REGION,
(Object **) &s->pci_address_space,
qdev_prop_allow_set_link_before_realize, 0);
object_property_add_link(obj, PCI_HOST_PROP_SYSTEM_MEM, TYPE_MEMORY_REGION,
(Object **) &s->system_memory,
qdev_prop_allow_set_link_before_realize, 0);
object_property_add_link(obj, PCI_HOST_PROP_IO_MEM, TYPE_MEMORY_REGION,
(Object **) &s->io_memory,
qdev_prop_allow_set_link_before_realize, 0);
}
거기에 대응하는 MemoryRegionOps는 각각 pci_host_conf_le_ops와 pci_host_data_le_ops다.
I/O 주소의 등록은 Northbridge의 구현 함수 i440fx_pcihost_realize에서 완성된다.
static void i440fx_pcihost_realize(DeviceState *dev, Error **errp)
{
ERRP_GUARD();
I440FXState *s = I440FX_PCI_HOST_BRIDGE(dev);
PCIHostState *phb = PCI_HOST_BRIDGE(dev);
SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
PCIBus *b;
PCIDevice *d;
PCII440FXState *f;
unsigned i;
memory_region_add_subregion(s->io_memory, 0xcf8, &phb->conf_mem);
sysbus_init_ioports(sbd, 0xcf8, 4);
memory_region_add_subregion(s->io_memory, 0xcfc, &phb->data_mem);
sysbus_init_ioports(sbd, 0xcfc, 4);
/* register i440fx 0xcf8 port as coalesced pio */
memory_region_set_flush_coalesced(&phb->data_mem);
memory_region_add_coalescing(&phb->conf_mem, 0, 4);
b = pci_root_bus_new(dev, NULL, s->pci_address_space,
s->io_memory, 0, TYPE_PCI_BUS);
phb->bus = b;
d = pci_create_simple(b, 0, s->pci_type);
f = I440FX_PCI_DEVICE(d);
range_set_bounds(&s->pci_hole, s->below_4g_mem_size,
IO_APIC_DEFAULT_ADDRESS - 1);
/* setup pci memory mapping */
pc_pci_as_mapping_init(s->system_memory, s->pci_address_space);
/* if *disabled* show SMRAM to all CPUs */
memory_region_init_alias(&f->smram_region, OBJECT(d), "smram-region",
s->pci_address_space, SMRAM_C_BASE, SMRAM_C_SIZE);
memory_region_add_subregion_overlap(s->system_memory, SMRAM_C_BASE,
&f->smram_region, 1);
memory_region_set_enabled(&f->smram_region, true);
/* smram, as seen by SMM CPUs */
memory_region_init(&f->smram, OBJECT(d), "smram", 4 * GiB);
memory_region_set_enabled(&f->smram, true);
memory_region_init_alias(&f->low_smram, OBJECT(d), "smram-low",
s->ram_memory, SMRAM_C_BASE, SMRAM_C_SIZE);
memory_region_set_enabled(&f->low_smram, true);
memory_region_add_subregion(&f->smram, SMRAM_C_BASE, &f->low_smram);
object_property_add_const_link(qdev_get_machine(), "smram",
OBJECT(&f->smram));
init_pam(&f->pam_regions[0], OBJECT(d), &f->ram_memory,
&f->system_memory, &f->pci_address_space,
PAM_BIOS_BASE, PAM_BIOS_SIZE);
for (i = 0; i < ARRAY_SIZE(f->pam_regions) - 1; ++i) {
init_pam(&f->pam_regions[i + 1], OBJECT(d), s->ram_memory,
s->system_memory, s->pci_address_space,
PAM_EXPAN_BASE + i * PAM_EXPAN_SIZE, PAM_EXPAN_SIZE);
}
ram_addr_t ram_size = s->below_4g_mem_size + s->above_4g_mem_size;
ram_size = ram_size / 8 / 1024 / 1024;
if (ram_size > 255) {
ram_size = 255;
}
d->config[I440FX_COREBOOT_RAM_SIZE] = ram_size;
i440fx_update_memory_mappings(f);
}
memory_region_add_subregion 함수는 지정된 MemoryRegion을 시스템 I/O 주소 공간의 하위 MemoryRegion으로 설정한다.
sysbus_init_ioports는 SysBusDevice의 PIO 포트 번호 배열을 초기화한다.
i440fx_pcihost_realize 함수는 Northbridge의 CONFADDR와 CONFDATA 두 레지스터의 주소를 시스템 I/O 주소 공간에 추가하며, 그중 CONFADDR는 포트 0xcf8부터 시작하는 4개의 포트를 사용하고, CONFDATA는 포트 0xcfc부터 시작하는 4개의 포트를 사용한다.
x86 프로세서는 두 개의 I/O 포트 레지스터를 정의했는데, 각각 CONFIG_ADDRESS와 CONFIG_DATA 레지스터이고, 그 주소는 0xCF8과 0xCFC이다.
x86 프로세서는 이 두 I/O 포트 레지스터를 사용해서 PCI 디바이스의 컨피그 공간에 접근한다.
PCI 버스 규범에서도 이 두 레지스터를 예로 들어 프로세서가 어떻게 PCI 디바이스의 컨피그 공간에 접근하는지를 설명한다.
그중에서 CONFIG_ADDRESS 레지스터는 PCI 디바이스의 ID 번호를 저장하고, CONFIG_DATA 레지스터는 컨피그 읽기/쓰기를 진행할 데이터를 저장한다.
CONFIG_ADDRESS 레지스터는 PowerPC 프로세서의 CFG_ADDR 레지스터의 사용 방법과 유사하고,
CONFIG_DATA 레지스터는 PowerPC 프로세서의 CFG_DATA 레지스터의 사용 방법과 유사하다.
CONFIG_ADDRESS 레지스터의 구조는 다음과 같다.

- Enable 비트, 제31비트. 이 비트가 1일 때, CONFIG_DATA 레지스터에 대해 읽기/쓰기를 진행하면 PCI 버스의 컨피그 사이클을 유발한다.
- Bus Number 필드, 제23~16비트, PCI 디바이스의 버스 번호를 기록한다.
- Device Number 필드, 제15~11비트, PCI 디바이스의 디바이스 번호를 기록한다.
- Function Number 필드, 제10~8비트, PCI 디바이스의 기능 번호를 기록한다.
- Register Number 필드, 제7~2비트, PCI 디바이스의 레지스터 번호를 기록한다.
x86 프로세서가 CONFIG_DATA 레지스터에 대해 I/O 읽기/쓰기 접근을 진행할 때, 그리고 CONFIG_ADDR 레지스터의 Enable 비트가 1일 때, HOST 메인 브리지는 이 I/O 읽기/쓰기 접근을 PCI 읽기/쓰기 버스 트랜잭션으로 변환한 후, PCI 버스에 보낸다.
PCI 버스는 CONFIG_ADDR 레지스터에 저장된 ID 번호에 근거해서 PCI 컨피그 읽기/쓰기 요청을 지정된 PCI 디바이스의 지정된 컨피그 레지스터에 보낸다.
CONFADDR에 쓰는 동작은 컨피그 레지스터의 값을 설정해서 선택할 PCI 디바이스를 지정하며, 이후의 데이터 접근에 사용된다.
static void pci_host_config_write(void *opaque, hwaddr addr,
uint64_t val, unsigned len)
{
PCIHostState *s = opaque;
PCI_DPRINTF("%s addr " HWADDR_FMT_plx " len %d val %"PRIx64"\n",
__func__, addr, len, val);
if (addr != 0 || len != 4) {
return;
}
s->config_reg = val;
}
pci_host_config_write 함수는 가상 머신이 선택한 PCI 디바이스의 주소를 PCIHostState의 config_reg 레지스터에 저장한다.
CONFADDR는 반드시 4바이트 단위로 접근해야 한다.
Intel 440FX PCIset 매뉴얼을 통해 CONFADDR 레지스터에 쓰는 데이터의 의미를 알 수 있다.
CONFADDR 레지스터의 31비트는 PCI 디바이스의 컨피그 기능을 활성화할지 여부를 나타낸다.
PCI 디바이스의 컨피그 공간을 읽으려면 이 비트를 1로 설정해야 한다.
24비트부터 30비트는 예약 비트다.
16비트부터 23비트는 PCI 버스 번호를 나타낸다.
11비트부터 15비트는 선택된 버스 위의 PCI 디바이스 번호를 나타낸다.
8비트부터 10비트는 선택된 버스 위의 디바이스 번호에 대응하는 PCI 디바이스의 기능 번호를 나타낸다.
2비트부터 7비트는 선택된 버스, 디바이스, 기능 번호에 대응하는 PCI 디바이스의 레지스터 값을 나타낸다.
0비트부터 1비트는 예약 비트다.
종합하면, CONFADDR 레지스터가 PCI 디바이스의 주소를 지정한 다음, PCI 디바이스를 선택한 후에는 PC 컨피그 공간에 데이터를 쓸 수 있다. 아래는 CONFDATA 레지스터에 쓰는 값이다.
static void pci_host_data_write(void *opaque, hwaddr addr,
uint64_t val, unsigned len)
{
PCIHostState *s = opaque;
if (s->config_reg & (1u << 31)) {
pci_data_write(s->bus, s->config_reg | (addr & 3), val, len);
}
}
먼저 컨피그 레지스터의 값 중 제31비트가 1인지(활성화) 판단하고, 활성화된 경우 pci_data_write 함수를 호출해서 디바이스의 컨피그 공간에 쓰기 시작한다.
void pci_data_write(PCIBus *s, uint32_t addr, uint32_t val, unsigned len)
{
PCIDevice *pci_dev = pci_dev_find_by_addr(s, addr);
uint32_t config_addr = addr & (PCI_CONFIG_SPACE_SIZE - 1);
if (!pci_dev) {
trace_pci_cfg_write("empty", extract32(addr, 16, 8),
extract32(addr, 11, 5), extract32(addr, 8, 3),
config_addr, val);
return;
}
pci_host_config_write_common(pci_dev, config_addr, PCI_CONFIG_SPACE_SIZE,
val, len);
}
pci_data_write 함수는 먼저 CONFADDR 중의 값을 통해 pci_dev_find_by_addr 함수를 호출해서 접근해야 할 PCI 디바이스를 찾는다.
그 다음에 pci_host_config_write_common 함수를 호출해서 이 디바이스의 PCI 컨피그 공간에 쓴다.
주의할 점은, addr과 (PCI_CONIG_SPACE_SIZE-1)를 AND 연산해서 addr을 PCI 컨피그 공간 크기인 256바이트 이내로 제한한다는 것이다.
/* Size of the standard PCI config space */
#define PCI_CONFIG_SPACE_SIZE 0x100
void pci_host_config_write_common(PCIDevice *pci_dev, uint32_t addr,
uint32_t limit, uint32_t val, uint32_t len)
{
pci_adjust_config_limit(pci_get_bus(pci_dev), &limit);
if (limit <= addr) {
return;
}
assert(len <= 4);
/* non-zero functions are only exposed when function 0 is present,
* allowing direct removal of unexposed functions.
*/
if ((pci_dev->qdev.hotplugged && !pci_get_function_0(pci_dev)) ||
!pci_dev->has_power) {
return;
}
trace_pci_cfg_write(pci_dev->name, pci_dev_bus_num(pci_dev),
PCI_SLOT(pci_dev->devfn),
PCI_FUNC(pci_dev->devfn), addr, val);
pci_dev->config_write(pci_dev, addr, val, MIN(len, limit - addr));
}
pci_host_config_write_common 함수는 몇 가지 기본 검사를 진행한 후에 디바이스 자신의 config_write 콜백 함수를 호출한다.
게스트의 PCI 컨피그 쓰기가 디바이스까지 도달하는 흐름

5) SeaBIOS의 BAR 베이스 주소 설정
지금까지는 가상 머신이 어떻게 Northbridge의 MMIO를 통해 PCI 디바이스의 컨피그 공간을 읽고 쓰는지를 분석했다.
PCI 디바이스의 컨피그 공간에는 MMIO의 주소, 즉 BAR 정보가 있는데, 거기에 BAR의 베이스가 저장된다.
가상 머신은 이러한 BAR를 읽고 쓰는 방식으로 디바이스와 통신할 수 있다.
그러나 QEMU가 디바이스 초기화 및 구현 과정 중에서는 베이스를 설정하지 않았다.
그렇다면 이러한 주소는 어떻게 설정되는 걸까? 이건 사실 실행 시에 설정되는데, 아래에서는 BIOS의 각도에서 주소가 어떻게 설정되는지 분석한다.
qemu/x86_64-softmmu/qemu-system-x86_64 -m 1024 -smp 4 -hda /home/test/test.img --enable-kvm -vnc :0 -
위 가상 머신에 대응하는 PCI 디바이스는 다음과 같이 표시된다.
$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234: 1111 (rev 02)
00:03.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
00:04.0 Unclassified device [00ff]: Device 1234: 11e8 (rev 10)
/proc/iomem을 읽으면 디바이스의 MMIO 주소 분포를 볼 수 있다
40000000-febfffff : PCI Bus 0000:00
fd000000-fdffffff : 0000:00:02.0
fd000000-fd15ffff : efifb
fea00000-feafffff : 0000:00:04.0
feb00000-feb3ffff : 0000:00:03.0
feb40000-feb5ffff : 0000:00:03.0
feb40000-feb5ffff : e1000
feb70000-feb7ffff : 0000:00:02.0
SeaBIOS의 로그 파일 1.txt를 보면 위의 PCI 디바이스가 BAR를 초기화한 정보를 볼 수 있다.
PCI: map device bdf=00:03.0 bar 1, addr 0000c000, size 00000040 [io]
PCI: map device bdf=00:01.1 bar 4, addr 0000c040, size 00000010 [io]
PCI: map device bdf=00:04.0 bar 0, addr fea00000, size 00100000 [mem]
PCI: map device bdf=00:03.0 bar 6, addr feb00000, size 00040000 [mem]
PCI: map device bdf=00:03.0 bar 0, addr feb40000, size 00020000 [mem]
PCI: map device bdf=00:02.0 bar 6, addr feb60000, size 00100000 [mem]
PCI: map device bdf=00:02.0 bar 2, addr feb70000, size 00001000 [mem]
PCI: map device bdf=00:02.0 bar 0, addr fd000000, size 01000000 [prefmem]
위 로그에서 다음을 알 수 있다.
- IDE 컨트롤러는 1개의 I/O BAR가 있다.
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
PCI: map device bdf=00:01.1 bar 4, addr 0000c040, size 00000010 [io]
- VGA는 3개의 mem BAR가 있다.
00:02.0 VGA compatible controller: Device 1234: 1111 (rev 02)
PCI: map device bdf=00:02.0 bar 6, addr feb60000, size 00100000 [mem]
PCI: map device bdf=00:02.0 bar 2, addr feb70000, size 00001000 [mem]
PCI: map device bdf=00:02.0 bar 0, addr fd000000, size 01000000 [prefmem]
- 네트워크 카드는 2개의 mem BAR와 1개의 I/O BAR가 있다.
00:03.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)
PCI: map device bdf=00:03.0 bar 1, addr 0000c000, size 00000040 [io]
PCI: map device bdf=00:04.0 bar 0, addr fea00000, size 00100000 [mem]
PCI: map device bdf=00:03.0 bar 6, addr feb00000, size 00040000 [mem]
PCI: map device bdf=00:03.0 bar 0, addr feb40000, size 00020000 [mem]
- edu 디바이스는 1개의 mem BAR가 있다.
00:04.0 Unclassified device [00ff]: Device 1234: 11e8 (rev 10)
PCI: map device bdf=00:01.1 bar 4, addr 0000c040, size 00000010 [io]
SeaBIOS 로그에서도 디바이스 BAR의 주소를 출력한다.
가상 머신 BAR 메모리 맵

부가설명
- mem BAR: 일반 메모리 매핑 영역
- PREFMEM BAR: prefetchable memory (미리 읽어와도 되는 메모리, 보통 큰 영역)
핵심 규칙 두 가지
- 같은 종류 BAR들은 연속으로 모아서 배치
- 각 영역은 정렬(alignment) 해야 함 — 정렬 기준은 "그 종류 중 가장 큰 BAR의 크기"
아래에서는, 코드 계층에서 이 과정이 어떻게 완성되는지 분석한다.
SeaBIOS의 호출 체인 dopost->maininit->platform_hardware_setup->qemu_platform_setup->pci_setup->pci_bios_map_devices 과정에서, 마지막의 이 함수가 PCI 디바이스 BAR 설정을 완성하는 일을 맡는다.
거기에는 I/O, MEM 및 PREFMEM 세 종류 BAR의 설정이 포함된다. MEM과 PREFMEM은 함께 처리되는데, 여기서는 위의 명령어 동작을 예로 들어, SeaBIOS가 어떻게 PCI 디바이스에 BAR 베이스 주소를 설정하는지 논의한다.
SeaBIOS BAR 설정 호출 체인

pci_bios_map_devices는 먼저 pci_bios_init_root_regions_io와 pci_bios_init_root_regions_mem 함수를 호출해서 초기화 작업을 한다.
mem을 예로 들면, 호출되는 것은 src/fw/pcinit.c 중의 pci_bios_init_root_regions_mem 함수다.
pci_bios_init_root_regions_mem 함수 분석
static int pci_bios_init_root_regions_mem(struct pci_bus *bus)
{
struct pci_region *r_end = &bus->r[PCI_REGION_TYPE_PREFMEM];
struct pci_region *r_start = &bus->r[PCI_REGION_TYPE_MEM];
if (pci_region_align(r_start) < pci_region_align(r_end)) {
// Swap regions to improve alignment.
r_end = r_start;
r_start = &bus->r[PCI_REGION_TYPE_PREFMEM];
}
u64 sum = pci_region_sum(r_end);
u64 align = pci_region_align(r_end);
r_end->base = ALIGN_DOWN((pcimem_end - sum), align);
sum = pci_region_sum(r_start);
align = pci_region_align(r_start);
r_start->base = ALIGN_DOWN((r_end->base - sum), align);
if ((r_start->base < pcimem_start) ||
(r_start->base > pcimem_end))
// Memory range requested is larger than available.
return -1;
return 0;
}
pci_region 구조체는 이 가상 머신의 모든 디바이스의 어떤 한 종류 BAR (예: PCI_REGION_TYPE_MEM은 mem BAR를 나타냄)를 표시한다.
struct pci_region {
/* pci region assignments */
u64 base;
struct hlist_head list;
};
struct pci_region의 base 멤버는 이 종류 BAR의 시작 주소를 나타낸다. list 멤버는 이 종류 BAR의 모든 디바이스를 연결하는 데 사용된다.
PCI: map device bdf=00:03.0 bar 1, addr 0000c000, size 00000040 [io]
PCI: map device bdf=00:01.1 bar 4, addr 0000c040, size 00000010 [io]
PCI: map device bdf=00:04.0 bar 0, addr fea00000, size 00100000 [mem]
PCI: map device bdf=00:03.0 bar 6, addr feb00000, size 00040000 [mem]
PCI: map device bdf=00:03.0 bar 0, addr feb40000, size 00020000 [mem]
PCI: map device bdf=00:02.0 bar 6, addr feb60000, size 00100000 [mem]
PCI: map device bdf=00:02.0 bar 2, addr feb70000, size 00001000 [mem]
PCI: map device bdf=00:02.0 bar 0, addr fd000000, size 01000000 [prefmem]
pci_region_align 함수는 가장 큰 align 값을 반환하는데, 각 디바이스 BAR 주소의 alignment가 바로 그 크기다.
여기서는 먼저 align을 비교해서, 큰 것은 되도록 앞쪽에 배치한다.
명령어로 시작한 가상 머신에서 가장 큰 것은 VGA의 16MB ROM 영역이라, PREFMEM이 더 앞쪽에 놓이고, 즉 그 메모리 주소도 비교적 낮다.
pci_region_sum 함수는 어떤 한 종류 BAR의 모든 공간의 합을 반환하며, pcimem_end를 0xfec00000으로 설정한다.
static u64 pci_region_sum(struct pci_region *r)
{
u64 sum = 0;
struct pci_region_entry *entry;
hlist_for_each_entry(entry, &r->list, node) {
sum += entry->size;
}
return sum;
}
MEM/PREFMEM 베이스 주소 계산 예 (위 예제)

① mem BAR 배치 (r_end)
- 모든 디바이스의 mem BAR 크기를 다 더함: 0x100000 + 0x40000 + 0x20000 + 0x100000 + 0x1000 = 0x171000 (총합)
- 정렬 기준(align) = mem BAR 중 가장 큰 것 = 0x100000
- 배치 방식: 높은 주소(0xfec00000)에서 아래로 내려가며 채움
- 0xfec00000 - 0x171000 = (총합만큼 내려감)
- 그 결과를 0x100000에 맞춰 정렬 → 0xfea00000
- 그래서 mem BAR 영역의 시작 = r_end->base = 0xfea00000
② PREFMEM BAR 배치 (r_start)
- PREFMEM은 하나뿐, 크기 0x1000000 (16MB)
- 정렬 기준 = 0x1000000
- mem 영역 시작(0xfea00000)에서 또 아래로:
- 0xfea00000 - 0x1000000 = 0xfd000000
- 그래서 PREFMEM 시작 = 0xfd000000
③ 마무리
각 종류 BAR의 베이스 주소를 다 계산한 뒤, pci_region_map_entries 함수로 각 디바이스의 BAR에 실제 주소를 기록
static void pci_region_map_entries(struct pci_bus *busses, struct pci_region *r)
{
struct hlist_node *n;
struct pci_region_entry *entry;
hlist_for_each_entry_safe(entry, n, &r->list, node) {
u64 addr = r->base;
r->base += entry->size;
if (entry->bar == -1)
// Update bus base address if entry is a bridge region
busses[entry->dev->secondary_bus].r[entry->type].base = addr;
pci_region_map_one_entry(entry, addr);
hlist_del(&entry->node);
free(entry);
}
}
이 함수는 pci_region_map_one_entry 함수를 호출해서 각 항목의 설정을 완성한다.
static void
pci_region_map_one_entry(struct pci_region_entry *entry, u64 addr)
{
if (entry->bar >= 0) {
dprintf(1, "PCI: map device bdf=%pP"
" bar %d, addr %08llx, size %08llx [%s]\n",
entry->dev,
entry->bar, addr, entry->size, region_type_name[entry->type]);
pci_set_io_region_addr(entry->dev, entry->bar, addr, entry->is64);
return;
}
u16 bdf = entry->dev->bdf;
u64 limit = addr + entry->size - 1;
if (entry->type == PCI_REGION_TYPE_IO) {
pci_config_writeb(bdf, PCI_IO_BASE, addr >> PCI_IO_SHIFT);
pci_config_writeb(bdf, PCI_IO_BASE_UPPER16, 0);
pci_config_writeb(bdf, PCI_IO_LIMIT, limit >> PCI_IO_SHIFT);
pci_config_writew(bdf, PCI_IO_LIMIT_UPPER16, 0);
}
if (entry->type == PCI_REGION_TYPE_MEM) {
pci_config_writew(bdf, PCI_MEMORY_BASE, addr >> PCI_MEMORY_SHIFT);
pci_config_writew(bdf, PCI_MEMORY_LIMIT, limit >> PCI_MEMORY_SHIFT);
}
if (entry->type == PCI_REGION_TYPE_PREFMEM) {
pci_config_writew(bdf, PCI_PREF_MEMORY_BASE, addr >> PCI_PREF_MEMORY_SHIFT);
pci_config_writew(bdf, PCI_PREF_MEMORY_LIMIT, limit >> PCI_PREF_MEMORY_SHIFT);
pci_config_writel(bdf, PCI_PREF_BASE_UPPER32, addr >> 32);
pci_config_writel(bdf, PCI_PREF_LIMIT_UPPER32, limit >> 32);
}
}
static void
pci_set_io_region_addr(struct pci_device *pci, int bar, u64 addr, int is64)
{
u32 ofs = pci_bar(pci, bar);
pci_config_writel(pci->bdf, ofs, addr);
if (is64)
pci_config_writel(pci->bdf, ofs + 4, addr >> 32);
}
(1) pci_bar 함수
pci_bar 함수는 써넣어야 할 BAR의 PCI 컨피그 공간에서의 주소를 얻는 데 사용된다.
int pci_bar(PCIDevice *d, int reg)
{
uint8_t type;
/* PCIe virtual functions do not have their own BARs */
assert(!pci_is_vf(d));
if (reg != PCI_ROM_SLOT)
return PCI_BASE_ADDRESS_0 + reg * 4;
type = d->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION;
return type == PCI_HEADER_TYPE_BRIDGE ? PCI_ROM_ADDRESS1 : PCI_ROM_ADDRESS;
}
(2) pci_config_writel 함수
그 다음 pci_config_writel 함수를 호출한다.
static inline void pci_config_writel(int bdf, uint8_t addr, uint32_t val)
{
*(volatile uint32_t *)(pci_conf_base + bdf * 256 + addr) = val;
}
static inline void pci_config_writew(int bdf, uint8_t addr, uint16_t val)
{
*(volatile uint16_t *)(pci_conf_base + bdf * 256 + addr) = val;
}
static inline void pci_config_writeb(int bdf, uint8_t addr, uint8_t val)
{
*(volatile uint8_t *)(pci_conf_base + bdf * 256 + addr) = val;
}
이상의 과정을 통해, SeaBIOS는 모든 디바이스의 BAR 베이스 주소를 다 할당했다.
7) pci_default_write_config와 BAR 주소 업데이트
앞에서 SeaBIOS는 모든 디바이스의 BAR 베이스에 값을 할당했다. 이어서 BAR 주소 설정과 관련된 코드를 분석한다.
void pci_default_write_config(PCIDevice *d, uint32_t addr, uint32_t val_in, int l)
{
int i, was_irq_disabled = pci_irq_disabled(d);
uint32_t val = val_in;
assert(addr + l <= pci_config_size(d));
for (i = 0; i < l; val >>= 8, ++i) {
uint8_t wmask = d->wmask[addr + i];
uint8_t w1cmask = d->w1cmask[addr + i];
assert(!(wmask & w1cmask));
d->config[addr + i] = (d->config[addr + i] & ~wmask) | (val & wmask);
d->config[addr + i] &= ~(val & w1cmask); /* W1C: Write 1 to Clear */
}
if (ranges_overlap(addr, l, PCI_BASE_ADDRESS_0, 24) ||
ranges_overlap(addr, l, PCI_ROM_ADDRESS, 4) ||
ranges_overlap(addr, l, PCI_ROM_ADDRESS1, 4) ||
range_covers_byte(addr, l, PCI_COMMAND))
pci_update_mappings(d);
if (ranges_overlap(addr, l, PCI_COMMAND, 2)) {
pci_update_irq_disabled(d, was_irq_disabled);
memory_region_set_enabled(&d->bus_master_enable_region,
(pci_get_word(d->config + PCI_COMMAND)
& PCI_COMMAND_MASTER) && d->has_power);
}
msi_write_config(d, addr, val_in, l);
msix_write_config(d, addr, val_in, l);
pcie_sriov_config_write(d, addr, val_in, l);
}
pci_default_write_config 함수는 먼저 전달된 값을 PCI 디바이스의 컨피그 공간에 써넣는다. 주소가 BAR의 주소이기 때문에, pci_update_mappings 함수를 호출하게 된다.
static void pci_update_mappings(PCIDevice *d)
{
PCIIORegion *r;
int i;
pcibus_t new_addr;
for(i = 0; i < PCI_NUM_REGIONS; i++) {
r = &d->io_regions[i];
/* this region isn't registered */
if (!r->size)
continue;
new_addr = pci_bar_address(d, i, r->type, r->size);
if (!d->has_power) {
new_addr = PCI_BAR_UNMAPPED;
}
/* This bar isn't changed */
if (new_addr == r->addr)
continue;
/* now do the real mapping */
if (r->addr != PCI_BAR_UNMAPPED) {
trace_pci_update_mappings_del(d->name, pci_dev_bus_num(d),
PCI_SLOT(d->devfn),
PCI_FUNC(d->devfn),
i, r->addr, r->size);
memory_region_del_subregion(r->address_space, r->memory);
}
r->addr = new_addr;
if (r->addr != PCI_BAR_UNMAPPED) {
trace_pci_update_mappings_add(d->name, pci_dev_bus_num(d),
PCI_SLOT(d->devfn),
PCI_FUNC(d->devfn),
i, r->addr, r->size);
memory_region_add_subregion_overlap(r->address_space,
r->addr, r->memory, 1);
}
}
pci_update_vga(d);
}
(1) BAR 주소 얻기
먼저 pci_bar_address 함수에서 BAR의 주소를 얻는다.
new_addr = pci_bar_address(d, i, r->type, r->size);
if (!d->has_power) {
new_addr = PCI_BAR_UNMAPPED;
}
(2) io_regions의 addr에 복사하기
그 다음 io_regions의 addr에 복사한다.
/* now do the real mapping */
if (r->addr != PCI_BAR_UNMAPPED) {
trace_pci_update_mappings_del(d->name, pci_dev_bus_num(d),
PCI_SLOT(d->devfn),
PCI_FUNC(d->devfn),
i, r->addr, r->size);
memory_region_del_subregion(r->address_space, r->memory);
}
r->addr = new_addr;
(3) MemoryRegion에 추가하기
마지막으로 memory_region_add_subregion_overlap 함수를 호출해서 이 주소를 PCI MemoryRegion에 추가하고, 그것의 하위 MR로 삼는다.
if (r->addr != PCI_BAR_UNMAPPED) {
trace_pci_update_mappings_add(d->name, pci_dev_bus_num(d),
PCI_SLOT(d->devfn),
PCI_FUNC(d->devfn),
i, r->addr, r->size);
memory_region_add_subregion_overlap(r->address_space,
r->addr, r->memory, 1);
}
(4) 메모리 디스패치 테이블 구축
다시 메모리 제출을 거치면, QEMU 내에서 메모리 디스패치 테이블이 구축된다. 그래서 가상 머신이 이 블록의 MMIO 메모리에 접근할 때, 그것에 대응하는 디바이스의 콜백 함수가 처리를 맡게 된다.
BAR 베이스 변경 → 게스트 MMIO 접근까지 전체 흐름

전체 시리즈 요약

6) PCI 디바이스 인터럽트
이전까지 운영체제가 장치의 MMIO/PIO 기반 주소를 어떻게 구동하는지, 그리고 QEMU가 이 메커니즘을 어떻게 시뮬레이션하는지 확인했다.
운영체제가 장치와 통신하는 또 다른 방향은 장치가 이벤트를 발생시키고, 인터럽트 메커니즘을 통해 운영체제에 알리는 것이다. 장치 레지스터와 I/O 포트는 운영체제가 장치로 향하는 통신을 제공하고, 인터럽트 메커니즘은 장치가 운영체제로 향하는 통신을 제공한다.
먼저 전체 구조를 한눈에 보자

(1) PCI 장치 인터럽트 핀
각 PCI 장치마다 4개의 인터럽트 핀이 있는데, 모두 인터럽트를 트리거할 수 있다.
단일 기능 장치는 INTA# 인터럽트 핀을 사용해 인터럽트를 트리거하며, 다기능 장치만 INTB#, INTC#, INTD# 등의 인터럽트 핀을 사용한다.
어떤 인터럽트 핀을 사용할지는 보통 QEMU가 장치를 realize할 때 PCI 장치 설정 공간 내 0x3d 위치의 한 바이트에 기록되어 나타난다. 예를 들어 아래 코드는 e1000 장치가 INTA# 인터럽트 핀을 사용한다는 것을 보여준다.
hw/net/e1000.c 중
static void pci_e1000_realize(PCIDevice *pci_dev, Error **errp)
{
DeviceState *dev = DEVICE(pci_dev);
E1000State *d = E1000(pci_dev);
uint8_t *pci_conf;
uint8_t *macaddr;
pci_dev->config_write = e1000_write_config;
pci_conf = pci_dev->config;
/* TODO: RST# value should be 0, PCI spec 6.2.4 */
pci_conf[PCI_CACHE_LINE_SIZE] = 0x10;
pci_conf[PCI_INTERRUPT_PIN] = 1; /* interrupt pin A */
e1000_mmio_setup(d);
pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &d->mmio);
pci_register_bar(pci_dev, 1, PCI_BASE_ADDRESS_SPACE_IO, &d->io);
qemu_macaddr_default_if_unset(&d->conf.macaddr);
macaddr = d->conf.macaddr.a;
e1000x_core_prepare_eeprom(d->eeprom_data,
e1000_eeprom_template,
sizeof(e1000_eeprom_template),
PCI_DEVICE_GET_CLASS(pci_dev)->device_id,
macaddr);
d->nic = qemu_new_nic(&net_e1000_info, &d->conf,
object_get_typename(OBJECT(d)), dev->id,
&dev->mem_reentrancy_guard, d);
qemu_format_nic_info_str(qemu_get_queue(d->nic), macaddr);
d->autoneg_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL, e1000_autoneg_timer, d);
d->mit_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, e1000_mit_timer, d);
d->flush_queue_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
e1000_flush_queue_timer, d);
}
QEMU에는 PCI_INTERRUPT_PIN이 여러 곳에서 정의되어 있지만, 어디든 모두 다음과 같다.
#define PCI_INTERRUPT_PIN 0x3d /* 8 bits */
PCI Agent 장치의 설정 공간
PCI 장치 설정 공간의 구조를 보자. 0x3d(Interrupt Pin)와 0x3c(Interrupt Line) 위치가 인터럽트와 관련된 부분이다.

(2) PCI 장치 인터럽트 라우팅
PCI 버스 위에서 걸 수 있는 장치는 많지만, 일반적으로 인터럽트 컨트롤러의 인터럽트 라인은 한정되어 있다.
게다가 일부 인터럽트 라인은 이미 메인보드 상의 장치에 할당되어 있어서, 보통 PCI 장치에 남겨지는 인터럽트는 4개 혹은 8개뿐이다.
QEMU의 i440fx 메인보드에는 PCI 장치용 인터럽트가 4개뿐이다. PCI 장치가 사용하는 인터럽트 핀과 인터럽트 컨트롤러의 인터럽트 라인을 연결하는 것을 보통 PCI 장치의 인터럽트 라우팅이라고 한다.
PCI 장치 인터럽트 라우터는 3가지 개념을 다룬다

첫 번째는 이미 소개한 PCI 장치의 인터럽트 핀,
두 번째는 인터럽트 컨트롤러의 인터럽트 라인,
세 번째는 이른바 PCI 링크 장치(PCI Linking device, LNK)다.
PCI 링크 장치는 PCI 버스와 인터럽트 라인을 연결하는 장치라고 이해하면 된다. i440fx 메인보드는 PCI 장치에 4개의 인터럽트 라인을 시뮬레이션하므로, 보통 LNKA, LNKB, LNKC, LNKD라는 4개의 PCI 링크 장치가 있다.
PCI 슬롯의 각 INTx 핀이 LNK 장치에 어떻게 교차 연결되는지 보자

PCI 장치 인터럽트 라우팅은 두 부분으로 나뉜다.
하나는 장치 INTA를 LNKA에 교차로 연결하는 부분으로, 이렇게 하면 각 LNKA의 부하를 균등하게 할 수 있다.
대다수 장치가 INTA로 인터럽트를 트리거하기 때문에, 이렇게 연결하면 각 장치의 INTA가 서로 다른 PCI 링크 장치에 연결된다.
PCI 인터럽트 라우팅의 두 번째 부분은 PCI 링크 장치를 구체적인 IRQ 라인으로 라우팅하는 것인데, 이것은 보통 BIOS가 설정한다.
여기서 주의할 점은, LNK[A-D] 라우팅에서 IRQ로 가는 것이 반드시 일대일 대응은 아니라는 점이다. 두 개의 LNK*가 같은 IRQ로 연결되는 경우도 있을 수 있다.
PCI 링크 장치에서 인터럽트 컨트롤러로 가는 라우팅 정보는 SeaBIOS를 통해 설정된다.
SeaBIOS의 pci_bios_init_device 함수가 piix3/4 장치를 초기화한다.
호출 흐름

pci_bios_init_device 함수
static void pci_bios_init_device(struct pci_device *pci)
{
dprintf(1, "PCI: init bdf=%pP id=%04x:%04x\n"
, pci, pci->vendor, pci->device);
/* map the interrupt */
u16 bdf = pci->bdf;
int pin = pci_config_readb(bdf, PCI_INTERRUPT_PIN);
if (pin != 0)
pci_config_writeb(bdf, PCI_INTERRUPT_LINE, pci_slot_get_irq(pci, pin));
pci_init_device(pci_device_tbl, pci, NULL);
/* enable memory mappings */
pci_config_maskw(bdf, PCI_COMMAND, 0,
PCI_COMMAND_IO | PCI_COMMAND_MEMORY | PCI_COMMAND_SERR);
/* enable SERR# for forwarding */
if (pci->header_type & PCI_HEADER_TYPE_BRIDGE)
pci_config_maskw(bdf, PCI_BRIDGE_CONTROL, 0,
PCI_BRIDGE_CTL_SERR);
}
pci_init_device 함수
int pci_init_device(const struct pci_device_id *ids,
struct pci_device *pci, void *arg)
{
while (ids->vendid || ids->class_mask) {
if ((ids->vendid == PCI_ANY_ID || ids->vendid == pci->vendor) &&
(ids->devid == PCI_ANY_ID || ids->devid == pci->device) &&
!((ids->class ^ pci->class) & ids->class_mask)) {
if (ids->func)
ids->func(pci, arg);
return 0;
}
ids++;
}
return -1;
}
pci_init_device 함수에 전달되는 실제 인자는 다음과 같다
pci_init_device(pci_device_tbl, pci, NULL);
pci_device_tbl 정의
static const struct pci_device_id pci_device_tbl[] = {
/* PIIX3/PIIX4 PCI to ISA bridge */
PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82371SB_0,
piix_isa_bridge_setup),
PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82371AB_0,
piix_isa_bridge_setup),
PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH9_LPC,
mch_isa_bridge_setup),
/* STORAGE IDE */
PCI_DEVICE_CLASS(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82371SB_1,
PCI_CLASS_STORAGE_IDE, piix_ide_setup),
PCI_DEVICE_CLASS(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82371AB,
PCI_CLASS_STORAGE_IDE, piix_ide_setup),
PCI_DEVICE_CLASS(PCI_ANY_ID, PCI_ANY_ID, PCI_CLASS_STORAGE_IDE,
storage_ide_setup),
/* PIC, IBM, MPIC & MPIC2 */
PCI_DEVICE_CLASS(PCI_VENDOR_ID_IBM, 0x0046, PCI_CLASS_SYSTEM_PIC,
pic_ibm_setup),
PCI_DEVICE_CLASS(PCI_VENDOR_ID_IBM, 0xFFFF, PCI_CLASS_SYSTEM_PIC,
pic_ibm_setup),
/* PIIX4 Power Management device (for ACPI) */
PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82371AB_3,
piix4_pm_setup),
PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH9_SMBUS,
ich9_smbus_setup),
/* 0xff00 */
PCI_DEVICE_CLASS(PCI_VENDOR_ID_APPLE, 0x0017, 0xff00, apple_macio_setup),
PCI_DEVICE_CLASS(PCI_VENDOR_ID_APPLE, 0x0022, 0xff00, apple_macio_setup),
/* Intel IGD OpRegion setup */
PCI_DEVICE_CLASS(PCI_VENDOR_ID_INTEL, PCI_ANY_ID, PCI_CLASS_DISPLAY_VGA,
intel_igd_setup),
PCI_DEVICE_END,
};
piix_isa_bridge_setup 함수
/* PIIX3/PIIX4 PCI to ISA bridge */
static void piix_isa_bridge_setup(struct pci_device *pci, void *arg)
{
int i, irq;
u8 elcr[2];
elcr[0] = 0x00;
elcr[1] = 0x00;
for (i = 0; i < 4; i++) {
irq = pci_irqs[i];
/* set to trigger level */
elcr[irq >> 3] |= (1 << (irq & 7));
/* activate irq remapping in PIIX */
pci_config_writeb(pci->bdf, 0x60 + i, irq);
}
outb(elcr[0], PIIX_PORT_ELCR1);
outb(elcr[1], PIIX_PORT_ELCR2);
dprintf(1, "PIIX3/PIIX4 init: elcr=%02x %02x\n", elcr[0], elcr[1]);
}
코드를 보면, piix3/4 장치 설정 공간의 0x60에서 시작하는 위치에 PCI 링크 장치에서 인터럽트 라인으로 가는 라우팅 관계가 쓰여진다.
/* activate irq remapping in PIIX */
pci_config_writeb(pci->bdf, 0x60 + i, irq);
LNK[A-D] → IRQ 매핑
LNKA, LNKB, LNKC, LNKD는 각각 10, 10, 11, 11 이렇게 4개의 인터럽트 라인에 대응한다.
pci_irqs[]가 같은 파일(roms/seabios/src/fw/pciinit.c)에 정의되어 있다.
/* host irqs corresponding to PCI irqs A-D */
const u8 pci_irqs[4] = {
10, 10, 11, 11
};
매핑 관계를 시각화하면 다음과 같다

ACPI를 통한 PCI 장치 → PCI 링크 장치 라우팅
PCI 장치에서 PCI 링크 장치로 가는 라우팅 정보는 ACPI 테이블에 쓰여진다.
ACPI의 영문 풀네임은 Advanced Configuration and Power Interface이고, 한국어로는 고급 구성 및 전원 관리 인터페이스로 번역된다.
ACPI는 프로세서 하드웨어와 운영체제 사이의 인터페이스를 제공해서, 프로세서와 장치의 전원을 관리하는 데 사용되고, 외부 장치가 사용하는 시스템 자원을 설정할 수도 있다.
ACPI는 일련의 기술 사양을 사용해 프로세서와 장치 자원을 관리한다.
PCI 장치의 인터럽트 라우팅은 build_prt 함수를 통해 완성되는데, 이 함수는 128개 항목의 배열을 담은 데이터를 만든다.
각 항목은 한 장치의 라우팅 정보를 나타내며, 그 장치가 어떤 PCI 링크 장치로 라우팅되는지 설명한다. 매핑 관계는 다음 공식에 따라 완성된다.
(slot+pin)&3 -> "LNK[D|A|B|C]"
(4) QEMU에서 PCI 장치가 인터럽트를 트리거하는 흐름
전체 호출 흐름

4-1. IRQ 라우팅 초기화 (pc_init1)
PCI 버스의 IRQ 라우팅 설정은 pc_init1 함수에서 pci_bus_map_irqs와 pci_bus_irqs 함수를 호출하여 완성한다.
/* PC hardware initialisation */
static void pc_init1(MachineState *machine,
const char *host_type, const char *pci_type)
{
PCMachineState *pcms = PC_MACHINE(machine);
PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);
X86MachineState *x86ms = X86_MACHINE(machine);
MemoryRegion *system_memory = get_system_memory();
MemoryRegion *system_io = get_system_io();
PCIBus *pci_bus = NULL;
ISABus *isa_bus;
int piix3_devfn = -1;
qemu_irq smi_irq;
GSIState *gsi_state;
BusState *idebus[MAX_IDE_BUS];
ISADevice *rtc_state;
MemoryRegion *ram_memory;
MemoryRegion *pci_memory = NULL;
MemoryRegion *rom_memory = system_memory;
ram_addr_t lowmem;
uint64_t hole64_size = 0;
……
if (pcmc->pci_enabled) {
Object *phb;
……
pci_bus_map_irqs(pci_bus,
xen_enabled() ? xen_pci_slot_get_pirq
: pc_pci_slot_get_pirq);
}
……
if (xen_enabled()) {
pci_device_set_intx_routing_notifier(
pci_dev, piix_intx_routing_notifier_xen);
/*
* Xen supports additional interrupt routes from the PCI devices to
* the IOAPIC: the four pins of each PCI device on the bus are also
* connected to the IOAPIC directly.
* These additional routes can be discovered through ACPI.
*/
pci_bus_irqs(pci_bus, xen_intx_set_irq, pci_dev,
XEN_IOAPIC_NUM_PIRQS);
}
……
}
4-2. pci_bus_map_irqs / pci_bus_irqs 함수
void pci_bus_map_irqs(PCIBus *bus, pci_map_irq_fn map_irq)
{
bus->map_irq = map_irq;
}
void pci_bus_irqs(PCIBus *bus, pci_set_irq_fn set_irq,
void *irq_opaque, int nirq)
{
bus->set_irq = set_irq;
bus->irq_opaque = irq_opaque;
bus->nirq = nirq;
g_free(bus->irq_count);
bus->irq_count = g_malloc0(nirq * sizeof(bus->irq_count[0]));
}
pci_bus_irqs 함수에 전달되는 실제 인자인 XEN_IOAPIC_NUM_PIRQS가 나타내는 것은 사실 PCI 링크 장치의 개수다.
PCI가 인터럽트 컨트롤러에 연결되는 설정은 BIOS 또는 커널이 PIIX3의 PIRQ[A-D] 4개 핀을 통해 설정하는 것이다.
4-3. pc_pci_slot_get_pirq 함수
/*
* Return the global irq number corresponding to a given device irq
* pin. We could also use the bus number to have a more precise mapping.
*/
static int pc_pci_slot_get_pirq(PCIDevice *pci_dev, int pci_intx)
{
int slot_addend;
slot_addend = PCI_SLOT(pci_dev->devfn) - 1;
return (pci_intx + slot_addend) & 3;
}
pc_pci_get_pirq 함수는 장치가 연결된 PCI 링크 장치를 얻는다.
장치가 사용하는 핀이 x이고 장치의 기능 번호가 y라고 가정하면, 그것이 연결되는 곳은 (x+y) & 3이다.
주의할 점은 이런 관계가 필수는 아니고, 단지 권장되는 연결 방식이라는 것이다.
int xen_pci_slot_get_pirq(PCIDevice *pci_dev, int irq_num)
{
return irq_num + ((PCI_SLOT(pci_dev->devfn)) << 2);
}
4-4. pci_set_irq 함수 - 인터럽트 트리거의 시작
다음으로 PCI 장치가 어떻게 가상 머신 내 운영체제로 인터럽트를 트리거하는지 분석해보자.
PCI 장치는 pci_set_irq 함수를 호출해 인터럽트를 트리거한다.
void pci_set_irq(PCIDevice *pci_dev, int level)
{
int intx = pci_intx(pci_dev);
pci_irq_handler(pci_dev, intx, level);
}
그 중 두 번째 인자는 high인지 low인지 전압 레벨을 나타낸다.
(1) pci_set_irq 함수는 먼저 pci_intx 함수를 호출해 장치가 사용하는 INTX 핀을 얻는다.
static inline int pci_intx(PCIDevice *pci_dev)
{
return pci_get_byte(pci_dev->config + PCI_INTERRUPT_PIN) - 1;
}
이 함수는 설정 공간의 0x3d(PCI_INTERRUPT_PIN) 위치에서 인터럽트 핀 번호를 읽는다. 앞서 본 PCI Agent 장치의 설정 공간 그림에서 0x3C 행의 "Int. Pin" 필드가 그것이다.
(2) 그 다음 pci_irq_handler 함수를 호출한다.
/* 0 <= irq_num <= 3. level must be 0 or 1 */
static void pci_irq_handler(void *opaque, int irq_num, int level)
{
PCIDevice *pci_dev = opaque;
int change;
assert(0 <= irq_num && irq_num < PCI_NUM_PINS);
assert(level == 0 || level == 1);
change = level - pci_irq_state(pci_dev, irq_num);
if (!change)
return;
pci_set_irq_state(pci_dev, irq_num, level);
pci_update_irq_status(pci_dev);
if (pci_irq_disabled(pci_dev))
return;
pci_change_irq_level(pci_dev, irq_num, change);
}
4-5. pci_irq_handler 내부 동작

1) pci_irq_handler 함수는 먼저 현재 인터럽트 라인 상태가 바뀌었는지 판단한다. 바뀌지 않았으면 바로 리턴한다.
static inline int pci_irq_state(PCIDevice *d, int irq_num)
{
return (d->irq_state >> irq_num) & 0x1;
}
2) 인터럽트 라인 상태가 바뀌었으면, pci_set_irq_state와 pci_update_irq_status 함수를 호출해 장치 상태를 설정한다.
static inline void pci_set_irq_state(PCIDevice *d, int irq_num, int level)
{
d->irq_state &= ~(0x1 << irq_num);
d->irq_state |= level << irq_num;
}
/* Update interrupt status bit in config space on interrupt
* state change. */
static void pci_update_irq_status(PCIDevice *dev)
{
if (dev->irq_state) {
dev->config[PCI_STATUS] |= PCI_STATUS_INTERRUPT;
} else {
dev->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
}
}
3) 그 다음 pci_change_irq_level 함수를 호출해 인터럽트를 트리거한다.
static void pci_change_irq_level(PCIDevice *pci_dev, int irq_num, int change)
{
PCIBus *bus;
for (;;) {
int dev_irq = irq_num;
bus = pci_get_bus(pci_dev);
assert(bus->map_irq);
irq_num = bus->map_irq(pci_dev, irq_num);
trace_pci_route_irq(dev_irq, DEVICE(pci_dev)->canonical_path, irq_num,
pci_bus_is_root(bus) ? "root-complex"
: DEVICE(bus->parent_dev)->canonical_path);
if (bus->set_irq)
break;
pci_dev = bus->parent_dev;
}
pci_bus_change_irq_level(bus, irq_num, change);
}
4-6. pci_change_irq_level의 버스 순회
pci_change_irq_level은 루프를 돌면서 부모 버스로 거슬러 올라가는데, 그 동작을 시각화하면 다음과 같다.

1) pci_change_irq_level 함수는 현재 장치에 대응하는 PCI 버스를 얻는다. 코드 조각은 다음과 같다.
bus = pci_get_bus(pci_dev);
2) 그 다음 그 콜백 함수 map_irq를 호출한다.
irq_num = bus->map_irq(pci_dev, irq_num);
루트(root) 버스의 경우, 이 콜백 함수는 바로 위쪽 pc_init1 함수에서 초기화한 pc_pci_slot_get_pirq 함수(또는 xen_pci_slot_get_pirq 함수)다. 이 함수는 실제 인터럽트 라인을 반환한다.
3) 그 다음 PCI 버스의 set_irq 콜백을 호출한다.
pci_bus_change_irq_level(bus, irq_num, change);
4-7. pci_bus_change_irq_level 함수
static void pci_bus_change_irq_level(PCIBus *bus, int irq_num, int change)
{
assert(irq_num >= 0);
assert(irq_num < bus->nirq);
bus->irq_count[irq_num] += change;
bus->set_irq(bus->irq_opaque, irq_num, bus->irq_count[irq_num] != 0);
}
마지막으로 이런 일련의 함수 호출을 거쳐, PCI 장치는 가상 머신 내 운영체제로 인터럽트를 트리거한다.
(5) 핵심 개념 요약

(6) 핵심 함수와 파일 정리
| 단계 | 함수 / 파일 | 역할 |
| 장치 정의 | pci_e1000_realize / hw/net/e1000.c | pci_conf[PCI_INTERRUPT_PIN] = 1로 INTA 핀 설정 |
| BIOS 초기화 | pci_bios_init_device / roms/seabios/src/fw/pciinit.c | 설정 공간의 PCI_INTERRUPT_LINE에 IRQ 쓰기 |
| BIOS 초기화 | piix_isa_bridge_setup / roms/seabios/src/fw/pciinit.c | PIIX3/4 설정 공간 0x60+i에 LNK→IRQ 매핑 기록 |
| QEMU 초기화 | pc_init1 / hw/i386/pc_piix.c | pci_bus_map_irqs, pci_bus_irqs 호출 |
| QEMU 초기화 | pc_pci_slot_get_pirq / hw/i386/pc_piix.c | (pci_intx + slot - 1) & 3으로 LNK 인덱스 계산 |
| 인터럽트 트리거 | pci_set_irq / hw/pci/pci.c | 진입점. INTX 핀 조회 후 핸들러 호출 |
| 인터럽트 트리거 | pci_intx / include/hw/pci/pci_device.h | 설정 공간 0x3d에서 INTX 핀 번호 읽기 |
| 인터럽트 트리거 | pci_irq_handler / hw/pci/pci.c | 상태 변경 판단 및 후속 처리 |
| 인터럽트 트리거 | pci_irq_state / pci_set_irq_state / pci_update_irq_status / hw/pci/pci.c | 인터럽트 라인 상태 관리 |
| 인터럽트 트리거 | pci_change_irq_level / hw/pci/pci.c | 버스 거슬러 올라가며 IRQ 재매핑 |
| 인터럽트 트리거 | pci_bus_change_irq_level / hw/pci/pci.c | 최종적으로 bus->set_irq 호출 |
마무리
PCI 장치 인터럽트의 큰 그림은 결국 다음 세 줄로 요약된다.
- 장치 측: 설정 공간의 0x3d(PCI_INTERRUPT_PIN)에 어떤 INTx 핀을 쓸지 박아둔다.
- 초기화 측: SeaBIOS와 ACPI가 (a) 장치 INTx → LNK 매핑, (b) LNK → IRQ 매핑을 미리 설정한다.
- 런타임 측: 장치가 pci_set_irq를 호출하면, pci_intx로 핀을 읽고 → pci_irq_handler에서 상태 변화 검사 → pci_change_irq_level에서 버스를 거슬러 올라가며 map_irq로 IRQ를 재매핑 → 루트 버스의 set_irq로 최종적으로 가상 머신 OS에 인터럽트가 전달된다.
핵심은 "설정 공간 - BIOS - ACPI - QEMU 런타임" 네 축이 모두 같은 매핑 테이블을 공유하면서, 정적인 라우팅(BIOS/ACPI가 설정)과 동적인 트리거(QEMU 런타임이 실행)가 분리되어 있다는 점이다.
'System Programming > Hypervisor' 카테고리의 다른 글
| QEMU 코드 분석 #7 VirtIO 디바이스 초기화 — QEMU 백엔드 realize & device_plugged (0) | 2026.03.05 |
|---|---|
| QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조 (0) | 2025.02.14 |
| QEMU 코드 분석 #4 Machine & i440fx (0) | 2024.09.19 |
| QEMU 코드 분석 #3 메모리 가상화 & 메모리 초기화 (0) | 2024.07.08 |
| QEMU 코드 분석 #2 KVM CPU가상화 (0) | 2024.07.08 |