Into the Apple System Management Controller

Tags: software-dev · C++ · MacOS

System Management Controller

Apple use a kernel extension called AppleSMC.kext to store information about a mac, including CPU temperatures, fan speeds, battery status, and so on. Accessing this data can be tough. There are a couple open efforts out there to provide APIs for the SMC: notably beltex's SMC controller which is pretty recent (last update in 2017):

Other open source programs use SMC in order to display fan or temperature information:

I'll document here a simple c implementation, the C++ or Swift versions aren't much different.

Accessing the SMC

Okay. I'm very new to the whole macOS architecture. I've got background in linux, and I once ran a FreeBSD vm, but that's as close as I've come to hacking around in macOS (until now) so please forgive me if I muck up the terminology a wee bit.

To access the SMC, you need to first access the SMC IO service, using the IOKit Framework. As I understand is, a Framework is Apple-speak for a library. Include the header, and load the service:

#include <stdlib.h>
#include <stdio.h>
#include <IOKit/IOKitLib.h>


int main()
{
  io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
                                                     IOServiceMatching("AppleSMC"));

  if (!service) {
    fprintf(stderr, "Error loading IOService for AppleSMC\n");
    exit(1);
  }

  // do something with service here

  IOObjectRelease(service);
}

We've also checked that the service is returned properly (should be non-zero) and then released it after we're done with it.

Next, we need to ask the service for a connection to the SMC, so we flesh out the part in the middle:

io_connect_t conn;

// Open SMC and check return value
if (!IOServiceOpen(service, mach_task_self(), 0, &conn)) {

  // Do something with SMC here

  // Close SMC
  if (IOServiceClose(this->conn)) {
    fprintf("Error closing SMC connection\n");
  }

}
else {
  fprintf("Failed to open SMC connection.\n");
}

Here, the service loads a connection to the SMC, and we check that it was loaded properly.

Now we have a connection! How to interact with it? 🤔

Communicating with the Machine

The SMC communicates by passing in structs to ask it for something, and it replies with another of the same struct. Hard work has been done by others to uncover the nature of this struct, which was unveiled in a piece of the Apple PowerManagement source code. From Beltex's libsmc:

/**
Defined by AppleSMC.kext.

This is the predefined struct that must be passed to communicate with the
AppleSMC driver. While the driver is closed source, the definition of this
struct happened to appear in the Apple PowerManagement project at around
version 211, and soon after disappeared. It can be seen in the PrivateLib.c
file under pmconfigd.

https://www.opensource.apple.com/source/PowerManagement/PowerManagement-211/
*/

So we define the structs as follows:

typedef struct {
    unsigned char  major;
    unsigned char  minor;
    unsigned char  build;
    unsigned char  reserved;
    unsigned short release;
} SMCVersion;


typedef struct {
    uint16_t version;
    uint16_t length;
    uint32_t cpuPLimit;
    uint32_t gpuPLimit;
    uint32_t memPLimit;
} SMCPLimitData;


// Struct containing information about the SMC key requested
typedef struct {
    IOByteCount dataSize;
    uint32_t    dataType;
    uint8_t     dataAttributes;
} SMCKeyInfoData;


// Struct passed in to/out from the SMC.
typedef struct {
    uint32_t       key;
    SMCVersion     vers;
    SMCPLimitData  pLimitData;
    SMCKeyInfoData keyInfo;
    uint8_t        result;
    uint8_t        status;
    uint8_t        data8;
    uint32_t       data32;
    uint8_t        bytes[32];
} SMCParamStruct;

This last one is the main struct that contains all the information for the SMC to give us what we want. Most of it is pretty self explanatory.

I just want to note that the key is a four character code in 8bit ascii, re-expressed as a 32 bit unsigned integer. We can write simple to/from conversion functions:

uint32_t key_to_uint(char *key)
{
  uint32_t rv = 0;
  rv += key[0] << 24;
  rv += key[1] << 16;
  rv += key[2] << 8;
  rv += key[3];
  return rv;
}

void key_to_char(uint32_t key, char *rv)
{
  rv[0] = key >> 24;
  rv[1] = (key >> 16) & 0xff;
  rv[2] = (key >> 8) & 0xff;
  rv[3] = key & 0xff;
}

These functions make assumptions about the char arrays passed to them: that they are blocks of memory at least 4 chars in size which is a bit dangerous. It would be best to check to be safe, perhaps using `strlen`.

A final piece of the puzzle are the interacting specifiers or selectors: we need to tell the SMC what it is that we want.

typedef enum {
    kSMCUserClientOpen  = 0,
    kSMCUserClientClose = 1,
    kSMCHandleYPCEvent  = 2,
    kSMCReadKey         = 5,
    kSMCWriteKey        = 6,
    kSMCGetKeyCount     = 7,
    kSMCGetKeyFromIndex = 8,
    kSMCGetKeyInfo      = 9
} selector_t;

The three important ones for us here are kSMCGetKeyInfo, kSMCReadKey, kSMCWriteKey.

We have out envelope, now we need to fill it, and send it away. (I mean, we need to make a call to the SMC and pass in out input struct, and receive a filled out output struct.)

SMCParamStruct in_struct = {0}, out_struct = {0};

in_struct.key = key_to_uint("TC0P");
in_struct.data8 = kSMCGetKeyInfo;

size_t s = sizeof(SMCParamStruct);
kern_return_t result = IOConnectCallStructMethod(conn,
                                                 kSMCHandleYPCEvent,
                                                 &input_struct,
                                                 s,
                                                 &output_struct,
                                                 &s);

if (!result) {
  if (!output_struct.result) {

    // next part goes here

  }
  else {
    fprintf(stderr, "Call to SMC failed.\n");
  }
}
else {
  fprintf(stderr, "Call to SMC failed.\n");
}

In this simple call, the SMC is asked for information about the key "TC0P". This key gets the Temperature for CPU0, in its Proximity. Other keys include "F0Ac" - Actual speed of Fan 0, and "BATP" - battery power level in percent. Take these with a pinch of salt: Apple changes the keys about adding new ones and dropping old. You can poll your system for keys by trying loads of combinations of four characters and seeing what sticks, or look at dumps obtained by others and posted online (an example).

With that key info we verify that we have the right key, and we find out the format it is stored in. SMC data are stored in a 32 byte array, but its exact format can vary. Is the data a float or int? is it a fixed point? Is it a UInt16?

Decoding the data

This is given in the SMCKeyInfoData struct in the dataType member. As with key, this is a four character code written as a long unsigned int, so we can decode it using the function we wrote earlier.

As with the keys, the data types change too. Common types are the "fpe2", "sp78", "flt ", and "ui8 " types. Note the spaces in the last two.

fpe2 and sp78

FPE, I think, stands for fixed point exponent 2. The data is unsigned, and is 2 bytes long. The last two bits are reserved for fractional data. (source.)

double from_fpe2(uint8_t *data)
{
  int value = (data[0] << 6) + (data[1] >> 2);
  int fraction = data[1] & 3;

  return double_from_val_frac(value, fraction, 2);
}

We can write another function to convert the value/fraction pair into a double:

double double_from_val_frac(int value, int fraction, int n)
{
  double rv = (double)value;

  double running_fraction = 0.5;

  // check each individual bit in fraction, is set, add another
  // fraction onto the return value rv
  for (int shift = n-1; shift >= 0; shift--) {
    if ((fraction >> shift) & 1) {
      rv += running_fraction;
    }

    running_fraction /= 2.0;
  }

  return rv;
}

I am certain there is a better way to do this, however this is what I came up with and it works well enough.

SP78 is very similar, but it is signed, and there are 8 fractional bits instead of only 2:

double from_sp78(uint8_t *data)
{
  int sign = (data[0] >> 7) ? -1 : 1;
  int value = sign * (data[0] & 0x7f);
  int fraction = data[1];

  return double_from_val_frac(value, fraction, 8);
}

flt_

flt_ is quite simple: a single precision floating point over four bytes. This is the same float as used by the processor, so we don't even need to do the conversion!

double from_flt_(uint8_t *data)
{
  // cast data to a pointer to float, and deref
  float f = *(float*)data;
  return (double)f;
}

It looks horrible, but it works!

ui8_

Finally, there's ui8, which is a unsigned int in a single byte. This is again trivial to convert:

uint8_t from_ui8_(uint8_t *data)
{
  return data[0];
}

Measuring CPU Temperature

We've almost done something useful! We've got the key data for our desired key of "TC0P" - CPU temperature. This should be a "flt " - or it was for me. Other people have reported "sp78" for temperatures. Your mileage may vary on this.

Whatever your SMC says the data type is, we can now get the value of the key using a successive call to the SMC:

uint32_t type = out_struct.keyInfo.dataType;

in_struct.keyInfo.dataSize = out_struct.keyInfo.dataSize;
in_struct.data8 = kSMCReadKey;

s = sizeof(SMCParamStruct);
result = IOConnectCallStructMethod(conn,
                                   kSMCHandleYPCEvent,
                                   &input_struct,
                                   s,
                                   &output_struct,
                                   &s);

if (!result) {
  if (!output_struct.result) {

    double temperature = from_flt_(out_struct.bytes);
    fprintf(stdout, "CPU Proximity temperature %fºC", temperature);

  }
  else {
    fprintf(stderr, "Call to SMC Read failed.\n");
  }
}
else {
  fprintf(stderr, "Call to SMC Read failed.\n");
}

And that's reading! There's a similar process for writing, just with a different key in the input param, and you set the in_param.bytes member as the payload for the write operation.

Conclusion

Now that was a very brief intro to the SMC, with some resources dotted about. The program above has not been tested or run, and it would be a mess to write out in full. I have a better implementation in C++ on github, or there's beltex's c or Swift versions linked at the top of this post.


Questions? Comments? Get in touch on Twitter!