DEBUG机制


简述

在UEFI开发中,非常重要的一个部分就是添加串口调试信息打印,这个通过DEBUG宏来完成。

在UEFI的代码中可以看到非常多的DEBUG代码,比如:

DEBUG ((EFI_D_INFO, "PlatformBootManagerBeforeConsole\n"));

需要注意的有几点:

  1. 括号是双重的,下面以第一个参数,第二个参数来分别称呼EFI_D_xx和字符串,但实际上稍微有点问题;

  2. 第二个参数是一个字符串,这个字符串里面也可以带格式化标志,下面是DEBUG常用的标志:

  • %a:表示ASCII字符串;
  • %c:表示ASCII字符;
  • %d:表示十进制,可以有%02d这样的用法;
  • %g:表示GUID;
  • %p:表示指针;
  • %r:表示函数的返回状态字符串,类型是EFI_STATUS;
  • %x:表示十六进制,可以有%016lx这样的用法,这样不足的前置位由0补充;
  • %lx:表示64位的十六进制;
  • %ld:表示64位的十进制;
  • %s/%S:表示宽字符串,就是类似L""的字符串,类型是CHAR16;
  1. 第一个参数表示打印级别,下面是说有的打印级别:

    //
    // Declare bits for PcdDebugPrintErrorLevel and the ErrorLevel parameter of DebugPrint()
    //
    #define DEBUG_INIT      0x00000001  // Initialization
    #define DEBUG_WARN      0x00000002  // Warnings
    #define DEBUG_LOAD      0x00000004  // Load events
    #define DEBUG_FS        0x00000008  // EFI File system
    #define DEBUG_POOL      0x00000010  // Alloc & Free (pool)
    #define DEBUG_PAGE      0x00000020  // Alloc & Free (page)
    #define DEBUG_INFO      0x00000040  // Informational debug messages
    #define DEBUG_DISPATCH  0x00000080  // PEI/DXE/SMM Dispatchers
    #define DEBUG_VARIABLE  0x00000100  // Variable
    #define DEBUG_BM        0x00000400  // Boot Manager
    #define DEBUG_BLKIO     0x00001000  // BlkIo Driver
    #define DEBUG_NET       0x00004000  // SNP Driver
    #define DEBUG_UNDI      0x00010000  // UNDI Driver
    #define DEBUG_LOADFILE  0x00020000  // LoadFile
    #define DEBUG_EVENT     0x00080000  // Event messages
    #define DEBUG_GCD       0x00100000  // Global Coherency Database changes
    #define DEBUG_CACHE     0x00200000  // Memory range cachability changes
    #define DEBUG_VERBOSE   0x00400000  // Detailed debug messages that may// significantly impact boot performance
    #define DEBUG_ERROR     0x80000000  // Error//
    // Aliases of debug message mask bits
    //
    #define EFI_D_INIT      DEBUG_INIT
    #define EFI_D_WARN      DEBUG_WARN
    #define EFI_D_LOAD      DEBUG_LOAD
    #define EFI_D_FS        DEBUG_FS
    #define EFI_D_POOL      DEBUG_POOL
    #define EFI_D_PAGE      DEBUG_PAGE
    #define EFI_D_INFO      DEBUG_INFO
    #define EFI_D_DISPATCH  DEBUG_DISPATCH
    #define EFI_D_VARIABLE  DEBUG_VARIABLE
    #define EFI_D_BM        DEBUG_BM
    #define EFI_D_BLKIO     DEBUG_BLKIO
    #define EFI_D_NET       DEBUG_NET
    #define EFI_D_UNDI      DEBUG_UNDI
    #define EFI_D_LOADFILE  DEBUG_LOADFILE
    #define EFI_D_EVENT     DEBUG_EVENT
    #define EFI_D_VERBOSE   DEBUG_VERBOSE
    #define EFI_D_ERROR     DEBUG_ERROR
    

    关于上面的宏定义:

    • 首先这里宏定义了两层,这是因为EDK和EDKII的兼容关系,一般还是用EFI开头的版本比较好;
    • 有一个比较特别的是EFI_D_ERROR,它是最高位为1,表示的错误;
    • 哪些级别会被打印出来取决于全局PCD变量的配置,这个在后面会讲到
    • DEBUG中打印字符的长度是有限制的,最多200个字符(可能不同的实现会不一样)。

DEBUG宏实现

  1. DEBUG宏的位置是在MdePkg\Include\Library\DebugLib.h文件中:

    /**  Macro that calls DebugPrint().If MDEPKG_NDEBUG is not defined and the DEBUG_PROPERTY_DEBUG_PRINT_ENABLED bit of PcdDebugProperyMask is set, then this macro passes Expression to DebugPrint().@param  Expression  Expression containing an error level, a format string, and a variable argument list based on the format string.**/
    #if !defined(MDEPKG_NDEBUG)      #define DEBUG(Expression)        \do {                           \if (DebugPrintEnabled ()) {  \_DEBUG (Expression);       \}                            \} while (FALSE)
    #else#define DEBUG(Expression)
    #endif
    

    需要注意这里的MDEPKG_NDEBUG宏,如果开启了这个宏,表示所有的DEBUG信息都不会有打印了,因为走了#else分支。

    而MDEPKG_NDEBUG这个宏一般定义在dsc文件中,这里以OvmfPkgX64.dsc为例:

    [BuildOptions]GCC:*_UNIXGCC_*_CC_FLAGS             = -DMDEPKG_NDEBUGGCC:RELEASE_*_*_CC_FLAGS             = -DMDEPKG_NDEBUGINTEL:RELEASE_*_*_CC_FLAGS           = /D MDEPKG_NDEBUGMSFT:RELEASE_*_*_CC_FLAGS            = /D MDEPKG_NDEBUG
    

    可以看到在Release版本中通常会定义它来确定调试的打印。

  2. 除了MDEPKG_NDEBUG这个宏,这里还有一个判断条件:DebugPrintEnabled ()

    这个函数定义在DebugLib中,不同的Pkg可能会有不同的实现,还是以OvmfPkgX64.dsc为例:

    !ifdef $(DEBUG_ON_SERIAL_PORT)DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
    !elseDebugLib|OvmfPkg/Library/PlatformDebugLibIoPort/PlatformDebugLibIoPort.inf
    !endif
    

    这里我们看下BaseDebugLibSerialPort.inf中的实现:

    BOOLEAN
    EFIAPI
    DebugPrintEnabled (VOID)
    {return (BOOLEAN) ((PcdGet8(PcdDebugPropertyMask) & DEBUG_PROPERTY_DEBUG_PRINT_ENABLED) != 0);
    }
    

    从中可以看到它是去判断PcdDebugPropertyMask这个PCD变量的值,而它定义在OvmfPkgX64.dsc中:

    !ifdef $(SOURCE_DEBUG_ENABLE)gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x17
    !elsegEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x2F
    !endif
    

    另外一个宏DEBUG_PROPERTY_DEBUG_PRINT_ENABLED定义如下:

    //
    // Declare bits for PcdDebugPropertyMask
    //
    #define DEBUG_PROPERTY_DEBUG_ASSERT_ENABLED       0x01
    #define DEBUG_PROPERTY_DEBUG_PRINT_ENABLED        0x02
    #define DEBUG_PROPERTY_DEBUG_CODE_ENABLED         0x04
    #define DEBUG_PROPERTY_CLEAR_MEMORY_ENABLED       0x08
    #define DEBUG_PROPERTY_ASSERT_BREAKPOINT_ENABLED  0x10
    #define DEBUG_PROPERTY_ASSERT_DEADLOOP_ENABLED    0x20
    

    所以可以确定对于OvmfPkgX64.dsc,函数DebugPrintEnabled ()返回的是TRUE。

  3. 之后需要关注的是_DEBUG宏:

    /**Internal worker macro that calls DebugPrint().This macro calls DebugPrint() passing in the debug error level, a formatstring, and a variable argument list.__VA_ARGS__ is not supported by EBC compiler, Microsoft Visual Studio .NET 2003and Microsoft Windows Server 2003 Driver Development Kit (Microsoft WINDDK) version 3790.1830.@param  Expression  Expression containing an error level, a format string,and a variable argument list based on the format string.
    **/#if !defined(MDE_CPU_EBC) && (!defined (_MSC_VER) || _MSC_VER > 1400)#define _DEBUG_PRINT(PrintLevel, ...)              \do {                                             \if (DebugPrintLevelEnabled (PrintLevel)) {     \DebugPrint (PrintLevel, ##__VA_ARGS__);      \}                                              \} while (FALSE)#define _DEBUG(Expression)   _DEBUG_PRINT Expression
    #else
    #define _DEBUG(Expression)   DebugPrint Expression
    #endif
    

    这里又涉及到几个判断条件。

    • 首先是编译器的支持情况,这个在注释中已经说明。

    • 另外一个是DebugPrintLevelEnabled()函数,它有一个参数PrintLevel,它就是DEBUG函数的第一个参数。

      这个函数也定义在DebugLib库中,我们同样使用BaseDebugLibSerialPort.inf中的实现:

      /**Returns TRUE if any one of the bit is set both in ErrorLevel and PcdFixedDebugPrintErrorLevel.This function compares the bit mask of ErrorLevel and PcdFixedDebugPrintErrorLevel.@retval  TRUE    Current ErrorLevel is supported.@retval  FALSE   Current ErrorLevel is not supported.
      **/
      BOOLEAN
      EFIAPI
      DebugPrintLevelEnabled (IN  CONST UINTN        ErrorLevel)
      {return (BOOLEAN) ((ErrorLevel & PcdGet32(PcdFixedDebugPrintErrorLevel)) != 0);
      }
      

      这里也涉及到一个PCD变量,它同样定义在MdePkg.dec文件中:

        ## This flag is used to control build time optimization based on debug print level.#  Its default value is 0xFFFFFFFF to expose all debug print level.#  BIT0  - Initialization message.<BR>#  BIT1  - Warning message.<BR>#  BIT2  - Load Event message.<BR>#  BIT3  - File System message.<BR>#  BIT4  - Allocate or Free Pool message.<BR>#  BIT5  - Allocate or Free Page message.<BR>#  BIT6  - Information message.<BR>#  BIT7  - Dispatcher message.<BR>#  BIT8  - Variable message.<BR>#  BIT10 - Boot Manager message.<BR>#  BIT12 - BlockIo Driver message.<BR>#  BIT14 - Network Driver message.<BR>#  BIT16 - UNDI Driver message.<BR>#  BIT17 - LoadFile message.<BR>#  BIT19 - Event message.<BR>#  BIT20 - Global Coherency Database changes message.<BR>#  BIT21 - Memory range cachability changes message.<BR>#  BIT22 - Detailed debug message.<BR>#  BIT31 - Error message.<BR># @Prompt Fixed Debug Message Print Level.gEfiMdePkgTokenSpaceGuid.PcdFixedDebugPrintErrorLevel|0xFFFFFFFF|UINT32|0x30001016
      

      它的值是全FF,所以这个函数必定返回TRUE,也因为这个DEBUG宏的第一个参数在这里并没有派上用处。

DebugPrint()的实现

之后就涉及到了真正的函数DebugPrint(),它也定义在DebugLib中,还是以BaseDebugLibSerialPort.inf为例:

VOIDEFIAPIDebugPrint (IN  UINTN        ErrorLevel,IN  CONST CHAR8  *Format,...){CHAR8    Buffer[MAX_DEBUG_MESSAGE_LENGTH];VA_LIST  Marker;//// If Format is NULL, then ASSERT().//ASSERT (Format != NULL);//// Check driver debug mask value and global mask//if ((ErrorLevel & GetDebugPrintErrorLevel ()) == 0) {return;}//// Convert the DEBUG() message to an ASCII String//VA_START (Marker, Format);AsciiVSPrint (Buffer, sizeof (Buffer), Format, Marker);VA_END (Marker);//// Send the print string to a Serial Port //SerialPortWrite ((UINT8 *)Buffer, AsciiStrLen (Buffer));}

函数也比较简单,有几点说明:

  • Buffer是一个有大小的数组,这说明DEBUG能够打印的信息是有大小限制的;
  • GetDebugPrintErrorLevel()函数获取PCD变量,并与DEBUG的第一个参数进行比较,来确定是否需要输出打印;

这里使用的PCD变量是PcdDebugPrintErrorLevel,它的值在dsc文件中定义:

  # DEBUG_INIT      0x00000001  // Initialization# DEBUG_WARN      0x00000002  // Warnings# DEBUG_LOAD      0x00000004  // Load events# DEBUG_FS        0x00000008  // EFI File system# DEBUG_POOL      0x00000010  // Alloc & Free (pool)# DEBUG_PAGE      0x00000020  // Alloc & Free (page)# DEBUG_INFO      0x00000040  // Informational debug messages# DEBUG_DISPATCH  0x00000080  // PEI/DXE/SMM Dispatchers# DEBUG_VARIABLE  0x00000100  // Variable# DEBUG_BM        0x00000400  // Boot Manager# DEBUG_BLKIO     0x00001000  // BlkIo Driver# DEBUG_NET       0x00004000  // SNP Driver# DEBUG_UNDI      0x00010000  // UNDI Driver# DEBUG_LOADFILE  0x00020000  // LoadFile# DEBUG_EVENT     0x00080000  // Event messages# DEBUG_GCD       0x00100000  // Global Coherency Database changes# DEBUG_CACHE     0x00200000  // Memory range cachability changes# DEBUG_VERBOSE   0x00400000  // Detailed debug messages that may#                             // significantly impact boot performance# DEBUG_ERROR     0x80000000  // Error# jw_debug, change 0x8000004F to 0x80000040gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000040

之后是SerialPortWrite()函数,它是具体输出到某个介质的实现了。

ReportStatusCodeEx

除了BaseDebugLibSerialPort.inf这一种实现方式外(OVMF使用了这种方式),还有一个更加普遍的实现:PeiDxeDebugLibReportStatusCode.inf,它的实现方式是:

  //// Send the DebugInfo record//REPORT_STATUS_CODE_EX (EFI_DEBUG_CODE,(EFI_SOFTWARE_DXE_BS_DRIVER | EFI_DC_UNSPECIFIED),0,NULL,&gEfiStatusCodeDataTypeDebugGuid,DebugInfo,TotalSize);

这里又使用了一个宏REPORT_STATUS_CODE_EX,它的实现如下:

#define REPORT_STATUS_CODE_EX(Type,Value,Instance,CallerId,ExtendedDataGuid,ExtendedData,ExtendedDataSize)  \(ReportProgressCodeEnabled() && ((Type) & EFI_STATUS_CODE_TYPE_MASK) == EFI_PROGRESS_CODE)             ?  \ReportStatusCodeEx(Type,Value,Instance,CallerId,ExtendedDataGuid,ExtendedData,ExtendedDataSize)        :  \(ReportErrorCodeEnabled() && ((Type) & EFI_STATUS_CODE_TYPE_MASK) == EFI_ERROR_CODE)                   ?  \ReportStatusCodeEx(Type,Value,Instance,CallerId,ExtendedDataGuid,ExtendedData,ExtendedDataSize)        :  \(ReportDebugCodeEnabled() && ((Type) & EFI_STATUS_CODE_TYPE_MASK) == EFI_DEBUG_CODE)                   ?  \ReportStatusCodeEx(Type,Value,Instance,CallerId,ExtendedDataGuid,ExtendedData,ExtendedDataSize)        :  \EFI_UNSUPPORTED

这里会根据xxxEnabled()和一个type参数来确定是否执行函数ReportStatusCodeEx()。

其中Enabled是根据dsc文件中的PCD变量来的,以ReportDebugCodeEnabled()为例:

/**Returns TRUE if status codes of type EFI_DEBUG_CODE are enabledThis function returns TRUE if the REPORT_STATUS_CODE_PROPERTY_DEBUG_CODE_ENABLEDbit of PcdReportStatusCodeProperyMask is set.  Otherwise FALSE is returned.@retval  TRUE   The REPORT_STATUS_CODE_PROPERTY_DEBUG_CODE_ENABLED bit ofPcdReportStatusCodeProperyMask is set.@retval  FALSE  The REPORT_STATUS_CODE_PROPERTY_DEBUG_CODE_ENABLED bit ofPcdReportStatusCodeProperyMask is clear.
**/
BOOLEAN
EFIAPI
ReportDebugCodeEnabled (VOID)
{return (BOOLEAN) ((PcdGet8 (PcdReportStatusCodePropertyMask) & REPORT_STATUS_CODE_PROPERTY_DEBUG_CODE_ENABLED) != 0);
}

PCD变量指的是PcdReportStatusCodePropertyMask,它的值可以是

//
// Declare bits for PcdReportStatusCodePropertyMask
//
#define REPORT_STATUS_CODE_PROPERTY_PROGRESS_CODE_ENABLED          0x00000001
#define REPORT_STATUS_CODE_PROPERTY_ERROR_CODE_ENABLED             0x00000002
#define REPORT_STATUS_CODE_PROPERTY_DEBUG_CODE_ENABLED             0x00000004

这里的3个值刚好和REPORT_STATUS_CODE_EX宏里面的一一对应。

关于REPORT_STATUS_CODE_EX的第一个参数type也对应的有3种:

///
/// Definition of code types. All other values masked by
/// EFI_STATUS_CODE_TYPE_MASK are reserved for use by
/// this specification.
///
///@{#define EFI_PROGRESS_CODE             0x00000001
#define EFI_ERROR_CODE                0x00000002
#define EFI_DEBUG_CODE                0x00000003
///@}

对于我们的DEBUG宏来说,使用的是EFI_DEBUG_CODE这种类型。

事实上REPORT_STATUS_CODE_EX宏可以单独的拿出来用,并使用不同的type类型,比如说BdsEntry.c中就有:

      REPORT_STATUS_CODE_EX (EFI_ERROR_CODE,PcdGet32 (PcdErrorCodeSetVariable),0,NULL,&gEdkiiStatusCodeDataTypeVariableGuid,SetVariableStatus,sizeof (EDKII_SET_VARIABLE_STATUS) + NameSize + DataSize);

ReportStatusCodeEx()函数具体实现

之后就涉及到函数ReportStatusCodeEx()了。它的实现非常多,PEI阶段、DXE阶段、SMM模块,RUNTIME模块等等都有不同的实现。

下面以DXE阶段为例,它的实现位于MdeModulePkg/Library/DxeReportStatusCodeLib/DxeReportStatusCodeLib.inf(对于Coreboot来说)。它的实现并不复杂,不过要注意其中有优先级的变化,这在实际应用中可能导致一些问题。

  1. ReportStatusCodeEx()函数又调用了InternalReportStatusCode()函数,而后者是通gEfiStatusCodeRuntimeProtocolGuid对应的Protocol来进行输出的。

  2. 对于gEfiStatusCodeRuntimeProtocolGuid,它是在IntelFrameworkModulePkg/Universal/StatusCode/RuntimeDxe/StatusCodeRuntimeDxe.inf中安装的(对于Coreboot来说),该Protocol只包含一个函数:ReportDispatcher,在InternalReportStatusCode()函数中就是调用了该函数实现。

    ReportDispatcher()函数需要关注的主要代码如下:

  if (FeaturePcdGet (PcdStatusCodeUseSerial)) {SerialStatusCodeReportWorker (CodeType,Value,Instance,CallerId,Data);}if (FeaturePcdGet (PcdStatusCodeUseMemory)) {RtMemoryStatusCodeReportWorker (CodeType,Value,Instance);}if (FeaturePcdGet (PcdStatusCodeUseDataHub)) {DataHubStatusCodeReportWorker (CodeType,Value,Instance,CallerId,Data);}if (FeaturePcdGet (PcdStatusCodeUseOEM)) {//// Call OEM hook status code library API to report status code to OEM device//OemHookStatusCodeReport (CodeType,Value,Instance,CallerId,Data);}

根据不同的PCD变量,可以选在不同的打印输出,甚至还有自定义的方式。

SerialStatusCodeReportWorker()为例,它到最后也是调用了SerialPortWrite(),这跟OVMF中的实现就对应上了。跟OVMF版本不同的是这里可以有更多的扩展。

ASSERT 宏实现

以上就是DEBUG的实现。另外再补充一个与DEBUG同一级别的调试代码ASSERT,它最终对应的代码是:

VOID
EFIAPI
DebugAssert (IN CONST CHAR8  *FileName,IN UINTN        LineNumber,IN CONST CHAR8  *Description)

使用ASSERT的时候要特别注意,当ASSERT之后代码可以进入CPU Dead Loop,这个时候代码就无法继续执行下去。

通常在发行版的UEFI中不会让这种情况出现,这个使用就需要设置DSC(或者DEC)中的一个PCD:PcdDebugPropertyMask,它可以设置不同的值,每个BIT代表的意义如下:

//
// Declare bits for PcdDebugPropertyMask
//
#define DEBUG_PROPERTY_DEBUG_ASSERT_ENABLED       0x01
#define DEBUG_PROPERTY_DEBUG_PRINT_ENABLED        0x02
#define DEBUG_PROPERTY_DEBUG_CODE_ENABLED         0x04
#define DEBUG_PROPERTY_CLEAR_MEMORY_ENABLED       0x08
#define DEBUG_PROPERTY_ASSERT_BREAKPOINT_ENABLED  0x10
#define DEBUG_PROPERTY_ASSERT_DEADLOOP_ENABLED    0x20

当设置了DEBUG_PROPERTY_ASSERT_DEADLOOP_ENABLED就会进入Dead Loop:

  //// Generate a Breakpoint, DeadLoop, or NOP based on PCD settings//if ((PcdGet8(PcdDebugPropertyMask) & DEBUG_PROPERTY_ASSERT_BREAKPOINT_ENABLED) != 0) {CpuBreakpoint ();} else if ((PcdGet8(PcdDebugPropertyMask) & DEBUG_PROPERTY_ASSERT_DEADLOOP_ENABLED) != 0) {CpuDeadLoop ();}

还没有说ASSERT的使用,这里举个例子:

ASSERT (FileHandle != NULL);

需要特别注意,*这里()中的条件是我们希望的*,所以ASSERT(FALSE)才是真正错误的情况。

其实还有一种类型的ASSERT:ASSERT_EFI_ERROR (Status);

就是说当Status是错误的返回值时就ASSERT。

EDK2之debug相关推荐

  1. 使用WinDbg搭建edk2 DEBUG环境

    1 使用WinDbg搭建edk2 DEBUG环境 相信所有开发UEFI的小伙伴在刚接触UEFI的时候肯定都是一头雾水,等到稍微入门了一点之后,当我们想开发一个新功能的时候碰到了一些奇奇怪怪的错误想要去 ...

  2. EDK2从搭建到运行

    参考书籍:UEFI编程实践 安装开发工具 可自行选择其他版本,此处使用VS2019 安装VS2019 安装路径可自定义: 勾选:使用C++的桌面开发 勾选:C++ Clang工具(最开始没有安装,导致 ...

  3. EDK2源码下载及环境搭建

    一.EDK2源码下载 上一片笔记中已经下载了git工具这里用git工具来下载edk2源码及编译工具 首先从github中将edk文件导入到我们的gittee仓库中再从我们的gitee仓库中下拉到我们的 ...

  4. 在centos7上编译EDK2

    环境 centos7 [root@localhost ~]# uname -a Linux localhost.localdomain 3.10.0-1160.el7.x86_64 #1 SMP Mo ...

  5. 使用VS2019配置EDK2安装教程

    1. 安装必要环境 ① 安装Visual Studio 2019.注意:如果机器上已安装了其他版本的Visual Studio,需要先将其卸载,而后再安装Visual Studio 2019. 安装时 ...

  6. UEFI开发与调试---edk2中的Package

    在开始编写UEFI APP之前,我们需要先对UEFI包和模块的概念有个了解. 在edk2的根目录下,我们可以发现有很多*Pkg命令的目录,这些实际上都是各个不同的包,每个包中都是一组模块的集合,每个包 ...

  7. linux下安装EDK2开发环境,EDK2开发环境搭建 - osc_y9wmeuxa的个人空间 - OSCHINA - 中文开源技术交流社区...

    EDK2开发环境搭建 来源 https://blog.csdn.net/rikeyone/article/details/80759724 EDK2全称为"uEFI Development ...

  8. 【转载】【UEFI学习】edk2中各个包介绍

    AppPkg UEFI Application Development Kit是一系列用来进行uefi app开发的套件,标准依赖库,工具以及demo,目标是降低UEFI app的开发门槛. MdeP ...

  9. Linux debug 常用命令

    CentOS/Redhat/Fedora 系統命令: 1. 安装软件源 # 导入public key rpm --import https://www.elrepo.org/RPM-GPG-KEY-e ...

最新文章

  1. 【嵌入式】从STM32F103ZET6移植到STM32F103RCT6的流程
  2. [Cocos2d-x]视差滚屏效果的实现
  3. Solr Cache使用介绍及分析
  4. 42 github 开源代码 ——README.md语法/相关操作等
  5. springMVC实现增删改查
  6. springmvc中Date类型转换
  7. TCP/IP参考模型入门
  8. 每天一点正则表达式积累(六)
  9. 里氏替换原则_趣谈设计模式之里氏替代原则
  10. Linux - Ubuntu Server基础
  11. Win10卸载微软sql服务器,卸载 SQL Server Management Studio
  12. iphone 控制 android手机,苹果手机如何远程控制安卓手机
  13. 韩顺平的php东方航空_韩顺平PHP从入门到精通视频教程
  14. android 8.0手机无法更新版本,微信8.0安卓机怎么安装更新 安卓微信更新不了8.0解决办法一览...
  15. pure-ftpd安装与使用
  16. 一本纯属个人的兴趣的书籍即将在未来面世
  17. C语言中静态变量的概念和用法
  18. 遇到押金不退,该怎么办?
  19. C语言中,的三种作用
  20. 第三十三课第九章Storage Structure Relationships

热门文章

  1. python有vlookup的功能么,vlookup函数功能非常强大,那在Python中如何实现?
  2. ChinaSoft 论坛巡礼 | 程序设计教育论坛
  3. 高效的使用DOM操作
  4. Java中的main( )函数
  5. C#的ListBox加入隐含对象处理手法与Delphi的对比
  6. watermark-removal: 一款超赞的开源图片去水印解决方案
  7. Null ModelAndView returned to DispatcherServlet with name ‘springmvc‘: assuming HandlerAdapter compl
  8. 如何设计一触式微交互
  9. 通过Timer和UpdatePanel控件实现NBA比赛的文字直播
  10. 顶点计划:996问题讨论