Into the Apple System Management Controller
17 Jul 2020System 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:
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.
Resources
Questions? Comments? Get in touch on Twitter!