The first thing you are going to want to do is perform PCI enumeration. There are basically two ways to do PCI enumeration, either port based or memory mapped based. The port based is the older way and the memory mapped PCI configuration space was introduced with PCI express. The port based approach still works with newer machines and also works with emulators/virtual machines for backwards compatibility. Therefore I will use this approach in the rest of my explanation. This also works as it is a little more complicated and if you understand how to do port based enumeration the memory mapped enumeration is pretty simple. I should add that some non-PC compatible x86 boards do not support port based PCI enumeration.
To access the PCI configuration space (in order to perform enumeration) via ports you need to know about two specific ports:
CONFIG_ADDRESS
- port 0xCF8
CONFIG_DATA
- port 0xCFC
Basically what you do is you write an address to CONFIG_ADDRESS
and read or write the value to that address using CONFIG_DATA
. The address written to CONFIG_ADDRESS
is of the form:
31 30 - 24 23 - 16 15 - 11 10 - 8 7 - 2 1 - 0
Enable Bit Reserved Bus Number Device Number Function Number Register Number 00
The enable bit should basically always be set to 1 assuming you are going to use CONFIG_DATA
to read or write a value.
So say you wish to read the first register for the first device in the configuration, you would do something like:
outl(CONFIG_ADDRESS, 1 << 31);
uint32_t result = inl(CONFIG_DATA)
To determine what each register is you can look at this table:

So in the previous example the first register contains both the device ID and vendor ID.
Now you that you know how to read a register from the PCI configuration space you need to know when you have found a device. Basically if the first register is 0xFFFFFFFF then no device is connected to that bus at that device number. The brute force way is to scan all the buses and devices and then scan the functions if you find a device. More sophisticated ways will look at the header type of the first device on the first bus to determine which buses need to be checked. You can find more information about how to do more sophisticated enumeration here.
To do the memory mapped based approach you will need to find the MCFG table specified in ACPI (also included in the simpler SFI). This will give you the physical address that you can use for the PCI enumeration. All memory addresses will be an offset from this address. Also one of the key differences is that the configuration space has been extended so the address bit fields do not match the port based enumeration bit fields. Addresses for PCI express use the following format:
31 - 28 27 - 20 19 - 15 14 - 12 11 - 8 7 - 2 1 - 0
ZEROS bus number device number function number extended register number register number offset
This is the offset from the physical addressed obtained from the MCFG table. You can find more info about PCI express here.
Finally, while it is device specific, the most useful information you can get from the PCI configuration space is typically the base address registers. These are typically the physical addresses that the device is memory mapped to assuming it is memory mapped or are the port address assuming it is port based. Of course the interrupt pin and line are also useful.