ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

UEFI固件PEI阶段核心机制:PEIM、PPI与HOB深度解析

UEFI固件PEI阶段核心机制:PEIM、PPI与HOB深度解析

1. 项目概述:深入理解PEI阶段的基石

如果你在UEFI固件开发或者系统底层启动流程的调试中摸爬滚打过,那么对“PEI阶段”这个词一定不会陌生。它就像是系统上电后,CPU从沉睡中苏醒,开始执行的第一段“热身操”。这个阶段环境极其简陋,内存控制器可能还没初始化,我们能依赖的只有CPU缓存和一小块SRAM(在Intel平台上称为Cache as RAM,简称CAR)。而“PEI阶段扩展——PEIM PPI HOB”这个主题,正是要拆解在这个特殊时期,各个功能模块(PEIM)如何被发现、如何相互协作,以及如何为后续阶段(DXE)准备好“行李”(HOB)的核心机制。理解它,就等于拿到了打开固件初始化黑盒的第一把钥匙。

简单来说,PEI(Pre-EFI Initialization)阶段是UEFI启动流程中紧随SEC(安全验证)之后的阶段。它的核心任务是在资源受限的环境下,完成最基础的硬件初始化,并建立起一个简单的服务框架,为后续更复杂的DXE(Driver Execution Environment)阶段铺平道路。而实现这一目标,主要依靠三个核心概念:PEIM、PPI和HOB。PEIM是执行具体任务的模块,PPI是它们之间沟通的“协议”或“接口”,HOB则是它们留给后续阶段的“数据包裹”。本次,我们就从一个资深固件开发者的视角,彻底搞懂这三者是如何联动,构建起整个PEI阶段生态的。

2. PEI阶段核心架构与设计思路拆解

2.1 PEI阶段的任务与挑战

为什么需要一个PEI阶段?直接让所有驱动在内存可用后运行不行吗?答案在于复杂性和可靠性。现代计算机硬件平台纷繁复杂,CPU、芯片组、内存、各种控制器之间存在严格的依赖关系。比如,你必须先初始化内存控制器,才能使用大容量内存;必须先配置好PCI Express的根复合体,才能枚举到挂载的设备。这些初始化操作必须按严格的顺序进行,并且在资源(尤其是内存)极度匮乏的环境下完成。

PEI阶段的设计哲学就是“分而治之”和“按需服务”。它将庞大的初始化任务分解成一个个独立的、功能单一的PEIM(PEI Module)。每个PEIM只负责一件明确的事,例如“初始化CPU”、“初始化内存控制器”、“报告系统内存布局”。这种模块化设计带来了极大的灵活性,OEM厂商可以根据自己的硬件平台,选择和组合不同的PEIM,构建出定制化的初始化流程。

然而,模块化带来了新的问题:这些模块如何知道彼此的存在?如何调用对方提供的服务?如何传递数据?这就是PPI和HOB机制要解决的核心问题。整个PEI阶段的架构可以看作是一个微型的、面向服务的架构(SOA)在受限环境下的实现。

2.2 PEIM:执行具体任务的积木

PEIM本质上就是一个符合PE/COFF格式的可执行映像,它在编译时被链接到特定的基地址,以便在CAR中运行。一个PEIM通常会导出两个关键的函数:

  1. 入口函数:这是PEIM的执行起点,负责该模块的核心初始化工作。
  2. PPI描述符:一个数据结构,用来声明“本模块实现了哪些PPI服务”以及“本模块依赖哪些其他PPI服务”。

PEI Foundation(PEI基础服务)是PEI阶段的“操作系统内核”,它负责调度PEIM的执行。其调度策略的核心是“依赖解析”。PEI Foundation会维护一个PPI数据库。当一个PEIM被安装(通常是固化在Flash中或由前一个PEIM加载)时,PEI Foundation会检查它的PPI描述符:

  • 如果该PEIM所依赖的所有PPI服务都已经在数据库中存在,那么PEI Foundation会立即调用它的入口函数执行它。
  • 如果它所依赖的PPI服务还不存在,那么这个PEIM会被放入一个等待队列,直到它所依赖的PPI被其他PEIM安装到数据库后,它才会被唤醒执行。

这种机制确保了初始化的顺序性。例如,“内存初始化PEIM”会安装一个“内存服务PPI”。后续所有需要访问内存的PEIM(如“PCI枚举PEIM”)都会声明依赖这个“内存服务PPI”,从而保证它们一定在内存可用之后才执行。

2.3 PPI:模块间的服务契约

PPI(PEIM-to-PEIM Interface)是PEI阶段服务抽象的核心。你可以把它理解为一个C语言的结构体,里面定义了一组函数指针。这个结构体就是“服务契约”,它规定了服务的名称(一个全局唯一的GUID)和具体提供了哪些功能函数。

例如,一个假设的“EFI_PEI_CPU_IO_PPI”可能包含如下函数指针:

typedef struct _EFI_PEI_CPU_IO_PPI { EFI_PEI_CPU_IO_MEM_READ MemRead; EFI_PEI_CPU_IO_MEM_WRITE MemWrite; EFI_PEI_CPU_IO_IO_READ IoRead; EFI_PEI_CPU_IO_IO_WRITE IoWrite; // ... 其他操作 } EFI_PEI_CPU_IO_PPI;

当一个PEIM完成了CPU I/O空间的初始化后,它就会实例化这样一个结构体,填充好具体的函数(例如针对x86架构的in/out指令实现),然后调用PEI Foundation提供的InstallPpi()服务,将这个结构体的指针和对应的GUID注册到PPI数据库中。

之后,任何其他PEIM如果需要读写I/O端口,它只需要在自己的入口函数中,通过PEI Foundation的LocatePpi()服务,根据GUID查找到这个EFI_PEI_CPU_IO_PPI的指针,就可以直接调用IoRead/IoWrite等功能了。这种通过接口和GUID寻址的方式,实现了模块间的完全解耦。

注意:PPI的安装和查找是PEI阶段最高频的操作之一。在调试时,如果某个PEIM未能按预期执行,首先应该检查它所依赖的PPI是否已成功安装。使用UEFI调试工具(如Intel的ITP或软件模拟器如QEMU的调试输出)查看PPI数据库的状态是关键的排查手段。

2.4 HOB:阶段间的数据传递者

HOB(Hand-Off Block)是PEI阶段留给DXE阶段的“遗产”。由于PEI阶段结束后,CAR环境会被销毁,PEI阶段积累的所有动态数据都会丢失。但是,DXE阶段需要知道很多PEI阶段的成果,比如:“可用的系统内存有多大,分布在哪些地址范围?”“已经发现了哪些物理设备?”“平台的特定配置信息是什么?”

HOB就是用来持久化这些信息的机制。它是一个在内存中构建的链表结构。PEI Foundation会在永久内存(通常是初始化好的DRAM)可用后,首先分配一块内存作为“HOB列表”的起点。此后,PEIM们就可以调用BuildHob()系列函数,创建不同类型的HOB,并将其添加到这个链表中。

常见的HOB类型包括:

  • 资源描述HOB:描述系统内存、I/O资源、预留内存等。
  • 固件卷HOB:描述包含DXE阶段驱动程序的固件卷位置。
  • 内存分配HOB:记录在PEI阶段进行的永久内存分配。
  • GUID扩展HOB:一种通用结构,允许任何模块通过自定义的GUID来传递任意格式的数据。

HOB列表的头部指针会作为一个固定的PPI(EFI_PEI_HOB_POINTERS_PPI)安装,最终在从PEI过渡到DXE时,通过CPU的某个寄存器(例如x86的EAX)传递给DXE Foundation。DXE Foundation拿到这个指针后,就可以遍历整个HOB列表,获取所有必要信息来构建完整的运行时环境。

3. 核心流程解析与实操要点

3.1 PEI阶段的执行流全景图

让我们把上述概念串起来,看一个典型的PEI阶段执行流程:

  1. SEC阶段结束:CPU从复位向量开始执行,经过SEC阶段的安全验证后,跳转到PEI入口点,并将一个称为“PEI核心服务指针”的简单结构传递给PEI Foundation。
  2. PEI Foundation初始化:PEI Foundation利用传递过来的指针,初始化其内部状态和PPI数据库。此时,数据库中已经预置了一些核心PPI,如EFI_PEI_SERVICES(提供InstallPpiLocatePpiBuildHob等基础服务)。
  3. 调度执行PEIM:PEI Foundation开始扫描固件卷中的PEIM映像。对于每个PEIM: a. 读取其PPI描述符,检查依赖。 b. 若依赖满足,将其加载到CAR中并执行其入口函数。 c. 入口函数执行过程中,该PEIM会安装它实现的PPI,也可能构建HOB。 d. 执行完毕,PEI Foundation继续检查下一个PEIM或等待队列中的PEIM。
  4. 关键节点:永久内存初始化:一个特殊的PEIM(通常是芯片组相关)会初始化DRAM。成功后,它会安装“内存服务PPI”并构建第一个HOB——资源描述HOB,来标记这块内存可用。这个动作是一个分水岭,之后就可以在永久内存上构建HOB列表了。
  5. 继续执行依赖内存的PEIM:那些依赖“内存服务PPI”的PEIM现在可以被调度执行了。它们可能会发现更多硬件(如PCI设备),并构建相应的HOB(如GUID扩展HOB来描述设备信息)。
  6. 发现并准备DXE阶段:一个特定的PEIM(如DxeLoad)会负责定位包含DXE Foundation和DXE驱动程序的固件卷,并构建“固件卷HOB”来描述它们的位置。
  7. PEI阶段结束:当所有PEIM执行完毕,或者某个PEIM显式调用PeiServicesInstallPpi安装了一个特殊的“最终PPI”(如EFI_PEI_END_OF_PEI_PHASE_PPI)时,PEI阶段进入收尾。PEI Foundation将HOB列表的指针放入约定好的寄存器,然后跳转到DXE Foundation的入口点。

3.2 编写一个自定义PEIM的实战要点

假设我们需要编写一个PEIM来读取主板的特定型号ID,并将其通过HOB传递给DXE。以下是关键步骤和代码要点:

1. 定义模块的GUID和PPI(如果需要):

// 自定义一个PPI来提供读取型号ID的服务(可选,如果其他PEIM也需要此数据) #define MY_BOARD_INFO_PPI_GUID \ {0x12345678, 0x1234, 0x1234, {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}} typedef struct _MY_BOARD_INFO_PPI { UINT32 BoardId; } MY_BOARD_INFO_PPI; // 定义本PEIM的GUID #define MY_BOARD_ID_PEIM_GUID \ {0x87654321, 0x4321, 0x4321, {0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12}}

2. 实现PEIM入口函数:

EFI_STATUS EFIAPI MyBoardIdPeimEntry ( IN EFI_PEI_FILE_HANDLE FileHandle, IN CONST EFI_PEI_SERVICES **PeiServices ) { EFI_STATUS Status; MY_BOARD_INFO_PPI *BoardInfoPpi; UINT32 BoardId; VOID *Hob; // 1. 从硬件(例如特定IO端口或PCI配置空间)读取主板ID BoardId = BoardDetect(); // 假设的硬件读取函数 // 2. (可选)安装一个PPI,供其他PEIM使用此信息 BoardInfoPpi = (MY_BOARD_INFO_PPI*)AllocateZeroPool(sizeof(MY_BOARD_INFO_PPI)); if (BoardInfoPpi == NULL) { return EFI_OUT_OF_RESOURCES; } BoardInfoPpi->BoardId = BoardId; Status = (*PeiServices)->InstallPpi(PeiServices, &gMyBoardInfoPpiGuid, BoardInfoPpi); if (EFI_ERROR(Status)) { FreePool(BoardInfoPpi); return Status; } // 3. 构建一个GUID扩展HOB,将信息传递给DXE // 首先确保永久内存HOB列表已经存在(依赖内存服务PPI) Hob = (*PeiServices)->GetHobList(PeiServices); if (Hob == NULL) { // HOB列表尚未创建,可能内存还未初始化。我们可以选择: // a) 声明依赖EFI_PEI_MEMORY_DISCOVERED_PPI,确保本PEIM在内存之后执行。 // b) 暂时不构建HOB,或者将数据暂存到其他PPI中。 // 这里假设我们已声明依赖,HOB列表存在。 return EFI_NOT_READY; } // 构建HOB Hob = (*PeiServices)->BuildGuidDataHob ( PeiServices, &gMyBoardInfoPpiGuid, // 使用同一个或新的GUID来标识数据 &BoardId, sizeof(BoardId) ); if (Hob == NULL) { return EFI_OUT_OF_RESOURCES; } return EFI_SUCCESS; }

3. 编写模块的.inf文件(描述文件):

[Defines] INF_VERSION = 0x00010005 BASE_NAME = MyBoardIdPeim FILE_GUID = 87654321-4321-4321-f0de-bc9a78563412 MODULE_TYPE = PEIM VERSION_STRING = 1.0 ENTRY_POINT = MyBoardIdPeimEntry [Sources] MyBoardIdPeim.c [Packages] MdePkg/MdePkg.dec YourPlatformPkg/YourPlatformPkg.dec [LibraryClasses] PeimEntryPoint DebugLib PeiServicesLib PeiServicesTablePointerLib HobLib # 提供BuildGuidDataHob等函数 [Ppis] ## 声明本PEIM依赖的PPI,确保执行顺序 gEfiPeiMemoryDiscoveredPpiGuid # 依赖内存初始化完成,以便构建HOB # 如果BoardDetect函数需要CPU IO,可能还需要: # gEfiPeiCpuIoPpiInstalledGuid [Depex] gEfiPeiMemoryDiscoveredPpiGuid # Depex语句,逻辑上声明依赖关系

实操心得:在编写PEIM时,.inf文件中的[Depex]部分至关重要,它直接决定了PEI Foundation调度本模块的时机。务必仔细分析你的PEIM需要哪些服务(PPI),并在此准确声明。错误的依赖关系会导致模块过早执行(可能因资源未就绪而失败)或过晚执行(错过时机)。对于不依赖任何其他PPI的“根模块”,其[Depex]可以为空。

3.3 PPI数据库的调试与查看技巧

在真实平台或模拟器上调试时,查看PPI数据库的状态是诊断问题的利器。虽然这高度依赖于调试工具,但思路是通用的。

  1. 利用调试输出:许多PEI Foundation和PEIM在安装或查找PPI时,会打印调试信息。确保你的固件镜像编译时包含了DEBUG宏,并通过串口或调试器查看输出。你会看到类似“Install PPI: GUID XXXXXXXX-XXXX-...”和“Locate PPI: GUID XXXXXXXX-XXXX-... Found”的信息。
  2. 使用模拟器:在像EDK2的OvmfPkg(用于QEMU)这样的模拟环境中,你可以插入断点,并直接调用调试器的函数来遍历内部数据结构。例如,在UEFI Shell(DXE阶段)之后,有一些第三方工具可以反向解析HOB列表,从中找到PEI阶段传递过来的PPI描述信息。
  3. 芯片厂商工具:Intel的ITP(In-Target Probe)等硬件调试器,可以配合特定的调试脚本,在PEI阶段暂停CPU,并直接查看内存中PPI数据库的链表结构。

一个常见的调试场景是:你的PEIM始终不执行。首先检查其.inf[Depex]。然后,在调试输出中搜索它依赖的PPI的GUID,看看是哪个模块安装的,是否安装成功。如果没有安装成功,则需要去排查那个提供PPI的模块为何失败。

4. 从PEI到DXE:HOB的传递与使用详解

4.1 HOB列表的构建与移交

HOB列表的构建始于“内存被发现”的那一刻。EFI_PEI_MEMORY_DISCOVERED_PPI被安装后,PEI Foundation会调用PeiServicesInstallPpi来安装EFI_PEI_HOB_POINTERS_PPI。这个PPI包含了一个指向第一个HOB(PHITHOB, Phase Handoff Information Table)的指针。

所有后续的BuildHob调用,都会在PHITHOB之后顺序添加新的HOB。HOB有一个统一的头部EFI_HOB_GENERIC_HEADER,其中包含HobTypeHobLength,这样就能以链表方式遍历。

当PEI阶段结束时,EFI_PEI_END_OF_PEI_PHASE_PPI被安装,PEI Foundation执行最后的清理工作,并将PHITHOB的地址(即整个HOB列表的起始地址)写入一个预先约定好的CPU寄存器(在x86 UEFI规范中,通常是通过EFI_PEI_SERVICESSetBootMode等最终服务设置,并在跳转前由架构特定代码放入EAX)。DXE Foundation的入口函数会从这个寄存器中读取HOB列表指针。

4.2 在DXE驱动中检索HOB信息

DXE驱动可以通过GetHobList()函数获取HOB列表指针,然后遍历查找所需信息。EDK2提供了丰富的库函数来简化这个过程:

#include <Library/HobLib.h> EFI_STATUS MyDxeDriverEntry ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ) { VOID *HobList; EFI_HOB_GUID_TYPE *GuidHob; UINT32 *BoardIdFromHob; // 获取HOB列表 HobList = GetHobList(); // 遍历查找我们自定义的GUID HOB GuidHob = GetFirstGuidHob (&gMyBoardInfoPpiGuid); if (GuidHob == NULL) { DEBUG ((EFI_D_ERROR, “MyBoardId HOB not found!\n”)); return EFI_NOT_FOUND; } // 获取HOB中的数据 BoardIdFromHob = (UINT32 *) GET_GUID_HOB_DATA (GuidHob); DEBUG ((EFI_D_INFO, “Board ID from PEI: 0x%08x\n”, *BoardIdFromHob)); // 使用BoardIdFromHob进行后续操作... return EFI_SUCCESS; }

除了自定义的GUID HOB,DXE驱动更常用的是标准HOB,例如通过GetSystemMemoryMap()来获取内存布局,通过GetBootModeHob()来获取启动模式等。这些信息是DXE驱动进行资源分配、设备初始化的基础。

4.3 HOB与PPI的对比与选用原则

在PEI阶段,数据既可以通过PPI共享给其他PEIM,也可以通过HOB传递给DXE。如何选择?

  • 使用PPI的场景

    • 数据需要在PEI阶段内部被多个其他PEIM使用。
    • 提供的是动态服务(一组函数),而不仅仅是静态数据。例如,读写I/O、访问SPI Flash等。
    • 数据的生命周期仅限于PEI阶段,DXE阶段不再需要。
  • 使用HOB的场景

    • 数据必须传递到DXE及后续阶段(如BDS、OS运行时)。
    • 数据是静态的、一次性的描述信息。例如,内存映射、设备列表、平台配置。
    • 数据量可能较大,或者结构复杂。

一个典型的模式是:一个PEIM通过硬件探测产生数据,先安装一个PPI让其他PEIM在PEI阶段使用该数据(例如,早期显示初始化),同时构建一个HOB,将数据的副本传递给DXE阶段(例如,供操作系统ACPI表使用)。

5. 常见问题排查与实战避坑指南

5.1 PEIM执行顺序错乱或未执行

这是最常见的问题之一。

  • 症状:预期的硬件初始化未发生,或者后续模块因找不到依赖服务而失败。
  • 排查步骤
    1. 检查.inf文件的[Depex]部分:确认依赖的PPI GUID书写正确,且与提供该PPI的模块定义的GUID完全一致(包括大小写)。一个字符的错误都会导致依赖解析失败。
    2. 验证依赖PPI是否已安装:通过调试输出,查看你依赖的PPI是否被成功安装。搜索其GUID。如果没有,则需要去排查提供该PPI的模块本身为何失败。
    3. 检查模块是否被包含到固件卷中:确认你的PEIM的.inf文件被平台的.dsc文件引用,并且最终被生成工具打包到正确的固件卷(FV)中。你可以检查编译生成的FDF文件映射或最终的固件镜像布局。
    4. 循环依赖:极少数情况下,两个PEIM互相依赖对方的PPI,导致死锁。PEI Foundation通常能检测并报告此类错误。需要重新设计模块,引入第三个模块或拆分功能来打破循环。

避坑技巧:在开发初期,可以暂时将PEIM的[Depex]设为TRUE,这意味着它不依赖任何PPI,将在PEI Foundation初始化后立即执行。这可以用于快速验证模块的基本功能。待功能稳定后,再仔细添加正确的依赖关系。

5.2 PPI安装成功但查找失败

  • 症状:调试显示PPI已安装,但依赖它的PEIM在LocatePpi时返回EFI_NOT_FOUND
  • 可能原因
    • GUID不匹配:安装和查找时使用的GUID常量看似相同,但可能来自不同的头文件定义,实际值不同。务必使用同一个头文件中定义的GUID变量。
    • PPI数据库损坏:在CAR环境下进行非法内存写操作可能会破坏PPI数据库链表。使用内存检查工具(如MemTest86的早期版本或硬件调试器)排查内存越界问题。
    • 时机问题LocatePpi调用发生在该PPI被安装之前?虽然依赖机制会控制PEIM执行顺序,但在PEIM的入口函数内部,如果过早调用LocatePpi(例如在依赖的PPI安装之前),也会失败。确保查找操作在依赖服务就绪后进行。

5.3 HOB在DXE阶段找不到

  • 症状:DXE驱动无法通过GetFirstGuidHob找到预期的HOB。
  • 排查步骤
    1. 确认HOB构建时机:构建HOB的PEIM必须在永久内存初始化之后执行。检查该PEIM是否依赖gEfiPeiMemoryDiscoveredPpiGuidgEfiPeiMemoryDiscoveredPpiGuid。如果没有依赖,它可能在内存可用前就执行并尝试构建HOB,此时BuildHob会失败(返回NULL),但你的代码可能忽略了错误检查。
    2. 检查HOB构建函数的返回值:总是检查BuildGuidDataHob等函数的返回值是否为NULL
    3. 验证GUID:确保构建HOB和查找HOB时使用的GUID完全一致。和PPI的GUID问题类似。
    4. 检查HOB列表指针传递:在从PEI到DXE的跳转点设置断点,检查传递给DXE的HOB列表指针(如EAX寄存器)是否有效,并指向一个有效的PHITHOB。
    5. HOB数据被覆盖:如果PEI阶段或DXE早期有模块在HOB列表所在的内存区域进行了错误的写操作,会破坏HOB结构。这需要通过调试器查看HOB内存区域的实际内容来诊断。

5.4 内存不足导致PEI阶段失败

在永久内存初始化之前,所有PEIM都运行在CAR中,空间极其有限(通常只有几十到几百KB)。

  • 症状:模块加载失败、分配池失败、或出现不可预知崩溃。
  • 应对策略
    • 优化PEIM体积:使用编译器优化选项,移除不必要的调试代码和符号。仔细审查库依赖,避免链接进不必要的大库。
    • 延迟初始化:将非关键的功能推迟到DXE阶段进行。PEI阶段只做“不得不做”的初始化。
    • 使用临时内存:在芯片组支持的情况下,尽早初始化一部分内存作为“临时内存”,可以显著缓解CAR的压力。这需要芯片组特定PEIM的支持。
    • 监控CAR使用:通过分析编译生成的MAP文件,了解每个PEIM的代码和数据大小。使用调试输出,在运行时打印CAR的分配情况。

5.5 调试手段有限

PEI阶段缺乏完整的控制台和文件系统,调试是一大挑战。

  • 推荐方法
    • 串口调试:最可靠的方法。确保平台串口早期初始化,并在PEIM中大量使用DEBUG宏输出关键信息。信息格式要简洁明了,包含模块名和关键状态。
    • Post Code:许多服务器主板带有两位或四位POST代码显示器。在关键路径的开始和结束处输出特定的POST码,可以帮助定位卡住的阶段。
    • 模拟器:在QEMU+OVMF环境中进行前期开发和逻辑调试,可以利用GDB进行单步调试和内存查看,效率远高于真机。
    • 变量存储:在CAR中预留一小块区域作为“持久化调试变量”,即使系统重置,只要不断电,数据仍在。可以将错误代码、计数器等写入该区域,在下次启动时(或通过硬件调试器)读取。

理解PEI阶段的扩展机制——PEIM通过PPI交互,通过HOB传递数据——是深入UEFI固件开发的关键。这不仅仅是记住几个API,而是要建立起一种“在资源受限环境下构建可扩展服务”的思维模型。在实际项目中,最耗时的往往不是编写功能代码,而是调试模块间的依赖、执行顺序和数据传递。清晰的架构设计、严谨的依赖声明、以及丰富的调试日志,是保证PEI阶段稳定可靠的三驾马车。当你能够熟练地运用PPI和HOB来组织你的初始化代码时,你就真正掌握了系统启动过程中这段“于无声处听惊雷”的关键阶段。

返回列表