mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git
synced 2024-12-29 09:12:07 +00:00
2b1bd50754
The first error message in device_id_check() is obscure and can be misleading because the cause of the error is unlikely to be found in the struct definition in mod_devicetable.h. This type of error occurs when an array is passed to an incorrect type of MODULE_DEVICE_TABLE(). [Example 1] static const struct acpi_device_id foo_ids[] = { { "FOO" }, { /* sentinel */ }, }; MODULE_DEVICE_TABLE(of, foo_ids); Currently, modpost outputs a meaningless suggestion: ERROR: modpost: ...: sizeof(struct of_device_id)=200 is not a modulo of the size of section __mod_device_table__of__<identifier>=64. Fix definition of struct of_device_id in mod_devicetable.h The root cause here is that MODULE_DEVICE_TABLE(of, ...) is used instead of the correct MODULE_DEVICE_TABLE(acpi, ...). This commit provides a more intuitive error message: ERROR: modpost: ...: type mismatch between foo_ids[] and MODULE_DEVICE_TABLE(of, ...) The second error message, related to a missing terminator, is too verbose. [Example 2] static const struct acpi_device_id foo_ids[] = { { "FOO" }, }; MODULE_DEVICE_TABLE(acpi, foo_ids); The current error message is overly long, and does not pinpoint the incorrect array: ...: struct acpi_device_id is 32 bytes. The last of 1 is: 0x46 0x4f 0x4f 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 ERROR: modpost: ...: struct acpi_device_id is not terminated with a NULL entry! This commit changes it to a more concise error message, sufficient to identify the incorrect array: ERROR: modpost: ...: foo_ids[] is not terminated with a NULL entry Lastly, this commit squashes device_id_check() into do_table() and changes fatal() into error(), allowing modpost to continue processing other modules. Signed-off-by: Masahiro Yamada <masahiroy@kernel.org>
1529 lines
44 KiB
C
1529 lines
44 KiB
C
/* Simple code to turn various tables in an ELF file into alias definitions.
|
|
* This deals with kernel datastructures where they should be
|
|
* dealt with: in the kernel source.
|
|
*
|
|
* Copyright 2002-2003 Rusty Russell, IBM Corporation
|
|
* 2003 Kai Germaschewski
|
|
*
|
|
*
|
|
* This software may be used and distributed according to the terms
|
|
* of the GNU General Public License, incorporated herein by reference.
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
|
|
#include "list.h"
|
|
#include "xalloc.h"
|
|
|
|
#include "modpost.h"
|
|
#include "devicetable-offsets.h"
|
|
|
|
/* We use the ELF typedefs for kernel_ulong_t but bite the bullet and
|
|
* use either stdint.h or inttypes.h for the rest. */
|
|
#if KERNEL_ELFCLASS == ELFCLASS32
|
|
typedef Elf32_Addr kernel_ulong_t;
|
|
#define BITS_PER_LONG 32
|
|
#else
|
|
typedef Elf64_Addr kernel_ulong_t;
|
|
#define BITS_PER_LONG 64
|
|
#endif
|
|
#ifdef __sun__
|
|
#include <inttypes.h>
|
|
#else
|
|
#include <stdint.h>
|
|
#endif
|
|
|
|
#include <ctype.h>
|
|
#include <stdbool.h>
|
|
|
|
/**
|
|
* module_alias_printf - add auto-generated MODULE_ALIAS()
|
|
*
|
|
* @mod: module
|
|
* @append_wildcard: append '*' for future extension if not exist yet
|
|
* @fmt: printf(3)-like format
|
|
*/
|
|
static void __attribute__((format (printf, 3, 4)))
|
|
module_alias_printf(struct module *mod, bool append_wildcard,
|
|
const char *fmt, ...)
|
|
{
|
|
struct module_alias *new, *als;
|
|
size_t len;
|
|
int n;
|
|
va_list ap;
|
|
|
|
/* Determine required size. */
|
|
va_start(ap, fmt);
|
|
n = vsnprintf(NULL, 0, fmt, ap);
|
|
va_end(ap);
|
|
|
|
if (n < 0) {
|
|
error("vsnprintf failed\n");
|
|
return;
|
|
}
|
|
|
|
len = n + 1; /* extra byte for '\0' */
|
|
|
|
if (append_wildcard)
|
|
len++; /* extra byte for '*' */
|
|
|
|
new = xmalloc(sizeof(*new) + len);
|
|
|
|
/* Now, really print it to the allocated buffer */
|
|
va_start(ap, fmt);
|
|
n = vsnprintf(new->str, len, fmt, ap);
|
|
va_end(ap);
|
|
|
|
if (n < 0) {
|
|
error("vsnprintf failed\n");
|
|
free(new);
|
|
return;
|
|
}
|
|
|
|
if (append_wildcard && (n == 0 || new->str[n - 1] != '*')) {
|
|
new->str[n] = '*';
|
|
new->str[n + 1] = '\0';
|
|
}
|
|
|
|
/* avoid duplication */
|
|
list_for_each_entry(als, &mod->aliases, node) {
|
|
if (!strcmp(als->str, new->str)) {
|
|
free(new);
|
|
return;
|
|
}
|
|
}
|
|
|
|
list_add_tail(&new->node, &mod->aliases);
|
|
}
|
|
|
|
typedef uint32_t __u32;
|
|
typedef uint16_t __u16;
|
|
typedef unsigned char __u8;
|
|
|
|
/* UUID types for backward compatibility, don't use in new code */
|
|
typedef struct {
|
|
__u8 b[16];
|
|
} guid_t;
|
|
|
|
typedef struct {
|
|
__u8 b[16];
|
|
} uuid_t;
|
|
|
|
#define UUID_STRING_LEN 36
|
|
|
|
/* MEI UUID type, don't use anywhere else */
|
|
typedef struct {
|
|
__u8 b[16];
|
|
} uuid_le;
|
|
|
|
/* Big exception to the "don't include kernel headers into userspace, which
|
|
* even potentially has different endianness and word sizes, since
|
|
* we handle those differences explicitly below */
|
|
#include "../../include/linux/mod_devicetable.h"
|
|
|
|
struct devtable {
|
|
const char *device_id;
|
|
unsigned long id_size;
|
|
void (*do_entry)(struct module *mod, void *symval);
|
|
};
|
|
|
|
/* Define a variable f that holds the value of field f of struct devid
|
|
* based at address m.
|
|
*/
|
|
#define DEF_FIELD(m, devid, f) \
|
|
typeof(((struct devid *)0)->f) f = TO_NATIVE(*(typeof(f) *)((m) + OFF_##devid##_##f))
|
|
|
|
/* Define a variable f that holds the address of field f of struct devid
|
|
* based at address m. Due to the way typeof works, for a field of type
|
|
* T[N] the variable has type T(*)[N], _not_ T*.
|
|
*/
|
|
#define DEF_FIELD_ADDR(m, devid, f) \
|
|
typeof(((struct devid *)0)->f) *f = ((m) + OFF_##devid##_##f)
|
|
|
|
#define ADD(str, sep, cond, field) \
|
|
do { \
|
|
strcat(str, sep); \
|
|
if (cond) \
|
|
sprintf(str + strlen(str), \
|
|
sizeof(field) == 1 ? "%02X" : \
|
|
sizeof(field) == 2 ? "%04X" : \
|
|
sizeof(field) == 4 ? "%08X" : "", \
|
|
field); \
|
|
else \
|
|
sprintf(str + strlen(str), "*"); \
|
|
} while(0)
|
|
|
|
static inline void add_uuid(char *str, uuid_le uuid)
|
|
{
|
|
int len = strlen(str);
|
|
|
|
sprintf(str + len, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
uuid.b[3], uuid.b[2], uuid.b[1], uuid.b[0],
|
|
uuid.b[5], uuid.b[4], uuid.b[7], uuid.b[6],
|
|
uuid.b[8], uuid.b[9], uuid.b[10], uuid.b[11],
|
|
uuid.b[12], uuid.b[13], uuid.b[14], uuid.b[15]);
|
|
}
|
|
|
|
static inline void add_guid(char *str, guid_t guid)
|
|
{
|
|
int len = strlen(str);
|
|
|
|
sprintf(str + len, "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
|
|
guid.b[3], guid.b[2], guid.b[1], guid.b[0],
|
|
guid.b[5], guid.b[4], guid.b[7], guid.b[6],
|
|
guid.b[8], guid.b[9], guid.b[10], guid.b[11],
|
|
guid.b[12], guid.b[13], guid.b[14], guid.b[15]);
|
|
}
|
|
|
|
/* USB is special because the bcdDevice can be matched against a numeric range */
|
|
/* Looks like "usb:vNpNdNdcNdscNdpNicNiscNipNinN" */
|
|
static void do_usb_entry(void *symval,
|
|
unsigned int bcdDevice_initial, int bcdDevice_initial_digits,
|
|
unsigned char range_lo, unsigned char range_hi,
|
|
unsigned char max, struct module *mod)
|
|
{
|
|
char alias[500];
|
|
DEF_FIELD(symval, usb_device_id, match_flags);
|
|
DEF_FIELD(symval, usb_device_id, idVendor);
|
|
DEF_FIELD(symval, usb_device_id, idProduct);
|
|
DEF_FIELD(symval, usb_device_id, bcdDevice_lo);
|
|
DEF_FIELD(symval, usb_device_id, bDeviceClass);
|
|
DEF_FIELD(symval, usb_device_id, bDeviceSubClass);
|
|
DEF_FIELD(symval, usb_device_id, bDeviceProtocol);
|
|
DEF_FIELD(symval, usb_device_id, bInterfaceClass);
|
|
DEF_FIELD(symval, usb_device_id, bInterfaceSubClass);
|
|
DEF_FIELD(symval, usb_device_id, bInterfaceProtocol);
|
|
DEF_FIELD(symval, usb_device_id, bInterfaceNumber);
|
|
|
|
strcpy(alias, "usb:");
|
|
ADD(alias, "v", match_flags&USB_DEVICE_ID_MATCH_VENDOR,
|
|
idVendor);
|
|
ADD(alias, "p", match_flags&USB_DEVICE_ID_MATCH_PRODUCT,
|
|
idProduct);
|
|
|
|
strcat(alias, "d");
|
|
if (bcdDevice_initial_digits)
|
|
sprintf(alias + strlen(alias), "%0*X",
|
|
bcdDevice_initial_digits, bcdDevice_initial);
|
|
if (range_lo == range_hi)
|
|
sprintf(alias + strlen(alias), "%X", range_lo);
|
|
else if (range_lo > 0 || range_hi < max) {
|
|
if (range_lo > 0x9 || range_hi < 0xA)
|
|
sprintf(alias + strlen(alias),
|
|
"[%X-%X]",
|
|
range_lo,
|
|
range_hi);
|
|
else {
|
|
sprintf(alias + strlen(alias),
|
|
range_lo < 0x9 ? "[%X-9" : "[%X",
|
|
range_lo);
|
|
sprintf(alias + strlen(alias),
|
|
range_hi > 0xA ? "A-%X]" : "%X]",
|
|
range_hi);
|
|
}
|
|
}
|
|
if (bcdDevice_initial_digits < (sizeof(bcdDevice_lo) * 2 - 1))
|
|
strcat(alias, "*");
|
|
|
|
ADD(alias, "dc", match_flags&USB_DEVICE_ID_MATCH_DEV_CLASS,
|
|
bDeviceClass);
|
|
ADD(alias, "dsc", match_flags&USB_DEVICE_ID_MATCH_DEV_SUBCLASS,
|
|
bDeviceSubClass);
|
|
ADD(alias, "dp", match_flags&USB_DEVICE_ID_MATCH_DEV_PROTOCOL,
|
|
bDeviceProtocol);
|
|
ADD(alias, "ic", match_flags&USB_DEVICE_ID_MATCH_INT_CLASS,
|
|
bInterfaceClass);
|
|
ADD(alias, "isc", match_flags&USB_DEVICE_ID_MATCH_INT_SUBCLASS,
|
|
bInterfaceSubClass);
|
|
ADD(alias, "ip", match_flags&USB_DEVICE_ID_MATCH_INT_PROTOCOL,
|
|
bInterfaceProtocol);
|
|
ADD(alias, "in", match_flags&USB_DEVICE_ID_MATCH_INT_NUMBER,
|
|
bInterfaceNumber);
|
|
|
|
module_alias_printf(mod, true, "%s", alias);
|
|
}
|
|
|
|
/* Handles increment/decrement of BCD formatted integers */
|
|
/* Returns the previous value, so it works like i++ or i-- */
|
|
static unsigned int incbcd(unsigned int *bcd,
|
|
int inc,
|
|
unsigned char max,
|
|
size_t chars)
|
|
{
|
|
unsigned int init = *bcd, i, j;
|
|
unsigned long long c, dec = 0;
|
|
|
|
/* If bcd is not in BCD format, just increment */
|
|
if (max > 0x9) {
|
|
*bcd += inc;
|
|
return init;
|
|
}
|
|
|
|
/* Convert BCD to Decimal */
|
|
for (i=0 ; i < chars ; i++) {
|
|
c = (*bcd >> (i << 2)) & 0xf;
|
|
c = c > 9 ? 9 : c; /* force to bcd just in case */
|
|
for (j=0 ; j < i ; j++)
|
|
c = c * 10;
|
|
dec += c;
|
|
}
|
|
|
|
/* Do our increment/decrement */
|
|
dec += inc;
|
|
*bcd = 0;
|
|
|
|
/* Convert back to BCD */
|
|
for (i=0 ; i < chars ; i++) {
|
|
for (c=1,j=0 ; j < i ; j++)
|
|
c = c * 10;
|
|
c = (dec / c) % 10;
|
|
*bcd += c << (i << 2);
|
|
}
|
|
return init;
|
|
}
|
|
|
|
static void do_usb_entry_multi(struct module *mod, void *symval)
|
|
{
|
|
unsigned int devlo, devhi;
|
|
unsigned char chi, clo, max;
|
|
int ndigits;
|
|
|
|
DEF_FIELD(symval, usb_device_id, match_flags);
|
|
DEF_FIELD(symval, usb_device_id, idVendor);
|
|
DEF_FIELD(symval, usb_device_id, idProduct);
|
|
DEF_FIELD(symval, usb_device_id, bcdDevice_lo);
|
|
DEF_FIELD(symval, usb_device_id, bcdDevice_hi);
|
|
DEF_FIELD(symval, usb_device_id, bDeviceClass);
|
|
DEF_FIELD(symval, usb_device_id, bInterfaceClass);
|
|
|
|
devlo = match_flags & USB_DEVICE_ID_MATCH_DEV_LO ?
|
|
bcdDevice_lo : 0x0U;
|
|
devhi = match_flags & USB_DEVICE_ID_MATCH_DEV_HI ?
|
|
bcdDevice_hi : ~0x0U;
|
|
|
|
/* Figure out if this entry is in bcd or hex format */
|
|
max = 0x9; /* Default to decimal format */
|
|
for (ndigits = 0 ; ndigits < sizeof(bcdDevice_lo) * 2 ; ndigits++) {
|
|
clo = (devlo >> (ndigits << 2)) & 0xf;
|
|
chi = ((devhi > 0x9999 ? 0x9999 : devhi) >> (ndigits << 2)) & 0xf;
|
|
if (clo > max || chi > max) {
|
|
max = 0xf;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Some modules (visor) have empty slots as placeholder for
|
|
* run-time specification that results in catch-all alias
|
|
*/
|
|
if (!(idVendor | idProduct | bDeviceClass | bInterfaceClass))
|
|
return;
|
|
|
|
/* Convert numeric bcdDevice range into fnmatch-able pattern(s) */
|
|
for (ndigits = sizeof(bcdDevice_lo) * 2 - 1; devlo <= devhi; ndigits--) {
|
|
clo = devlo & 0xf;
|
|
chi = devhi & 0xf;
|
|
if (chi > max) /* If we are in bcd mode, truncate if necessary */
|
|
chi = max;
|
|
devlo >>= 4;
|
|
devhi >>= 4;
|
|
|
|
if (devlo == devhi || !ndigits) {
|
|
do_usb_entry(symval, devlo, ndigits, clo, chi, max, mod);
|
|
break;
|
|
}
|
|
|
|
if (clo > 0x0)
|
|
do_usb_entry(symval,
|
|
incbcd(&devlo, 1, max,
|
|
sizeof(bcdDevice_lo) * 2),
|
|
ndigits, clo, max, max, mod);
|
|
|
|
if (chi < max)
|
|
do_usb_entry(symval,
|
|
incbcd(&devhi, -1, max,
|
|
sizeof(bcdDevice_lo) * 2),
|
|
ndigits, 0x0, chi, max, mod);
|
|
}
|
|
}
|
|
|
|
static void do_of_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[500];
|
|
int len;
|
|
char *tmp;
|
|
|
|
DEF_FIELD_ADDR(symval, of_device_id, name);
|
|
DEF_FIELD_ADDR(symval, of_device_id, type);
|
|
DEF_FIELD_ADDR(symval, of_device_id, compatible);
|
|
|
|
len = sprintf(alias, "of:N%sT%s", (*name)[0] ? *name : "*",
|
|
(*type)[0] ? *type : "*");
|
|
|
|
if ((*compatible)[0])
|
|
sprintf(&alias[len], "%sC%s", (*type)[0] ? "*" : "",
|
|
*compatible);
|
|
|
|
/* Replace all whitespace with underscores */
|
|
for (tmp = alias; tmp && *tmp; tmp++)
|
|
if (isspace(*tmp))
|
|
*tmp = '_';
|
|
|
|
module_alias_printf(mod, false, "%s", alias);
|
|
module_alias_printf(mod, false, "%sC*", alias);
|
|
}
|
|
|
|
/* Looks like: hid:bNvNpN */
|
|
static void do_hid_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, hid_device_id, bus);
|
|
DEF_FIELD(symval, hid_device_id, group);
|
|
DEF_FIELD(symval, hid_device_id, vendor);
|
|
DEF_FIELD(symval, hid_device_id, product);
|
|
|
|
ADD(alias, "b", bus != HID_BUS_ANY, bus);
|
|
ADD(alias, "g", group != HID_GROUP_ANY, group);
|
|
ADD(alias, "v", vendor != HID_ANY_ID, vendor);
|
|
ADD(alias, "p", product != HID_ANY_ID, product);
|
|
|
|
module_alias_printf(mod, false, "hid:%s", alias);
|
|
}
|
|
|
|
/* Looks like: ieee1394:venNmoNspNverN */
|
|
static void do_ieee1394_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, ieee1394_device_id, match_flags);
|
|
DEF_FIELD(symval, ieee1394_device_id, vendor_id);
|
|
DEF_FIELD(symval, ieee1394_device_id, model_id);
|
|
DEF_FIELD(symval, ieee1394_device_id, specifier_id);
|
|
DEF_FIELD(symval, ieee1394_device_id, version);
|
|
|
|
ADD(alias, "ven", match_flags & IEEE1394_MATCH_VENDOR_ID,
|
|
vendor_id);
|
|
ADD(alias, "mo", match_flags & IEEE1394_MATCH_MODEL_ID,
|
|
model_id);
|
|
ADD(alias, "sp", match_flags & IEEE1394_MATCH_SPECIFIER_ID,
|
|
specifier_id);
|
|
ADD(alias, "ver", match_flags & IEEE1394_MATCH_VERSION,
|
|
version);
|
|
|
|
module_alias_printf(mod, true, "ieee1394:%s", alias);
|
|
}
|
|
|
|
/* Looks like: pci:vNdNsvNsdNbcNscNiN or <prefix>_pci:vNdNsvNsdNbcNscNiN. */
|
|
static void do_pci_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256];
|
|
/* Class field can be divided into these three. */
|
|
unsigned char baseclass, subclass, interface,
|
|
baseclass_mask, subclass_mask, interface_mask;
|
|
|
|
DEF_FIELD(symval, pci_device_id, vendor);
|
|
DEF_FIELD(symval, pci_device_id, device);
|
|
DEF_FIELD(symval, pci_device_id, subvendor);
|
|
DEF_FIELD(symval, pci_device_id, subdevice);
|
|
DEF_FIELD(symval, pci_device_id, class);
|
|
DEF_FIELD(symval, pci_device_id, class_mask);
|
|
DEF_FIELD(symval, pci_device_id, override_only);
|
|
|
|
switch (override_only) {
|
|
case 0:
|
|
strcpy(alias, "pci:");
|
|
break;
|
|
case PCI_ID_F_VFIO_DRIVER_OVERRIDE:
|
|
strcpy(alias, "vfio_pci:");
|
|
break;
|
|
default:
|
|
warn("Unknown PCI driver_override alias %08X\n",
|
|
override_only);
|
|
}
|
|
|
|
ADD(alias, "v", vendor != PCI_ANY_ID, vendor);
|
|
ADD(alias, "d", device != PCI_ANY_ID, device);
|
|
ADD(alias, "sv", subvendor != PCI_ANY_ID, subvendor);
|
|
ADD(alias, "sd", subdevice != PCI_ANY_ID, subdevice);
|
|
|
|
baseclass = (class) >> 16;
|
|
baseclass_mask = (class_mask) >> 16;
|
|
subclass = (class) >> 8;
|
|
subclass_mask = (class_mask) >> 8;
|
|
interface = class;
|
|
interface_mask = class_mask;
|
|
|
|
if ((baseclass_mask != 0 && baseclass_mask != 0xFF)
|
|
|| (subclass_mask != 0 && subclass_mask != 0xFF)
|
|
|| (interface_mask != 0 && interface_mask != 0xFF)) {
|
|
warn("Can't handle masks in %s:%04X\n",
|
|
mod->name, class_mask);
|
|
return;
|
|
}
|
|
|
|
ADD(alias, "bc", baseclass_mask == 0xFF, baseclass);
|
|
ADD(alias, "sc", subclass_mask == 0xFF, subclass);
|
|
ADD(alias, "i", interface_mask == 0xFF, interface);
|
|
|
|
module_alias_printf(mod, true, "%s", alias);
|
|
}
|
|
|
|
/* looks like: "ccw:tNmNdtNdmN" */
|
|
static void do_ccw_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, ccw_device_id, match_flags);
|
|
DEF_FIELD(symval, ccw_device_id, cu_type);
|
|
DEF_FIELD(symval, ccw_device_id, cu_model);
|
|
DEF_FIELD(symval, ccw_device_id, dev_type);
|
|
DEF_FIELD(symval, ccw_device_id, dev_model);
|
|
|
|
ADD(alias, "t", match_flags&CCW_DEVICE_ID_MATCH_CU_TYPE,
|
|
cu_type);
|
|
ADD(alias, "m", match_flags&CCW_DEVICE_ID_MATCH_CU_MODEL,
|
|
cu_model);
|
|
ADD(alias, "dt", match_flags&CCW_DEVICE_ID_MATCH_DEVICE_TYPE,
|
|
dev_type);
|
|
ADD(alias, "dm", match_flags&CCW_DEVICE_ID_MATCH_DEVICE_MODEL,
|
|
dev_model);
|
|
|
|
module_alias_printf(mod, true, "ccw:%s", alias);
|
|
}
|
|
|
|
/* looks like: "ap:tN" */
|
|
static void do_ap_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, ap_device_id, dev_type);
|
|
|
|
module_alias_printf(mod, false, "ap:t%02X*", dev_type);
|
|
}
|
|
|
|
/* looks like: "css:tN" */
|
|
static void do_css_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, css_device_id, type);
|
|
|
|
module_alias_printf(mod, false, "css:t%01X", type);
|
|
}
|
|
|
|
/* Looks like: "serio:tyNprNidNexN" */
|
|
static void do_serio_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, serio_device_id, type);
|
|
DEF_FIELD(symval, serio_device_id, proto);
|
|
DEF_FIELD(symval, serio_device_id, id);
|
|
DEF_FIELD(symval, serio_device_id, extra);
|
|
|
|
ADD(alias, "ty", type != SERIO_ANY, type);
|
|
ADD(alias, "pr", proto != SERIO_ANY, proto);
|
|
ADD(alias, "id", id != SERIO_ANY, id);
|
|
ADD(alias, "ex", extra != SERIO_ANY, extra);
|
|
|
|
module_alias_printf(mod, true, "serio:%s", alias);
|
|
}
|
|
|
|
/* looks like: "acpi:ACPI0003" or "acpi:PNP0C0B" or "acpi:LNXVIDEO" or
|
|
* "acpi:bbsspp" (bb=base-class, ss=sub-class, pp=prog-if)
|
|
*
|
|
* NOTE: Each driver should use one of the following : _HID, _CIDs
|
|
* or _CLS. Also, bb, ss, and pp can be substituted with ??
|
|
* as don't care byte.
|
|
*/
|
|
static void do_acpi_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, acpi_device_id, id);
|
|
DEF_FIELD(symval, acpi_device_id, cls);
|
|
DEF_FIELD(symval, acpi_device_id, cls_msk);
|
|
|
|
if ((*id)[0])
|
|
module_alias_printf(mod, false, "acpi*:%s:*", *id);
|
|
else {
|
|
char alias[256];
|
|
int i, byte_shift, cnt = 0;
|
|
unsigned int msk;
|
|
|
|
for (i = 1; i <= 3; i++) {
|
|
byte_shift = 8 * (3-i);
|
|
msk = (cls_msk >> byte_shift) & 0xFF;
|
|
if (msk)
|
|
sprintf(&alias[cnt], "%02x",
|
|
(cls >> byte_shift) & 0xFF);
|
|
else
|
|
sprintf(&alias[cnt], "??");
|
|
cnt += 2;
|
|
}
|
|
module_alias_printf(mod, false, "acpi*:%s:*", alias);
|
|
}
|
|
}
|
|
|
|
/* looks like: "pnp:dD" */
|
|
static void do_pnp_device_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, pnp_device_id, id);
|
|
char acpi_id[sizeof(*id)];
|
|
|
|
/* fix broken pnp bus lowercasing */
|
|
for (unsigned int i = 0; i < sizeof(acpi_id); i++)
|
|
acpi_id[i] = toupper((*id)[i]);
|
|
module_alias_printf(mod, false, "pnp:d%s*", *id);
|
|
module_alias_printf(mod, false, "acpi*:%s:*", acpi_id);
|
|
}
|
|
|
|
/* looks like: "pnp:dD" for every device of the card */
|
|
static void do_pnp_card_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, pnp_card_device_id, devs);
|
|
|
|
for (unsigned int i = 0; i < PNP_MAX_DEVICES; i++) {
|
|
const char *id = (char *)(*devs)[i].id;
|
|
char acpi_id[PNP_ID_LEN];
|
|
|
|
if (!id[0])
|
|
break;
|
|
|
|
/* fix broken pnp bus lowercasing */
|
|
for (unsigned int j = 0; j < sizeof(acpi_id); j++)
|
|
acpi_id[j] = toupper(id[j]);
|
|
|
|
/* add an individual alias for every device entry */
|
|
module_alias_printf(mod, false, "pnp:d%s*", id);
|
|
module_alias_printf(mod, false, "acpi*:%s:*", acpi_id);
|
|
}
|
|
}
|
|
|
|
/* Looks like: pcmcia:mNcNfNfnNpfnNvaNvbNvcNvdN. */
|
|
static void do_pcmcia_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
unsigned int i;
|
|
DEF_FIELD(symval, pcmcia_device_id, match_flags);
|
|
DEF_FIELD(symval, pcmcia_device_id, manf_id);
|
|
DEF_FIELD(symval, pcmcia_device_id, card_id);
|
|
DEF_FIELD(symval, pcmcia_device_id, func_id);
|
|
DEF_FIELD(symval, pcmcia_device_id, function);
|
|
DEF_FIELD(symval, pcmcia_device_id, device_no);
|
|
DEF_FIELD_ADDR(symval, pcmcia_device_id, prod_id_hash);
|
|
|
|
for (i=0; i<4; i++) {
|
|
(*prod_id_hash)[i] = TO_NATIVE((*prod_id_hash)[i]);
|
|
}
|
|
|
|
ADD(alias, "m", match_flags & PCMCIA_DEV_ID_MATCH_MANF_ID,
|
|
manf_id);
|
|
ADD(alias, "c", match_flags & PCMCIA_DEV_ID_MATCH_CARD_ID,
|
|
card_id);
|
|
ADD(alias, "f", match_flags & PCMCIA_DEV_ID_MATCH_FUNC_ID,
|
|
func_id);
|
|
ADD(alias, "fn", match_flags & PCMCIA_DEV_ID_MATCH_FUNCTION,
|
|
function);
|
|
ADD(alias, "pfn", match_flags & PCMCIA_DEV_ID_MATCH_DEVICE_NO,
|
|
device_no);
|
|
ADD(alias, "pa", match_flags & PCMCIA_DEV_ID_MATCH_PROD_ID1, (*prod_id_hash)[0]);
|
|
ADD(alias, "pb", match_flags & PCMCIA_DEV_ID_MATCH_PROD_ID2, (*prod_id_hash)[1]);
|
|
ADD(alias, "pc", match_flags & PCMCIA_DEV_ID_MATCH_PROD_ID3, (*prod_id_hash)[2]);
|
|
ADD(alias, "pd", match_flags & PCMCIA_DEV_ID_MATCH_PROD_ID4, (*prod_id_hash)[3]);
|
|
|
|
module_alias_printf(mod, true, "pcmcia:%s", alias);
|
|
}
|
|
|
|
static void do_vio_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256];
|
|
char *tmp;
|
|
DEF_FIELD_ADDR(symval, vio_device_id, type);
|
|
DEF_FIELD_ADDR(symval, vio_device_id, compat);
|
|
|
|
sprintf(alias, "vio:T%sS%s", (*type)[0] ? *type : "*",
|
|
(*compat)[0] ? *compat : "*");
|
|
|
|
/* Replace all whitespace with underscores */
|
|
for (tmp = alias; tmp && *tmp; tmp++)
|
|
if (isspace (*tmp))
|
|
*tmp = '_';
|
|
|
|
module_alias_printf(mod, true, "%s", alias);
|
|
}
|
|
|
|
static void do_input(char *alias,
|
|
kernel_ulong_t *arr, unsigned int min, unsigned int max)
|
|
{
|
|
unsigned int i;
|
|
|
|
for (i = min / BITS_PER_LONG; i < max / BITS_PER_LONG + 1; i++)
|
|
arr[i] = TO_NATIVE(arr[i]);
|
|
for (i = min; i < max; i++)
|
|
if (arr[i / BITS_PER_LONG] & (1ULL << (i%BITS_PER_LONG)))
|
|
sprintf(alias + strlen(alias), "%X,*", i);
|
|
}
|
|
|
|
/* input:b0v0p0e0-eXkXrXaXmXlXsXfXwX where X is comma-separated %02X. */
|
|
static void do_input_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, input_device_id, flags);
|
|
DEF_FIELD(symval, input_device_id, bustype);
|
|
DEF_FIELD(symval, input_device_id, vendor);
|
|
DEF_FIELD(symval, input_device_id, product);
|
|
DEF_FIELD(symval, input_device_id, version);
|
|
DEF_FIELD_ADDR(symval, input_device_id, evbit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, keybit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, relbit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, absbit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, mscbit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, ledbit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, sndbit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, ffbit);
|
|
DEF_FIELD_ADDR(symval, input_device_id, swbit);
|
|
|
|
ADD(alias, "b", flags & INPUT_DEVICE_ID_MATCH_BUS, bustype);
|
|
ADD(alias, "v", flags & INPUT_DEVICE_ID_MATCH_VENDOR, vendor);
|
|
ADD(alias, "p", flags & INPUT_DEVICE_ID_MATCH_PRODUCT, product);
|
|
ADD(alias, "e", flags & INPUT_DEVICE_ID_MATCH_VERSION, version);
|
|
|
|
sprintf(alias + strlen(alias), "-e*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_EVBIT)
|
|
do_input(alias, *evbit, 0, INPUT_DEVICE_ID_EV_MAX);
|
|
sprintf(alias + strlen(alias), "k*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_KEYBIT)
|
|
do_input(alias, *keybit,
|
|
INPUT_DEVICE_ID_KEY_MIN_INTERESTING,
|
|
INPUT_DEVICE_ID_KEY_MAX);
|
|
sprintf(alias + strlen(alias), "r*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_RELBIT)
|
|
do_input(alias, *relbit, 0, INPUT_DEVICE_ID_REL_MAX);
|
|
sprintf(alias + strlen(alias), "a*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_ABSBIT)
|
|
do_input(alias, *absbit, 0, INPUT_DEVICE_ID_ABS_MAX);
|
|
sprintf(alias + strlen(alias), "m*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_MSCIT)
|
|
do_input(alias, *mscbit, 0, INPUT_DEVICE_ID_MSC_MAX);
|
|
sprintf(alias + strlen(alias), "l*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_LEDBIT)
|
|
do_input(alias, *ledbit, 0, INPUT_DEVICE_ID_LED_MAX);
|
|
sprintf(alias + strlen(alias), "s*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_SNDBIT)
|
|
do_input(alias, *sndbit, 0, INPUT_DEVICE_ID_SND_MAX);
|
|
sprintf(alias + strlen(alias), "f*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_FFBIT)
|
|
do_input(alias, *ffbit, 0, INPUT_DEVICE_ID_FF_MAX);
|
|
sprintf(alias + strlen(alias), "w*");
|
|
if (flags & INPUT_DEVICE_ID_MATCH_SWBIT)
|
|
do_input(alias, *swbit, 0, INPUT_DEVICE_ID_SW_MAX);
|
|
|
|
module_alias_printf(mod, false, "input:%s", alias);
|
|
}
|
|
|
|
static void do_eisa_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, eisa_device_id, sig);
|
|
module_alias_printf(mod, false, EISA_DEVICE_MODALIAS_FMT "*", *sig);
|
|
}
|
|
|
|
/* Looks like: parisc:tNhvNrevNsvN */
|
|
static void do_parisc_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, parisc_device_id, hw_type);
|
|
DEF_FIELD(symval, parisc_device_id, hversion);
|
|
DEF_FIELD(symval, parisc_device_id, hversion_rev);
|
|
DEF_FIELD(symval, parisc_device_id, sversion);
|
|
|
|
ADD(alias, "t", hw_type != PA_HWTYPE_ANY_ID, hw_type);
|
|
ADD(alias, "hv", hversion != PA_HVERSION_ANY_ID, hversion);
|
|
ADD(alias, "rev", hversion_rev != PA_HVERSION_REV_ANY_ID, hversion_rev);
|
|
ADD(alias, "sv", sversion != PA_SVERSION_ANY_ID, sversion);
|
|
|
|
module_alias_printf(mod, true, "parisc:%s", alias);
|
|
}
|
|
|
|
/* Looks like: sdio:cNvNdN. */
|
|
static void do_sdio_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, sdio_device_id, class);
|
|
DEF_FIELD(symval, sdio_device_id, vendor);
|
|
DEF_FIELD(symval, sdio_device_id, device);
|
|
|
|
ADD(alias, "c", class != (__u8)SDIO_ANY_ID, class);
|
|
ADD(alias, "v", vendor != (__u16)SDIO_ANY_ID, vendor);
|
|
ADD(alias, "d", device != (__u16)SDIO_ANY_ID, device);
|
|
|
|
module_alias_printf(mod, true, "sdio:%s", alias);
|
|
}
|
|
|
|
/* Looks like: ssb:vNidNrevN. */
|
|
static void do_ssb_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, ssb_device_id, vendor);
|
|
DEF_FIELD(symval, ssb_device_id, coreid);
|
|
DEF_FIELD(symval, ssb_device_id, revision);
|
|
|
|
ADD(alias, "v", vendor != SSB_ANY_VENDOR, vendor);
|
|
ADD(alias, "id", coreid != SSB_ANY_ID, coreid);
|
|
ADD(alias, "rev", revision != SSB_ANY_REV, revision);
|
|
|
|
module_alias_printf(mod, true, "ssb:%s", alias);
|
|
}
|
|
|
|
/* Looks like: bcma:mNidNrevNclN. */
|
|
static void do_bcma_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, bcma_device_id, manuf);
|
|
DEF_FIELD(symval, bcma_device_id, id);
|
|
DEF_FIELD(symval, bcma_device_id, rev);
|
|
DEF_FIELD(symval, bcma_device_id, class);
|
|
|
|
ADD(alias, "m", manuf != BCMA_ANY_MANUF, manuf);
|
|
ADD(alias, "id", id != BCMA_ANY_ID, id);
|
|
ADD(alias, "rev", rev != BCMA_ANY_REV, rev);
|
|
ADD(alias, "cl", class != BCMA_ANY_CLASS, class);
|
|
|
|
module_alias_printf(mod, true, "bcma:%s", alias);
|
|
}
|
|
|
|
/* Looks like: virtio:dNvN */
|
|
static void do_virtio_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, virtio_device_id, device);
|
|
DEF_FIELD(symval, virtio_device_id, vendor);
|
|
|
|
ADD(alias, "d", device != VIRTIO_DEV_ANY_ID, device);
|
|
ADD(alias, "v", vendor != VIRTIO_DEV_ANY_ID, vendor);
|
|
|
|
module_alias_printf(mod, true, "virtio:%s", alias);
|
|
}
|
|
|
|
/*
|
|
* Looks like: vmbus:guid
|
|
* Each byte of the guid will be represented by two hex characters
|
|
* in the name.
|
|
*/
|
|
|
|
static void do_vmbus_entry(struct module *mod, void *symval)
|
|
{
|
|
int i;
|
|
DEF_FIELD_ADDR(symval, hv_vmbus_device_id, guid);
|
|
char guid_name[(sizeof(*guid) + 1) * 2];
|
|
|
|
for (i = 0; i < (sizeof(*guid) * 2); i += 2)
|
|
sprintf(&guid_name[i], "%02x", TO_NATIVE((guid->b)[i/2]));
|
|
|
|
module_alias_printf(mod, false, "vmbus:%s", guid_name);
|
|
}
|
|
|
|
/* Looks like: rpmsg:S */
|
|
static void do_rpmsg_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, rpmsg_device_id, name);
|
|
|
|
module_alias_printf(mod, false, RPMSG_DEVICE_MODALIAS_FMT, *name);
|
|
}
|
|
|
|
/* Looks like: i2c:S */
|
|
static void do_i2c_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, i2c_device_id, name);
|
|
|
|
module_alias_printf(mod, false, I2C_MODULE_PREFIX "%s", *name);
|
|
}
|
|
|
|
static void do_i3c_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, i3c_device_id, match_flags);
|
|
DEF_FIELD(symval, i3c_device_id, dcr);
|
|
DEF_FIELD(symval, i3c_device_id, manuf_id);
|
|
DEF_FIELD(symval, i3c_device_id, part_id);
|
|
DEF_FIELD(symval, i3c_device_id, extra_info);
|
|
|
|
ADD(alias, "dcr", match_flags & I3C_MATCH_DCR, dcr);
|
|
ADD(alias, "manuf", match_flags & I3C_MATCH_MANUF, manuf_id);
|
|
ADD(alias, "part", match_flags & I3C_MATCH_PART, part_id);
|
|
ADD(alias, "ext", match_flags & I3C_MATCH_EXTRA_INFO, extra_info);
|
|
|
|
module_alias_printf(mod, false, "i3c:%s", alias);
|
|
}
|
|
|
|
static void do_slim_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, slim_device_id, manf_id);
|
|
DEF_FIELD(symval, slim_device_id, prod_code);
|
|
|
|
module_alias_printf(mod, false, "slim:%x:%x:*", manf_id, prod_code);
|
|
}
|
|
|
|
/* Looks like: spi:S */
|
|
static void do_spi_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, spi_device_id, name);
|
|
|
|
module_alias_printf(mod, false, SPI_MODULE_PREFIX "%s", *name);
|
|
}
|
|
|
|
static const struct dmifield {
|
|
const char *prefix;
|
|
int field;
|
|
} dmi_fields[] = {
|
|
{ "bvn", DMI_BIOS_VENDOR },
|
|
{ "bvr", DMI_BIOS_VERSION },
|
|
{ "bd", DMI_BIOS_DATE },
|
|
{ "br", DMI_BIOS_RELEASE },
|
|
{ "efr", DMI_EC_FIRMWARE_RELEASE },
|
|
{ "svn", DMI_SYS_VENDOR },
|
|
{ "pn", DMI_PRODUCT_NAME },
|
|
{ "pvr", DMI_PRODUCT_VERSION },
|
|
{ "rvn", DMI_BOARD_VENDOR },
|
|
{ "rn", DMI_BOARD_NAME },
|
|
{ "rvr", DMI_BOARD_VERSION },
|
|
{ "cvn", DMI_CHASSIS_VENDOR },
|
|
{ "ct", DMI_CHASSIS_TYPE },
|
|
{ "cvr", DMI_CHASSIS_VERSION },
|
|
{ NULL, DMI_NONE }
|
|
};
|
|
|
|
static void dmi_ascii_filter(char *d, const char *s)
|
|
{
|
|
/* Filter out characters we don't want to see in the modalias string */
|
|
for (; *s; s++)
|
|
if (*s > ' ' && *s < 127 && *s != ':')
|
|
*(d++) = *s;
|
|
|
|
*d = 0;
|
|
}
|
|
|
|
|
|
static void do_dmi_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
int i, j;
|
|
DEF_FIELD_ADDR(symval, dmi_system_id, matches);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(dmi_fields); i++) {
|
|
for (j = 0; j < 4; j++) {
|
|
if ((*matches)[j].slot &&
|
|
(*matches)[j].slot == dmi_fields[i].field) {
|
|
sprintf(alias + strlen(alias), ":%s*",
|
|
dmi_fields[i].prefix);
|
|
dmi_ascii_filter(alias + strlen(alias),
|
|
(*matches)[j].substr);
|
|
strcat(alias, "*");
|
|
}
|
|
}
|
|
}
|
|
|
|
module_alias_printf(mod, false, "dmi*%s:", alias);
|
|
}
|
|
|
|
static void do_platform_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, platform_device_id, name);
|
|
|
|
module_alias_printf(mod, false, PLATFORM_MODULE_PREFIX "%s", *name);
|
|
}
|
|
|
|
static void do_mdio_entry(struct module *mod, void *symval)
|
|
{
|
|
char id[33];
|
|
int i;
|
|
DEF_FIELD(symval, mdio_device_id, phy_id);
|
|
DEF_FIELD(symval, mdio_device_id, phy_id_mask);
|
|
|
|
for (i = 0; i < 32; i++) {
|
|
if (!((phy_id_mask >> (31-i)) & 1))
|
|
id[i] = '?';
|
|
else if ((phy_id >> (31-i)) & 1)
|
|
id[i] = '1';
|
|
else
|
|
id[i] = '0';
|
|
}
|
|
|
|
/* Terminate the string */
|
|
id[32] = '\0';
|
|
|
|
module_alias_printf(mod, false, MDIO_MODULE_PREFIX "%s", id);
|
|
}
|
|
|
|
/* Looks like: zorro:iN. */
|
|
static void do_zorro_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
DEF_FIELD(symval, zorro_device_id, id);
|
|
|
|
ADD(alias, "i", id != ZORRO_WILDCARD, id);
|
|
|
|
module_alias_printf(mod, false, "zorro:%s", alias);
|
|
}
|
|
|
|
/* looks like: "pnp:dD" */
|
|
static void do_isapnp_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, isapnp_device_id, vendor);
|
|
DEF_FIELD(symval, isapnp_device_id, function);
|
|
module_alias_printf(mod, false, "pnp:d%c%c%c%x%x%x%x*",
|
|
'A' + ((vendor >> 2) & 0x3f) - 1,
|
|
'A' + (((vendor & 3) << 3) | ((vendor >> 13) & 7)) - 1,
|
|
'A' + ((vendor >> 8) & 0x1f) - 1,
|
|
(function >> 4) & 0x0f, function & 0x0f,
|
|
(function >> 12) & 0x0f, (function >> 8) & 0x0f);
|
|
}
|
|
|
|
/* Looks like: "ipack:fNvNdN". */
|
|
static void do_ipack_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
DEF_FIELD(symval, ipack_device_id, format);
|
|
DEF_FIELD(symval, ipack_device_id, vendor);
|
|
DEF_FIELD(symval, ipack_device_id, device);
|
|
|
|
ADD(alias, "f", format != IPACK_ANY_FORMAT, format);
|
|
ADD(alias, "v", vendor != IPACK_ANY_ID, vendor);
|
|
ADD(alias, "d", device != IPACK_ANY_ID, device);
|
|
|
|
module_alias_printf(mod, true, "ipack:%s", alias);
|
|
}
|
|
|
|
/*
|
|
* Append a match expression for a single masked hex digit.
|
|
* outp points to a pointer to the character at which to append.
|
|
* *outp is updated on return to point just after the appended text,
|
|
* to facilitate further appending.
|
|
*/
|
|
static void append_nibble_mask(char **outp,
|
|
unsigned int nibble, unsigned int mask)
|
|
{
|
|
char *p = *outp;
|
|
unsigned int i;
|
|
|
|
switch (mask) {
|
|
case 0:
|
|
*p++ = '?';
|
|
break;
|
|
|
|
case 0xf:
|
|
p += sprintf(p, "%X", nibble);
|
|
break;
|
|
|
|
default:
|
|
/*
|
|
* Dumbly emit a match pattern for all possible matching
|
|
* digits. This could be improved in some cases using ranges,
|
|
* but it has the advantage of being trivially correct, and is
|
|
* often optimal.
|
|
*/
|
|
*p++ = '[';
|
|
for (i = 0; i < 0x10; i++)
|
|
if ((i & mask) == nibble)
|
|
p += sprintf(p, "%X", i);
|
|
*p++ = ']';
|
|
}
|
|
|
|
/* Ensure that the string remains NUL-terminated: */
|
|
*p = '\0';
|
|
|
|
/* Advance the caller's end-of-string pointer: */
|
|
*outp = p;
|
|
}
|
|
|
|
/*
|
|
* looks like: "amba:dN"
|
|
*
|
|
* N is exactly 8 digits, where each is an upper-case hex digit, or
|
|
* a ? or [] pattern matching exactly one digit.
|
|
*/
|
|
static void do_amba_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256];
|
|
unsigned int digit;
|
|
char *p = alias;
|
|
DEF_FIELD(symval, amba_id, id);
|
|
DEF_FIELD(symval, amba_id, mask);
|
|
|
|
if ((id & mask) != id)
|
|
fatal("%s: Masked-off bit(s) of AMBA device ID are non-zero: id=0x%08X, mask=0x%08X. Please fix this driver.\n",
|
|
mod->name, id, mask);
|
|
|
|
for (digit = 0; digit < 8; digit++)
|
|
append_nibble_mask(&p,
|
|
(id >> (4 * (7 - digit))) & 0xf,
|
|
(mask >> (4 * (7 - digit))) & 0xf);
|
|
|
|
module_alias_printf(mod, false, "amba:d%s", alias);
|
|
}
|
|
|
|
/*
|
|
* looks like: "mipscdmm:tN"
|
|
*
|
|
* N is exactly 2 digits, where each is an upper-case hex digit, or
|
|
* a ? or [] pattern matching exactly one digit.
|
|
*/
|
|
static void do_mips_cdmm_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, mips_cdmm_device_id, type);
|
|
|
|
module_alias_printf(mod, false, "mipscdmm:t%02X*", type);
|
|
}
|
|
|
|
/* LOOKS like cpu:type:x86,venVVVVfamFFFFmodMMMM:feature:*,FEAT,*
|
|
* All fields are numbers. It would be nicer to use strings for vendor
|
|
* and feature, but getting those out of the build system here is too
|
|
* complicated.
|
|
*/
|
|
|
|
static void do_x86cpu_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, x86_cpu_id, feature);
|
|
DEF_FIELD(symval, x86_cpu_id, family);
|
|
DEF_FIELD(symval, x86_cpu_id, model);
|
|
DEF_FIELD(symval, x86_cpu_id, vendor);
|
|
|
|
ADD(alias, "ven", vendor != X86_VENDOR_ANY, vendor);
|
|
ADD(alias, "fam", family != X86_FAMILY_ANY, family);
|
|
ADD(alias, "mod", model != X86_MODEL_ANY, model);
|
|
strcat(alias, ":feature:*");
|
|
if (feature != X86_FEATURE_ANY)
|
|
sprintf(alias + strlen(alias), "%04X*", feature);
|
|
|
|
module_alias_printf(mod, false, "cpu:type:x86,%s", alias);
|
|
}
|
|
|
|
/* LOOKS like cpu:type:*:feature:*FEAT* */
|
|
static void do_cpu_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, cpu_feature, feature);
|
|
|
|
module_alias_printf(mod, false, "cpu:type:*:feature:*%04X*", feature);
|
|
}
|
|
|
|
/* Looks like: mei:S:uuid:N:* */
|
|
static void do_mei_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD_ADDR(symval, mei_cl_device_id, name);
|
|
DEF_FIELD_ADDR(symval, mei_cl_device_id, uuid);
|
|
DEF_FIELD(symval, mei_cl_device_id, version);
|
|
|
|
add_uuid(alias, *uuid);
|
|
ADD(alias, ":", version != MEI_CL_VERSION_ANY, version);
|
|
|
|
module_alias_printf(mod, false, MEI_CL_MODULE_PREFIX "%s:%s:*",
|
|
(*name)[0] ? *name : "*", alias);
|
|
}
|
|
|
|
/* Looks like: rapidio:vNdNavNadN */
|
|
static void do_rio_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, rio_device_id, did);
|
|
DEF_FIELD(symval, rio_device_id, vid);
|
|
DEF_FIELD(symval, rio_device_id, asm_did);
|
|
DEF_FIELD(symval, rio_device_id, asm_vid);
|
|
|
|
ADD(alias, "v", vid != RIO_ANY_ID, vid);
|
|
ADD(alias, "d", did != RIO_ANY_ID, did);
|
|
ADD(alias, "av", asm_vid != RIO_ANY_ID, asm_vid);
|
|
ADD(alias, "ad", asm_did != RIO_ANY_ID, asm_did);
|
|
|
|
module_alias_printf(mod, true, "rapidio:%s", alias);
|
|
}
|
|
|
|
/* Looks like: ulpi:vNpN */
|
|
static void do_ulpi_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, ulpi_device_id, vendor);
|
|
DEF_FIELD(symval, ulpi_device_id, product);
|
|
|
|
module_alias_printf(mod, false, "ulpi:v%04xp%04x", vendor, product);
|
|
}
|
|
|
|
/* Looks like: hdaudio:vNrNaN */
|
|
static void do_hda_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, hda_device_id, vendor_id);
|
|
DEF_FIELD(symval, hda_device_id, rev_id);
|
|
DEF_FIELD(symval, hda_device_id, api_version);
|
|
|
|
ADD(alias, "v", vendor_id != 0, vendor_id);
|
|
ADD(alias, "r", rev_id != 0, rev_id);
|
|
ADD(alias, "a", api_version != 0, api_version);
|
|
|
|
module_alias_printf(mod, true, "hdaudio:%s", alias);
|
|
}
|
|
|
|
/* Looks like: sdw:mNpNvNcN */
|
|
static void do_sdw_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, sdw_device_id, mfg_id);
|
|
DEF_FIELD(symval, sdw_device_id, part_id);
|
|
DEF_FIELD(symval, sdw_device_id, sdw_version);
|
|
DEF_FIELD(symval, sdw_device_id, class_id);
|
|
|
|
ADD(alias, "m", mfg_id != 0, mfg_id);
|
|
ADD(alias, "p", part_id != 0, part_id);
|
|
ADD(alias, "v", sdw_version != 0, sdw_version);
|
|
ADD(alias, "c", class_id != 0, class_id);
|
|
|
|
module_alias_printf(mod, true, "sdw:%s", alias);
|
|
}
|
|
|
|
/* Looks like: fsl-mc:vNdN */
|
|
static void do_fsl_mc_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, fsl_mc_device_id, vendor);
|
|
DEF_FIELD_ADDR(symval, fsl_mc_device_id, obj_type);
|
|
|
|
module_alias_printf(mod, false, "fsl-mc:v%08Xd%s", vendor, *obj_type);
|
|
}
|
|
|
|
/* Looks like: tbsvc:kSpNvNrN */
|
|
static void do_tbsvc_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, tb_service_id, match_flags);
|
|
DEF_FIELD_ADDR(symval, tb_service_id, protocol_key);
|
|
DEF_FIELD(symval, tb_service_id, protocol_id);
|
|
DEF_FIELD(symval, tb_service_id, protocol_version);
|
|
DEF_FIELD(symval, tb_service_id, protocol_revision);
|
|
|
|
if (match_flags & TBSVC_MATCH_PROTOCOL_KEY)
|
|
sprintf(alias + strlen(alias), "k%s", *protocol_key);
|
|
else
|
|
strcat(alias + strlen(alias), "k*");
|
|
ADD(alias, "p", match_flags & TBSVC_MATCH_PROTOCOL_ID, protocol_id);
|
|
ADD(alias, "v", match_flags & TBSVC_MATCH_PROTOCOL_VERSION,
|
|
protocol_version);
|
|
ADD(alias, "r", match_flags & TBSVC_MATCH_PROTOCOL_REVISION,
|
|
protocol_revision);
|
|
|
|
module_alias_printf(mod, true, "tbsvc:%s", alias);
|
|
}
|
|
|
|
/* Looks like: typec:idNmN */
|
|
static void do_typec_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, typec_device_id, svid);
|
|
DEF_FIELD(symval, typec_device_id, mode);
|
|
|
|
ADD(alias, "m", mode != TYPEC_ANY_MODE, mode);
|
|
|
|
module_alias_printf(mod, false, "typec:id%04X%s", svid, alias);
|
|
}
|
|
|
|
/* Looks like: tee:uuid */
|
|
static void do_tee_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, tee_client_device_id, uuid);
|
|
|
|
module_alias_printf(mod, true,
|
|
"tee:%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
uuid->b[0], uuid->b[1], uuid->b[2], uuid->b[3], uuid->b[4],
|
|
uuid->b[5], uuid->b[6], uuid->b[7], uuid->b[8], uuid->b[9],
|
|
uuid->b[10], uuid->b[11], uuid->b[12], uuid->b[13], uuid->b[14],
|
|
uuid->b[15]);
|
|
}
|
|
|
|
/* Looks like: wmi:guid */
|
|
static void do_wmi_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, wmi_device_id, guid_string);
|
|
|
|
if (strlen(*guid_string) != UUID_STRING_LEN) {
|
|
warn("Invalid WMI device id 'wmi:%s' in '%s'\n",
|
|
*guid_string, mod->name);
|
|
return;
|
|
}
|
|
|
|
module_alias_printf(mod, false, WMI_MODULE_PREFIX "%s", *guid_string);
|
|
}
|
|
|
|
/* Looks like: mhi:S */
|
|
static void do_mhi_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, mhi_device_id, chan);
|
|
module_alias_printf(mod, false, MHI_DEVICE_MODALIAS_FMT, *chan);
|
|
}
|
|
|
|
/* Looks like: mhi_ep:S */
|
|
static void do_mhi_ep_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, mhi_device_id, chan);
|
|
|
|
module_alias_printf(mod, false, MHI_EP_DEVICE_MODALIAS_FMT, *chan);
|
|
}
|
|
|
|
/* Looks like: ishtp:{guid} */
|
|
static void do_ishtp_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
DEF_FIELD_ADDR(symval, ishtp_device_id, guid);
|
|
|
|
add_guid(alias, *guid);
|
|
|
|
module_alias_printf(mod, false, ISHTP_MODULE_PREFIX "{%s}", alias);
|
|
}
|
|
|
|
static void do_auxiliary_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, auxiliary_device_id, name);
|
|
|
|
module_alias_printf(mod, false, AUXILIARY_MODULE_PREFIX "%s", *name);
|
|
}
|
|
|
|
/*
|
|
* Looks like: ssam:dNcNtNiNfN
|
|
*
|
|
* N is exactly 2 digits, where each is an upper-case hex digit.
|
|
*/
|
|
static void do_ssam_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256] = {};
|
|
|
|
DEF_FIELD(symval, ssam_device_id, match_flags);
|
|
DEF_FIELD(symval, ssam_device_id, domain);
|
|
DEF_FIELD(symval, ssam_device_id, category);
|
|
DEF_FIELD(symval, ssam_device_id, target);
|
|
DEF_FIELD(symval, ssam_device_id, instance);
|
|
DEF_FIELD(symval, ssam_device_id, function);
|
|
|
|
ADD(alias, "t", match_flags & SSAM_MATCH_TARGET, target);
|
|
ADD(alias, "i", match_flags & SSAM_MATCH_INSTANCE, instance);
|
|
ADD(alias, "f", match_flags & SSAM_MATCH_FUNCTION, function);
|
|
|
|
module_alias_printf(mod, false, "ssam:d%02Xc%02X%s",
|
|
domain, category, alias);
|
|
}
|
|
|
|
/* Looks like: dfl:tNfN */
|
|
static void do_dfl_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, dfl_device_id, type);
|
|
DEF_FIELD(symval, dfl_device_id, feature_id);
|
|
|
|
module_alias_printf(mod, true, "dfl:t%04Xf%04X", type, feature_id);
|
|
}
|
|
|
|
/* Looks like: cdx:vNdN */
|
|
static void do_cdx_entry(struct module *mod, void *symval)
|
|
{
|
|
char alias[256];
|
|
|
|
DEF_FIELD(symval, cdx_device_id, vendor);
|
|
DEF_FIELD(symval, cdx_device_id, device);
|
|
DEF_FIELD(symval, cdx_device_id, subvendor);
|
|
DEF_FIELD(symval, cdx_device_id, subdevice);
|
|
DEF_FIELD(symval, cdx_device_id, class);
|
|
DEF_FIELD(symval, cdx_device_id, class_mask);
|
|
DEF_FIELD(symval, cdx_device_id, override_only);
|
|
|
|
switch (override_only) {
|
|
case 0:
|
|
strcpy(alias, "cdx:");
|
|
break;
|
|
case CDX_ID_F_VFIO_DRIVER_OVERRIDE:
|
|
strcpy(alias, "vfio_cdx:");
|
|
break;
|
|
default:
|
|
warn("Unknown CDX driver_override alias %08X\n",
|
|
override_only);
|
|
return;
|
|
}
|
|
|
|
ADD(alias, "v", vendor != CDX_ANY_ID, vendor);
|
|
ADD(alias, "d", device != CDX_ANY_ID, device);
|
|
ADD(alias, "sv", subvendor != CDX_ANY_ID, subvendor);
|
|
ADD(alias, "sd", subdevice != CDX_ANY_ID, subdevice);
|
|
ADD(alias, "c", class_mask == 0xFFFFFF, class);
|
|
|
|
module_alias_printf(mod, false, "%s", alias);
|
|
}
|
|
|
|
static void do_vchiq_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD_ADDR(symval, vchiq_device_id, name);
|
|
|
|
module_alias_printf(mod, false, "vchiq:%s", *name);
|
|
}
|
|
|
|
/* Looks like: coreboot:tN */
|
|
static void do_coreboot_entry(struct module *mod, void *symval)
|
|
{
|
|
DEF_FIELD(symval, coreboot_device_id, tag);
|
|
|
|
module_alias_printf(mod, false, "coreboot:t%08X", tag);
|
|
}
|
|
|
|
/* Does namelen bytes of name exactly match the symbol? */
|
|
static bool sym_is(const char *name, unsigned namelen, const char *symbol)
|
|
{
|
|
if (namelen != strlen(symbol))
|
|
return false;
|
|
|
|
return memcmp(name, symbol, namelen) == 0;
|
|
}
|
|
|
|
static void do_table(const char *name, void *symval, unsigned long size,
|
|
unsigned long id_size,
|
|
const char *device_id,
|
|
void (*do_entry)(struct module *mod, void *symval),
|
|
struct module *mod)
|
|
{
|
|
unsigned int i;
|
|
|
|
if (size % id_size || size < id_size) {
|
|
error("%s: type mismatch between %s[] and MODULE_DEVICE_TABLE(%s, ...)\n",
|
|
mod->name, name, device_id);
|
|
return;
|
|
}
|
|
|
|
/* Verify the last entry is a terminator */
|
|
for (i = size - id_size; i < size; i++) {
|
|
if (*(uint8_t *)(symval + i)) {
|
|
error("%s: %s[] is not terminated with a NULL entry\n",
|
|
mod->name, name);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Leave last one: it's the terminator. */
|
|
size -= id_size;
|
|
|
|
for (i = 0; i < size; i += id_size)
|
|
do_entry(mod, symval + i);
|
|
}
|
|
|
|
static const struct devtable devtable[] = {
|
|
{"hid", SIZE_hid_device_id, do_hid_entry},
|
|
{"ieee1394", SIZE_ieee1394_device_id, do_ieee1394_entry},
|
|
{"pci", SIZE_pci_device_id, do_pci_entry},
|
|
{"ccw", SIZE_ccw_device_id, do_ccw_entry},
|
|
{"ap", SIZE_ap_device_id, do_ap_entry},
|
|
{"css", SIZE_css_device_id, do_css_entry},
|
|
{"serio", SIZE_serio_device_id, do_serio_entry},
|
|
{"acpi", SIZE_acpi_device_id, do_acpi_entry},
|
|
{"pcmcia", SIZE_pcmcia_device_id, do_pcmcia_entry},
|
|
{"vio", SIZE_vio_device_id, do_vio_entry},
|
|
{"input", SIZE_input_device_id, do_input_entry},
|
|
{"eisa", SIZE_eisa_device_id, do_eisa_entry},
|
|
{"parisc", SIZE_parisc_device_id, do_parisc_entry},
|
|
{"sdio", SIZE_sdio_device_id, do_sdio_entry},
|
|
{"ssb", SIZE_ssb_device_id, do_ssb_entry},
|
|
{"bcma", SIZE_bcma_device_id, do_bcma_entry},
|
|
{"virtio", SIZE_virtio_device_id, do_virtio_entry},
|
|
{"vmbus", SIZE_hv_vmbus_device_id, do_vmbus_entry},
|
|
{"rpmsg", SIZE_rpmsg_device_id, do_rpmsg_entry},
|
|
{"i2c", SIZE_i2c_device_id, do_i2c_entry},
|
|
{"i3c", SIZE_i3c_device_id, do_i3c_entry},
|
|
{"slim", SIZE_slim_device_id, do_slim_entry},
|
|
{"spi", SIZE_spi_device_id, do_spi_entry},
|
|
{"dmi", SIZE_dmi_system_id, do_dmi_entry},
|
|
{"platform", SIZE_platform_device_id, do_platform_entry},
|
|
{"mdio", SIZE_mdio_device_id, do_mdio_entry},
|
|
{"zorro", SIZE_zorro_device_id, do_zorro_entry},
|
|
{"isapnp", SIZE_isapnp_device_id, do_isapnp_entry},
|
|
{"ipack", SIZE_ipack_device_id, do_ipack_entry},
|
|
{"amba", SIZE_amba_id, do_amba_entry},
|
|
{"mipscdmm", SIZE_mips_cdmm_device_id, do_mips_cdmm_entry},
|
|
{"x86cpu", SIZE_x86_cpu_id, do_x86cpu_entry},
|
|
{"cpu", SIZE_cpu_feature, do_cpu_entry},
|
|
{"mei", SIZE_mei_cl_device_id, do_mei_entry},
|
|
{"rapidio", SIZE_rio_device_id, do_rio_entry},
|
|
{"ulpi", SIZE_ulpi_device_id, do_ulpi_entry},
|
|
{"hdaudio", SIZE_hda_device_id, do_hda_entry},
|
|
{"sdw", SIZE_sdw_device_id, do_sdw_entry},
|
|
{"fslmc", SIZE_fsl_mc_device_id, do_fsl_mc_entry},
|
|
{"tbsvc", SIZE_tb_service_id, do_tbsvc_entry},
|
|
{"typec", SIZE_typec_device_id, do_typec_entry},
|
|
{"tee", SIZE_tee_client_device_id, do_tee_entry},
|
|
{"wmi", SIZE_wmi_device_id, do_wmi_entry},
|
|
{"mhi", SIZE_mhi_device_id, do_mhi_entry},
|
|
{"mhi_ep", SIZE_mhi_device_id, do_mhi_ep_entry},
|
|
{"auxiliary", SIZE_auxiliary_device_id, do_auxiliary_entry},
|
|
{"ssam", SIZE_ssam_device_id, do_ssam_entry},
|
|
{"dfl", SIZE_dfl_device_id, do_dfl_entry},
|
|
{"ishtp", SIZE_ishtp_device_id, do_ishtp_entry},
|
|
{"cdx", SIZE_cdx_device_id, do_cdx_entry},
|
|
{"vchiq", SIZE_vchiq_device_id, do_vchiq_entry},
|
|
{"coreboot", SIZE_coreboot_device_id, do_coreboot_entry},
|
|
{"of", SIZE_of_device_id, do_of_entry},
|
|
{"usb", SIZE_usb_device_id, do_usb_entry_multi},
|
|
{"pnp", SIZE_pnp_device_id, do_pnp_device_entry},
|
|
{"pnp_card", SIZE_pnp_card_device_id, do_pnp_card_entry},
|
|
};
|
|
|
|
/* Create MODULE_ALIAS() statements.
|
|
* At this time, we cannot write the actual output C source yet,
|
|
* so we write into the mod->dev_table_buf buffer. */
|
|
void handle_moddevtable(struct module *mod, struct elf_info *info,
|
|
Elf_Sym *sym, const char *symname)
|
|
{
|
|
void *symval;
|
|
char *zeros = NULL;
|
|
const char *type, *name;
|
|
size_t typelen;
|
|
static const char *prefix = "__mod_device_table__";
|
|
|
|
/* We're looking for a section relative symbol */
|
|
if (!sym->st_shndx || get_secindex(info, sym) >= info->num_sections)
|
|
return;
|
|
|
|
/* We're looking for an object */
|
|
if (ELF_ST_TYPE(sym->st_info) != STT_OBJECT)
|
|
return;
|
|
|
|
/* All our symbols are of form __mod_device_table__<type>__<name>. */
|
|
if (!strstarts(symname, prefix))
|
|
return;
|
|
type = symname + strlen(prefix);
|
|
|
|
name = strstr(type, "__");
|
|
if (!name)
|
|
return;
|
|
typelen = name - type;
|
|
name += strlen("__");
|
|
|
|
/* Handle all-NULL symbols allocated into .bss */
|
|
if (info->sechdrs[get_secindex(info, sym)].sh_type & SHT_NOBITS) {
|
|
zeros = calloc(1, sym->st_size);
|
|
symval = zeros;
|
|
} else {
|
|
symval = sym_get_data(info, sym);
|
|
}
|
|
|
|
for (int i = 0; i < ARRAY_SIZE(devtable); i++) {
|
|
const struct devtable *p = &devtable[i];
|
|
|
|
if (sym_is(type, typelen, p->device_id)) {
|
|
do_table(name, symval, sym->st_size, p->id_size,
|
|
p->device_id, p->do_entry, mod);
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(zeros);
|
|
}
|