blob: 2970e77a6db5790b88f98b865bd6b2df8c68fa25 [file] [log] [blame]
/* 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;
}