快捷搜索:  汽车  科技

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)ioremap( pci_resource_start(pdev BAR_1) pci_resource_len(pdev BAR_1) ); pci_resource_start只是一个宏定义:调用pci_resource_start在硬件加电初始化时,BIOS统一检查所有的pci设备,并为每个设备分配一个物理地址,该地址通过BIOS获得并写到设备的配置空间内,驱动程序就可以将网卡的普通控制寄存器映射到一段内存空间内,CPU通过访问映射后的虚拟地址来操控网卡的寄存器。当操作系统初始化时,其为每个PCI设备分配一个pci_dev结构,并将前面分配的物理地址写到PCI_dev的resource字段中。在网卡驱动程序中则可以通过读取pci_dev中的resource字段获得网卡的寄存器配置空间地址,其由函数pci_resource_start()和pci_resource_end()获

为什么要在自制操作系统上写网卡驱动?请看这里:

如何在自制操作系统写网卡驱动程序(1)怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(1)

配置信息

参考:https://blog.csdn.net/qq_31799983/article/details/106976145?spm=1001.2101.3001.6650.8&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-8-106976145-blog-80163665.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-8-106976145-blog-80163665.pc_relevant_default&utm_relevant_index=11

通过读这个配置信息的class_code字段,我们就知道PCI连接的设备是网卡?还是显卡?还是硬盘?还是声卡?

这个配置信息的Device ID,Vendor ID 表明PCI连接的设备的型号和制造厂商.

这个配置信息的Base Address 0 里存储了PCI连接的设备的地址映射内存中的地址。

总之,只要读到这个配置信息,我们就可以找到网卡,找到网卡,才能控制网卡向外发送信息,接收信息等。

那么具体如何读取这个配置信息呢?通过I/O口0xCF8和0xCFC 通过这两个端口,就可以读取到配置信息了。如下两行代码:

io_out32(0xCF8 addr);// 把配置信息的地址addr输出到I/O端口0xCF8 indata = io_in32(0xCFC); //从I/O端口0xCFC获取到配置信息。

这里面的addr是什么?是配置信息所在的地址,可以这样生成:

unsigned int bus_max=0xff; unsigned int dev_max=0x1f; unsigned int func_max=0x07; // 遍历配置信息 for(bus=0;bus<=bus_max; bus) { for(dev=0;dev<=dev_max; dev) { for(func=0;func<=func_max; func) { // 生成配置信息的地址 unsigned int addr = 0x80000000L | (bus<<16) | (dev<<11) | (func<<8) | (0<<2); io_out32(0xCF8 addr); indata = io_in32(0xCFC); } } }

这里生成配置信息的地址:0x80000000 | (bus<16) | (dev<<11) | (func<<8) | (0<<2);

这是什么意思呢?

首先这是一个8*4=32bits的数,4个bytes 这4个bytes的意义:

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(2)

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(3)

最高位31位要设置成1,才能表示对PCI的配置信息操作。如果此位为0,表示要PCI所连接的设备的操作。

30--24是保留位,我们可以写入我们特定信息。

23--16位是总线号,15--11位是设备号,10--8位是功能号,7--2位是配置信息中某条信息的偏移地址。

所以,

0x80000000 | (bus<16) | (dev<<11) | (func<<8) | (offset<<2);

的意思是对配置信息中的总线号为bus 设备号为dev 功能号为func所指定的配置信息中的第offset个4字节的信息进行操作。

第0个4字节的信息,其实就是配置信息的第一行,Device ID 和Vendor ID.

那么第三行就是class code 第4行就是header type 了。

所以,我们要访问所有的配置信息,只用改变offset的值,使其为分别为0--15,就可以分别读取到所有的配置信息了。

那么地址中的bus dev func是什么意思呢?

跟CPU连接的PCI有很多,用bus dev func就把不同的PCI区分开了。那么与当前CPU连接的PCI的bus dev func分别是多少呢?

也就是说,bus dev func到底填多少合适?到底网卡连接的pci对应的bus dev func应该是多少呢?

可以写for循环去搜索,当bus dev func取到合适的值时,我们读取到的配置信息的class_code应该是02。

因为class_code是02时,表示PCI连接的设备是网卡。

所以,我们在以上代码中写了for循环,去遍历所有的bus dev func的值,看看哪些位置有pci 并且这个pci连接的是网卡。

那么如何知道哪些bus dev func的值对应的有pci? 只用看其对应的配置信息的第1行,如果第1行的DeviceID 和 Vendor ID不是0xFFFF 就说明有配置信息。

有配置信息,说明PCI连接的有设备,但不一定是网卡,还可能是显卡,硬盘等。

所以,我们还要进一步看其class_code的值,如果是02 就表示是网卡。

另外,遍历总线号bus时,由于bus的数字是存储于adder的23--16位的,一共8个bit位,所以,其取值范围为0--255,

遍历设备号dev时,由于其存储于addr的15--11位,共5位,所以,其取值范围为0--31.

遍历功能号func时,由于其存储于addr的10-8位,共3位,所以,其取值范围为0-7.

那么最终,我们的代码为:

void check_pci(unsigned char *buf_back struct BOOTINFO *binfo) { char s[200]; unsigned int indata; int bus dev func; unsigned int bus_max=0xff; unsigned int dev_max=0x1f; unsigned int func_max=0x07; // 设置打印信息的显示位置 int start_row=250; int start_col=50; int row_inc=0; int i; sprintf(s "buf dev func vender_id device_id header_type class_code "); putfonts8_asc(buf_back binfo->scrnx start_col start_row-1*16 COL8_FFFFFF s); for(bus=0;bus<=bus_max; bus) { for(dev=0;dev<=dev_max; dev) { for(func=0;func<=func_max; func) { unsigned int addr = 0x80000000L | (bus<<16) | (dev<<11) | (func<<8) | (0<<2); io_out32(0xCF8 addr); indata = io_in32(0xCFC); // 查看当前bus dev func处有无pci if( ((indata & 0xffff) != 0xffff) && (indata !=0)) { //如果有,获取当前pci的第4行的header type unsigned int addr1 = addr | (3<<2); io_out32(0xCF8 addr1); unsigned int header_type = (io_in32(0xCFC)&0x00ff0000)>>16; //获取当前pci的第3行的class code 这个可以看到设备是否是网卡 unsigned int addr2 = addr | (2<<2); io_out32(0xCF8 addr2); unsigned int class_code = (io_in32(0xCFC)&0xff000000)>>24; // 显示pci的device id vendor id header type class code sprintf(s "d d d 0xx 0xx 0xx 0xx " bus dev func indata&0xffff (indata&0xffff0000)>>16 header_type class_code); putfonts8_asc(buf_back binfo->scrnx start_col start_row row_inc*16 COL8_FFFFFF s); row_inc ; } } } } return; }

把函数check_pci添加到bootpack.c的for循环之前:

check_pci(buf_back binfo);//打印pci信息 // 图层刷新 sheet_refresh(sht_back 0 0 sht_back->bxsize sht_back->bysize);

显示效果如下

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(4)

可以看到,在bus=0 dev=0 func=0时,对应的pci所连接的设备的vender_id是0x8086 表示是Inter的,device_id是1237 header_type是0,class_code是0x06 表明不是网卡,0x06具体表示什么?看下表:

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(5)

表明它是桥设备。

根据这个表,咱们打印出的第3行是0x01 表示海量存储器

根据这个表,咱们打印出的第4行是0x03 表示网络控制器,即显卡

根据这个表,咱们打印出的第5行是0x02 表示显示控制器,即网卡。

那么到这里,我们在bus号为0 dev号为3,func号为0的位置,找到了一张网卡。它的厂商是0x10ech 设备号是8029h

好了,到这里,今天我们通过I/O端口,读取到了网卡对应的PCI.

下一步就可以通过网卡对应的PCI来控制网卡收发信息。

附录:

在30天操作系统上写网卡驱动需要从头开始,是比较琐碎的事,为此,我搜索了一定的源码和资料。

源码就是linux系统的各版本内核的源码,它带有很多网卡的驱动。

资料就是对pci,总线,以及内存映射,网络通信协议等资料。

在后续的驱动编写中,可能要反复的来复习这些资料。

比如:http://www.lab-z.com/2pciaccess/

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(6)

这里面说了两种方法访问配置信息,第一种就是通过端口的0xCF8 0xCFC;第二种是通过内存映射。

比如直接访问如下的内存区域即可得到:

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(7)

我还没有试。留着以后参考中。

还有一个细节,汇编对端口的读写,要特别注意。

比如我这里一开始端口的读写程序有bug 所以读取信息一直是错误的。

最后把 端口的读写程序从

_io_out32: ; void io_out32(int port int data); MOV EDX [ESP 4] ; port MOV EAX [ESP 8] ; data OUT EDX EAX RET

改为

_io_out32: ; void io_out32(int port int data); MOV DX [ESP 4] ; port MOV EAX [ESP 8] ; data OUT DX EAX RET

后,就正常了,

因为端口的地址0xCF8,不需要EDX 用EDX反而不能表示端口了,就无法给实现往端口上输出信息。

关于端口读写,还有用嵌入汇编的方式,比如:http://blog.chinaunix.net/uid-186409-id-2822610.html

另外,使用端口对PCI配置信息的读取有个例子不错,可以参考:

https://cloud.tencent.com/developer/article/1199972

这里写了一个window上的代码和一个linux上的代码。

比如我用linux上的代码后,输入的结果如下:

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(8)

运行的时候,需要用sudo。因为这里设计到IO读取,所以需要root权限。

另外运行这个例子的时候,我就有个疑问:问什么这个例子明明是个应用程序,不是操作系统本身的代码,它却可以访问IO端口?

一般应用程序的代码,由于GDT的定义,应该是不能操作I/O端口的。

后来的代码里发现了

if ( iopl(3) < 0 ) { printf("iopl set error\n"); return -1; }

iopl函数用于获取io端口的访问权限,如果这个函数获取返回值大于0,就可以通过操作系统所提供的中断函数来对io端口进行访问了。

总之:应用程序要访问操作系统所占用的哪些资源,都得通过中断函数来访问。通过中断函数,我们就可以设计权限,加以控制,保证操作系统代码本身的安全性。

以下是一些样例:

在30天操作系统的代码上,添加读pci配置的程序check_pci的过程并不顺利,一开始写到屏幕上的信息无法刷新出来,就通过鼠标移动过去,把这些信息“擦”出来。当然后来发现使用刷新函数的时候,刷新错对象向,应该刷新sht_back 就过刷新sht_win了,sht_win表示的是tast_a窗口,如下图。

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(9)

第一次成功

上图中显示了很多0xffff 这是因为程序什么也没有输出,所以我们调试的时候,就把所有的信息都输出的屏幕上了;我把没有cpi时的信息显示到左边,有cpi时的信息显示到了右边。

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(10)

更改刷新函数后,可以正常刷新了

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(11)

用MAC上的qemu打开了30天自制的操作系统,发现多了一个设备0x100e的设备

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(12)

增加打印PCI所连接设备的header type和class_code信息

在MAC上

怎么重新收集网卡驱动和网络协议(在自制操作系统上写网卡驱动)(13)

增加打印,在mac上的qemu上运行30天操作系统代码

可以看到,这里一共6个设备,比在windows上的qemu多了一个设备,多了一些PCI桥设备。网卡的型号变成100e了。

既然找到网卡了,下一步就是启动网卡,读取网卡的mac地址,命令网卡发送数据等了。

具体怎么实现呢?

1比如重启网卡设备,就是给网卡的某寄存器写入命令,用I/O方法,或者内存映射的方法。具体详细信息可以查看网卡信息的datasheet。

重启网卡设备,则是通过向映射后的网卡的相应寄存器写入命令,其通过映射后的首地址及相应的寄存器偏移量找到该寄存器的位置,然后通过函数writeb()写该寄存器。有关相关寄存器对应的偏移量,一般是通过网卡的相关的datasheet获得。

2.如果要获取网卡的MAC地址,则一般通过函数readb()读取首地址开始的前六位。

后面我们就获取到网卡的MAC地址,然后给网卡的寄存器里写值,来命令网卡收发信息。或者把网卡动作与操作系统的中断绑定起来。

猜您喜欢: