Accessing SMBIOS information with Go

This blog was originally written for Gopher Academy’s Advent 2017 series, but it is also mirrored here for convenience. Enjoy!

Please note that I am no longer employed by DigitalOcean.

While speaking with coworkers recently, one of them posed a question:

How can we discover the capacity of each memory DIMM in a machine?

Some veteran Linux users may be familiar with the dmidecode utility, which can access SMBIOS/DMI information exposed by hardware. This utility can expose a huge amount of information about the hardware and BIOS software on a machine. But how does it work under the hood?

This post will explain how to access this information, and demonstrate the open source go-smbios package that can be used to retrieve and leverage this information in Go.

Introduction to SMBIOS/DMI

System Management BIOS, or SMBIOS, is a specification that defines data structures that can be used to access information exposed by hardware and the system BIOS in a standardized way.

SMBIOS is often confused with Desktop Management Interface, or DMI, but it is essentially an evolution of the original DMI specification. This is where the Linux dmidecode utility’s name originates.

What kind of information is exposed by this interface? We can use the dmidecode utility to take a peek at some of this information.

$ sudo dmidecode | head -n 12
# dmidecode 3.0
Getting SMBIOS data from sysfs.
SMBIOS 2.7 present.
69 structures occupying 3435 bytes.
Table at 0x000E0FC0.

Handle 0x0000, DMI type 4, 42 bytes
Processor Information
        Socket Designation: CPU Socket - U3E1
        Type: Central Processor
        Family: Core i7
        Manufacturer: Intel(R) Corporation

The utility exposes quite a lot of information, but even from this small sample, we can note several important features:

SMBIOS has dozens of different structures, and each can encode a variety of data.

Retrieving SMBIOS information with Go

SMBIOS information consists of two crucial pieces: an “entry point” structure, and a table of data structures which carry SMBIOS information.

On modern Linux machines, the entry point structure and table can be found using two special files in sysfs:

$ ls /sys/firmware/dmi/tables/
DMI  smbios_entry_point

While this is certainly convenient, the standard approach on other UNIX-like operating systems is to directly scan system memory for a magic string, using /dev/mem.

The basic algorithm is:

Discovering and decoding SMBIOS entry points in Go

In simplified Go code (please always check your errors), discovering the entry point looks something like:

// Open /dev/mem and seek to the starting memory address.
const start, end = 0x000f0000, 0x000fffff

mem, _ := os.Open("/dev/mem")
_, _ = mem.Seek(start, io.SeekStart)

// Iterate one "paragraph" of memory at a time until we either find the entry point
// or reach the end bound.
const paragraph = 16
b := make([]byte, paragraph)

var addr int
for addr = start; addr < end; addr += paragraph {
    _, _ = io.ReadFull(mem, b)

    // Both the 32-bit and 64-bit entry point have a similar prefix.
    if bytes.HasPrefix(b, []byte("_SM")) {
        return addr, nil
    }
}

Now that we’ve discovered the location of the entry point in memory, we can begin decoding the entry point structure. Depending on your machine, you may encounter a 32-bit or 64-bit SMBIOS entry point.

// Prevent unbounded reads since this structure should be small.
b, _ := ioutil.ReadAll(io.LimitReader(mem, 64))
if l := len(b); l < 4 {
    return nil, fmt.Errorf("too few bytes for SMBIOS entry point magic: %d", l)
}

// Did we find a 32-bit entry point, or 64-bit entry point?
switch {
case bytes.HasPrefix(b, []byte("_SM_")):
    return parse32(b)
case bytes.HasPrefix(b, []byte("_SM3_")):
    return parse64(b)
}

I’ll spare you the details of each entry point structure, but they contain some key information exposed by the dmidecode utility, as discussed previously:

With this information, we can finally begin decoding the structures table.

Decoding the SMBIOS structure table in Go

Each SMBIOS structure contains a header that indicates:

Following the header, each structure contains a “formatted” section that carries arbitrary bytes, and optionally, zero or more “strings” that the formatted section can point to. This data must be decoded in a manner specific to each structure type, as laid out in the SMBIOS specification.

To start: let’s jump to the location of the table and begin decoding it. The structures stream continues until a special “end of table” structure is reached.

// Seek to the address of the structures table, and set up a decoder.
_, _ = mem.Seek(addr, io.SeekStart)
dec := newDecoder(mem)

var ss []*Structure
for {
    s, _ := dec.next()
    ss = append(ss, s)

    // End-of-table structure indicates end of stream.
    if s.Header.Type == typeEndOfTable {
        break
    }
}

Within our decoder.next method, we must deal with each structure’s header, formatted section, and zero or more strings:

// Decode the header structure.
h, _ := dec.parseHeader()

// Length of formatted section is length specified by header, minus
// the length of the header itself.
l := int(h.Length) - headerLen
fb, _ := dec.parseFormatted(l)

// Strings may or may not be present; only advance the decoder
// if they are.
ss, _ := dec.parseStrings()

return &Structure{
    Header:    *h,
    Formatted: fb,
    Strings:   ss,
}

This process continues until the end of table structure is reached, or an EOF is returned by the stream.

Decoding memory DIMM information from SMBIOS structures in Go

As previously mentioned, the formatted section and strings in an SMBIOS structure can be used to retrieve information stored in a specific format. The SMBIOS specification can be used as a reference for the format of individual structures.

With that said, let’s take a look back at our problem statement:

How can we discover the capacity of each memory DIMM in a machine?

The structure we want to decode is the “Memory Device (Type 17)” structure. One of these structures appears in the SMBIOS stream, per DIMM slot on the motherboard.

// Only look at memory devices.
if s.Header.Type != 17 {
    continue
}

// Formatted section contains a variety of data, but only parse the DIMM size.
size := int(binary.LittleEndian.Uint16(s.Formatted[8:10]))
// String 0 is the DIMM slot's identifier.
name := s.Strings[0]

// If 0, no DIMM present in this slot.
if size == 0 {
    fmt.Printf("[% 3s] empty\n", name)
    continue
}

// An extended uint32 DIMM size field appears if 0x7fff is present in size.
if size == 0x7fff {
    size = int(binary.LittleEndian.Uint32(s.Formatted[24:28]))
}

// Size units depend on MSB.  Little endian MSB for uint16 is in second byte.
// 0 means megabytes, 1 means kilobytes.
unit := "KB"
if s.Formatted[9]&0x80 == 0 {
    unit = "MB"
}

fmt.Printf("[% 3s] DIMM: %d %s\n", name, size, unit)

Now that we’ve put this all together, we can see the results from two of my Linux machines at home:

desktop $ sudo ./lsdimms
SMBIOS 2.7.0
[ChannelA-DIMM0] DIMM: 4096 MB
[ChannelA-DIMM1] DIMM: 4096 MB
[ChannelB-DIMM0] DIMM: 4096 MB
[ChannelB-DIMM1] DIMM: 4096 MB
server $ sudo ./lsdimms
SMBIOS 3.0.0
[DIMM 0] empty
[DIMM 1] DIMM: 16384 MB
[DIMM 0] empty
[DIMM 1] DIMM: 16384 MB

There are dozens of other structure type available, but with this information, we can now see the exact configuration and capacity of memory DIMMs in my machines.

Summary

As you can see, a great deal of useful information about your machine can be exposed using SMBIOS. Check out the dmidecode utility to see what kind of information is available! If you’d like to incorporate this data in your Go programs, I recommend that you check out the go-smbios package.

This package handles the nitty-gritty details of exposing SMBIOS data from a variety of operating systems (most UNIX-like systems, but I’d love to add macOS and Windows support!). At this time, it doesn’t contain any code for decoding specific structure types, but this is something I’d love to incorporate in a higher-level package in the future! If you’d like to collaborate, please reach out!

Finally, if you ever find yourself working on a text or binary format parser, I highly encourage trying out go-fuzz to discover any potential crashes in your parser. go-fuzz is an invaluable tool, and liberal use of it will save you many headaches down the road. For a great introduction to go-fuzz, check out Damian Gryski’s detailed walkthrough.

If you have any questions, feel free to contact me: “mdlayher” on Gophers Slack! You can also find me on both GitHub and Twitter with the same username.