查看: 1852|回复: 12

[原创内容] 【澎湖冰洲的家】IOKit驱动详解(6)

[复制链接]
penghubingzhou 发表于 2020-12-19 11:50 | 显示全部楼层 |阅读模式
快御云安全
本帖最后由 penghubingzhou 于 2020-12-19 15:00 编辑

各位好,我是澎湖冰洲。上一节我们讲解了在IOKit里面调用内存分配的几种主要方式,包括了IOMalloc这种常见的堆内存分配方式,不知道各位有没有学会呢?今天我们给各位讲解的是IOKit里面一个特别的类——抽象类。这种类是IOKit里面一个十分特别的类。注意:今天的内容与IOKit的关联度不是十分大,主要是补充涉及OOP语言的类知识,提前剧透下~

抽象类,顾名思义,就是一个抽象的类。那么啥叫抽象的类呢?如果你学过Java,你的书本可能会这样告诉你:在OOP语言里,所有的对象都是通过类来描述的,但反过来,不是所有类都会用来描述对象,如果一个类没有包括足够的信息来描述一个具体的对象,这种类我们称之为抽象类。有的同学就会说了,冰洲,你这个定义我觉得也很“抽象”啊?不要紧,我给各位举个例子。假设现在有这么一个类叫“狗”,还有好几个类叫做“金毛”、“狼狗”、“腊肠”、“吉娃娃”……这些类里,“狗”这个类没有指明具体的某种狗,可以是“金毛”,也可能是“吉娃娃”或者“腊肠”,所以“狗”这个类,我们就可以把它叫做抽象类,因为它指代了一类具有相同属性对象的抽象特征(都是“狗”,有“狗”的一些特性比如温顺听主人话、四条腿、吃肉等等);而相应地,“腊肠”、“吉娃娃”等类,则是对“狗”这个抽象类的具体化,指明了具体是哪种东西,我们称这样的类为“实例类”。

从刚才这个例子里,我们其实不难发现一些抽象类的特性:
1、抽象类不是某个具体的类,而是一类具有相同属性的对象的抽象化描述(“吉娃娃”、“腊肠”等都是“狗”)
2、抽象类不能直接用来使用,必须通过子类继承并“实例化”为实例类后才能使用(单说“狗”这个类没有任何意义,只有具体指代到某个具体种类的狗如“狼狗”、“吉娃娃”等才有具体的意义)
3、抽象类一定会有一类共同的抽象特征,我们称之为“抽象函数”(“狗”都会听主人话,那它们具体是怎么听主人的话呢)
penghubingzhou  楼主| 发表于 2020-12-19 11:50 | 显示全部楼层
本帖最后由 penghubingzhou 于 2020-12-19 14:10 编辑

说完了抽象类的定义,我们需要再回到C++这。作为一个支持OOP特性的语言,C++当然也是支持抽象类声明的。涉及C++如何声明抽象类不是我们这里探讨的内容了,有兴趣的同学可以自行百度。这里我们要说的是IOKit如何声明抽象类。


在IOKit里,任何OSObject类的子类,都可以定义为一个抽象类。还记得前面我们第四节讲过的OSDeclareAbstractStructors以及OSDefineMetaClassAndAbstractStructors这两个宏吧?当你想声明一个抽象类的时候,只需要用它来替代原有的OSDeclareDefaultStructors以及OSDefineMetaClassAndStructors即可。这样我们声明的一个IOKit驱动类就变成了基于IOKit构造的抽象类了。当然了,到这我们的抽象类还没有构造完成,因为根据前面所述,我们还没有给这个抽象类构造一些“抽象函数”,那么这些抽象函数我们该怎么做呢?这里就需要引入C++里面两个重要的概念:虚函数以及纯虚函数。


按照图中的顺序,在一个驱动的生命周期里,一个OSMetaClass的子类会执行这么多的函数。我们不难发现,除了probe()函数以外,它们都是成对出现的,声明了一个,就必然会声明另一个。其中,我们必须会写的成对函数是init()与free(),以及start()与stop()。而probe()函数虽然不成对出现,但也是必要的函数。所以,我们需要在我们这个驱动里,声明并实现相应的函数。


  1. ...
  2. virtual bool init(OSDictionary* dict) override;

  3. virtual void free(void) override;

  4. virtual IOService* probe(IOService* provider, SInt32* score) override;

  5. virtual bool start(IOService* provider) override;

  6. virtual void stop(IOService* provider) override;
复制代码



不知道你有没有注意到,在这我写的几个函数前面,全部标注了virtual字样,这个关键字在C++的含义就是标注这个函数为虚函数。啥是虚函数呢?简单来说,虚函数是C++为了实现类的多态性而引入的一个机制。我们都知道,当我们想写一个驱动的时候,这个驱动一定得是IOService类的子类(包括直接子类和间接子类),但是,如果没有虚函数这个东西的存在,也就是我们没有virtual这个关键字,当我们企图继承IOService的start函数并书写我们需要的函数的时候,你会发现,它总是访问IOService的start函数而非我们自己所写的驱动的start函数,这个就不是我们所期望的了。因此,虚函数的存在,实际上为我们访问子类的重写函数提供了方便,它让我们访问的函数总是我们所重写的那个函数。实际上,在IOService里面,涉及start、init这些驱动可能重写的函数,都被标记为virtual了,以保证我们可以重写并正确执行这些函数。


那么什么又是纯虚函数呢?很简单,就是在标注virtual的虚函数后面再加上个=0。 比如我们有这样一个抽象类的函数的头文件:

  1. #include <libkern/c++/OSObject.h>
复制代码


可以看到,在这个抽象类里,我们声明了两个函数aaa、bbb,它们都是虚函数,且函数aaa前面有virtual关键字,后面又跟了 = 0,表示这是一个纯虚函数。这样,我们构造的类a就成为了一个C++的抽象类。前面我们说过,抽象类必须要实例化才能用。那么假设我们现在再写一个新类企图这样实例化a,你看行么?


  1. #include <libkern/c++/OSObject.h>
复制代码


如果你真的这样写了,我相信你一定会在编译时收到这样一条报错信息:
  1. Allocating an object of abstract class type 'b'。
复制代码

它的含义是:企图分配一个抽象类类型“b“。奇怪啊,我们不是已经声明了b这个类为实例化类了么(OSDeclareDefaultStructors),就算我们没给这个类写任何函数,它也不应该返回一个这样的企图分配抽象类的错误啊?实际上,造成这个错误的根本原因在于,我们虽然将b声明为实例化类了,但却根本没有实现父类的”抽象函数“。而在C++里面,所谓的”抽象函数“,就是我们刚刚所述的纯虚函数,它代表了这个函数目前没有函数体,没有所谓的函数实现。如果这类”抽象函数“没有在子类里面实现,那么我们声明的子类将还是一个抽象类而非实例类,这就造成了我们刚才的所谓”分配抽象类“的错误。


现在让我们修改下这个头文件:

  1. #include <libkern/c++/OSObject.h>
  2. class b : public a{      
  3.      OSDeclareDefaultStructors(b)
  4.      virtual void aaa(uint32_t you, uint32_t me) ;
  5. };
复制代码

然后在b类的cpp文件里面添加如下的函数代码实现:
  1. #include "b.hpp"
  2. OSDefineMetaClassAndStructors(b, a)
  3. void b:aaa(uint32_t you, uint32_t me){    uint32_t s;    s = me + you;}

复制代码


让我们再编译一下,现在即便是我们没有给bbb这个虚函数在子类里面定义,这个也可以编译过去了。


从这个例子里我们可以总结一下:
1、虚函数是提供子类重写函数访问途径的一类函数,负责实现类的多态;而纯虚函数是特殊的虚函数,负责给抽象类提供”抽象函数“
2、一个子类若想实例化抽象类,除了自己要用OSDeclareDefaultStructors和OSDefineMetaClassAndStructors声明为实例化类以外,还必须实现父类抽象类的所有”抽象函数“,也就是纯虚函数
3、没有实现父类所有纯虚函数的子类,依旧只能是抽象类,不能作为实例类来分配
回复

使用道具 举报

penghubingzhou  楼主| 发表于 2020-12-19 11:50 | 显示全部楼层
本帖最后由 penghubingzhou 于 2020-12-19 14:54 编辑

说了这么多,也没让大家体会到抽象类具体在IOKit里面有什么用,接下来让我们通过一个具体的实例来了解下抽象类的具体作用。

欢迎来到IOEthernetController的世界!这个东西是由苹果自己书写的一个提供者驱动类,负责实现有关以太网卡的一些具体功能(它的头文件可以在你的Xcode的SDK里查看,具体位置为IOKit/network/IOEthernetController.h)。比如我的是13的SDK,这个文件就是这样定义的:


  1. class IOEthernetController : public IONetworkController
  2. {
  3.     OSDeclareAbstractStructors( IOEthernetController )


  4. protected:
  5.         struct IOECTSCallbackEntry;        .....
复制代码

可以看到,IOEthernetController这个类继承自IONetworkController这个类,属于IOService的间接子类。这个类自身被声明为了一个抽象类,这是因为不同的以太网卡虽然都是以太网卡,但根据接口的不同,又可以分为PCI以太网卡、USB以太网卡、蓝牙以太网卡等等,所以IOEthernetController被定义为了抽象类。在这个抽象类里,定义了很多涉及以太网卡的函数,比如getPacketFilters、getMaxPacketSize等等等等(有关这些函数的具体含义,请参阅头文件)。这其中,有一个函数是值得我们注意的:

  1. virtual IOReturn getHardwareAddress(IOEthernetAddress * addrP) = 0;
复制代码

这个函数是IOEthernetController里唯一一个纯虚函数,作用是获取以太网卡的永久站点地址。也就是说,如果你想实例化一个IOEthernetController子类,别的函数你可以不实现不重写,但这个函数你是必须实现的。来看几个典型的例子,第一个是Laura Müller(mieze)大神写的RTL1111以太网卡驱动相关实现:



  1. ......
  2. OSDefineMetaClassAndStructors(RTL8111, super)
  3. ......
  4. /* Methods inherited from IOEthernetController. */
  5. IOReturn RTL8111::getHardwareAddress(IOEthernetAddress *addr){   
  6.      IOReturn result = kIOReturnError;        
  7.      DebugLog("getHardwareAddress() ===>\n");        
  8.      if (addr) {        
  9.          bcopy(&currMacAddr.bytes, addr->bytes, kIOEthernetAddressSize);        
  10.          result = kIOReturnSuccess;   
  11.      }        
  12.     DebugLog("getHardwareAddress() <===\n");
  13.     return result;
  14. }
  15. ......
复制代码

不难发现,这个以太网卡驱动的主类RTL8111类也是一个实例化的IOEthernetController子类,重写的纯虚函数通过bcopy currMacAdddr里面的数据实现地址的获取。

第二个例子我们来看下Rehabman写的NullEthernet虚拟以太网卡驱动:





  1. ......
  2. OSDefineMetaClassAndStructors(org_rehabman_NullEthernet, IOEthernetController)
  3. ......
  4. /* Methods inherited from IOEthernetController. */
  5. IOReturn NullEthernet::getHardwareAddress(IOEthernetAddress *addr)
  6. {
  7.     IOReturn result = kIOReturnError;
  8.    
  9.     DebugLog("getHardwareAddress() ===>\n");
  10.    
  11.     if (addr) {
  12.         bcopy(m_rgMacAddr, addr->bytes, kIOEthernetAddressSize);
  13.         result = kIOReturnSuccess;
  14.     }
  15.    
  16.     DebugLog("getHardwareAddress() <===\n");

  17.     return result;
  18. }
  19. ......
复制代码

与mieze的RTL8111类似,这个驱动也是通过bcopy特定的字节段实现的地址获取。

最后,再来看下我写的GalioEthernet9601转接卡驱动:



  1. ......
  2. OSDefineMetaClassAndStructors(DM9601V2, super)
  3. ......
  4. IOReturn DM9601V2::getHardwareAddress(IOEthernetAddress *ea){   
  5.      uint32_t i;
  6.         
  7.      IOLog("%s::getHardwareAddress!\n", getName());        
  8.      for (i=0; i<6; i++){        
  9.           ea->bytes[i] = fEaddr[i];   
  10.      }        

  11.     return kIOReturnSuccess;
  12. }
  13. ......
复制代码


这里,我是通过读取前面填充的fEaddr数组,来读写到ea指针的成员byte缓冲区数组里,实现地址的读取。


从以上的例子里我们不难发现,虽然接口不同(RTL8111是PCI,NullEthernet是虚拟设备,GalioEthernet9601是USB),实现的某些具体函数也不同(getHardwareAddress),但通过共同继承并实例化IOEthernetController这个抽象类,不同的以太网卡都实现了自己的网卡功能,代码书写量少,并且从代码易读性上来说也非常的易读,也很好地体现了C++这种OOP语言继承性和多态性的优点。这样我们就可以通过一个IOEthernetController抽象类,实现了一大类以太网卡的功能。

今天关于IOKit的抽象类的问题就探讨到这了,欢迎各位的批评指正。我们下一节见~
回复

使用道具 举报

Bat.bat 发表于 2020-12-19 15:14 | 显示全部楼层
支持,辛苦
回复

使用道具 举报

paihuaizhe2018 发表于 2020-12-19 15:14 | 显示全部楼层
谢谢楼主,辛苦了
回复

使用道具 举报

paihuaizhe2018 发表于 2020-12-19 15:15 | 显示全部楼层
谢谢楼主,辛苦了
回复

使用道具 举报

123456fac 发表于 2020-12-19 21:52 | 显示全部楼层
这个黑不了,这个也不能黑,这个是高手 . . .  
回复

使用道具 举报

penghubingzhou  楼主| 发表于 2020-12-19 21:57 | 显示全部楼层
123456fac 发表于 2020-12-19 21:52
这个黑不了,这个也不能黑,这个是高手 . . .

蛤?
回复

使用道具 举报

yingkyojing 发表于 2020-12-19 22:01 | 显示全部楼层
完全看不懂 顶一下
回复

使用道具 举报

13617315081 发表于 2020-12-19 22:24 | 显示全部楼层
喔艹,牛逼
我只认识它们是26个字母中的字母,,,,,,其它一概不知。
回复

使用道具 举报

zenbarski 发表于 2020-12-19 22:33 | 显示全部楼层
penghubingzhou 发表于 2020-12-19 11:50
说了这么多,也没让大家体会到抽象类具体在IOKit里面有什么用,接下来让我们通过一个具体的实例来了解下抽象 ...

支持,辛苦
回复

使用道具 举报

龙卷风05 发表于 2020-12-19 22:45 | 显示全部楼层
感谢楼主,顶一下!
回复

使用道具 举报

头像被屏蔽
mendax1234 发表于 2021-1-31 16:24 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋手机版联系我们

Copyright © 2005-2025 PCBeta. All rights reserved.

Powered by Discuz!  CDN加速及安全服务由「快御」提供

请勿发布违反中华人民共和国法律法规的言论,会员观点不代表远景论坛官方立场。

远景在线 ( 苏ICP备17027154号 )|远景论坛 |Win11论坛 |Win10论坛 |Win8论坛 |Win7论坛 |WP论坛 |Office论坛

GMT+8, 2025-2-15 11:11

快速回复 返回顶部 返回列表