/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved. * Copyright 2016 Joyent, Inc. * Copyright 2019 Western Digital Corporation * Copyright 2020 OmniOS Community Edition (OmniOSce) Association. * Copyright 2022 Oxide Computer Company */ /* * This file contains the x86 PCI platform resource discovery backend. This uses * data from a combination of sources, preferring ACPI, if present, and if not, * falling back to either the PCI hot-plug resource table or the mps tables. * * Today, to get information from ACPI we need to start from a dev_info_t. This * is partly why the PRD interface has a callback for getting information about * a dev_info_t. It also means we cannot initialize the tables with information * until all devices have been initially scanned. */ #include #include #include #include #include #include #include #include #include #include #include #include "mps_table.h" #include "pcihrt.h" extern int pci_bios_maxbus; int pci_prd_debug = 0; #define dprintf if (pci_prd_debug) printf #define dcmn_err if (pci_prd_debug != 0) cmn_err static int tbl_init = 0; static uchar_t *mps_extp = NULL; static uchar_t *mps_ext_endp = NULL; static struct php_entry *hrt_hpep; static uint_t hrt_entry_cnt = 0; static int acpi_cb_cnt = 0; static pci_prd_upcalls_t *prd_upcalls; static void mps_probe(void); static void acpi_pci_probe(void); static int mps_find_bus_res(uint32_t, pci_prd_rsrc_t, struct memlist **); static void hrt_probe(void); static int hrt_find_bus_res(uint32_t, pci_prd_rsrc_t, struct memlist **); static int acpi_find_bus_res(uint32_t, pci_prd_rsrc_t, struct memlist **); static uchar_t *find_sig(uchar_t *cp, int len, char *sig); static int checksum(unsigned char *cp, int len); static ACPI_STATUS acpi_wr_cb(ACPI_RESOURCE *rp, void *context); static void acpi_trim_bus_ranges(void); /* * -1 = attempt ACPI resource discovery * 0 = don't attempt ACPI resource discovery * 1 = ACPI resource discovery successful */ volatile int acpi_resource_discovery = -1; struct memlist *acpi_io_res[PCI_MAX_BUS_NUM]; struct memlist *acpi_mem_res[PCI_MAX_BUS_NUM]; struct memlist *acpi_pmem_res[PCI_MAX_BUS_NUM]; struct memlist *acpi_bus_res[PCI_MAX_BUS_NUM]; /* * This indicates whether or not we have a traditional x86 BIOS present or not. */ static boolean_t pci_prd_have_bios = B_TRUE; /* * This value is set up as part of PCI configuration space initialization. */ extern int pci_bios_maxbus; static void acpi_pci_probe(void) { ACPI_HANDLE ah; int bus; if (acpi_resource_discovery == 0) return; for (bus = 0; bus <= pci_bios_maxbus; bus++) { dev_info_t *dip; dip = prd_upcalls->pru_bus2dip_f(bus); if (dip == NULL || (ACPI_FAILURE(acpica_get_handle(dip, &ah)))) continue; (void) AcpiWalkResources(ah, "_CRS", acpi_wr_cb, (void *)(uintptr_t)bus); } if (acpi_cb_cnt > 0) { acpi_resource_discovery = 1; acpi_trim_bus_ranges(); } } /* * Trim overlapping bus ranges in acpi_bus_res[] * Some BIOSes report root-bridges with bus ranges that * overlap, for example:"0..255" and "8..255". Lower-numbered * ranges are trimmed by upper-numbered ranges (so "0..255" would * be trimmed to "0..7", in the example). */ static void acpi_trim_bus_ranges(void) { struct memlist *ranges, *current; int bus; ranges = NULL; /* * Assumptions: * - there exists at most 1 bus range entry for each bus number * - there are no (broken) ranges that start at the same bus number */ for (bus = 0; bus < PCI_MAX_BUS_NUM; bus++) { struct memlist *prev, *orig, *new; /* skip buses with no range entry */ if ((orig = acpi_bus_res[bus]) == NULL) continue; /* * create copy of existing range and overload * 'prev' pointer to link existing to new copy */ new = memlist_alloc(); new->ml_address = orig->ml_address; new->ml_size = orig->ml_size; new->ml_prev = orig; /* sorted insertion of 'new' into ranges list */ for (current = ranges, prev = NULL; current != NULL; prev = current, current = current->ml_next) if (new->ml_address < current->ml_address) break; if (prev == NULL) { /* place at beginning of (possibly) empty list */ new->ml_next = ranges; ranges = new; } else { /* place in list (possibly at end) */ new->ml_next = current; prev->ml_next = new; } } /* scan the list, perform trimming */ current = ranges; while (current != NULL) { struct memlist *next = current->ml_next; /* done when no range above current */ if (next == NULL) break; /* * trim size in original range element * (current->ml_prev points to the original range) */ if ((current->ml_address + current->ml_size) > next->ml_address) current->ml_prev->ml_size = next->ml_address - current->ml_address; current = next; } /* discard the list */ memlist_free_all(&ranges); /* OK if ranges == NULL */ } static int acpi_find_bus_res(uint32_t bus, pci_prd_rsrc_t type, struct memlist **res) { ASSERT3U(bus, <, PCI_MAX_BUS_NUM); switch (type) { case PCI_PRD_R_IO: *res = acpi_io_res[bus]; break; case PCI_PRD_R_MMIO: *res = acpi_mem_res[bus]; break; case PCI_PRD_R_PREFETCH: *res = acpi_pmem_res[bus]; break; case PCI_PRD_R_BUS: *res = acpi_bus_res[bus]; break; default: *res = NULL; break; } /* memlist_count() treats NULL head as zero-length */ return (memlist_count(*res)); } static struct memlist ** rlistpp(UINT8 t, UINT8 caching, int bus) { switch (t) { case ACPI_MEMORY_RANGE: if (caching == ACPI_PREFETCHABLE_MEMORY) return (&acpi_pmem_res[bus]); else return (&acpi_mem_res[bus]); break; case ACPI_IO_RANGE: return (&acpi_io_res[bus]); break; case ACPI_BUS_NUMBER_RANGE: return (&acpi_bus_res[bus]); break; } return (NULL); } static void acpi_dbg(uint_t bus, uint64_t addr, uint64_t len, uint8_t caching, uint8_t type, char *tag) { char *s; switch (type) { case ACPI_MEMORY_RANGE: s = "MEM"; break; case ACPI_IO_RANGE: s = "IO"; break; case ACPI_BUS_NUMBER_RANGE: s = "BUS"; break; default: s = "???"; break; } dprintf("ACPI: bus %x %s/%s %lx/%lx (Caching: %x)\n", bus, tag, s, addr, len, caching); } static ACPI_STATUS acpi_wr_cb(ACPI_RESOURCE *rp, void *context) { int bus = (intptr_t)context; /* ignore consumed resources */ if (rp->Data.Address.ProducerConsumer == 1) return (AE_OK); switch (rp->Type) { case ACPI_RESOURCE_TYPE_IRQ: /* never expect to see a PCI bus produce an Interrupt */ dprintf("%s\n", "IRQ"); break; case ACPI_RESOURCE_TYPE_DMA: /* never expect to see a PCI bus produce DMA */ dprintf("%s\n", "DMA"); break; case ACPI_RESOURCE_TYPE_START_DEPENDENT: dprintf("%s\n", "START_DEPENDENT"); break; case ACPI_RESOURCE_TYPE_END_DEPENDENT: dprintf("%s\n", "END_DEPENDENT"); break; case ACPI_RESOURCE_TYPE_IO: if (rp->Data.Io.AddressLength == 0) break; acpi_cb_cnt++; memlist_insert(&acpi_io_res[bus], rp->Data.Io.Minimum, rp->Data.Io.AddressLength); if (pci_prd_debug != 0) { acpi_dbg(bus, rp->Data.Io.Minimum, rp->Data.Io.AddressLength, 0, ACPI_IO_RANGE, "IO"); } break; case ACPI_RESOURCE_TYPE_FIXED_IO: /* only expect to see this as a consumer */ dprintf("%s\n", "FIXED_IO"); break; case ACPI_RESOURCE_TYPE_VENDOR: dprintf("%s\n", "VENDOR"); break; case ACPI_RESOURCE_TYPE_END_TAG: dprintf("%s\n", "END_TAG"); break; case ACPI_RESOURCE_TYPE_MEMORY24: /* only expect to see this as a consumer */ dprintf("%s\n", "MEMORY24"); break; case ACPI_RESOURCE_TYPE_MEMORY32: /* only expect to see this as a consumer */ dprintf("%s\n", "MEMORY32"); break; case ACPI_RESOURCE_TYPE_FIXED_MEMORY32: /* only expect to see this as a consumer */ dprintf("%s\n", "FIXED_MEMORY32"); break; case ACPI_RESOURCE_TYPE_ADDRESS16: if (rp->Data.Address16.Address.AddressLength == 0) break; acpi_cb_cnt++; memlist_insert(rlistpp(rp->Data.Address16.ResourceType, rp->Data.Address.Info.Mem.Caching, bus), rp->Data.Address16.Address.Minimum, rp->Data.Address16.Address.AddressLength); if (pci_prd_debug != 0) { acpi_dbg(bus, rp->Data.Address16.Address.Minimum, rp->Data.Address16.Address.AddressLength, rp->Data.Address.Info.Mem.Caching, rp->Data.Address16.ResourceType, "ADDRESS16"); } break; case ACPI_RESOURCE_TYPE_ADDRESS32: if (rp->Data.Address32.Address.AddressLength == 0) break; acpi_cb_cnt++; memlist_insert(rlistpp(rp->Data.Address32.ResourceType, rp->Data.Address.Info.Mem.Caching, bus), rp->Data.Address32.Address.Minimum, rp->Data.Address32.Address.AddressLength); if (pci_prd_debug != 0) { acpi_dbg(bus, rp->Data.Address32.Address.Minimum, rp->Data.Address32.Address.AddressLength, rp->Data.Address.Info.Mem.Caching, rp->Data.Address32.ResourceType, "ADDRESS32"); } break; case ACPI_RESOURCE_TYPE_ADDRESS64: if (rp->Data.Address64.Address.AddressLength == 0) break; acpi_cb_cnt++; memlist_insert(rlistpp(rp->Data.Address64.ResourceType, rp->Data.Address.Info.Mem.Caching, bus), rp->Data.Address64.Address.Minimum, rp->Data.Address64.Address.AddressLength); if (pci_prd_debug != 0) { acpi_dbg(bus, rp->Data.Address64.Address.Minimum, rp->Data.Address64.Address.AddressLength, rp->Data.Address.Info.Mem.Caching, rp->Data.Address64.ResourceType, "ADDRESS64"); } break; case ACPI_RESOURCE_TYPE_EXTENDED_ADDRESS64: if (rp->Data.ExtAddress64.Address.AddressLength == 0) break; acpi_cb_cnt++; memlist_insert(rlistpp(rp->Data.ExtAddress64.ResourceType, rp->Data.Address.Info.Mem.Caching, bus), rp->Data.ExtAddress64.Address.Minimum, rp->Data.ExtAddress64.Address.AddressLength); if (pci_prd_debug != 0) { acpi_dbg(bus, rp->Data.ExtAddress64.Address.Minimum, rp->Data.ExtAddress64.Address.AddressLength, rp->Data.Address.Info.Mem.Caching, rp->Data.ExtAddress64.ResourceType, "EXTADDRESS64"); } break; case ACPI_RESOURCE_TYPE_EXTENDED_IRQ: /* never expect to see a PCI bus produce an Interrupt */ dprintf("%s\n", "EXTENDED_IRQ"); break; case ACPI_RESOURCE_TYPE_GENERIC_REGISTER: /* never expect to see a PCI bus produce an GAS */ dprintf("%s\n", "GENERIC_REGISTER"); break; } return (AE_OK); } static void mps_probe(void) { uchar_t *extp; struct mps_fps_hdr *fpp = NULL; struct mps_ct_hdr *ctp; uintptr_t ebda_start, base_end; ushort_t ebda_seg, base_size, ext_len, base_len, base_end_seg; base_size = *((ushort_t *)(0x413)); ebda_seg = *((ushort_t *)(0x40e)); ebda_start = ((uint32_t)ebda_seg) << 4; if (ebda_seg != 0) { fpp = (struct mps_fps_hdr *)find_sig( (uchar_t *)ebda_start, 1024, "_MP_"); } if (fpp == NULL) { base_end_seg = (base_size > 512) ? 0x9FC0 : 0x7FC0; if (base_end_seg != ebda_seg) { base_end = ((uintptr_t)base_end_seg) << 4; fpp = (struct mps_fps_hdr *)find_sig( (uchar_t *)base_end, 1024, "_MP_"); } } if (fpp == NULL) { fpp = (struct mps_fps_hdr *)find_sig( (uchar_t *)0xF0000, 0x10000, "_MP_"); } if (fpp == NULL) { dprintf("MP Spec table doesn't exist"); return; } else { dprintf("Found MP Floating Pointer Structure at %p\n", (void *)fpp); } if (checksum((uchar_t *)fpp, fpp->fps_len * 16) != 0) { dprintf("MP Floating Pointer Structure checksum error"); return; } ctp = (struct mps_ct_hdr *)(uintptr_t)fpp->fps_mpct_paddr; if (ctp->ct_sig != 0x504d4350) { /* check "PCMP" signature */ dprintf("MP Configuration Table signature is wrong"); return; } base_len = ctp->ct_len; if (checksum((uchar_t *)ctp, base_len) != 0) { dprintf("MP Configuration Table checksum error"); return; } if (ctp->ct_spec_rev != 4) { /* not MPSpec rev 1.4 */ dprintf("MP Spec 1.1 found - extended table doesn't exist"); return; } if ((ext_len = ctp->ct_ext_tbl_len) == 0) { dprintf("MP Spec 1.4 found - extended table doesn't exist"); return; } extp = (uchar_t *)ctp + base_len; if (((checksum(extp, ext_len) + ctp->ct_ext_cksum) & 0xFF) != 0) { dprintf("MP Extended Table checksum error"); return; } mps_extp = extp; mps_ext_endp = mps_extp + ext_len; } static int mps_find_bus_res(uint32_t bus, pci_prd_rsrc_t rsrc, struct memlist **res) { struct sasm *sasmp; uchar_t *extp; int res_cnt, type; ASSERT3U(bus, <, PCI_MAX_BUS_NUM); if (mps_extp == NULL) return (0); switch (rsrc) { case PCI_PRD_R_IO: type = IO_TYPE; break; case PCI_PRD_R_MMIO: type = MEM_TYPE; break; case PCI_PRD_R_PREFETCH: type = PREFETCH_TYPE; break; case PCI_PRD_R_BUS: type = BUSRANGE_TYPE; break; default: *res = NULL; return (0); } extp = mps_extp; res_cnt = 0; while (extp < mps_ext_endp) { switch (*extp) { case SYS_AS_MAPPING: sasmp = (struct sasm *)extp; if (sasmp->sasm_as_type == type && sasmp->sasm_bus_id == bus) { uint64_t base, len; base = (uint64_t)sasmp->sasm_as_base | (uint64_t)sasmp->sasm_as_base_hi << 32; len = (uint64_t)sasmp->sasm_as_len | (uint64_t)sasmp->sasm_as_len_hi << 32; memlist_insert(res, base, len); res_cnt++; } extp += SYS_AS_MAPPING_SIZE; break; case BUS_HIERARCHY_DESC: extp += BUS_HIERARCHY_DESC_SIZE; break; case COMP_BUS_AS_MODIFIER: extp += COMP_BUS_AS_MODIFIER_SIZE; break; default: cmn_err(CE_WARN, "Unknown descriptor type %d" " in BIOS Multiprocessor Spec table.", *extp); memlist_free_all(res); return (0); } } return (res_cnt); } static void hrt_probe(void) { struct hrt_hdr *hrtp; dprintf("search PCI Hot-Plug Resource Table starting at 0xF0000\n"); if ((hrtp = (struct hrt_hdr *)find_sig((uchar_t *)0xF0000, 0x10000, "$HRT")) == NULL) { dprintf("NO PCI Hot-Plug Resource Table"); return; } dprintf("Found PCI Hot-Plug Resource Table at %p\n", (void *)hrtp); if (hrtp->hrt_ver != 1) { dprintf("PCI Hot-Plug Resource Table version no. <> 1\n"); return; } hrt_entry_cnt = (uint_t)hrtp->hrt_entry_cnt; dprintf("No. of PCI hot-plug slot entries = 0x%x\n", hrt_entry_cnt); hrt_hpep = (struct php_entry *)(hrtp + 1); } static int hrt_find_bus_res(uint32_t bus, pci_prd_rsrc_t type, struct memlist **res) { int res_cnt; struct php_entry *hpep; ASSERT3U(bus, <, PCI_MAX_BUS_NUM); if (hrt_hpep == NULL || hrt_entry_cnt == 0) return (0); hpep = hrt_hpep; res_cnt = 0; for (uint_t i = 0; i < hrt_entry_cnt; i++, hpep++) { if (hpep->php_pri_bus != bus) continue; if (type == PCI_PRD_R_IO) { if (hpep->php_io_start == 0 || hpep->php_io_size == 0) continue; memlist_insert(res, (uint64_t)hpep->php_io_start, (uint64_t)hpep->php_io_size); res_cnt++; } else if (type == PCI_PRD_R_MMIO) { if (hpep->php_mem_start == 0 || hpep->php_mem_size == 0) continue; memlist_insert(res, ((uint64_t)hpep->php_mem_start) << 16, ((uint64_t)hpep->php_mem_size) << 16); res_cnt++; } else if (type == PCI_PRD_R_PREFETCH) { if (hpep->php_pfmem_start == 0 || hpep->php_pfmem_size == 0) continue; memlist_insert(res, ((uint64_t)hpep->php_pfmem_start) << 16, ((uint64_t)hpep->php_pfmem_size) << 16); res_cnt++; } } return (res_cnt); } static uchar_t * find_sig(uchar_t *cp, int len, char *sig) { long i; /* Search for the "_MP_" or "$HRT" signature */ for (i = 0; i < len; i += 16) { if (cp[0] == sig[0] && cp[1] == sig[1] && cp[2] == sig[2] && cp[3] == sig[3]) return (cp); cp += 16; } return (NULL); } static int checksum(unsigned char *cp, int len) { int i; unsigned int cksum; for (i = cksum = 0; i < len; i++) cksum += (unsigned int) *cp++; return ((int)(cksum & 0xFF)); } uint32_t pci_prd_max_bus(void) { return ((uint32_t)pci_bios_maxbus); } struct memlist * pci_prd_find_resource(uint32_t bus, pci_prd_rsrc_t rsrc) { struct memlist *res = NULL; if (bus > pci_bios_maxbus) return (NULL); if (tbl_init == 0) { tbl_init = 1; acpi_pci_probe(); if (pci_prd_have_bios) { hrt_probe(); mps_probe(); } } if (acpi_find_bus_res(bus, rsrc, &res) > 0) return (res); if (pci_prd_have_bios && hrt_find_bus_res(bus, rsrc, &res) > 0) return (res); if (pci_prd_have_bios) (void) mps_find_bus_res(bus, rsrc, &res); return (res); } typedef struct { pci_prd_root_complex_f ppac_func; void *ppac_arg; } pci_prd_acpi_cb_t; static ACPI_STATUS pci_process_acpi_device(ACPI_HANDLE hdl, UINT32 level, void *ctx, void **rv) { ACPI_DEVICE_INFO *adi; int busnum; pci_prd_acpi_cb_t *cb = ctx; /* * Use AcpiGetObjectInfo() to find the device _HID * If not a PCI root-bus, ignore this device and continue * the walk */ if (ACPI_FAILURE(AcpiGetObjectInfo(hdl, &adi))) return (AE_OK); if (!(adi->Valid & ACPI_VALID_HID)) { AcpiOsFree(adi); return (AE_OK); } if (strncmp(adi->HardwareId.String, PCI_ROOT_HID_STRING, sizeof (PCI_ROOT_HID_STRING)) && strncmp(adi->HardwareId.String, PCI_EXPRESS_ROOT_HID_STRING, sizeof (PCI_EXPRESS_ROOT_HID_STRING))) { AcpiOsFree(adi); return (AE_OK); } AcpiOsFree(adi); /* * acpica_get_busno() will check the presence of _BBN and * fail if not present. It will then use the _CRS method to * retrieve the actual bus number assigned, it will fall back * to _BBN should the _CRS method fail. */ if (ACPI_SUCCESS(acpica_get_busno(hdl, &busnum))) { /* * Ignore invalid _BBN return values here (rather * than panic) and emit a warning; something else * may suffer failure as a result of the broken BIOS. */ if (busnum < 0) { dcmn_err(CE_NOTE, "pci_process_acpi_device: invalid _BBN 0x%x", busnum); return (AE_CTRL_DEPTH); } if (cb->ppac_func((uint32_t)busnum, cb->ppac_arg)) return (AE_CTRL_DEPTH); return (AE_CTRL_TERMINATE); } /* PCI and no _BBN, continue walk */ return (AE_OK); } void pci_prd_root_complex_iter(pci_prd_root_complex_f func, void *arg) { void *rv; pci_prd_acpi_cb_t cb; cb.ppac_func = func; cb.ppac_arg = arg; /* * First scan ACPI devices for anything that might be here. After that, * go through and check the old BIOS IRQ routing table for additional * buses. Note, slot naming from the IRQ table comes later. */ (void) AcpiGetDevices(NULL, pci_process_acpi_device, &cb, &rv); pci_bios_bus_iter(func, arg); } /* * If there is actually a PCI IRQ routing table present, then we want to use * this to go back and update the slot name. In particular, if we have no PCI * IRQ routing table, then we use the existing slot names that were already set * up for us in picex_slot_names_prop() from the capability register. Otherwise, * we actually delete all slot-names properties from buses and instead use * something from the IRQ routing table if it exists. * * Note, the property is always deleted regardless of whether or not it exists * in the IRQ routing table. Finally, we have traditionally kept "pcie0" names * as special as apparently that can't be represented in the IRQ routing table. */ void pci_prd_slot_name(uint32_t bus, dev_info_t *dip) { char slotprop[256]; int len; char *slotcap_name; if (pci_irq_nroutes == 0) return; if (dip != NULL) { if (ddi_prop_lookup_string(DDI_DEV_T_ANY, pci_bus_res[bus].dip, DDI_PROP_DONTPASS, "slot-names", &slotcap_name) != DDI_SUCCESS || strcmp(slotcap_name, "pcie0") != 0) { (void) ndi_prop_remove(DDI_DEV_T_NONE, pci_bus_res[bus].dip, "slot-names"); } } len = pci_slot_names_prop(bus, slotprop, sizeof (slotprop)); if (len > 0) { if (dip != NULL) { ASSERT((len % sizeof (int)) == 0); (void) ndi_prop_update_int_array(DDI_DEV_T_NONE, pci_bus_res[bus].dip, "slot-names", (int *)slotprop, len / sizeof (int)); } else { cmn_err(CE_NOTE, "!BIOS BUG: Invalid bus number in PCI " "IRQ routing table; Not adding slot-names " "property for incorrect bus %d", bus); } } } boolean_t pci_prd_multi_root_ok(void) { return (acpi_resource_discovery > 0); } int pci_prd_init(pci_prd_upcalls_t *upcalls) { if (ddi_prop_exists(DDI_DEV_T_ANY, ddi_root_node(), DDI_PROP_DONTPASS, "efi-systab")) { pci_prd_have_bios = B_FALSE; } prd_upcalls = upcalls; return (0); } void pci_prd_fini(void) { int bus; for (bus = 0; bus <= pci_bios_maxbus; bus++) { memlist_free_all(&acpi_io_res[bus]); memlist_free_all(&acpi_mem_res[bus]); memlist_free_all(&acpi_pmem_res[bus]); memlist_free_all(&acpi_bus_res[bus]); } } static struct modlmisc pci_prd_modlmisc_i86pc = { .misc_modops = &mod_miscops, .misc_linkinfo = "i86pc PCI Resource Discovery" }; static struct modlinkage pci_prd_modlinkage_i86pc = { .ml_rev = MODREV_1, .ml_linkage = { &pci_prd_modlmisc_i86pc, NULL } }; int _init(void) { return (mod_install(&pci_prd_modlinkage_i86pc)); } int _info(struct modinfo *modinfop) { return (mod_info(&pci_prd_modlinkage_i86pc, modinfop)); } int _fini(void) { return (mod_remove(&pci_prd_modlinkage_i86pc)); }