PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

admin 2023年7月25日16:20:36评论19 views字数 5916阅读19分43秒阅读模式




PCI初始化的主要目标


PCI设备(包括PCI桥),与主板之间的连接关系为:设备-[(PCI次层总线)-(PCI-PCI桥)]*-{(PCI主总线)-(宿主-PCI桥)}-主板(*表示0或多层)。总之,所有PCI设备,都直接或间接的连接到了主板上,并且每个PCI设备的内部,必然存在一些存储单元,服务于设备功能,包括寄存器、RAM,甚至ROM,本文将它们称为”功能存储区间”,以便与”配置寄存器组”(用于设备配置的存储区间)区分。
PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


◆为”功能存储区间”,分配地址
为了避免存储单元相互冲突,使CPU可以正常访问所有PCI设备的所有”功能存储区间”,PCI初始化阶段,必须将这些区间,映射到独立的内存或I/O地址区间*(存储单元可以用内存访问指令访问,还是用I/O指令访问,就是由映射到什么区间决定,而跟存储单元是什么介质,以及所在的硬件无关)*。


◆为PCI桥,分配地址窗口
PCI桥作为一种特殊的PCI设备,除了服务于自身的”功能存储区间”,还要为次层总线占据一份内存或I/O区间(总线上没有寄存器,占用的地址空间由所在PCI桥记录),以供次层总线上的PCI设备分配,并且用于判断一个地址,是否在次层总线内部,对来自上游或下游的访问地址进行过滤。


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


◆将可以产生中断的设备,连接到中断控制器

“PCI设备-PCI插槽-中断请求路径互连器”之间,中断请求线的连接都是由硬件设计决定,只有”中断请求路径互连器-中断控制器”之间的连接,有些情况需要软件设置,一旦所有元件之间的连接都确定了,PCI设备连接着中断控制器的哪根线,就也确定了,并且由内核负责,将其记录到PCI设备的PCI_INTERRUPT_LINE配置寄存器*(注意,这个寄存器只起记录作用,修改其值,并不会改变中断请求线的连接关系)*。


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


◆管理对象分配
分别为所有PCI总线和设备,分配pci_bus和pci_dev管理对象,记录设备信息(比如,将映射的内存和I/O区间,记录到resource成员,将连接的中断控制器请求线,记录到irq成员)。




配置寄存器组”头部


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

“配置寄存器组”,一方面提供设备的出厂信息,另一方面,用于系统软件对设备进行配置。其中,前64字节,必须按照PCI标准使用,称为”配置寄存器组”的头部,头部又分为”0型”、”1型”和”2型”,不过,不管哪种头部,前16字节的格式都是一样的(头部类型就包含在其中),只是后48字节格式不同。头部之后的192字节,由具体设备自行使用,如果不需要,没有也是可以的。


2.1. type0 header


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


◆PCI_HEADER_TYPE(见:drivers/pci/pci.c,pci_setup_device()函数)
头部类型:0,普通PCI设备(包括非桥设备,以及除”PCI-PCI”和”PCI-CardBus”之外的桥设备(比如:”宿主-PCI”桥、”PCI-ISA”桥等));1,PCI-PCI桥;2,PCI-CardBus桥(专用于笔记本)。


◆PCI_CLASS_DEVICE(见:include/linux/pci_ids.h, 1~118行)


高8位
|- 0x02:网络设备
| |- 低8位 == 0 && PCI_CLASS_PROG == 0
| |- Ethernet网卡
|- 0x07:简单通信控制器
| |- 低8位 == 0x01:并行口
| |- PCI_CLASS_PROG
| |- 0:单向
| |- 1:双向
| |- 2:符合ECP 1.0规定
|- 0x06:PCI桥
|- 低8位
|- 0x00:"宿主-PCI"桥
|- 0x01:"PCI-ISA"桥
|- 0x04:"PCI-PCI"桥
|- 0x07:"PCI-CARDBUS"桥


◆PCI_VENDOR_ID
制造厂商代号。


◆PCI_DEVICE_ID
设备代号(用于区分同一家厂商的不同产品)。


◆PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5
设备中除了配置寄存器,还包含另外一些寄存器,用于实现设备功能,为了表述方便,可以将其称为”功能寄存器”,PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5作为配置寄存器,就是用于配置前提供设备中各个”功能寄存器”区间的大小,配置后为这些区间设置合适的PCI地址。另外,直接读取PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5,得到的是区间地址加上一些标志位,先往里面写入全1再读,就是区间长度。


◆PCI_ROM_ADDRESS
设备中除了包含”功能寄存器”,还有可能包含一块ROM,PCI_ROM_ADDRESS配置寄存器,就是用于提供ROM大小,以及配置ROM的PCI地址。


2.2. type1 header


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


◆PCI_PRIMARY_BUS
PCI桥的上游总线号。


◆PCI_SECONDARY_BUS
PCI桥的下游总线号。


◆PCI_SUBORDINATE_BUS
以PCI桥下游总线为根的总线树上,最大的总线号(为总线编号时,是按”深度优先”的方式进行遍历,所以根的编号不是最大),CPU可以以此判断,要访问的PCI地址,是否落在当前PCI桥连接的总线树上,避免遍历所有总线。


2.3. IO空间”0xCF8~0xCFF”


根据PCI规范说明,可以通过”0xCF8”和”0xCFC”两个32位寄存器,读写PCI设备的”配置寄存器组”(对于内核开发,直接按照规范使用即可,不需要关心PCI规范的硬件实现,比如为什么是”0xCF8”和”0xCFC”这两个I/O端口号,以及根据其中内容寻找并读写PCI设备的具体过程)

PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

地址寄存器:用于指定配置寄存器地址;
数据寄存器:对于读操作,用于从中获取读取结果,对于写操作,用于往里填充写入内容。

由于目标地址包含总线号和设备号,所以读写前,所有PCI总线和设备,都要有自己的编号。其中,对于总线编号, PCI桥提供了PCI_PRIMARY_BUS和PCI_SECONDARY_BUS配置寄存器,由软件进行设置,而设备编号,却没有对应的配置寄存器。个人理解,这是因为,总线编号,受总线在树中的位置决定,是PCI厂商无法预测的,而设备编号,只要能够表明设备在单条总线上的局部位置即可,所以可以与PCI插槽位置等效,这在PCI总线出厂时就能确定,所以不需要软件执行枚举编号。

至此,还可以看出PCI设备中,配置寄存器与”功能存储区间”读写渠道的区别:通过”0xCF8”和”0xCFC”寄存器读写”配置寄存器”->通过”配置寄存器”,配置”功能存储区间”地址->直接根据配置地址访问”功能存储区间”。





PCI BIOS


BIOS程序烧刻在ROM(只读/不挥发内存),其中,有些厂商的主板,BIOS提供了PCI操作功能,就称为”PCI BIOS”。对于CPU来说,主板加电后,BIOS程序立即就可以执行,而内核程序,必须先由BIOS从MBR(主引导扇区),加载引导程序,再由引导程序加载到RAM(内存),才可以执行。


3.1. 加电自检阶段


“PCI BIOS”包含的很多PCI操作功能,在加电自检阶段就会被执行,本意是为内核省事情。但是,由于一些厂商的BIOS,存在这样或那样的问题,以及嵌入式主板中,甚至没有BIOS,所以Linux内核自己实现了一套独立的PCI操作接口,可以完全不依赖BIOS,不过对于已经在自检阶段完成的操作,Linux内核会尽量保持BIOS的设置,有些情况,会调用自己实现的接口,对其进行补充或修正,有些情况,甚至不再调用自己实现的接口,完全接受BIOS的处理结果。


3.2. 内核执行阶段


“PCI BIOS”还有一些PCI操作功能,作为接口提供给内核程序使用,当内核希望自己完成一些PCI操作,又不想过分关注PCI硬件spec时,就可以通过lcall指令,直接调用“PCI BIOS”提供的功能。不过,同样为了避免BIOS中的各种问题,以及没有BIOS的情况,Linux内核对这些接口,也自己实现了一份,并且,Linux内核中的PCI操作函数,分别通过CONFIG_PCI_BIOS和CONFIG_PCI_DIRECT宏,选择调用哪套接口。显然,这两个宏至少要有一个是打开的,并且,两者互不相斥,都打开时,内核通常优先使用自己的接口,或用自己的接口,对BIOS接口的操作结果进行修正。


3.3. 内核对BIOS的依赖


《Linux内核源代码情景分析》,p1025:
PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


《Linux内核源代码情景分析》,p1030:
PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

对于书上的说法,我一开始误解为:不管什么情况,即使没有BIOS,内核也能自己完成PCI操作,而是否依赖,可以通过CONFIG_PCI_BIOS宏,进行控制。


带着这个误解,分析完i386架构下的pci_init()函数后,我产生了一个困惑:没有看到设置PCI设备PCI_IO_BASE和PCI_MEMORY_BASE寄存器的代码。然而,为各个PCI设备配置内存和I/O空间,可是PCI初始化的根本目标之一,所以既然内核没有设置,那就一定依赖了BIOS的设置,这就跟我的误解冲突了。

为了重新理解书上的说法,我首先想到的是,不定义CONFIG_PCI_BIOS宏,只会影响内核代码的编译结果,关闭内核函数对BIOS接口的调用,并不能影响加电自检时BIOS执行哪些PCI操作,因为BIOS程序在主板出厂时,就已经固定了。

i386架构下,内核代码所有对BIOS接口的调用,都在pcibios_init()或它调用的函数中:


pcibios_init()
|- pci_find_bios() // #ifdef CONFIG_PCI_BIOS
| |- check_pcibios()
| | |- lcall &pci_indirect // 通过call指令,进入BIOS调用入口
| |- return &pci_bios_access
| |- pci_bios_OP_config_SIZE() // OP: read/write; SISE: byte/word/dword
| |- lcall &pci_indirect
|- pcibios_irq_init()
| |- pcibios_get_irq_routing_table() // #ifdef CONFIG_PCI_BIOS
| | |- lcall &pci_indirect
| |- pirq_find_router()
| |- pirq_router = &pirq_bios_router // #ifdef CONFIG_PCI_BIOS
| |- pcibios_set_irq_routing()
| |- lcall &pci_indirect
|- pcibios_sort() // #ifdef CONFIG_PCI_BIOS
|- pci_bios_find_device()
|- lcall &pci_indirect


另外,为防止看漏了,我对内核设置PCI_MEMORY_BASE寄存器的语句,进行了搜索:


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

结果发现,i386架构下,内核代码确实没有设置过PCI_IO_BASE和PCI_MEMORY_BASE寄存器,但在arm架构下(arch/arm/kernel/bios32.c),是由内核自己配置:


pcibios_init()
|- pci_assign_unassigned_resources()
|- pbus_assign_resources()
|- pci_setup_bridge()
|- pci_write_config_dword(bridge, PCI_IO_BASE, l)
|- pci_write_config_dword(bridge, PCI_MEMORY_BASE, l)


所以,结合以上两点可知,判断pcibios_init()是否依赖BIOS,还要看它是否依赖BIOS在加电自检阶段的操作结果,并且i386架构下的pcibios_init()函数,确实是依赖的。


那就是说,虽然Linux内核独立实现了一套PCI操作接口,使得Linux内核可以完全不依赖BIOS,实现任何PCI操作,不过,这只是为最终的PCI操作是否完全不依赖BIOS,提供了选择,比如arm CPU常用于嵌入式系统,通常根本没有BIOS,所以arm架构下实现PCI操作,必须选择完全使用内核接口,而对于i386 CPU,可以确定加电自检阶段,有些操作必然已由BIOS完成,并且完成的没问题时,就可以选择仍然”依赖”BIOS(可以依赖时,不依赖白不依赖)。


搞清楚这一点很重要,要不然分析代码时,就可能会期待在pci_init()函数中,看到根据所有PCI设备汇总信息,分配PCI地址的过程,却看不到,有些配置也会让人觉得”来历不明”,还没见到设置,内核就认为已经设置了。





pci_init()函数分析


// 声明:以下分析的代码,是针对i386 CPU的实现!!
pci_init()
|
|- pcibios_init() // arch/i386/kernel/pci-pc.c !!
| | // 早期,该函数完全通过调用BIOS接口实现,所以函数名中包含了"bios"
| | // 后来,该函数演变为,可以通过CONFIG_PCI_DIRECT和CONFIG_PCI_BIOS宏,选择调用BIOS接口,还是调用内核自己实现的接口
| | // 而且两者不互斥,如果2个宏都打开,内核通常优先使用自己的接口,或用自己的接口,对BIOS接口的操作结果进行修正
| |
| | // 获取BIOS提供的PCI操作接口 (#ifdef CONFIG_PCI_BIOS)
| |- pci_root_ops = pci_find_bios()
| | |
| | | // 0xe0000~0xffff0为BIOS占用的物理内存
| | |- for (check = (union bios32 *) __va(0xe0000); check <= (union bios32 *) __va(0xffff0); ++check)
| | |
| | | // 每个bios32对象,都包含16字节头部,其中包含signature和checksum字段


由于代码较长,请用PC阅读源文查看。




PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


看雪ID:jmpcall

https://bbs.kanxue.com/user-home-815036.htm

*本文为看雪论坛优秀文章,由 jmpcall 原创,转载请注明来自看雪社区

PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)


PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

球分享

PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

球点赞

PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

球在看

原文始发于微信公众号(看雪学苑):PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年7月25日16:20:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)http://cn-sec.com/archives/1904018.html

发表评论

匿名网友 填写信息