Cortex-M 内核本身提供了非常强大的异常处理机制。它可以非常有效的捕捉非法的内存访问以及其他一些异常。而我们常用的开发工具的异常处理就是使用了 Cortex-M 核的异常处理机制。

  在 ARM 平台上开发,开发工具的选择其实并不是很多,基本可以分为三大类:Keil MDK-ARM、IAR for ARM、GCC for ARM 系,其中用的比较多的基本就是 Keil MDK-ARM、IAR for ARM 这俩。而 GCC for ARM 系的 IDE 有很多,但是他们统一都是使用 GCC for ARM 作为编译器构建套件,IDE 都是各家自定义的。例如,ST 有 STM32CubeIDE、SEGGER 有 Embedded Studio。

工欲善其事必先利其器,下面我们就先从开发工具开始说起!

构建(Build)

  从源文件到可执行文件,需要经过预处理、编译、连接等一系列的处理过程,这个过程就被称为构建(Build) 。下图是一个典型的 ARM 程序构建流程图示:

构建使用的工具通常叫做 编译套件(Compile Collection)构建套件(Build Collection) 。目前,ARM 提供了两个版本的编译套件,下面是 ARM 的编译套件示意图:

最新的 ARM Compiler 6.x 使用了 LLVM 的 Clang 编译前端;ARM Compiler 5.x 使用的是爱迪生编译前端。通常,编译套件提供的工具都是命令行工具,且会独立于 IDE 独立提供(特例 IAR 就没有独立提供)。相对于桌面构建套件 GCC,它没有提供命令行的调试器!

下面是个对比图

关于 ARM 平台的各种编译套件,详见博文 ARM 之七 主流编译器(armcc、iar、gcc for arm)详细介绍

由于种种原因,构建也被称为编译!严格来说,编译只是构建中的一步。

集成开发环境(IDE)

  通常,编译套件提供的工具都是命令行工具。使用起来相对比较麻烦!集成开发环境(IDE,Integrated Development Environment )应运而生。
  集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具。集成了代码编写功能、分析功能、编译功能、调试功能等一体化的开发软件服务套。Keil MDK-ARM 就是其中之一了:

  • 代码编辑器:Keil uVision5(代码高亮使用的是 Scintilla)
  • 调试器:Keil uVision5 (含各种分析工具)
  • 编译套件:ARM 自家的 ARM Compiler 编译套件

下面我们就以 Keil MDK-ARM 为例来介绍一下!

Run-Time Environment

  Keil MDK-ARM 除了提供了 IDE 的功能外,还额外提供了一些嵌入式的软件解决方案,这些软件解决方案就是 Keil MDK-ARM 中的 Run-Time Environment。新建项目时,在选择设备后,RTE 窗口会自动打开。也可以通过菜单:Project ---> Mange ---> Run-Time Environment 打开。

其中,我们比较熟悉的应该是 CMSIS,无论用任何 IDE 来开发 ARM 平台的 MCU,CMSIS 都是必需的!可以看到里面有实时操作系统 RTOS、网络驱动、USB 驱动、文件系统等我们平时开发能用到的几乎所有东西。在使用时:

  • 绿色:当前所选的组件表示没有任何问题
  • 黄色:当前所选的组件有其他问题没有解决,需要进一步的选择其他配套组件或者操作。上图中 Validation Output 栏中会有说明
  • 红色:软件组件与其他组件冲突或没有安装在计算机上。上图中 Validation Output 栏中会有说明
  • 灰色:当前不可用。不适用于选定的 MCU。上图中 Validation Output 栏中会有说明
  1. 这些组件需要通过 PackInstaller 来安装(第一次安装 Keil MDK-ARM 后会自动打开,之后可以在菜单中找到)
  2. 目前仅 Keil 5 版本,且 专业版 授权才可以使用
  3. 新版本的 Keil 中,ST 的 MCU 驱动库已经从之前的标准外设库更换为了 HAL 库,在使用时会要求启动 STM32CubeMX 进行配置

  在实际工作中,我们项目更多的是自己手动移植,即使使用 Keil 建立项目,往往也仅仅是使用了 CMSIS 这一小部分,具体对比如下:

map 文件

  链接器列表文件或 Map 文件包含有关链接/定位过程的大量信息。在 Keil 中,需要通过 Project -> Options for Target -> Listing 界面如下的配置才可以输出 map 文件:

其中,各选项的基本功能如下:

  • Select Folder for Listings…: 选择存放清单文件的文件夹

  • Page Width: 为清单文件指定每行字符数

  • Page Length: 为清单文件指定每页的行数

  • Assembler Listing: 为汇编源文件创建列表文件,对应产生 源文件名.lst 的文件

    • Cross Reference: 列出有关符号的交叉引用信息,包括它们的定义位置以及宏的内部和外部的使用位置
  • C Compiler Listing: 为 C 源文件创建列表文件,对应产生 源文件名.txt 的文件 和 源文件名.lst 的文件

  • C Preprocessor Listing: 指示编译器生成预处理文件。 宏调用将被展开并且注释将被删除 对应产生 源文件名.i 的文件

  • Linker Listing: 让链接器为目标项目创建映射文件(map 文件)。对应的 armlink 参数为 --list=filename ,如果不选择则不会生成文件,对应生成 用户指定名.map 的文件。生成的 MAP 文件如下图所示:

    通常需要配合以下参数一起使用:

    • Memory Map: 包含一个内存映射,其中包含镜像中每个加载区,执行区和输入节的地址和大小,包括调试和链接器生成的输入节。对应的 armlink 参数为 --map
    • Callgraph: 以 HTML 格式创建函数的静态调用图文件。 调用图给出了镜像中所有函数的定义和参考信息。对应的 armlink 参数为 --callgraph 。该项会独立生成一个 配置的输出名.htm 的文件。如下图所示:

      其中显示了详细的调用关系。最重要的是,其中还有使用的栈的大小!
    • Symbols: 列出本地,全局和链接器生成的符号以及符号值。对应的 armlink 参数为 --symbols
    • Cross Reference: 列出输入节之间的所有交叉引用。对应的 armlink 参数为 --xref
    • Size Info: 给出镜像中每个输入对象和库成员的代码和数据(RO 数据,RW 数据,ZI 数据和调试数据)的大小的列表。对应的 armlink 参数为 --info sizes
    • Totals Info: 提供输入对象和库的代码和数据(RO 数据,RW 数据,ZI 数据和调试数据)大小的总和。对应的 armlink 参数为 --info totals
    • Unused Section Info: 列出从镜像文件中删除的所有未使用的部分。对应的 armlink 参数为 --info unused
    • Veneers Info: 提供链接器生成的 Thumb/ARM 胶合代码的详细信息。对应的 armlink 参数为 --info veneers
    1. ARM/Thumb交互(ARM/Thumb interworking)是指对汇编语言和 C/C++ 语言的 ARM 和 Thumb 代码进行连接的方法,它进行两种状态(ARM 和 Thumb)间的切换。
    2. 胶合代码(Veneer):在进行 ARM/Thumb 交互时,有时需使用额外的代码,这些代码被称为 胶合代码(Veneer)。
    3. AAPCS 定义了 ARM 和 Thumb 过程调用的标准。

关于 Map 文件的详细介绍,见博文 ARM 之十 ARMCC(Keil) map 文件(映射文件)详解

分散加载文件

  分散加载文件是对分散加载机制的内容描述,也被称为分散描述文件。分散加载文件包含一个或多个加载域。 每个加载域可以包含一个或多个执行域。 一个典型的分散加载文件如下图所示:

分散加载文件的语法使用标准的 Backus-Naur Form (BNF) 表示法。关于 BNF 可以去 ARM 官网了解。

更详细的信息,请参考博文ARM 之十三 armlink(Keil) 分散加载机制详解 及 分散加载文件的编写 。

Initialization file(.ini file)

  Initialization file 是 Keil uVision5 调试器的脚本文件。调试器脚本文件是包含调试器命令的纯文本文件。这些文件不是由工具创建的,而是由使用者创建它们来满足自己的特定需求。通常,它们用于配置调试器,或在运行程序之前设置或初始化某些东西。Keil 中在如下地方指定调试器脚本文件:

Keil uVision 用户指南中介绍的所有命令以及调试函数都可以在脚本文件中使用。部分命令的使用与在调试界面中 Command Window 中直接输入命令效果是一样的。可用命令与函数如下图:

具体每个命令的用法,请参考手册。下面我们来介绍一些常用的命令或者操作:

  1. LOAD 指令用来指示 µVision 调试器加载目标文件,格式:LOAD path\filename [options]。µVision 分析文件的内容以确定文件类型(如果无法确定文件类型,则会显示错误消息)。
    path\filename 必选项,取值如下:

    • Absolute Object File 或 ELF/DWARF File: 由链接器/定位器生成的,并包含完整的符号调试信息,类型信息和与调试信息一起转换的行号的文件。 如果未指定其他选项,则调试器将执行目标复位并提供内存映射设置。
    • Intel HEX File: 由 Object-to-Hex-Converter 程序产生,不包含符号调试信息,类型信息和行号信息。 仅在 CPU 指令级别支持程序测试。 不支持源代码级和符号调试。 加载 HEX 文件时,不执行目标复位。 因此,可能需要发出显式的 RESET 命令。

    [options] 是可选项,取值如下:

    • INCREMENTAL:将调试信息添加到现有符号表中。 这样可以进行多应用程序调试。
    • NOCODE: 仅加载符号信息,而忽略代码记录。 NOCODE 防止现有程序代码被覆盖。 此选项需要预先加载监视器的 CPU 驱动程序(MON51,MON251或 MON166)。
    • NORESET:(仅在某些目标上可用)防止在加载程序后生成 RESET 信号。 对于不存在该选项的目标,请改用INCREMENTAL 选项,该选项可以有效地执行相同的操作。

    示例:

    1. LOAD UserAPP.axf
    2. LOAD %L INCREMENTAL
  2. 直接在 RAM 中调试代码,必须使用调试器脚本文件,具体步骤如下:

    1. 配置程序空间可内存空间均在 RAM 中(也可以选择使用分散加载文件)
    2. 定义宏值 VECT_TAB_SRAM,将中断向量表放到 RAM 中。
    3. 选择调试器的脚本文件

      脚本文件的内容如下所示:

      /*----------------------------------------------------------------------------* Name:    DebugInRAM.ini* Purpose: RAM Debug Initialization File*----------------------------------------------------------------------------*/FUNC void Setup (void) {SP = _RDWORD(0x20000000);             // Setup Stack PointerPC = _RDWORD(0x20000004);             // Setup Program CounterXPSR = 0x01000000;                    // Set Thumb bit_WDWORD(0xE000ED08, 0x20000000);      // Setup Vector Table Offset Register
      }LOAD %L INCREMENTAL                     // Download to RAM
      Setup();g, main
      
    4. 配置程序的下载算法(这一步实际上并不需要,至于为啥没找到 ARM 的官方文档有说明)

      至此,就可以愉快的在内存中调试程序了!
  3. 调试前配置某些寄存器。例如,我们在调试时,需要先关闭看门狗。默认 Jlink 会自动给我们处理,ST-link 则需要我们自己处理。

       /*-------------------------------------------------------------------** Define the function to enable the trace port**-----------------------------------------------------------------*/FUNC void EnableTPIU(void) {_WDWORD(0xE0042004, 0x000000E0);    // Set 4-pin tracing via DBGMCU_CR}/*-------------------------------------------------------------------** Invoke the function at debugger startup**-----------------------------------------------------------------*/EnableTPIU();
    

可以直接在 Keil MDK-ARM 的安装目录中搜索 *.ini,会在 PACK 目录下找到很多类型的调试器脚本文件!

  注意,在某些 MCU 项目中,Keil 会自动生成 ./DebugConfig/xxxx.dbgconf 的文件。 该文件具有与调试初始化文件相同的功能,但是它仅用于可以实现寄存器分配与设备无关的那些 MCU中,所以,并不是所有 MCU 都又该文件!该文件实质上替代了调试初始化文件,如果可用,建议使用该文件。 它还允许通过配置向导而不是手工编码来配置调试器。 要打开此文件进行编辑:Options For Target -> Debug -> Settings button -> Pack tab

注意,该功能貌似支持 ARM 自己的仿真器 ULINK!!!

异常处理

  程序 BUG 的处理一直都是一件让人头疼的事,调试则给我们指出了一条路。下面我们介绍一下在 Keil MDK-ARM 调试中,如何分析处理一些异常情况。

  异常的处理严格来说和 IDE 并没有关系,因为它本质上是内核提供的一项功能!ARM®Cortex®-M 处理器实现了一个有效的异常模型,可捕获非法的内存访问和一些不正确的程序条件。 为了尽早发现问题,所有Cortex-M处理器都包含一个故障异常机制。 如果检测到故障,则触发相应的故障异常,并执行其中一个故障异常处理程序。 异常主要有如下几种:

  • HardFault: HardFault 是默认异常,可以由于异常处理期间的错误或因为异常无法由其他任何异常机制处理而触发。
  • MemManage: 检测对内存管理单元(MPU)中定义的区域的内存访问冲突; 例如,仅从具有读/写访问权限的内存区域执行代码。
  • BusFault: 在指令获取,数据读/写,中断向量获取以及中断(进入/退出)时寄存器堆叠(保存/恢复)时检测内存访问错误。
  • UsageFault: 检测未定义指令的执行,加载/存储多字节时的未对齐的内存访问。 启用后,还将检测除零和其他未对齐的内存访问。

  HardFault 异常始终处于启用状态,并且具有固定的优先级(高于其他中断和异常,但低于不可屏蔽中断NMI)。 因此,在禁用故障异常或在执行故障异常处理程序的过程中发生故障的情况下,将执行 HardFault 异常。

  所有其他故障异常(MemManage fault、BusFault和UsageFault)都具有可编程的优先级。复位后,这些异常被禁用,并可能在系统或应用软件中使用系统控制块(SCB)中的寄存器启用。

异常优先级提升

  在某些情况下,具有可配置优先级的故障会被视为 HardFault。 这称为优先级升级,故障描述为升级为 HardFault。 在以下情况下会升级为 HardFault:

  • 故障处理程序会导致与正在处理的故障相同类型的故障。 之所以升级为HardFault,是因为处理程序无法抢占自身(它必须具有与当前优先级相同的优先级)。
  • 故障处理程序导致的故障与正在处理的故障具有相同或更低的优先级。 这是因为新故障的处理程序无法抢占当前正在执行的故障处理程序。
  • 发生故障,并且未启用该故障的处理程序。

  如果在进入BusFault 处理程序时在堆栈推送期间发生 BusFault,则 BusFault 不会升级为 HardFault。 这意味着,如果损坏的堆栈导致故障,即使处理程序的堆栈推送失败,故障处理程序也会执行。

异常类型

  下表显示了故障类型,故障处理程序,故障状态寄存器以及指示发生故障的寄存器位名称。

SCB

系统控制块(SCB)提供系统实现信息和系统控制。这包括系统异常的配置、控制和报告。

异常控制寄存器

系统控制块(SCB)的一些寄存器用于控制故障异常:

  • CCR: 配置和控制寄存器(CCR)用于控制 UsageFault 的行为,用于零除和未对齐的内存访问。寄存器地址 0xE000ED14,默认值为 0,按位使用,置 1 有效!

    在使用 Keil 调试时,可以直接使用 GUI 界面查看:
  • SHP: 系统处理程序优先级寄存器(SHP)控制异常优先级。寄存器地址 0xE000ED18,共有 12 个字节,复位值为 0,按字节使用。通常如下
    • SHP[0]: Memory Management Fault 的优先级
    • SHP[1]: BusFault 的优先级
    • SHP[2]: UsageFault 的优先级
  • SHCSR: 系统处理程序控制和状态寄存器(SHCSR)启用系统处理程序,并指示 BusFault,MemManage 故障和 SVC 异常的挂起状态。寄存器地址 0xE000ED24,默认值为 0,按位使用,置 1 有效!

异常状态寄存器

下表列出了故障状态寄存器和故障地址寄存器的名称,并显示了每个寄存器的存储器地址。

在使用 Keil 进行调试时,可以使用如下 GUI 界面查看:

关于寄存器中每个位的具体含义,参见 ARM 官方文档: Using Cortex-M3/M4/M7 Fault Exceptions .pdf。

示例

  ARM 官方提供了用于验证以上异常的示例,我们可以从 www.keil.com/appnotes/docs/apnt_209.asp 页面上下载,下载之后我们可以下载自己的 MCU 中进行测试。需要注意的是,示例仅仅是针对于 Cortex-M 核的,所以在调试的时候没法选择对应的下载算法,这个时候就可以用我们上面提到的内存调试了!

调试

  这里我只介绍如何进行在线调试。在线调试是指不打断当前程序运行的情况下进入调试状态,以方便观察程序当前运行状态,便于查找问题的调试方法。这种调试方法在 PC 端非常常见,就是被称为 Attach 的调试方式。其实,这个调试方式仅仅是 Keil 没有,如果使用其他类似的调试工具的话,在线调试一般都有:

Keil 在默认情况下不支持在线调试,Keil 如果要进行在线调试,必须手动配置如下:

定位异常位置

在 Keil 的调试状态下,定位异常发生的位置主要有以下两种方法:

  1. 在 Call Stack + Locals 窗口中右键单击硬错误处理程序,并选择Show Caller Code,此时 Keil 便会自动调整的异常发生的位置

    根据异常的类型,调试器将突出显示引起异常的指令或引起故障的指令之后的指令。 这取决于导致异常的指令是否实际完成了执行。
  2. 根据内存堆栈进行回溯。
      异常会保存寄存器 R0 ~ R3,R12,PC 和 LR 以及 MSP 或 PSP 的状态(取决于发生异常时使用的堆栈)。 当前链接寄存器 LR 包含用于正在处理的异常的 EXC_RETURN 值,该值指示哪个堆栈保存了来自应用程序上下文的保留寄存器值。 如果 EXC_RETURN 的位 2 为零,则使用主堆栈(保存 MSP),否则使用进程堆栈(PSP)。

    我们以上面的例子来具体看看如何操作:

    1. 查看寄存器窗口中寄存器的值

      跳转到异常后,LR 寄存器的值为 0xFFFFFFF9 = b_11111111111111111111111111111001,bit 2 = 0,表示主堆栈(MSP)包含最近存储的寄存器值。MSP 的值为 0x20011158

        这里需要注意,在正常的 C 语言函数调用中,LR 应该是 PC 的值,但是进入异常时放的值被称为 EXEC_RETURN,该值会在异常返回时被用到。下图是个正常函数调用时寄存器的情况:

    2. 在 Memory 窗口中,查看堆栈。这个堆栈就保存了 mcu 跳转到异常之前的信息

      • 返回地址 = 0x20001478 就是导致异常的指令的地址
      • R0 ~ R3、R12、LR 这些是发生异常之前寄存器中的值(可以与上面进入异常之前的图中的值对比)

      在异常入口处被压入栈空间的数据块被称为栈帧

    3. 在 Disassembly 窗口中,转到 返回地址中的显示的地址

      需要注意的是,换个地址其实是汇编语句的地址,如果直接看源码,可能位置稍有偏差!

跟踪(Trace)

  基于 Arm Cortex-M 处理器的设备使用 Arm CoreSight 技术引入了强大的调试和跟踪功能。调试就像照相机,程序(在断点处)停下来才能通过 JTAG/SWD 接口看调试信息;跟踪(Trace)就像录像机,可以通过 ETM 接口纪录、回放整个调试过程。

ITM

  ITM(Instrument Trace Macrocell,测量跟踪宏单元)是一个应用程序驱动的跟踪源,它支持 printf 样式调试来跟踪操作系统(OS)和应用程序事件,并发出诊断系统信息。该块的主要用途是:

  • 支持 printf 风格调试
  • 跟踪操作系统和应用程序事件
  • 发出诊断系统信息(DWT 生成这些包,ITM 发出它们)
  • 时间戳功能 时间戳是相对于数据包发出的。 ITM 包含一个 21 位计数器来生成时间戳。 Cortex®-M4时钟或串行线查看器(SWV)输出的位时钟速率为计数器提供时钟。

  ITM 发出的数据包将输出到 TPIU(跟踪端口接口单元)。 TPIU 的格式化程序会添加一些额外的数据包,然后将完整的数据包序列输出到调试器主机。在 STM32 中使用 ITM 之前,必须启用调试异常和监视器控制寄存器的 TRCEN 位。

  串行线查看器(Serial Wire Viewer,SWV)提供来自 Cortex-M3/M4/M7 设备内各种来源的实时数据跟踪信息。 当系统处理器继续全速运行时,它通过 SWO 引脚传输。所以,ITM 机制要求使用 SWD 方式接口。

ETM

  ETM(Embedded Trace Macrocell,嵌入式跟踪宏单元)可以重建程序执行。 使用数据监视点和跟踪(DWT)组件或指令跟踪宏单元(ITM)跟踪数据,以及使用嵌入式跟踪宏单元(ETM)执行跟踪指令。

参考

  1. Keil MDK-ARM 帮助手册
  2. Using Cortex-M3/M4/M7 Fault Exceptions .pdf
  3. Procedure Call Standard for the ARM® Architecture.pdf

ARM 之十二 Cortex-M 内核异常处理、异常定位方法、在线调试、Keil MDK-ARM 的使用相关推荐

  1. vmlinux 反汇编_ARM Linux内核驱动异常定位方法分析--反汇编方式

    通常认为,产生异常的地址是lr寄存器的值,从上面的异常信息可以看到[lr]的值是c01a4e30. 接下来,我们可以通过内核镜像文件反汇编来找到这个地址.内核编译完成后,会在内核代码根目录下生成vml ...

  2. jQuery学习(十二)—jQuery中对象的查找方法总结

    jQuery学习(十二)-jQuery中对象的查找方法总结 一.find方法 作用:在元素1中查找元素2,类似于选择器中的后代选择器 格式:元素1.find(元素2),元素2为CSS选择器或者jQue ...

  3. [ Coding七十二绝技 ] 如何利用Java异常快速分析源码

    [ Coding七十二绝技 ] 如何利用Java异常快速分析源码 参考文章: (1)[ Coding七十二绝技 ] 如何利用Java异常快速分析源码 (2)https://www.cnblogs.co ...

  4. jmeter(二十二)内存溢出原因及解决方法

    jmeter(二十二)内存溢出原因及解决方法 参考文章: (1)jmeter(二十二)内存溢出原因及解决方法 (2)https://www.cnblogs.com/imyalost/p/7901064 ...

  5. (十二)linux内核定时器

    目录 (一)内核定时器介绍 (二)内核定时器相关接口 (三)使用步骤 (四)实例代码 (一)内核定时器介绍 内核定时器并不是用来简单的定时操作,而是在定时时间后,触发事件的操作,类似定时器中断,是内核 ...

  6. 嵌入式Linux(二十二)Linux内核分析及移植

    1. 编译linux内核   NXP从linux官网下载内核,然后移植到自己的CPU,我们的移植是基于NXP,再移植到自己的开发板. 制作一个sh: #!/bin/sh make ARCH=arm C ...

  7. 2021年大数据Spark(二十二):内核原理

    目录 Spark内核原理 RDD 依赖 窄依赖(Narrow Dependency) ​​​​​​​Shuffle 依赖(宽依赖 Wide Dependency) ​​​​​​​如何区分宽窄依赖 ​​ ...

  8. 五十二、PHP内核探索:使用哈希表API ☞ Zend把与HashTable有关的API分成了好几类

    Zend把与HashTable有关的API分成了好几类以便于我们寻找,这些API的返回值大多都是常量SUCCESS或者FAILURE. 创建HashTable 下面在介绍函数原型的时候都使用了ht名称 ...

  9. JAVA抽象类空指针异常_[ Coding七十二绝技 ] 如何利用Java异常快速分析源码

    前言 异常一个神奇的东西,让广大程序员对它人又爱又恨. 爱它,通过它能快速定位错误,经过层层磨难能学到很多逼坑大法. 恨他,快下班的时刻,周末的早晨,它踏着七彩云毫无征兆的来了. 今天,要聊的是它的一 ...

最新文章

  1. 替换k个字符后最长重复子串
  2. 智能布线—更好的安全性
  3. golang 定义一个空切片_Golang简单入门教程——函数进阶使用
  4. Android Bitmap面面观
  5. Java基础面试16问
  6. 基于哈夫曼编码完成的文件压缩及解压
  7. hystrix源码小贴士之中断
  8. ligerGrid简单例子--通过后台转数据
  9. Python之十点半小游戏
  10. Cocos2d-x 设置竖屏的方法 2.0以上版本
  11. CIF、DCIF、D1分辨率
  12. 学好水彩,给自己做个手机壳吧
  13. VS程序中使用ODBC登陆sql数据库的时候出现18456错误
  14. im即时通讯开发:高可用、易伸缩、高并发的IM群聊、单聊架构方案设计
  15. !!. 与 ?. 的区别
  16. ORA-00911: 无效字符 细节一定要注意
  17. Task04: 文字图例尽眉目(12月datawhale组队)
  18. Reactor响应式流的核心机制——背压机制
  19. 2021年全球直线导轨市场规模大约为142亿元(人民币),预计2028年将达到195亿元
  20. R语言 向量排序与运算

热门文章

  1. Centos7 Java8的安装
  2. 黑客教父龚蔚演讲:钓鱼WiFi 也能照用不误
  3. JavaScript 专题之如何判断两个对象相等
  4. 数据结构导论初步理解
  5. openlayer 3 在layer上添加feature
  6. DataGridView数据验证CellValidating()
  7. INotifyPropertyChanged 接口
  8. SQL Server Compact的DLL文件介绍
  9. 介绍微软一个罕为人知的无敌命令
  10. 计算卷积神经网络中参数量