DXE Foundation

DXE 简介

 在PI 架构中,PEI 阶段之后为DXE 阶段。系统的大多数初始化工作在DXE 阶段中完

成。

组成DXE 阶段的模块有DXE Foundation, 若干DXE 驱动,若干UEFI 驱动。PEI Foundation 调用

DXE IPL PPI 加载和执行DXE Foundation 标志着DXE 阶段的开始。DXE Foundation 是DXE 阶段

的基础和核心模块。负责提供UEFI 规范中定义的Boot Service 和Runtime Servies. 并提供一组扩展的

非UEFI 规定的DXE Services. 此外,DXE Foundation 中有一个重要的组件: DXE Dispatcher. 它的功能

是在FV 中发现并以正确的顺序执行DXE 和UEFI 驱动。

 

Lock 锁机制

UEFI 是基于事件(Event) 的有限多任务模型。UEFI 只支持时钟而事件机制的核心即时钟中断。这与现代操作系统一般

使用系统时钟中断来实现基于时间片的多任务调试类似。DXE Foundation 提供了时钟中断的服务例程,但设计上DXE

Foundation 是硬件无关的,所以它不能直接操作系统时钟,而是依赖Timer Architectural Protocol 来设置自己的时钟中断

服务。

 

 

**/
VOID
EFIAPI
CoreTimerTick (
  IN UINT64   Duration
  )
{
  IEVENT          *Event;

  //
  // Check runtiem flag in case there are ticks while exiting boot services
  //
  CoreAcquireLock (&mEfiSystemTimeLock);

  //
  // Update the system time
  //
  mEfiSystemTime += Duration;

  //
  // If the head of the list is expired, fire the timer event
  // to process it
  //
  if (!IsListEmpty (&mEfiTimerList)) {
    Event = CR (mEfiTimerList.ForwardLink, IEVENT, Timer.Link, EVENT_SIGNATURE);

    if (Event->Timer.TriggerTime <= mEfiSystemTime) {
      CoreSignalEvent (mEfiCheckTimerEvent);
    }
  }

  CoreReleaseLock (&mEfiSystemTimeLock);
}

首先,CoreTimerTick () 取得保护mEfiSystemTime 全局变量的锁。

DXE Foundaton 用mEfiSystemTime 全局变量追踪系统启动以来经过的时间(以100ns为单位)。

每次时钟中断发生时,此次时钟中断距上次时钟中断的时间差被加入mEfiSystemTime.

mEfiTimerList 是维护系统中已经登记的定时器事件的链表。它是一个按照定时器触发时间升序排序的有序链表。

当驱动或者程序调用SetTimer() 来注册定时器事件时,DXE Foundaton 将事件插入定时器事件 链表。

CoreTimerTick() 遍历定时器事件链表,如果发现设定触发时间已经定时器事件,就将mEfiCheckTimerEvent 事件置为

已触发状态。

 

最后释放mEfiSystemTimeLock 锁。CoreReleaseLock 会调用CoreRestoreTpl() 恢复之前tpl.

在CoreRestoreTpl() 内的事件分发过程中mEficheckTimerEvent的回调函数CorecheckTimers() 首先被调用(因为

它的TPL 是TPL_HIGH_LEVEL-1). 真正地去做定时器事件的工作。

 

Dxe Dispatcher

 Dxe dispatcher 是DXE Foundation 必不可少的一个重要组成部分。它的作用是寻找存放在FV 中的DXE 驱动和UEFI

驱动并以适当的顺序执行。DXE dispatcher 如何确定适当的执行顺序呢?

    静态指定

    PI 规范规定, 一个FV 上可以有且最多只有一个执行顺序(a priori) 文件。 a priori 文件的内容是一个驱动文件名列表

(即guid数组), 显示地指定了此FV 上全部或部分驱动的执行顺序。注意一个FV 是否提供执行顺序( a priori) 文件是可

选的。如果没有,DXE Dispatcher 就只能用动态确定的方法来执行驱动。

当DXE Dispatcher 发现一个新的FV 的时候,首先在此FV 上用固定的GUID 文件名寻找a priori 文件。如果找到。 DXE

Dispatcher 严格按照文件内的驱动列表的顺序加载并执行列出的驱动。列表中的驱动如果有依赖表达式将会被忽略。

当a priori 文件列出的驱动全部执行完后,DXE Dispathcer 接着在FV 上寻找剩余的驱动,然后再用动态确定的方法来执行。

 

动态确定

除了a priori 文件之外,DXE Dispatcher 根据驱动之间的互相信赖关系来动态确定驱动的执行顺序。驱动的依赖表达式

dependency expression 来描述。依赖表达式经过编码后存储在DXE 驱动文件的依赖区中(dependency section)中。

一个DXE 驱动可以用一来表达式来指定它自己在另一个特定的驱动之前或之后执行,也可以用依赖表达式来告诉DXE

Dispatcher 只有某一个或一组协议被安装后自己才能被执行。一个DXE 驱动只有当它的依赖表达式描述的条件满足的时候

才会被DXE Dispatcher 执行。

 

注意,DXE Dispatcher 根据依赖表达式动态确定的执行顺序是弱有序的,也就是说,执行顺序在不同的平台上或者每次系统

启动过程中不一定是完全一样的。

 

全局变量

 

mDispatcher 专用锁。此锁的TPL 为TPL_HIGH_LEVEL, 意味着,用此锁保护的临界区不会被任何TPL 的代码打断。

 

mDiscoveredLists

系统中所有已经发现的驱动的列表。

mScheduledQueue

已经高度的驱动队列。队列中的驱动已经准备好被执行。

#define EFI_CORE_DRIVER_ENTRY_SIGNATURE SIGNATURE_32('d','r','v','r')
typedef struct {
  UINTN                           Signature;
  LIST_ENTRY                      Link;             // mDriverList

  LIST_ENTRY                      ScheduledLink;    // mScheduledQueue

  EFI_HANDLE                      FvHandle;
  EFI_GUID                        FileName;
  EFI_DEVICE_PATH_PROTOCOL        *FvFileDevicePath;
  EFI_FIRMWARE_VOLUME2_PROTOCOL   *Fv;

  VOID                            *Depex;
  UINTN                           DepexSize;

  BOOLEAN                         Before;
  BOOLEAN                         After;
  EFI_GUID                        BeforeAfterGuid;

  BOOLEAN                         Dependent;
  BOOLEAN                         Unrequested;
  BOOLEAN                         Scheduled;
  BOOLEAN                         Untrusted;
  BOOLEAN                         Initialized;
  BOOLEAN                         DepexProtocolError;

  EFI_HANDLE                      ImageHandle;
  BOOLEAN                         IsFvImage;

} EFI_CORE_DRIVER_ENTRY;

 

EFI_CORE_DRIVER_ENTRY 是DXE Dispatcher 使用的核心数据结构。该数据结构内有2个LIST_ENTRY 类型的域,意味

着结构可以被同时插入两个列表。Link用来插入已经发现的驱动列表,ScheduleLink 用来插入已经调试驱动列表。

 

/**
  Initialize the dispatcher. Initialize the notification function that runs when
  an FV2 protocol is added to the system.

**/
VOID
CoreInitializeDispatcher (
  VOID
  )
{
  PERF_FUNCTION_BEGIN ();

  mFwVolEvent = EfiCreateProtocolNotifyEvent (
                  &gEfiFirmwareVolume2ProtocolGuid,
                  TPL_CALLBACK,
                  CoreFwVolEventProtocolNotify,
                  NULL,
                  &mFwVolEventRegistration
                  );

  PERF_FUNCTION_END ();
}

CoreInitializeDispatcher() 是DXE Dispatcher 的初始化函数。它非常简单,就是调用UefiLib 的库函数

EfiCreateProtocolNotifyEvent() 创建Firmware Volume 2 protocol 的回调事件。一旦系统中有新的Firmware Volume 2

Protocol 安装, CoreFwVolEventProtocolNotify () 回调函数就会被调用 。

 

FvHandle = NULL;

  while (TRUE) {
    BufferSize = sizeof (EFI_HANDLE);
    Status = CoreLocateHandle (
               ByRegisterNotify,
               NULL,
               mFwVolEventRegistration,
               &BufferSize,
               &FvHandle
               );
    if (EFI_ERROR (Status)) {
      //
      // If no more notification events exit
      //
      return;
    }

    if (FvHasBeenProcessed (FvHandle)) {
      //
      // This Fv has already been processed so lets skip it!
      //
      continue;
    }

    //
    // Since we are about to process this Fv mark it as processed.
    //
    KnownHandle = FvIsBeingProcesssed (FvHandle);
    if (KnownHandle == NULL) {
      //
      // The FV with the same FV name guid has already been processed.
      // So lets skip it!
      //
      continue;
    }

协议回调函数的标准作法是将ByRegisterNotify 传入SearchType 参数的值,调用LocateHandle() 或者

LocatehandleBuffer() Boot Service 获得新安装该协议实例的句柄。句柄数可能不止一个,但这两个Boot Service

一次只能返回一个句柄,所以回调函数必须用一个循环来处理所有返回的句柄。

 

跳过已经处理过的FV, 否则将FV 句柄插入MFvHandleList 链表尾部。

mFvHandleList 记录所有已经处理FV 的句柄。

 

获取FV 句柄上的Firmware Volume 2 协议实例的指针。如果取不到,这是不应该发生的情况。然后,再

获取FV 句柄上的Device path 协议实例的指针。 如果没有, DXE Foundation 无法处理, 略过。

 


/**
  Add an entry to the mDiscoveredList. Allocate memory to store the DriverEntry,
  and initilize any state variables. Read the Depex from the FV and store it
  in DriverEntry. Pre-process the Depex to set the SOR, Before and After state.
  The Discovered list is never free'ed and contains booleans that represent the
  other possible DXE driver states.

  @param  Fv                    Fv protocol, needed to read Depex info out of
                                FLASH.
  @param  FvHandle              Handle for Fv, needed in the
                                EFI_CORE_DRIVER_ENTRY so that the PE image can be
                                read out of the FV at a later time.
  @param  DriverName            Name of driver to add to mDiscoveredList.
  @param  Type                  Fv File Type of file to add to mDiscoveredList.

  @retval EFI_SUCCESS           If driver was added to the mDiscoveredList.
  @retval EFI_ALREADY_STARTED   The driver has already been started. Only one
                                DriverName may be active in the system at any one
                                time.

**/
EFI_STATUS
CoreAddToDriverList (
  IN  EFI_FIRMWARE_VOLUME2_PROTOCOL   *Fv,
  IN  EFI_HANDLE                      FvHandle,
  IN  EFI_GUID                        *DriverName,
  IN  EFI_FV_FILETYPE                 Type
  )
{
  EFI_CORE_DRIVER_ENTRY               *DriverEntry;


  //
  // Create the Driver Entry for the list. ZeroPool initializes lots of variables to
  // NULL or FALSE.
  //
  DriverEntry = AllocateZeroPool (sizeof (EFI_CORE_DRIVER_ENTRY));
  ASSERT (DriverEntry != NULL);
  if (Type == EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE) {
    DriverEntry->IsFvImage = TRUE;
  }

  DriverEntry->Signature        = EFI_CORE_DRIVER_ENTRY_SIGNATURE;
  CopyGuid (&DriverEntry->FileName, DriverName);
  DriverEntry->FvHandle         = FvHandle;
  DriverEntry->Fv               = Fv;
  DriverEntry->FvFileDevicePath = CoreFvToDevicePath (Fv, FvHandle, DriverName);

  CoreGetDepexSectionAndPreProccess (DriverEntry);

  CoreAcquireDispatcherLock ();

  InsertTailList (&mDiscoveredList, &DriverEntry->Link);

  CoreReleaseDispatcherLock ();

  return EFI_SUCCESS;
}

 

为一个发现的驱动创建一个EFI_CORE_DRIVER_ENTRY 数据结构的实例并填入对应的信息, 包括文件名,FV 句柄、

Firmware Volume 2 协议实例的指针和文件的device path.

 


/**
  Read Depex and pre-process the Depex for Before and After. If Section Extraction
  protocol returns an error via ReadSection defer the reading of the Depex.

  @param  DriverEntry           Driver to work on.

  @retval EFI_SUCCESS           Depex read and preprossesed
  @retval EFI_PROTOCOL_ERROR    The section extraction protocol returned an error
                                and  Depex reading needs to be retried.
  @retval Error                 DEPEX not found.

**/
EFI_STATUS
CoreGetDepexSectionAndPreProccess (
  IN  EFI_CORE_DRIVER_ENTRY   *DriverEntry
  )
{
  EFI_STATUS                    Status;
  EFI_SECTION_TYPE              SectionType;
  UINT32                        AuthenticationStatus;
  EFI_FIRMWARE_VOLUME2_PROTOCOL *Fv;


  Fv = DriverEntry->Fv;

  //
  // Grab Depex info, it will never be free'ed.
  //
  SectionType         = EFI_SECTION_DXE_DEPEX;
  Status = Fv->ReadSection (
                DriverEntry->Fv,
                &DriverEntry->FileName,
                SectionType,
                0,
                &DriverEntry->Depex,
                (UINTN *)&DriverEntry->DepexSize,
                &AuthenticationStatus
                );
  if (EFI_ERROR (Status)) {
    if (Status == EFI_PROTOCOL_ERROR) {
      //
      // The section extraction protocol failed so set protocol error flag
      //
      DriverEntry->DepexProtocolError = TRUE;
    } else {
      //
      // If no Depex assume UEFI 2.0 driver model
      //
      DriverEntry->Depex = NULL;
      DriverEntry->Dependent = TRUE;
      DriverEntry->DepexProtocolError = FALSE;
    }
  } else {
    //
    // Set Before, After, and Unrequested state information based on Depex
    // Driver will be put in Dependent or Unrequested state
    //
    CorePreProcessDepex (DriverEntry);
    DriverEntry->DepexProtocolError = FALSE;
  }

  return Status;
}

读取驱动的DXE DEPEX 节,里面的内容是编码的依赖表达式。

 如果读不到,且返回值是EFI_PROTOCOL_ERROR, 原因是系统中没有对应的section extraction 协议来扫描封装节中

的内容,则将DepexProtocolError 域设为TRUE. 其他读不到情况,认为这个驱动是UEFI 驱动。

 

/**
  Preprocess dependency expression and update DriverEntry to reflect the
  state of  Before, After, and SOR dependencies. If DriverEntry->Before
  or DriverEntry->After is set it will never be cleared. If SOR is set
  it will be cleared by CoreSchedule(), and then the driver can be
  dispatched.

  @param  DriverEntry           DriverEntry element to update .

  @retval EFI_SUCCESS           It always works.

**/
EFI_STATUS
CorePreProcessDepex (
  IN  EFI_CORE_DRIVER_ENTRY   *DriverEntry
  )
{
  UINT8  *Iterator;

  Iterator = DriverEntry->Depex;
  if (*Iterator == EFI_DEP_SOR) {
    DriverEntry->Unrequested = TRUE;
  } else {
    DriverEntry->Dependent = TRUE;
  }

  if (*Iterator == EFI_DEP_BEFORE) {
    DriverEntry->Before = TRUE;
  } else if (*Iterator == EFI_DEP_AFTER) {
    DriverEntry->After = TRUE;
  }

  if (DriverEntry->Before || DriverEntry->After) {
    CopyMem (&DriverEntry->BeforeAfterGuid, Iterator + 1, sizeof (EFI_GUID));
  }

  return EFI_SUCCESS;
}

当依赖表达式的第一个操作码是BEFORE 或AFTER 时,分别设置EFI_CORE_DRIVER_ENTER 结构的Before域

和After 域为TRUE. 另外将依赖的模块GUID 文件拷贝到BeforeAfterGuid 域 。

 

CoreSchedule() 函数

CoreSchedule () 函数实现了Schedule() DXE Service. 它用来清除FV 中某个DXE 驱动的SOR 标志。如果一个DXE驱动

的依赖表达式中有SOR码,它不会被DXE Dispatcher 执行直到它的SOR标志被清除并且依赖表达式的剩余部分条件

满足。注意Schedule() DXE Service 不会自动调用DXE Dispatcher . 它只是清除SOR 标志。

 

 

 DriverEntry = CR(Link, EFI_CORE_DRIVER_ENTRY, Link, EFI_CORE_DRIVER_ENTRY_SIGNATURE);

#define OFFSET_OF(TYPE, Field) ((UINTN) &(((TYPE *)0)->Field))

Link 是EFI_CORE_DRIVER_ENTRY 的成员。

#define EFI_CORE_DRIVER_ENTRY_SIGNATURE SIGNATURE_32('d','r','v','r')
typedef struct {
  UINTN                           Signature;
  LIST_ENTRY                      Link;             // mDriverList

  LIST_ENTRY                      ScheduledLink;    // mScheduledQueue

  EFI_HANDLE                      FvHandle;
  EFI_GUID                        FileName;
  EFI_DEVICE_PATH_PROTOCOL        *FvFileDevicePath;
  EFI_FIRMWARE_VOLUME2_PROTOCOL   *Fv;

  VOID                            *Depex;
  UINTN                           DepexSize;

  BOOLEAN                         Before;
  BOOLEAN                         After;
  EFI_GUID                        BeforeAfterGuid;

  BOOLEAN                         Dependent;
  BOOLEAN                         Unrequested;
  BOOLEAN                         Scheduled;
  BOOLEAN                         Untrusted;
  BOOLEAN                         Initialized;
  BOOLEAN                         DepexProtocolError;

  EFI_HANDLE                      ImageHandle;
  BOOLEAN                         IsFvImage;

} EFI_CORE_DRIVER_ENTRY;

 

 

CoreIsScheduleable() 对依赖表达式求值来判断驱动是否可调度。

DXE 依赖表达式的设计原则是简单和快速高效的求值,因此编码依赖表达式的指令集小且基于栈。

编码后的依赖表达式是操作码和操作数组成的后缀式的紧凑字节流。表达式的求值过程是:

遇到操作数时,压栈保存。遇到操作码时从栈顶弹出操作数进行运算,结果再压栈。最后一个操作码

结束后,栈顶存的值就是整个表达式求值的结果。

//
// DXE Core Global Variables for all of the Architectural Protocols.
// If a protocol is installed mArchProtocols[].Present will be TRUE.
//
// CoreNotifyOnArchProtocolInstallation () fills in mArchProtocols[].Event
// and mArchProtocols[].Registration as it creates events for every array
// entry.
//
EFI_CORE_PROTOCOL_NOTIFY_ENTRY  mArchProtocols[] = {
  { &gEfiSecurityArchProtocolGuid,         (VOID **)&gSecurity,      NULL, NULL, FALSE },
  { &gEfiCpuArchProtocolGuid,              (VOID **)&gCpu,           NULL, NULL, FALSE },
  { &gEfiMetronomeArchProtocolGuid,        (VOID **)&gMetronome,     NULL, NULL, FALSE },
  { &gEfiTimerArchProtocolGuid,            (VOID **)&gTimer,         NULL, NULL, FALSE },
  { &gEfiBdsArchProtocolGuid,              (VOID **)&gBds,           NULL, NULL, FALSE },
  { &gEfiWatchdogTimerArchProtocolGuid,    (VOID **)&gWatchdogTimer, NULL, NULL, FALSE },
  { &gEfiRuntimeArchProtocolGuid,          (VOID **)&gRuntime,       NULL, NULL, FALSE },
  { &gEfiVariableArchProtocolGuid,         (VOID **)NULL,            NULL, NULL, FALSE },
  { &gEfiVariableWriteArchProtocolGuid,    (VOID **)NULL,            NULL, NULL, FALSE },
  { &gEfiCapsuleArchProtocolGuid,          (VOID **)NULL,            NULL, NULL, FALSE },
  { &gEfiMonotonicCounterArchProtocolGuid, (VOID **)NULL,            NULL, NULL, FALSE },
  { &gEfiResetArchProtocolGuid,            (VOID **)NULL,            NULL, NULL, FALSE },
  { &gEfiRealTimeClockArchProtocolGuid,    (VOID **)NULL,            NULL, NULL, FALSE },
  { NULL,                                  (VOID **)NULL,            NULL, NULL, FALSE }
};

 

 

UEFI 映像加截与执行

映像是指一类包含可执行代码的文件。UEFI 规范规定,UEFI 映像采用微软公司定义的PE32+

可执行文件格式。

PE 和COFF 文件格式规范专门为UEFI 执行环境定义了四种Subsystem. 分别为:

当UEFI Application 映像退出时,系统总是将它卸载并释放它占用的内存。I

UEFI 映像过LoadImage() boot Service 加载到内存中。LoadImage() Boot Service 中有一个Pe32+ 装载器,

它将pe 中的所有section 都装入内存。然后,对内存中的代码根据加载的实际地址进行重新定位。

StartImage() Boot Service 根据UEFI 规范为每一种支持的CPU 架构定义的调用约定(Calling Convention)

将控制权转交给一个在内存中已经装载好的UEFI 映像的入口地址。

 

  //
  // Searching for image hob
  //
  DxeCoreHob.Raw          = HobStart;
  while ((DxeCoreHob.Raw = GetNextHob (EFI_HOB_TYPE_MEMORY_ALLOCATION, DxeCoreHob.Raw)) != NULL) {
    if (CompareGuid (&DxeCoreHob.MemoryAllocationModule->MemoryAllocationHeader.Name, &gEfiHobMemoryAllocModuleGuid)) {
      //
      // Find Dxe Core HOB
      //
      break;
    }
    DxeCoreHob.Raw = GET_NEXT_HOB (DxeCoreHob);
  }
  ASSERT (DxeCoreHob.Raw != NULL);

遍历hob list. 寻找DXE Foundation 映像的Memory Allocation HOB. DXE IPL 将DXE Foundation

映像加载到内存后。 会为之创建Memory Allocation HOB 来描述DXE Foundation 在内存的起始

地址和长度。

 

可执行映像加载

EFI_STATUS
EFIAPI
CoreLoadImage (
  IN BOOLEAN                    BootPolicy,
  IN EFI_HANDLE                 ParentImageHandle,
  IN EFI_DEVICE_PATH_PROTOCOL   *FilePath,
  IN VOID                       *SourceBuffer   OPTIONAL,
  IN UINTN                      SourceSize,
  OUT EFI_HANDLE                *ImageHandle
  )
{
  EFI_STATUS    Status;
  EFI_HANDLE    Handle;

  PERF_LOAD_IMAGE_BEGIN (NULL);

  Status = CoreLoadImageCommon (
             BootPolicy,
             ParentImageHandle,
             FilePath,
             SourceBuffer,
             SourceSize,
             (EFI_PHYSICAL_ADDRESS) (UINTN) NULL,
             NULL,
             ImageHandle,
             NULL,
             EFI_LOAD_PE_IMAGE_ATTRIBUTE_RUNTIME_REGISTRATION | EFI_LOAD_PE_IMAGE_ATTRIBUTE_DEBUG_IMAGE_INFO_TABLE_REGISTRATION
             );

  Handle = NULL;
  if (!EFI_ERROR (Status)) {
    //
    // ImageHandle will be valid only Status is success.
    //
    Handle = *ImageHandle;
  }

  PERF_LOAD_IMAGE_END (Handle);

  return Status;
}

 

CoreLoadImage() 函数实现了LoadImage() Boot Service 。 函数本身很简单,就是调用

CoreLoadImageCommon () 函数。

 

CoreLoadImageEx() 函数

CoreloadImageEx() 函数实现了Load PE32 Image Protocol . LoadPeiImage() API. 它比CoreLoadImage()

函数更灵活,调用都可以先分配一块内存缓冲区,然后将缓冲区的起始地址和长度传入,这样装载器不需要

在内部为映像分配内存,而是将映像加载到指定的地址。如果加载过程中装载器发现调用者传入的缓冲区的长度

不够,会通过NumberOfPages 传出需要的以页为单位的长度。另外, Attribute 参数可以灵活地指定是否将要

加载的驱动进行Runtime 驱动登记(如果是Runtime驱动的话)和EFI 映像调试信息表登记。

 

  //
  // The caller must pass in a valid ParentImageHandle
  //
  if (ImageHandle == NULL || ParentImageHandle == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  ParentImage = CoreLoadedImageInfo (ParentImageHandle);
  if (ParentImage == NULL) {
    DEBUG((DEBUG_LOAD|DEBUG_ERROR, "LoadImageEx: Parent handle not an image handle\n"));
    return EFI_INVALID_PARAMETER;
  }

 

参数检查。如果ImageHandle 或者ParentImageHandle 为NULL, 则返回 EFI_INVALID_PARAMETER 错误码。

 

 根据交映像句柄获取父映像句柄获取你映像的Loaded Image Protocol 实例的指针。如果取不到,说明父映像

句柄非法,返回EFI_INVALID_PARAMETER 错误码。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值