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

VSole2023-07-25 09:49:04
一
PCI初始化的主要目标

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

◆为”功能存储区间”,分配地址

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

◆为PCI桥,分配地址窗口

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

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

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

◆管理对象分配

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

二
“配置寄存器组”头部

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

2.1. type0 header

◆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_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总线和设备,都要有自己的编号。其中,对于总线编号, 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:

《Linux内核源代码情景分析》,p1030:

对于书上的说法,我一开始误解为:不管什么情况,即使没有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寄存器的语句,进行了搜索:

结果发现,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地址的过程,却看不到,有些配置也会让人觉得”来历不明”,还没见到设置,内核就认为已经设置了。

pcibios
本作品采用《CC 协议》,转载必须注明作者和本文链接
PCI设备,与主板之间的连接关系为:设备-[-]*-{-}-主板。总之,所有PCI设备,都直接或间接的连接到了主板上,并且每个PCI设备的内部,必然存在一些存储单元,服务于设备功能,包括寄存器、RAM,甚至ROM,本文将它们称为”功能存储区间”,以便与”配置寄存器组”区分。◆管理对象分配分别为所有PCI总线和设备,分配pci_bus和pci_dev管理对象,记录设备信息。
QEMU逃逸系列
2022-12-01 09:19:27
qemu用于模拟设备运行,而qemu逃逸漏洞多发于模拟pci设备中,漏洞形成一般是修改qemu-system代码,所以漏洞存在于qemu-system文件内。而逃逸就是指利用漏洞从qemu-system模拟的这个小系统逃到主机内,从而在linux主机内达到命令执行的目的。
显示机器的处理器架构。/proc/cpuinfo 显示CPU info的信息。/proc/swaps 显示哪些swap被使用。/proc/net/dev 显示网络适配器及统计。/proc/mounts 显示已加载的文件系统。显示2007年的日历表。设置日期和时间 - 月日时分年.秒。将时间修改保存到 BIOS. 取消按预定时间关闭系统。的目录并同时删除其内容。f -atime +100 搜索在过去100天内未被使用过的执行文件。结尾的文件并定义其权限。halt 显示一个二进制文件或可执行文件的完整路径。file.iso /mnt/cdrom 挂载一个文件或ISO镜像文件
可能涉及的部件市电环境;电源、主板、CPU、内存、显示卡、其它可能的板卡;BIOS中的设置;开关及开关线、复位按钮及复位线本身的故障。启动与关闭类故障定义举例与启动、关闭过程有关的故障。显示的内容的观察:要注意屏幕报错的内容、死机的位置,以确定故障可能发生的部位。以下检查应在软件最小系统下进行。
渗透工程师常用命令速查手册
可能涉及的部件市电环境;电源、主板、CPU、内存、显示卡、其它可能的板卡;BIOS中的设置;开关及开关线、复位按钮及复位线本身的故障。2) 万用表; 3) 试电笔; 4) CPU负载。启动与关闭类故障定义举例与启动、关闭过程有关的故障。4) 显示的内容的观察:要注意屏幕报错的内容、死机的位置,以确定故障可能发生的部位。以下检查应在软件最小系统下进行。
arch #显示机器的处理器架构(1) uname -m #显示机器的处理器架构(2) uname -r #显示正在使用的内核版本 dmidecode -q #显示硬件系统部件 - (SMBIOS / DMI) hdparm -i /dev/hda #罗列一个磁盘的架构特性 hdparm -tT /dev/sda #在磁盘上执行测试性读取操作 cat /p
VSole
网络安全专家