macOS 内核之从 I/O Kit 电量管理开始

Oct 17, 2019 at 21:13:29

macOS 内核之从 I/O Kit 电量管理开始

在上一篇macOS 内核之 hw.epoch 是个什么东西?我们提到 XNU 内核包含了 BSD 和 Mach,其中 Mach Kernel 提供了 I/O Kit 给硬件厂商写驱动用的。这个部分在 NeXT 时期是用 Objective-C 提供的 API,叫做 Driver Kit,后来乔布斯回到苹果之后,升级了 BSD 和 Mach 的代码,于是在 OS X 中提供了 C++ 接口的 I/O Kit。

根据官方的这份文档,以下系统支持 I/O Kit:

  • iOS 2.0+
  • macOS 10.0+
  • Mac Catalyst 13.0+

I/O Kit 里我们可以通过三种不同的方式获取电池信息,位于 IOKit/pwr_mgt 的 Power Mangement 接口,位于 IOKit/ps 的 Power Sources 接口,以及通过 IOServiceGetMatchingService 获取 AppleSmartBattery Service 接口。

1. IOPM (Power Management) API

IOPM 接口需要使用 Mach Port 跟 IOKit 进行 IPC 通信,所以我们先来了解一点 Mach Port 的背景。

1.1 Mach Port

XNU 是一个混合内核,既有 BSD 又有 Mach Kernel,上层还有各种各样的技术,所以在 macOS 系统中,IPC (跨进程通信)的技术也多种多样。Mattt 在 NSHipster 上写过一篇 IPC 的文章: Inter-Process Communication - NSHipster 对此有过详解。

Mach Port 是在系统内核实现和维护的一种 IPC 消息队列,持有用于 IPC 通信的 mach messages。只有一个进程可以从对应的 port 里 dequeue 一条消息,这个进程被持有接收权利(receive-right)。可以有多个进程往某个 port 里 enqueue 消息,这些进程持有该 port 的发送权利(send-rights)。

如上图,PID 123 的进程往一个 port 里发送了一条消息,只有对应的接收端 PID 456 才能从 port 里取出这条消息。

我们可以简单把 mach port 看做是一个单向的数据发送渠道,构建一个消息结构体后通过mach_msg() 方法发出去。因为只能单向发送,所以当 B 进程收到了 A 进程发来的消息之后要自己创建一个新的 Port 然后又发回去 A 进程。

手动构建 mach message 发送是比较复杂的,大概长这个样子(代码来自 Mattt 的那篇文章):

natural_t data;
mach_port_t port;

struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_type_descriptor_t type;
} message;

message.header = (mach_msg_header_t) {
.msgh_remote_port = port,
.msgh_local_port = MACH_PORT_NULL,
.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0),
.msgh_size = sizeof(message)
};

message.body = (mach_msg_body_t) {
.msgh_descriptor_count = 1
};

message.type = (mach_msg_type_descriptor_t) {
.pad1 = data,
.pad2 = sizeof(data)
};

mach_msg_return_t error = mach_msg_send(&message.header);

if (error == MACH_MSG_SUCCESS) {
// ...
}

其中最关键的是 msgh_remote_portmsgh_local_port。上述代码是发送消息,所以 msgh_remote_port 就是要接收这条消息的那个进程的 port。我们得先知道这个 port 信息我们才能往里面发消息。另外例子中使用的是 mach_msg_send() 函数。

port name

留意到在上图中,PID 123 往一个名为 0xabcport 发消息,PID 456 则从名为 0xdefport 里取消息。这里 port name 只对当前进程有意义,并不需要全局一致,内核会自动根据进程 ID 和名字信息找到对应的进程。

Out-of-line memory

我们的代码在用户层调用,需要进出内核层,这是一进一出如果消息体里带上大量的信息就会非常慢。所以如果需要使用 mach message 来发送体积较大的信息,可以使用 “out-of-line memory” descriptor。

我们看到上面 Mattt 的代码使用 mach_msg_send() 函数来发送消息,message.body 带了一个 msgh_descriptor_count 为 1。这个 descriptor 是一个 natural_t。我看到这里的时候并没有搞懂系统是怎么做 OOL 的 copy-on-write 的。于是照例翻一下 XNU 的源码,我发现 Mattt 的例子并没有使用 OOL descriptor,而是使用了 type descriptor。

typedef struct
{
  natural_t         pad1;
  mach_msg_size_t       pad2;
  unsigned int          pad3 : 24;
  mach_msg_descriptor_type_t    type : 8;
} mach_msg_type_descriptor_t;

ool descriptor 的结构如下:

typedef struct
{
  uint64_t          address;
  boolean_t             deallocate: 8;
  mach_msg_copy_options_t       copy: 8;
  unsigned int          pad1: 8;
  mach_msg_descriptor_type_t    type: 8;
  mach_msg_size_t           size;
} mach_msg_ool_descriptor64_t;

使用时我们需要把内存地址发过去,内核只负责传递地址指针,等到进程接受到了这条消息之后才会从内存里 copy buffer。

1.2 使用 Master Port 和 IOKit 通信

在 IOKit 里面,所有的通信都通过 IOKit Master Port 来进行,使用以下函数可以获取 master port。

kern_return_t
IOMasterPort( mach_port_t   bootstrapPort,
          mach_port_t * masterPort );

实际使用时如下:

mach_port_t masterPort;
IOMasterPort(MACH_PORT_NULL, &masterPort)

默认把 bootstrapPort 置空。如果返回值是 kIOReturnSuccess 就成功构建了一个 mach_port_t 用于跟 IOKit 通信。

bootstrapPort

不过在这个 API 里面,获取单一 master port 好理解,那 bootstrapPort 这个参数又是用来干啥的呢?

在上面的例子中 PID 123 和 PID 456 是在已经获知对方的 port name 的前提下才有办法互相通信的。但是如果你不知道对方的 port name 呢?于是 XNU 系统提供了 bootstrap port 这个东西,由系统提供查询服务,这样所有的进程都可以去广播自己的 mach port 接收端的名字,也可以查询其他人的名字。

查询接口大概是这样:

mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "me.justinyan.example", &port);

注册接口大概是这样:

bootstrap_register(bootstrap_port, "me.justinyan.example", port);

同时 bootstrap port 是一个特殊的 port。其他的 mach port 在父进程被 fork() 的时候,子进程是不会继承 port 的,只有 bootstrap port 可以被继承。

但是,自从 OS X 10.5 开始,苹果引入了 Launchd 这么一个服务,同时弃用了 bootstrap_register() 接口。关于这件事情当时 darwin 开发团队有个长长的邮件列表做了激烈的讨论: Apple - Lists.apple.com

新的接口可以参考 CFMessagePortCreateLocal() 和这篇文章: Damien DeVille | Interprocess communication on iOS with Mach messages

IOPM 获取电池信息接口

上面罗里吧嗦一大堆全是 mach port 的事情,现在终于到正题了。代码非常简单:

NSDictionary* get_iopm_battery_info() {
    mach_port_t masterPort;
    CFArrayRef batteryInfo;
if (kIOReturnSuccess == IOMasterPort(MACH_PORT_NULL, &masterPort) &&
    kIOReturnSuccess == IOPMCopyBatteryInfo(masterPort, &batteryInfo) &&
    CFArrayGetCount(batteryInfo))
{
    CFDictionaryRef battery = CFDictionaryCreateCopy(NULL, CFArrayGetValueAtIndex(batteryInfo, 0));
    CFRelease(batteryInfo);
    return (__bridge_transfer NSDictionary*) battery;
}
return NULL;

}

NSDictionary *dict = get_iopm_battery_info();
NSLog(@"iopm dict: %@", dict);

输出:

iopm dict: {
    Amperage = 0;
    Capacity = 6360;
    Current = 6360;
    "Cycle Count" = 113;
    Flags = 5;
    Voltage = 12968;
}

可以看到电池循环次数、容量之类的信息,但是不多。IOPMLib.h 的注释说 不建议大家使用这个接口,可以考虑用 IOPowerSources API 代替。

2. IOPowerSources API

IOPowerSources 的接口比较简单,先用 IOPSCopyPowerSourcesInfo() 取到 info, 然后取 IOPSCopyPowerSourcesList(),最后再 copy 一下就完事了。

NSDictionary* get_iops_battery_info() {
    CFTypeRef info = IOPSCopyPowerSourcesInfo();
if (info == NULL)
    return NULL;


CFArrayRef list = IOPSCopyPowerSourcesList(info);

// Nothing we care about here...
if (list == NULL || !CFArrayGetCount(list)) {
    if (list)
        CFRelease(list);

    CFRelease(info);
    return NULL;
}

CFDictionaryRef battery = CFDictionaryCreateCopy(NULL, IOPSGetPowerSourceDescription(info, CFArrayGetValueAtIndex(list, 0)));

// Battery is released by ARC transfer.
CFRelease(list);
CFRelease(info);

return (__bridge_transfer NSDictionary* ) battery;

}

NSDictionary *iopsDict = get_iops_battery_info();
NSLog(@"iops dict: %@", iopsDict);

输出:

iops dict: {
    "Battery Provides Time Remaining" = 1;
    BatteryHealth = Good;
    Current = 0;
    "Current Capacity" = 100;
    DesignCycleCount = 1000;
    "Hardware Serial Number" = D**********;
    "Is Charged" = 1;
    "Is Charging" = 0;
    "Is Present" = 1;
    "Max Capacity" = 100;
    Name = "InternalBattery-0";
    "Power Source ID" = 9764963;
    "Power Source State" = "AC Power";
    "Time to Empty" = 0;
    "Time to Full Charge" = 0;
    "Transport Type" = Internal;
    Type = InternalBattery;
}

可以看到信息多了很多,还有 BatteryHealth 等信息,我们看到我的 MacBook 的电池设计循环次数是 DesignCycleCount = 1000,然后我已经循环 113 次了。

但是,这批信息里面没有带电池的设计容量。

3. IOPMPS Apple Smart Battery API

IOKit 里提供了一套 IOService 相关的接口,你可以往里面注册 IOService 服务,带个名字,一样是通过 IOMasterPort() 来通信。IOKit 主要是面向硬件驱动开发者的,所以如果你的硬件依赖另外一个硬件,但是另外一个硬件还没有接入,这时候你可以往 IOService 注册一个通知。使用 IOServiceAddMatchingNotification,等到你观察的硬件接入后调用了 registerService() 你就会收到对应的通知了。

这里我们直接用 IOServiceGetMatchingService() 来获取系统提供的 AppleSmartBattery service。

NSDictionary* get_iopmps_battery_info() {
    io_registry_entry_t entry = 0;
    entry = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceNameMatching("AppleSmartBattery"));
    if (entry == IO_OBJECT_NULL)
        return nil;
CFMutableDictionaryRef battery;
IORegistryEntryCreateCFProperties(entry, &battery, NULL, 0);
return (__bridge_transfer NSDictionary *) battery;

}

NSDictionary *iopmsDict = get_iopmps_battery_info();
NSLog(@"iopmsDict: %@", iopmsDict);

输出:

iopmsDict: {
    AdapterDetails =     {
        Current = 4300;
        PMUConfiguration = 2092;
        Voltage = 20000;
        Watts = 86;
    };
    AdapterInfo = 0;
    Amperage = 0;
    AppleRawAdapterDetails =     (
                {
            Current = 4300;
            PMUConfiguration = 2092;
            Voltage = 20000;
            Watts = 86;
        }
    );
    AppleRawCurrentCapacity = 6360;
    AppleRawMaxCapacity = 6360;
    AvgTimeToEmpty = 65535;
    AvgTimeToFull = 65535;
    BatteryData =     {
        AdapterPower = 1106486026;
        CycleCount = 113;
        DesignCapacity = 6669;
        PMUConfigured = 0;
        QmaxCell0 = 6812;
        QmaxCell1 = 6859;
        QmaxCell2 = 6784;
        ResScale = 200;
        StateOfCharge = 100;
        SystemPower = 4625;
        Voltage = 12968;
    };
    BatteryFCCData =     {
        DOD0 = 128;
        DOD1 = 144;
        DOD2 = 128;
        PassedCharge = 0;
        ResScale = 200;
    };
    BatteryInstalled = 1;
    BatteryInvalidWakeSeconds = 30;
    BatterySerialNumber = D**********;
    BestAdapterIndex = 3;
    BootPathUpdated = 1571194014;
    CellVoltage =     (
        4323,
        4322,
        4323,
        0
    );
    ChargerData =     {
        ChargingCurrent = 0;
        ChargingVoltage = 13020;
        NotChargingReason = 4;
    };
    CurrentCapacity = 6360;
    CycleCount = 113;
    DesignCapacity = 6669;
    DesignCycleCount70 = 0;
    DesignCycleCount9C = 1000;
    DeviceName = bq20z451;
    ExternalChargeCapable = 1;
    ExternalConnected = 1;
    FirmwareSerialNumber = 1;
    FullPathUpdated = 1571290629;
    FullyCharged = 1;
    IOGeneralInterest = "IOCommand is not serializable";
    IOReportLegend =     (
                {
            IOReportChannelInfo =             {
                IOReportChannelUnit = 0;
            };
            IOReportChannels =             (
                                (
                    7167869599145487988,
                    6460407809,
                    BatteryCycleCount
                )
            );
            IOReportGroupName = Battery;
        }
    );
    IOReportLegendPublic = 1;
    InstantAmperage = 0;
    InstantTimeToEmpty = 65535;
    IsCharging = 0;
    LegacyBatteryInfo =     {
        Amperage = 0;
        Capacity = 6360;
        Current = 6360;
        "Cycle Count" = 113;
        Flags = 5;
        Voltage = 12968;
    };
    Location = 0;
    ManufactureDate = 19722;
    Manufacturer = SMP;
    ManufacturerData = {length = 27, bytes = 0x00000000 *** };
    MaxCapacity = 6360;
    MaxErr = 1;
    OperationStatus = 58433;
    PackReserve = 200;
    PermanentFailureStatus = 0;
    PostChargeWaitSeconds = 120;
    PostDischargeWaitSeconds = 120;
    Temperature = 3067;
    TimeRemaining = 0;
    UserVisiblePathUpdated = 1571291169;
    Voltage = 12968;
}

可以看到比前面的两次输出多了很多。

CurrentCapacity = 6360;
DesignCapacity = 6669;

有了当前电池容量和设计容量,就可以得到我的电池还剩 95% 的容量。

4. 列出所有 IOService

以上三种方法我都是从 Hammerspoon 的源码中习得。通过阅读这部分接口学习了相关的一些内核层 API 的概念,很有意思。那么在 #3 中 Hammerspoon 的作者是怎么知道系统有一个 IOService 叫做 "AppleSmartBattery" 的呢?我们不妨把系统所有的 IOService 打印出来,然后 grep 看看里面有没有带 battery 或者 energy 关键字的。

IOKitLib.h 里有一个接口 IORegistryCreateIterator() 可以创建一个迭代器,把所有已注册的 IOService 取出来。

核心代码如下:

const char *plane = "IOService";
io_iterator_t it = MACH_PORT_NULL;
IORegistryCreateIterator(kIOMasterPortDefault, plane, kIORegistryIterateRecursively, &it) 

有一个开源库实现了这个功能,有兴趣的读者朋友可以看看这里: Siguza/iokit-utils: Dev tools for probing IOKit

➜  iokit-utils ./ioprint| grep -i battery
AppleSmartBatteryManager(AppleSmartBatteryManager)
AppleSmartBattery(AppleSmartBattery)

结果出来两个 battery 相关的,AppleSmartBattery 就是上述例子用到的,AppleSmartBatteryManager 则打印出如下结果:

iopmsDict: {
    CFBundleIdentifier = "com.apple.driver.AppleSmartBatteryManager";
    CFBundleIdentifierKernel = "com.apple.driver.AppleSmartBatteryManager";
    IOClass = AppleSmartBatteryManager;
    IOMatchCategory = IODefaultMatchCategory;
    IOPowerManagement =     {
        CapabilityFlags = 2;
        CurrentPowerState = 1;
        MaxPowerState = 1;
    };
    IOProbeScore = 0;
    IOPropertyMatch =     {
        IOSMBusSmartBatteryManager = 1;
    };
    IOProviderClass = IOSMBusController;
    IOUserClientClass = AppleSmartBatteryManagerUserClient;
}

只是一堆苹果自家驱动的信息而已。

5. 用于 iOS 系统

我在运行了 iOS 13.1.2 的 iPhone Xs Max 机器上进行了测试。iOS 工程引入 IOKit 会比较麻烦,因为这个 Framework 是不公开的,所以你得把所有的头文件导出来,并且把 #import <IOKit/xxx.h> 的地方都改掉。可以参考此文: [Tutorial] Import IOKit framework into Xcode project | Gary's ...Lasamia

实测 IOPMCopyBatteryInfo 在 iOS 上无效,估计是 iOS 直接不给 mach port 权限到上层。 IOPSCopyPowerSourcesListIOServiceNameMatching 能用。

iops dict: {
    "Battery Provides Time Remaining" = 1;
    "Current Capacity" = 100;
    "Is Charged" = 1;
    "Is Charging" = 0;
    "Is Present" = 1;
    "Max Capacity" = 100;
    Name = "InternalBattery-0";
    "Play Charging Chime" = 1;
    "Power Source ID" = 2490467;
    "Power Source State" = "AC Power";
    "Raw External Connected" = 1;
    "Show Charging UI" = 1;
    "Time to Empty" = 0;
    "Time to Full Charge" = 0;
    "Transport Type" = Internal;
    Type = InternalBattery;
}
iopmsDict: {
    BatteryInstalled = 1;
    ExternalConnected = 1;
}

可以看到信息比 macOS 的少了很多,并且没有包含 cycleCount 这个信息。

5.1 奇技淫巧 hack 之

但是毕竟 iOS 是有 IOKit 框架的,那么有没有什么奇技淫巧可以拿到 IOKit 的信息呢?eldade/UIDeviceListener: Obtain power information (battery health, charger details) for iOS without any private APIs.这个库可以在 iOS 7 - iOS 9.3 上捕获这部分信息。

所使用之操作也是非常有趣。从 iOS 3.0 开始,UIDevice 增加了 batteryStatebatteryLevel 这两个参数,并且允许开启电池监控 batteryMonitoringEnabled。通过上文我们已经知道,这些操作最终都是通过 IOKit 来进行的。

IOKit 会从 IORegistry 获取一份最新的电池信息,就像我们的 get_iopmps_battery_info() 方法一样。留意到从 IORegistry 取数据的接口长这样:

IORegistryEntryCreateCFProperties(
    io_registry_entry_t entry,
    CFMutableDictionaryRef * properties,
        CFAllocatorRef      allocator,
    IOOptionBits        options );

重点在第三个参数 CFAllocatorRef,通常情况下系统会用默认的 CFAllocatorGetDefault()。我们看看这个 allocator 长啥样CoreFoundation/CFBase.c:

typedef const struct CF_BRIDGED_TYPE(id) __CFAllocator * CFAllocatorRef;

// CFAllocator structure must match struct _malloc_zone_t!
// The first two reserved fields in struct _malloc_zone_t are for us with CFRuntimeBase
struct __CFAllocator {
CFRuntimeBase _base;
CFAllocatorRef _allocator;
CFAllocatorContext _context;
};

以及 CoreFoundation 提供了不少操作:

CFAllocatorGetDefault();
CFAllocatorGetContext();
CFAllocatorCreate();
CFAllocatorSetDefault();

如果能把系统的默认 allocator 替换成自己的实现,那么当我们打开 batteryMonitoringEnabled 然后电池发生变更的时候,系统就回去用 IORegistry 取一份电池信息,就会掉进我们替换掉的 allocator。这时候就能截取 allocator 刚刚 allocate 的内存信息了。真的佩服作者的脑洞。详细的实现大家可以看原来的库: eldade/UIDeviceListener,我们只看关键代码:

// 获取默认 allocator
_defaultAllocator = CFAllocatorGetDefault();

CFAllocatorContext context;

// 获取默认 allocator 的 context
CFAllocatorGetContext(_defaultAllocator, &context);

// 全部改成自己的实现, myAlloc/myRealloc/myFree 都是 C 函数
context.allocate = myAlloc;
context.reallocate = myRealloc;
context.deallocate = myFree;

// 用修改后的 context 创建新的 allocator
_myAllocator = CFAllocatorCreate(NULL, &context);

// 把自己创建的 allocator 替换掉系统的默认 allocator
CFAllocatorSetDefault(_myAllocator);

接下来看看 myAlloc 的实现:

void * myAlloc (CFIndex allocSize, CFOptionFlags hint, void *info)
{
    // 做一下线程检查
    VERIFY_LISTENER_THREAD();
// 实现一个新的 allocation
void *newAllocation = CFAllocatorAllocate([UIDeviceListener sharedUIDeviceListener].defaultAllocator, allocSize, hint);

// 失败就放过
if (newAllocation == NULL)
    return newAllocation;

// 有东西了,赶紧把新的内容塞进准备好的 allocations 变量里,这是个 C++ 的 std::set&lt;void *&gt;
if (hint &amp; __kCFAllocatorGCObjectMemory)
{
    [UIDeviceListener sharedUIDeviceListener].allocations-&gt;insert(newAllocation);
}
return newAllocation;

}

与此同时,通过 KVO 观察 UIDevice 公开的 batteryLevel 属性,接收 KVO 回调:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([change objectForKey: NSKeyValueChangeNewKey] != nil)
    {
        std::set<void *>::iterator it;
        for (it=_allocations->begin(); it!=_allocations->end(); ++it)
        {
            CFAllocatorRef *ptr = (CFAllocatorRef *) (NSUInteger)*it;
            void * ptrToObject = (void *) ((NSUInteger)*it + sizeof(CFAllocatorRef));
        if (*ptr == _myAllocator &amp;&amp; // Just a sanity check to make sure the first field is a pointer to our allocator
            [self isValidCFDictionary: ptrToObject])   // Check for valid CFDictionary
        {
            CFDictionaryRef dict = (CFDictionaryRef) ptrToObject;

            if ([self isChargerDictionary: dict]) // Check if this is the charger dictionary
            {
                // Found our dictionary. Let's clear the allocations array:
                _allocations-&gt;clear();

                // We make a deep copy of the dictionary using the default allocator so we don't
                // get callbacks when this object and any of its descendents get freed from the
                // wrong thread:

                CFDictionaryRef latestDictionary = (CFDictionaryRef) CFPropertyListCreateDeepCopy(_defaultAllocator, dict, kCFPropertyListImmutable);

                if (latestDictionary != nil)
                {
                    // Notify that new data is available, but that has to happen on the main thread.
                    // Because of the CFAllocator replacement, we generally shouldn't
                    // do ANYTHING on this thread other than stealing this dictionary from UIDevice...
                    dispatch_sync(dispatch_get_main_queue(), ^{
                        // Pass ownership of the CFDictionary to the main thread (using ARC):
                        NSDictionary *newPowerDataDictionary = CFBridgingRelease(latestDictionary);
                        [[NSNotificationCenter defaultCenter] postNotificationName:kUIDeviceListenerNewDataNotification object:self userInfo:newPowerDataDictionary];
                    });
                }

                return;
            }
        }
    }
}

}

上面一堆嵌套代码判断了一层又一层,最后做了一个 CFPropertyListCreateDeepCopy 然后通过通知转发出去。

CFDictionaryRef latestDictionary = (CFDictionaryRef) CFPropertyListCreateDeepCopy(_defaultAllocator, dict, kCFPropertyListImmutable);

严格来说这种写法并没有用到私有 API,但是非常取巧。如果内核实现代码不用 default allocator 来取 IORegistry 的信息这里就失效了。事实上从 iOS 10 开始这个做法确实也失效了。但是整个思路非常有趣,值得观摩。

5.2 遍历所有的 IOService

上面我们在 macOS 上通过取 AppleSmartBattery 这个 IOService 可以获得更多电池信息,但是在 iOS 上没有。那么我们还能不能寻找其他的 IOService 看看是否有携带了电池信息的呢?

此文iOS IOKit Browser - Christopher Lyon Anderson 使用私有 API 遍历了 iOS 上所有的 IOService,并且在他的截屏中是包含了电池信息的。我 clone 下来发现已经没有 cycleCount 信息了,但是这个项目有个地方挺有意思:

NSString *bundlePath = [[NSBundle bundleWithPath:@"/System/Library/Frameworks/IOKit.framework"] bundlePath];
    NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath];
    CFBundleRef cfBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)bundleURL);
self.IORegistryGetRootEntryShim = CFBundleGetFunctionPointerForName(cfBundle, CFSTR("IORegistryGetRootEntry"));

先取系统的 IOKit.framework,然后用 CoreFoundation 的接口来取函数指针,然后就可以使用这批 IOKit 的私有函数了。可惜此方法亦已无效。

6. 小结

iOS 方面暂时还未找到能展示 cycleCount 信息的方法,想必 Battery Health App 应该用了更加厉害的黑科技。可能只有越狱逆向一下才知道它是怎么做到的了。

之前因为 sysctl() 的缘故看了一下 XNU 的源码,结果发现内核层还是有不少有意思的东西。IOKit 作为驱动层的 API,除了获取电池信息之外还能干很多事情。

本文通过 IOKit 的简单接口,扩展学习了 XNU 的 IPC 通信机制 mach port。希望后续能通过这些工具做出点有意思的东西来。

内核系列文章

参考资料