| /* Copyright (C) 2006 Andi Kleen, SuSE Labs. |
| Portions Copyright (C) 2016 Sergio Gelato. |
| |
| Use SMBIOS/DMI to map address to DIMM description. |
| For reference see the SMBIOS specification 2.4, 3.0 |
| |
| dmi is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public |
| License as published by the Free Software Foundation; version |
| 2. |
| |
| dmi is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should find a copy of v2 of the GNU General Public License somewhere |
| on your Linux system; if not, write to the Free Software Foundation, |
| Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ |
| /* Notebook |
| add an option to dump existing errors in SMBIOS? |
| implement code to look up PCI resources too. |
| */ |
| #define _GNU_SOURCE 1 |
| #include <stdio.h> |
| #include <string.h> |
| #include <errno.h> |
| #include <sys/fcntl.h> |
| #include <unistd.h> |
| #include <stdlib.h> |
| #include <stddef.h> |
| #include <sys/mman.h> |
| |
| #include "mcelog.h" |
| #include "dmi.h" |
| #include "memutil.h" |
| |
| static int verbose = 0; |
| int dmi_forced; |
| int do_dmi; |
| |
| struct anchor { |
| char str[4]; /* _SM_ */ |
| char csum; |
| char entry_length; |
| char major; |
| char minor; |
| short maxlength; |
| char rev; |
| char fmt[5]; |
| char str2[5]; /* _DMI_ */ |
| char csum2; |
| unsigned short length; |
| unsigned table; |
| unsigned short numentries; |
| char bcdrev; |
| } __attribute__((packed)); |
| |
| static struct dmi_entry *entries; |
| static size_t entrieslen; |
| static int numentries; |
| static size_t dmi_length; |
| static struct dmi_entry **handle_to_entry; |
| |
| struct dmi_memdev **dmi_dimms; |
| struct dmi_memarray **dmi_arrays; |
| struct dmi_memdev_addr **dmi_ranges; |
| struct dmi_memarray_addr **dmi_array_ranges; |
| |
| static void collect_dmi_dimms(void); |
| static struct dmi_entry **dmi_collect(int type, int minsize, int *len); |
| static void dump_ranges(struct dmi_memdev_addr **, struct dmi_memdev **); |
| |
| static unsigned checksum(unsigned char *s, int len) |
| { |
| unsigned char csum = 0; |
| int i; |
| for (i = 0; i < len; i++) |
| csum += s[i]; |
| return csum; |
| } |
| |
| /* Check if entry is valid */ |
| static int check_entry(struct dmi_entry *e, struct dmi_entry **next) |
| { |
| char *end = (char *)entries + dmi_length; |
| char *s; |
| if (!e) |
| return 0; |
| s = (char *)e + e->length; |
| if (verbose > 3) |
| printf("length %d handle %x\n", e->length, e->handle); |
| do { |
| if (verbose > 3) |
| printf("string %s\n", s); |
| while (s < end-1 && *s) |
| s++; |
| if (s >= end-1) { |
| if (verbose > 0) |
| printf("handle %x length %d truncated\n", |
| e->handle, e->length); |
| return 0; |
| } |
| s++; |
| } while (*s); |
| if (s >= end) |
| *next = NULL; |
| else |
| *next = (struct dmi_entry *)(s + 1); |
| return 1; |
| } |
| |
| /* Relies on sanity checks in check_entry */ |
| char *dmi_getstring(struct dmi_entry *e, unsigned number) |
| { |
| char *s = (char *)e + e->length; |
| if (number == 0) |
| return ""; |
| do { |
| if (--number == 0) |
| return s; |
| while (*s) |
| s++; |
| s++; |
| } while (*s); |
| return NULL; |
| } |
| |
| static void fill_handles(void) |
| { |
| int i; |
| struct dmi_entry *e, *next; |
| e = entries; |
| handle_to_entry = xalloc(sizeof(void *) * 0xffff); |
| for (i = 0; i < numentries; i++, e = next) { |
| if (!check_entry(e, &next)) |
| break; |
| handle_to_entry[e->handle] = e; |
| } |
| } |
| |
| static int append_sysfs_dmi_entry(unsigned char type, int instance) |
| { |
| char filename[64]; /* 40 bytes should be enough */ |
| char buf[1024]; |
| int r; |
| ssize_t nr; |
| size_t l; |
| int fd; |
| r = snprintf(filename, sizeof(filename), |
| "/sys/firmware/dmi/entries/%hhu-%d/raw", |
| type, instance); |
| if (r < 0 || (unsigned int)r >= sizeof(filename)) { |
| Eprintf("Can't build pathname for DMI type %hhu instance %d\n", |
| type, instance); |
| return 0; |
| } |
| fd = open(filename, O_RDONLY); |
| if (fd == (-1)) { |
| if (errno != ENOENT) |
| perror(filename); |
| return 0; |
| } |
| l = dmi_length; |
| for (;;) { |
| nr = read(fd, buf, sizeof(buf)); |
| if (nr < 0) { |
| if (errno == EINTR) |
| continue; |
| perror(filename); |
| close(fd); |
| return 0; |
| } else if (nr > 0) { |
| while (l + nr > entrieslen) { |
| entrieslen += 4096; |
| entries = xrealloc(entries, entrieslen); |
| } |
| memcpy((char *)entries+l, buf, nr); |
| l += nr; |
| } else { |
| numentries ++; |
| dmi_length = l; |
| close(fd); |
| return 1; |
| } |
| } |
| } |
| |
| static void append_sysfs_dmi_entries(unsigned char type) |
| { |
| int i; |
| for (i=0; append_sysfs_dmi_entry(type, i); i++) ; |
| } |
| |
| static int get_efi_base_addr(size_t *address) |
| { |
| FILE *efi_systab; |
| const char *filename; |
| char linebuf[64]; |
| int ret = 0; |
| |
| *address = 0; /* Prevent compiler warning */ |
| |
| /* Linux 2.6.7 and up: /sys/firmware/efi/systab */ |
| filename = "/sys/firmware/efi/systab"; |
| if ((efi_systab = fopen(filename, "r")) != NULL) |
| goto check_symbol; |
| |
| /* Linux up to 2.6.6: /proc/efi/systab */ |
| filename = "/proc/efi/systab"; |
| if ((efi_systab = fopen(filename, "r")) != NULL) |
| goto check_symbol; |
| |
| /* Failed to open EFI interfaces */ |
| return ret; |
| |
| check_symbol: |
| while ((fgets(linebuf, sizeof(linebuf) - 1, efi_systab)) != NULL) { |
| char *addrp = strchr(linebuf, '='); |
| if (!addrp) |
| break; |
| *(addrp++) = '\0'; |
| |
| if (strcmp(linebuf, "SMBIOS") == 0) { |
| *address = strtoul(addrp, NULL, 0); |
| ret = 1; |
| break; |
| } |
| } |
| |
| if (fclose(efi_systab) != 0) |
| perror(filename); |
| |
| if (!ret || !*address){ |
| Lprintf("No valid SMBIOS entry point: Continue without DMI decoding"); |
| return 0; |
| } |
| |
| if (verbose) |
| printf("%s: SMBIOS entry point at 0x%08lx\n", filename, |
| (unsigned long)*address); |
| return ret; |
| } |
| |
| int opendmi(void) |
| { |
| struct anchor *a, *abase; |
| void *ebase; |
| void *p, *q; |
| int pagesize = getpagesize(); |
| int memfd; |
| off_t emapbase, corr; |
| size_t emapsize; |
| int err = -1; |
| const int segsize = 0x10000; |
| size_t entry_point_addr = 0; |
| size_t length = 0; |
| |
| if (entries) |
| return 0; |
| |
| if (access("/sys/firmware/dmi/entries/0-0/raw", R_OK) == 0) { |
| numentries = 0; |
| append_sysfs_dmi_entries(DMI_MEMORY_ARRAY); |
| append_sysfs_dmi_entries(DMI_MEMORY_DEVICE); |
| append_sysfs_dmi_entries(DMI_MEMORY_ARRAY_ADDR); |
| append_sysfs_dmi_entries(DMI_MEMORY_MAPPED_ADDR); |
| fill_handles(); |
| collect_dmi_dimms(); |
| return 0; |
| } |
| |
| memfd = open("/dev/mem", O_RDONLY); |
| if (memfd < 0) { |
| Eprintf("Cannot open /dev/mem for DMI decoding: %s", |
| strerror(errno)); |
| return -1; |
| } |
| |
| /* |
| * On EFI-based systems, the SMBIOS Entry Point structure can be |
| * located by looking in the EFI Configuration Table. |
| */ |
| if (get_efi_base_addr(&entry_point_addr)) { |
| size_t addr_start = round_down(entry_point_addr, pagesize); |
| size_t addr_end = round_up(entry_point_addr + 0x20, pagesize); |
| length = addr_end - addr_start; |
| |
| /* mmap() the address of SMBIOS structure table entry point. */ |
| abase = mmap(NULL, length, PROT_READ, MAP_SHARED, memfd, |
| addr_start); |
| if (abase == (struct anchor *)-1) { |
| Eprintf("Cannot mmap 0x%lx for efi mode: %s", |
| (unsigned long)entry_point_addr, |
| strerror(errno)); |
| goto legacy; |
| } |
| a = (struct anchor*)((char*)abase + (entry_point_addr - addr_start)); |
| goto fill_entries; |
| } |
| |
| legacy: |
| /* |
| * On non-EFI systems, the SMBIOS Entry Point structure can be located |
| * by searching for the anchor-string on paragraph (16-byte) boundaries |
| * within the physical memory address range 000F0000h to 000FFFFFh |
| */ |
| length = segsize - 1; |
| abase = mmap(NULL, length, PROT_READ, MAP_SHARED, memfd, 0xf0000); |
| |
| if (abase == (struct anchor *)-1) { |
| Eprintf("Cannot mmap 0xf0000 for legacy mode: %s", |
| strerror(errno)); |
| goto out; |
| } |
| |
| for (p = abase, q = p + segsize - 1; p < q; p += 0x10) { |
| if (!memcmp(p, "_SM_", 4) && |
| (checksum(p, ((struct anchor *)p)->entry_length) == 0)) |
| break; |
| } |
| |
| if (p >= q) { |
| Eprintf("Cannot find SMBIOS DMI tables"); |
| goto out_mmap; |
| } |
| |
| a = p; |
| |
| fill_entries: |
| if (verbose) |
| printf("DMI tables at %x, %u bytes, %u entries\n", |
| a->table, a->length, a->numentries); |
| emapbase = round_down(a->table, pagesize); |
| corr = a->table - emapbase; |
| emapsize = round_up(a->table + a->length, pagesize) - emapbase; |
| ebase = mmap(NULL, emapsize, PROT_READ, MAP_SHARED, memfd, emapbase); |
| if (ebase == MAP_FAILED) { |
| Eprintf("Cannot mmap SMBIOS tables at %x", a->table); |
| goto out_mmap; |
| } |
| entrieslen = a->length; |
| entries = xalloc_nonzero(entrieslen); |
| memcpy(entries, (char *)ebase+corr, entrieslen); |
| munmap(ebase, emapsize); |
| numentries = a->numentries; |
| dmi_length = a->length; |
| fill_handles(); |
| collect_dmi_dimms(); |
| err = 0; |
| |
| out_mmap: |
| munmap(abase, length); |
| out: |
| close(memfd); |
| return err; |
| } |
| |
| unsigned dmi_dimm_size(unsigned short size, char *unit) |
| { |
| unsigned mbflag = !(size & (1<<15)); |
| size &= ~(1<<15); |
| strcpy(unit, "KB"); |
| if (mbflag) { |
| unit[0] = 'M'; |
| if (size >= 1024) { |
| unit[0] = 'G'; |
| size /= 1024; |
| } |
| } |
| return size; |
| } |
| |
| static char *form_factors[] = { |
| "?", |
| "Other", "Unknown", "SIMM", "SIP", "Chip", "DIP", "ZIP", |
| "Proprietary Card", "DIMM", "TSOP", "Row of chips", "RIMM", |
| "SODIMM", "SRIMM", "FB-DIMM" |
| }; |
| static char *memory_types[] = { |
| "?", |
| "Other", "Unknown", "DRAM", "EDRAM", "VRAM", "SRAM", "RAM", |
| "ROM", "FLASH", "EEPROM", "FEPROM", "EPROM", "CDRAM", "3DRAM", |
| "SDRAM", "SGRAM", "RDRAM", "DDR", "DDR2", "DDR2 FB-DIMM", |
| "Reserved 0x15", "Reserved 0x16", "Reserved 0x17", "DDR3", |
| "FBD2", "DDR4", "LPDDR", "LPDDR2", "LPDDR3", "LPDDR4" |
| }; |
| |
| #define LOOKUP(array, val, buf) \ |
| ((val) >= NELE(array) ? \ |
| (sprintf(buf,"<%u>",(val)), (buf)) : \ |
| (array)[val]) |
| |
| static char *type_details[16] = { |
| "Reserved", "Other", "Unknown", "Fast-paged", "Static Column", |
| "Pseudo static", "RAMBUS", "Synchronous", "CMOS", "EDO", |
| "Window DRAM", "Cache DRAM", "Non-volatile", "Registered", |
| "Unbuffered", "LRDIMM" |
| }; |
| |
| static void dump_type_details(unsigned short td) |
| { |
| int i; |
| if (!td) |
| return; |
| for (i = 0; i < 16; i++) |
| if (td & (1<<i)) |
| Wprintf("%s ", type_details[i]); |
| } |
| |
| static void dump_memdev(struct dmi_memdev *md, unsigned long long addr) |
| { |
| char tmp[20]; |
| char unit[10]; |
| char *s; |
| |
| if (md->header.length < |
| offsetof(struct dmi_memdev, manufacturer)) { |
| if (verbose > 0) |
| printf("Memory device for address %llx too short %u\n", |
| addr, md->header.length); |
| return; |
| } |
| |
| Wprintf("%s ", LOOKUP(memory_types, md->memory_type, tmp)); |
| if (md->form_factor >= 3) |
| Wprintf("%s ", LOOKUP(form_factors, md->form_factor, tmp)); |
| if (md->speed != 0) |
| Wprintf("%hu Mhz ", md->speed); |
| dump_type_details(md->type_details); |
| Wprintf("Width %hu Data Width %hu Size %u %s\n", |
| md->total_width, md->data_width, |
| dmi_dimm_size(md->size, unit), unit); |
| |
| #define DUMPSTR(n,x) \ |
| if (md->x) { \ |
| s = dmi_getstring(&md->header, md->x); \ |
| if (s && *s && strcmp(s,"None")) \ |
| Wprintf(n ": %s\n", s); \ |
| } |
| DUMPSTR("Device Locator", device_locator); |
| DUMPSTR("Bank Locator", bank_locator); |
| if (md->header.length < offsetof(struct dmi_memdev, manufacturer)) |
| return; |
| DUMPSTR("Manufacturer", manufacturer); |
| DUMPSTR("Serial Number", serial_number); |
| DUMPSTR("Asset Tag", asset_tag); |
| DUMPSTR("Part Number", part_number); |
| } |
| |
| static void warnuser(void) |
| { |
| static int warned; |
| if (warned) |
| return; |
| warned = 1; |
| Wprintf("WARNING: " |
| "SMBIOS data is often unreliable. Take with a grain of salt!\n"); |
| } |
| |
| static int cmp_range(const void *a, const void *b) |
| { |
| struct dmi_memdev_addr *ap = *(struct dmi_memdev_addr **)a; |
| struct dmi_memdev_addr *bp = *(struct dmi_memdev_addr **)b; |
| return (int)ap->start_addr - (int)bp->end_addr; |
| } |
| |
| static int cmp_arr_range(const void *a, const void *b) |
| { |
| struct dmi_memarray_addr *ap = *(struct dmi_memarray_addr **)a; |
| struct dmi_memarray_addr *bp = *(struct dmi_memarray_addr **)b; |
| return (int)ap->start_addr - (int)bp->end_addr; |
| } |
| |
| #define COLLECT(var, id, ele) { \ |
| typedef typeof (**(var)) T; \ |
| var = (T **)dmi_collect(id, \ |
| offsetof(T, ele) + sizeof_field(T, ele), \ |
| &len); \ |
| } |
| |
| static void collect_dmi_dimms(void) |
| { |
| int len; |
| |
| COLLECT(dmi_ranges, DMI_MEMORY_MAPPED_ADDR, dev_handle); |
| qsort(dmi_ranges, len, sizeof(struct dmi_entry *), cmp_range); |
| COLLECT(dmi_dimms, DMI_MEMORY_DEVICE, device_locator); |
| if (verbose > 1) |
| dump_ranges(dmi_ranges, dmi_dimms); |
| COLLECT(dmi_arrays, DMI_MEMORY_ARRAY, location); |
| COLLECT(dmi_array_ranges, DMI_MEMORY_ARRAY_ADDR, array_handle); |
| qsort(dmi_array_ranges, len, sizeof(struct dmi_entry *),cmp_arr_range); |
| } |
| |
| #undef COLLECT |
| |
| static struct dmi_entry ** |
| dmi_collect(int type, int minsize, int *len) |
| { |
| struct dmi_entry **r; |
| struct dmi_entry *e, *next; |
| int i, k; |
| r = xalloc(sizeof(struct dmi_entry *) * (numentries + 1)); |
| k = 0; |
| e = entries; |
| next = NULL; |
| for (i = 0; i < numentries; i++, e = next) { |
| if (!check_entry(e, &next)) |
| break; |
| if (e->type != type) |
| continue; |
| if (e->length < minsize) { |
| if (verbose > 0) |
| printf("hnd %x size %d expected %d\n", |
| e->handle, e->length, minsize); |
| continue; |
| } |
| if (type == DMI_MEMORY_DEVICE && |
| ((struct dmi_memdev *)e)->size == 0) { |
| if (verbose > 0) |
| printf("entry %x disabled\n", e->handle); |
| continue; |
| } |
| r[k++] = e; |
| } |
| *len = k; |
| return r; |
| } |
| |
| #define FAILED " SMBIOS DIMM sanity check failed\n" |
| |
| int dmi_sanity_check(void) |
| { |
| int i, k; |
| int numdmi_dimms = 0; |
| int numranges = 0; |
| |
| if (dmi_ranges[0] == NULL) |
| return 0; |
| |
| for (k = 0; dmi_dimms[k]; k++) |
| numdmi_dimms++; |
| |
| /* Do we have multiple ranges? */ |
| for (k = 1; dmi_ranges[k]; k++) { |
| if (dmi_ranges[k]->start_addr <= dmi_ranges[k-1]->end_addr) { |
| return 0; |
| } |
| if (dmi_ranges[k]->start_addr >= dmi_ranges[k-1]->end_addr) |
| numranges++; |
| } |
| if (numranges == 1 && numdmi_dimms > 2) { |
| if (verbose > 0) |
| printf("Not enough unique address ranges." FAILED); |
| return 0; |
| } |
| |
| /* Unique locators? */ |
| for (k = 0; dmi_dimms[k]; k++) { |
| char *loc; |
| loc = dmi_getstring(&dmi_dimms[k]->header, |
| dmi_dimms[k]->device_locator); |
| if (!loc) { |
| if (verbose > 0) |
| printf("Missing locator." FAILED); |
| return 0; |
| } |
| for (i = 0; i < k; i++) { |
| char *b = dmi_getstring(&dmi_dimms[i]->header, |
| dmi_dimms[i]->device_locator); |
| if (!b) |
| continue; |
| if (!strcmp(b, loc)) { |
| if (verbose > 0) |
| printf("Ambiguous locators `%s'<->`%s'." |
| FAILED, b, loc); |
| return 0; |
| } |
| } |
| } |
| |
| return 1; |
| } |
| |
| #define DMIGET(p, member) \ |
| (offsetof(typeof(*(p)), member) + sizeof((p)->member) <= (p)->header.length ? \ |
| (p)->member : 0) |
| |
| static void |
| dump_ranges(struct dmi_memdev_addr **ranges, struct dmi_memdev **dmi_dimms) |
| { |
| int i; |
| printf("RANGES\n"); |
| for (i = 0; ranges[i]; i++) |
| printf("range %x-%x h %x a %x row %u ilpos %u ildepth %u\n", |
| ranges[i]->start_addr, |
| ranges[i]->end_addr, |
| ranges[i]->dev_handle, |
| DMIGET(ranges[i], memarray_handle), |
| DMIGET(ranges[i], row), |
| DMIGET(ranges[i], interleave_pos), |
| DMIGET(ranges[i], interleave_depth)); |
| printf("DMI_DIMMS\n"); |
| for (i = 0; dmi_dimms[i]; i++) |
| printf("dimm h %x width %u datawidth %u size %u set %u\n", |
| dmi_dimms[i]->header.handle, |
| dmi_dimms[i]->total_width, |
| DMIGET(dmi_dimms[i],data_width), |
| DMIGET(dmi_dimms[i],size), |
| DMIGET(dmi_dimms[i],device_set)); |
| } |
| |
| struct dmi_memdev **dmi_find_addr(unsigned long long addr) |
| { |
| struct dmi_memdev **devs; |
| int i, k; |
| |
| devs = xalloc(sizeof(void *) * (numentries+1)); |
| k = 0; |
| for (i = 0; dmi_ranges[i]; i++) { |
| struct dmi_memdev_addr *da = dmi_ranges[i]; |
| if (addr < ((unsigned long long)da->start_addr)*1024 || |
| addr >= ((unsigned long long)da->end_addr)*1024) |
| continue; |
| devs[k] = (struct dmi_memdev *)handle_to_entry[da->dev_handle]; |
| if (devs[k]) |
| k++; |
| } |
| |
| #if 0 |
| /* Need to implement proper decoding of interleaving sets before |
| enabling this. */ |
| int j, w; |
| for (i = 0; dmi_array_ranges[i]; i++) { |
| struct dmi_memarray_addr *d = dmi_array_ranges[i]; |
| if (addr < ((unsigned long long)d->start_addr)*1024 || |
| addr >= ((unsigned long long)d->end_addr)*1024) |
| continue; |
| for (w = 0; dmi_dimms[w]; w++) { |
| struct dmi_memdev *m = dmi_dimms[w]; |
| if (m->array_handle == d->array_handle) { |
| for (j = 0; j < k; j++) { |
| if (devs[j] == m) |
| break; |
| } |
| if (j == k) |
| devs[k++] = m; |
| } |
| } |
| } |
| #endif |
| |
| devs[k] = NULL; |
| return devs; |
| } |
| |
| void dmi_decodeaddr(unsigned long long addr) |
| { |
| struct dmi_memdev **devs = dmi_find_addr(addr); |
| if (devs[0]) { |
| int i; |
| warnuser(); |
| for (i = 0; devs[i]; i++) |
| dump_memdev(devs[i], addr); |
| } else { |
| Wprintf("No DIMM found for %llx in SMBIOS\n", addr); |
| } |
| free(devs); |
| } |
| |
| void dmi_set_verbosity(int v) |
| { |
| verbose = v; |
| } |
| |
| void checkdmi(void) |
| { |
| static int dmi_checked; |
| if (dmi_checked) |
| return; |
| dmi_checked = 1; |
| if (dmi_forced && !do_dmi) |
| return; |
| if (opendmi() < 0) { |
| if (dmi_forced) |
| exit(1); |
| do_dmi = 0; |
| return; |
| } |
| if (!dmi_forced) |
| do_dmi = dmi_sanity_check(); |
| } |
| |
| #define FREE(x) free(x), (x) = NULL |
| |
| void closedmi(void) |
| { |
| if (!entries) |
| return; |
| FREE(dmi_dimms); |
| FREE(dmi_arrays); |
| FREE(dmi_ranges); |
| FREE(dmi_array_ranges); |
| FREE(handle_to_entry); |
| FREE(entries); |
| entrieslen = 0; |
| } |