macOS内核拓展与用户态进程的通信实现(二)

前面已经讲解了基于KernControl API的通信实现,该实现相对简单,但有一些缺点。内核向应用层传输消息需调用ctl_enqueuedata接口,该接口实际将数据缓存至缓冲区,当需要瞬时大量传输消息时,缓冲区容量有限,将丢弃后来的数据。如果传输的数据有优先级,则需对ctl_enqueuedata接口进行二次封装,避免高优先级数据丢失。

IOKit Fundamentals 框架提供更加全面且方便的内核驱动API。IOService是大多数内核驱动的基类,提供驱动实例化相关的各项服务。IOUserClient是与用户态应用程序间通信的基类,通过继承并实现该类,可与客户端应用程序建立通信机制。IOSharedDataQueue是非常便于使用的内核与用户态进程进行数据交换的通用队列,用户可自行设置队列大小。基于IOKit中的IOService、IOUserClient、IOSharedDataQueue可方便的实现内核与用户进程的通信和数据传输。下面结合代码进行简单实现,更多代码细节和工程配置可参考NuwaStone项目。

IOService内核编程

由于本次编写的为内核拓展,仅需要IOService进行驱动的加载与卸载管理,这里的代码实现很简单,仅需重写start、stop方法。如有加载卸载时的自定义操作可在函数中实现。

DriverService.hpp

class DriverService : public IOService {
    OSDeclareDefaultStructors(DriverService);

public:
    // Called by the kernel when the kext is loaded
    bool start(IOService *provider) override;

    // Called by the kernel when the kext is unloaded
    void stop(IOService *provider) override;
    
private:
    void clearInstances();
};

IOUserClient内核编程

编写继承于IOUserClient的类后需重写如下方法,相关源文件实现请参考开源项目。registerNotificationPort和clientMemoryForType用于数据交换队列的配置,externalMethod配置对外函数调用接口,对外接口的函数原型如callYourMethod定义。

DriverClient.hpp

class DriverClient : public IOUserClient {
    OSDeclareDefaultStructors(DriverClient);
    
public:
    // Called as part of IOServiceOpen in clients.
    bool initWithTask(task_t owningTask, void *securityID, UInt32 type) override;

    // Called after initWithTask as part of IOServiceOpen.
    bool start(IOService *provider) override;

    // Called when this class is stopping.
    void stop(IOService *provider) override;

    // Called when a client manually disconnects (via IOServiceClose).
    IOReturn clientClose(void) override;

    // Called when a client dies.
    IOReturn clientDied(void) override;

    // Called during termination.
    bool didTerminate(IOService *provider, IOOptionBits options, bool *defer) override;

    // Called in clients with IOConnectSetNotificationPort. 用于数据传输
    IOReturn registerNotificationPort(mach_port_t port, UInt32 type, UInt32 refCon) override;

    // Called in clients with IOConnectMapMemory. 用于数据传输
    IOReturn clientMemoryForType(UInt32 type, IOOptionBits *options, IOMemoryDescriptor **memory) override;

    // Called in clients with IOConnectCallScalarMethod. 设置对外通信调用接口
    IOReturn externalMethod(UInt32 selector, IOExternalMethodArguments *arguments, IOExternalMethodDispatch *dispatch, OSObject *target, void *reference) override;

    // 自定义对外调用方法
    static IOReturn callYourMethod(OSObject *target, void *reference, IOExternalMethodArguments *arguments);
};

IOKit客户端编程

连接内核拓展前需先进行加载,加载调用KextManagerLoadKextWithIdentifier或KextManagerLoadKextWithURL即可。内核拓展需在plist中配置IOService及IOUserClient类名,在拓展启动后可通过类名进行查找匹配。首先需查找注册了指定类名的内核驱动,代码如下:

func startProvider() -> Bool {
        guard let service = IOServiceMatching("your service name") else {
            return false
        }
        
        Logger(.Info, "Wait for kext to be connected.")
        waitForDriver(matchingDict: service)
        return true
    }

service存放匹配成功的驱动字典,然后需要创建通信端口和处理队列进行处理连接请求。处理连接请求时所持有的IOService对象需注意释放。调用IOServiceOpen接口即可建立连接,后面的IOConnectCallScalarMethod表示调用驱动对外接口进行连接测试。函数返回前需将用于连接请求处理的端口释放。通过IOConnectCallScalarMethod或IOConnectCallStructMethod可调用驱动对外接口,其中ScalarMethod仅可传输有限数量的常量,StructMethod则可传输自定义结构体类型,相关驱动配置可参照NuwaStone项目。

func processConnectionRequest(iterator: io_iterator_t) {
        repeat {
            // 持有的对象需进行释放
            let nextService = IOIteratorNext(iterator)
            guard nextService != 0 else {
                break
            }
            
            // 建立与内核驱动的连接
            var result = IOServiceOpen(nextService, mach_task_self_, 0, &connection)
            if result != kIOReturnSuccess {
                Logger(.Error, "Failed to open kext service [\(String.init(format: "0x%x", result))].")
                IOObjectRelease(nextService)
                break
            }
            
            // 调用驱动方法测试连接
            result = IOConnectCallScalarMethod(connection, kNuwaUserClientOpen.rawValue, nil, 0, nil, nil)
            if result != kIOReturnSuccess {
                Logger(.Error, "An error occurred while opening the connection [\(result)].")
                IOObjectRelease(nextService)
                break
            }
            
            IOObjectRelease(nextService)
            IONotificationPortDestroy(notificationPort)
            isConnected = true
            Logger(.Info, "Connected with kext successfully.")
        } while true
    }
    
func waitForDriver(matchingDict: CFDictionary) {
        var iterator: io_iterator_t = 0
        let selfPointer = Unmanaged.passUnretained(self).toOpaque()
        let notificationQueue = DispatchQueue(label: "your queue name")
        
        let appearedCallback: IOServiceMatchingCallback = { refcon, iterator in
            let selfPtr = Unmanaged<YourClassName>.fromOpaque(refcon!).takeUnretainedValue()
            selfPtr.processConnectionRequest(iterator: iterator)
        }
        
        notificationPort = IONotificationPortCreate(kIOMasterPortDefault)
        IONotificationPortSetDispatchQueue(notificationPort, notificationQueue)
        IOServiceAddMatchingNotification(notificationPort, kIOMatchedNotification, matchingDict, appearedCallback, selfPointer, &iterator)
        processConnectionRequest(iterator: iterator)
    }

我们通过调用IOServiceOpen存放io_connect_t类型的对象建立了与内核驱动的连接,相应的,断开连接时需调用IOServiceClose接口。

func stopProvider() -> Bool {
        let result = IOServiceClose(connection)
        if result != KERN_SUCCESS {
            Logger(.Error, "Failed to close IOService [\(String.init(format: "0x%x", result))].")
            return false
        }
        
        connection = IO_OBJECT_NULL
        isConnected = false
        return true
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容