一 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地址的過程,卻看不到,有些配置也會讓人覺得”來歷不明”,還沒見到設置,內核就認為已經設置了。
奇安信集團
RacentYY
007bug
尚思卓越
ManageEngine卓豪
關鍵基礎設施安全應急響應中心
尚思卓越
007bug
虹科網絡安全