stm32启动过程解析startup启动文件

1.STM32的启动过程模式

1.1 根据boot引脚决定三种启动模式

在这里插入图片描述

  1. 复位后,在 SYSCLK 的第四个上升沿锁存 BOOT 引脚的值。BOOT0 为专用引脚,而 BOOT1 则与 GPIO 引脚共用。一旦完成对 BOOT1 的采样,相应 GPIO 引脚即进入空闲状态,可用于其它用途。BOOT0与BOOT1引脚的不同值指向了三种启动方式:

    • 从主Flash启动。主Flash指的是STM32的内置Flash。选择该启动模式后,内置Flash的起始地址将被重映射到0x00000000地址,代码将在该处开始执行。一般我们使用JTAG或者SWD模式下载调试程序时,就是下载到这里面,重启后也直接从这启动。
    • 从系统存储器启动。系统储存器指的是STM32的内置ROM,选择该启动模式后,内置ROM的起始地址将被重映射到0x00000000地址,代码在此处开始运行。ROM中有一段出厂预置的代码,这段代码起到一个桥的作用,允许外部通过UART/CAN或USB等将代码写入STM32的内置Flash中。这段代码也被称为ISP(In System Programing)代码,这种烧录代码的方式也被称为ISP烧录。关于ISP、ICP和IAP之间的区别将在后续章节中介绍。
    • 从嵌入式SRAM中启动。显然,该方法是在STM32的内置SRAM中启动,选择该启动模式后,内置SRAM的起始地址将被重映射到0x00000000地址,代码在此处开始运行。这种模式由于烧录程序过程中不需要擦写Flash,因此速度较快,适合调试,但是掉电丢失。
  2. 总结:不同的配置决定了,MCU将何处映射到0x00000000。从这里又可以看到一点,MCU眼里只有0x00000000。至于为啥可以从Flash(0x08000000)启动,就是因为MCU内部做了映射。从其他位置启动时同理,如第一列,从Flash启动时,原本储存在0x0800000的程序映射到0x000000位置,,如下图是STM32F4xx中文参考手册中的图,可以看到类似的表述。同时,在下图中也展示了STM32F4xx中统一编址下,各内存的地址分配,注意一点,即使相应的内存被映射到了0x00000000起始的地址,通过其原来地址依然是可以访问的。

在这里插入图片描述

1.2 内存空间解释

  1. 上电序列或系统复位后,ARM处理器先从0x0000 0000地址获取栈顶值,再从0x0000 0004地址获得引导代码的基地址,然后从引导代码的基地址开始执行程序。所选引导源对应的存储空间会被映射到引导存储空间,即从0x0000 0000开始的地址空间。
  2. 如果片上SRAM(开始于0x2000 0000的存储空间)被选为引导源,用户必须在应用程序初始化代码中通过修改NVIC异常向量表和偏移地址将向量表重置到SRAM中。
  3. 当主FLASH存储器被选择作为引导源,从0x0800 0000开始的存储空间会被映射到引导存储空间。由于主FLASH存储器的Bank0或Bank1均可映射到地址0x0800 0000(通过配置SYSCFG_CFG0寄存器的FMC_SWP控制位),所以,微控制器可以使用该方法从Bank0或Bank1中启动。
  4. 为了使能引导块功能,选项字节中的BB控制位需要被置位。当该控制位被置位并且主FLASH存储器被选择作为引导源,微控制器从引导装载程序中启动并且引导装载程序跳至主FLASH存储器的Bank1中执行代码。在应用程序初始化代码中,用户必须通过修改NVIC异常向量表和偏移地址将向量表重置到Bank1基地址。

1.3 启动后bootloader做了什么?

在这里插入图片描述

  1. 根据BOOT引脚确定了启动方式后,处理器进行的第二大步就是开始从0x00000000地址处开始执行代码,而该处存放的代码正是bootloader即.s启动文件,名为startup_xxxx.s,在.s启动文件中指明了向量表,默认在0x08000000处,栈顶是一个栈顶指针,之后就是Reset_handler复位中断入口向量地址等等中断服务函数向量地址,每个地址4字节,所以是从MSP偏移0x00004字节处取出Reset_Handler。
  2. bootloader,也可以叫启动文件,每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。

1.3 bootloader中对内存的搬移和初始化

本节针对程序在内置Flash中启动的情况进行分析。
在这里插入图片描述

  1. 我们知道烧录的镜像文件中包含只读代码段.text,已初始化数据段.data和未初始化的或者初始化为0的数据段.bss。代码段由于是只读的,所以是可以一直放在Flash中,CPU通过总线去读取代码执行就OK,但是.data段和.bss段由于会涉及读写,为了更高的读写效率是要一定搬到RAM中执行的,因此bootloader会执行很重要的一步,就是会在RAM中初始化.data和.bss段,搬移或清空相应内存区域。
  2. 因此我们知道,当启动方式选择的是从内置Flash启动的时候,代码依旧是在Flash中执行,而数据则会被拷贝到内部SRAM中,该过程是由bootloader完成的。bootloader在完成这些流程之后,就会将代码交给main函数开始执行用户代码。

1.4 ISP、IAP、ICP三种烧录方式

  • 虽然这个小节稍稍偏题,但是由于上面在3中启动方式中介绍过了ISP烧录,因此一并在此介绍剩下的两种烧录方式。

    1. ICP(In Circuit Programing)。在电路编程,可通过CPU的Debug Access Port 烧录代码,比如ARM Cortex的Debug Interface主要是SWD(Serial Wire Debug)或JTAG(Joint Test Action Group);
    2. ISP(In System Programing)。在系统编程,可借助MCU厂商预置的Bootloader 实现通过板载UART或USB接口烧录代码。
    3. IAP(In Applicating Programing)。在应用编程,由开发者实现Bootloader功能,比如STM32存储映射Code分区中的Flash本是存储用户应用程序的区间(上电从此处执行用户代码),开发者可以将自己实现的Bootloader存放到Flash区间,MCU上电启动先执行用户的Bootloader代码,该代码可为用户应用程序的下载、校验、增量/补丁更新、升级、恢复等提供支持,如果用户代码提供了网络访问功能,IAP 还能通过无线网络下载更新代码,实现OTA空中升级功能。
  • IAP和ISP 的区别。
    a、ISP程序一般是芯片厂家提供的。IAP一般是用户自己编写的
    b、ISP一般支持的烧录方式有限,只有串口等。IAP就比较灵活,可以灵活的使用各种通信协议烧录
    c、isp一般需要芯片进行一些硬件上的操作才行,IAP全部工作由程序完成,不需要去现场
    d、isp一般只需要按格式将升级文件通过串口发送就可以。IAP的话控制相对麻烦,如果是OTA的话还需要编写后台的。
    e、注意,这里介绍的bootloader功能显然跟之前介绍的启动文件bootloader有所区别,其目的是为了能接受外部镜像进行烧录,而不是为了运行普通用户程序。

2.启动文件startup_xxx.s(非GCC环境)

2.1 主要功能

  1. 在此我使用stm32f103-keil环境的启动文件startup_stm32f103xb.s为例进行简单的表述。startup_stm32f103xb.s是上电后执行的第一段代码:
    在这里插入图片描述
  2. 可看到该程序执行内容是:
1.初始化堆栈指针 SP=_initial_sp
2.初始化 PC 指针=Reset_Handler;并实现了复位的异常处理函数Reset_Handler
3.初始化中断向量表
4.配置系统时钟
5.调用 C 库函数_main 初始化用户堆栈,然后进入 main 函数。

2.2启动文件内容分析

0. 启动文件涉及的几个汇编命令
在这里插入图片描述

1. Stack栈
* 栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。当程序较大时,需要修改栈的大小,不然可能会出现的HardFault的错误。
在这里插入图片描述

第32行:表示开辟栈的大小为 0X400(1KB),EQU是伪指令,相当于C 中的 define。
第34行:开辟一段可读可写数据空间,ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。ARER 后面的关键字表示这个段的属性。段名为STACK,可以任意命名;NOINIT 表示不初始化;READWRITE 表示可读可写,ALIGN=3,表示按照 8 字节对齐。
第35行:SPACE 用于分配大小等于 Stack_Size连续内存空间,单位为字节。
第37行: __initial_sp表示栈顶地址。栈是由高向低生长的。

2. Heap堆

  • 堆主要用来动态内存的分配,像malloc()函数申请的内存就在堆中。
    在这里插入图片描述
  • 开辟堆的大小为 0X200(512 字节),名字为 HEAP,NOINIT 即不初始化,可读可写,8字节对齐。__heap_base 表示对的起始地址,__heap_limit 表示堆的结束地址。
  • PRESERVE8用于指定当前文件的堆栈按照8字节对齐,THUMB表示后面的指令兼容THUMB指令,现在Cortex-M系列的都使用THUMB-2指令集,THUMB-2是32位的,兼容16位和32位的指令,是thumb的超集。
    3. 向量表
    * Cortex-M3 内核规定,起始地址必须存放栈顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在 Cortex-M3 内核复位后,会自动从起始地址的下一个 32 位空间取出复位中断入口向量,跳转执行复位中断服务程序。Cortex-M3 内核固定了中断向量表的位置, 但是起始地址是可变化的,__initial_sp就是栈顶指针。
    * 向量表是一个WORD( 32 )数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。
    * 值得注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值,后面会具体讲解。
    在这里插入图片描述
    在这里插入图片描述
第55行:定义一块代码段,段名字是RESET,READONLY 表示只读。
第56-58行:使用EXPORT将3个标识符申明为可被外部引用,声明 __Vectors、__Vectors_End 和__Vectors_Size 具有全局属性。
第60行:__Vectors 表示向量表起始地址,DCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码,中断向量表 存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序。在60行之后,依次定义了中断服务程序的入口地址。
第121行:__Vectors_End 为向量表结束地址。
第123行:__Vectors_Size则是向量表的大小,向量表的大小是通过__Vectors 和__Vectors_End 相减得到的。

4. 复位程序Reset_Handler
* 复位程序是系统上电后执行的第一个程序,复位程序也是中断程序,只是这个程序比较特殊,因此单独提出来讲解。
在这里插入图片描述

第128行:定义了一个服务程序,PROC表示程序的开始。
第129行:使用EXPORT将Reset_Handler申明为可被外部引用,后面WEAK表示弱定义,如果外部文件定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位程序可以由用户在其他文件重新实现,这种写法在HAL库中是很常见的。
第130-131行:表示该标号来自外部文件,SystemInit()是一个库函数,在system_stm32f1xx.c中定义的,__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,这个是由编译器完成的,该函数最终会调用我们自己写的main函数,从而进入C世界中。
第132行:这是一条汇编指令,表示从存储器中加载SystemInit到一个寄存器R0的地址中。
第133行:汇编指令,表示跳转到寄存器R0的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。
第134行:和132行是一个意思,表示从存储器中加载__main到一个寄存器R0的地址中。
第135行:和133稍微不同,这里跳转到至指定寄存器的地址后,不会返回。
第136行:和PROC是对应的,表示程序的结束。

5. 中断服务程序

  • 我们平时要使用哪个中断,就需要编写相应的中断服务程序,只是启动文件把这些函数留出来了,但是内容都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置罢了。
    * 这部分没啥好说的,和服务程序类似的,只需要注意‘B .’语句,B表示跳转,这里跳转到一个‘.’,即表示无线循环。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    6. 堆栈初始化
    • 堆栈初始化是由一个IF条件来实现的,MICROLIB的定义与否决定了堆栈的初始化方式。

    • 这个定义是在Options->Target中设置的
      *

    • 需要注意的是,ALIGN表示对指令或者数据存放的地址进行对齐,缺省表示4字节对齐。

    • 如果没有定义 __MICROLIB , 则采用双段存储器模式,即堆区和栈区是分开的(如果不采用双段模式,因为堆和栈增长的方向是相反的,如果撞上了,程序会崩溃),且声明标号__user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。

在这里插入图片描述

以上就是Startup文件全部内容

3.启动文件分析(GCC环境)

GCC环境下STM32 的启动出除了需要 startup_xxxx.s 文件,还需要一个链接文件 .ld 文件:
在这里插入图片描述

3.1 .ld 链接文件

先从STM32L051C8Tx_FLASH.ld 文件来看,链接文件主要制定了入口函数,堆栈大小和数据段的整体布局。

3.1.1 开辟栈空间和堆空间

指定入口地址,开辟栈空间和堆空间:
在这里插入图片描述

第53行:指定入口地址为Reset_Handler复位中断
第56行:最大的RAM地址
第58-59行:指定堆、栈大小

3.1.2 指定布局

指定各数据段的布局:
在这里插入图片描述

第69行:输出文件布局
第72-79行:向量表,放在最前面
第80-94行:制定text段,存放代码
第97-103行:只读数据段

在这里插入图片描述

第134行:保存.data的地址
第137-146行:指定数据段在RAM中,只是刚开始保存在FLASH中,启动的时候拷贝到RAM
第150-162行:指定.bss段
第165-173行:最后根据前面定义的堆栈大小指定堆栈段

3.2 .s启动文件

3.2.1 基本说明

startup_stm32l051xx.s开头部分是基本说明:
在这里插入图片描述

第29行:CPU类型
第44行:thumb指令集
第38-46行:_sidata保存.data地址的变量,_sdata保存.data段的起始地址, _edata保存.data段的结束地址, _sbss保存.bss起始地址, _ebss保存.bss结束,这几个都在链接文件中定义

3.2.2 Reset_Handler

  • Reset_Handler 是程序最开始执行的地方,设置栈顶指针:
    在这里插入图片描述
第58-60行:.section设置新的代码段,.weak申明,.type将Reset_Handler指定为函数
第64行:系统初始化时钟
第67-70行:将.data段从flash放到RAM中去

3.2.3 跳转到SystemInit 和 main

回过头来看一下前面讲到的启动文件所做的工作:

设置堆栈指针 SP = _initial_sp
设置PC指针 = Reset_Handler
配置系统时钟
配置外部 SRAM 用于程序变量等数据存储(可选)
调用C库的 _main 函数,最终调用main函数

最终这里也是跳转到了main函数:

在这里插入图片描述

3.2.4 中断向量表部分

  • _estack是栈顶的值,这个值保存在flash的0地址出,flash的地址为0x08000000,所以0x08000000保存_estack的值,接下来就是Reset_Handler,和编译器环境中的启动文件部分一样
    在这里插入图片描述

参考

  1. https://blog.csdn.net/Setul/article/details/121685929
  2. https://jiayu.blog.csdn.net/article/details/135032170?fromshare=blogdetail&sharetype=blogdetail&sharerId=135032170&sharerefer=PC&sharesource=tao_sc&sharefrom=from_link
  3. https://blog.csdn.net/zhuimeng_ruili/article/details/119709888
  4. https://blog.csdn.net/weixin_42328389/article/details/120656722?fromshare=blogdetail&sharetype=blogdetail&sharerId=120656722&sharerefer=PC&sharesource=tao_sc&sharefrom=from_link

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/61167.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构查找-哈希表(开发地址法+线性探测法)+(创建+查找+删除代码)+(C语言代码)

#include<stdlib.h> #include<stdio.h> #include<stdbool.h> #define NULLKEY -1//单元为空 #define DELKEY -2//单元内容被删除 #define M 20 typedef struct {int key;//关键字int count;//统计哈希冲突探测次数 }HashTable; //插入到哈希表 void InsertHT…

〔 MySQL 〕数据类型

目录 1.数据类型分类 2 数值类型 2.1 tinyint类型 2.2 bit类型 2.3 小数类型 2.3.1 float 2.3.2 decimal 3 字符串类型 3.1 char 3.2 varchar 3.3 char和varchar比较 4 日期和时间类型 5 enum和set mysql表中建立属性列&#xff1a; 列名称&#xff0c;类型在后 n…

数据库审计工具--Yearning 3.1.9普民的使用指南

1 页面登录 登录地址:18000 &#xff08;不要勾选LDAP&#xff09; 2 修改用户密码 3 DML/DDL工单申请及审批 工单申请 根据需要选择【DML/DDL/查询】中的一种进行工单申请 填写工单信息提交SQL检测报错修改sql语句重新进行SQL检测&#xff0c;如检测失败可以进行SQL美化后…

tcp 超时计时器

在 TCP&#xff08;传输控制协议&#xff09;中有以下四种重要的计时器&#xff1a; 重传计时器&#xff08;Retransmission Timer&#xff09; 作用&#xff1a;用于处理数据包丢失的情况。当发送方发送一个数据段后&#xff0c;就会启动重传计时器。如果在计时器超时之前没有…

前端(4)——demo分享

这两天需要用HTML、CSS和js简单组合一个html网页用于展示一些数据内容&#xff0c;这是我简单组合别人的一些文件形成的简单demo&#xff0c;大家也可以拿过去使用。 登录界面&#xff1a; 场景选择界面&#xff0c;有五个场景&#xff0c;每个场景中都需要展示一些特定的数据…

Java-空链处理

什么是 null 在 Java 中&#xff0c;null 是一个非常常见的关键字&#xff0c;用于表示“没有值”或“空”。然而&#xff0c;对于初学者来说&#xff0c;null 的本质可能会感到有些困惑。在本文中&#xff0c;我们将详细探讨 null 在 Java 中的含义和使用。 在 Java 中&…

PyTorch使用教程-深度学习框架

PyTorch使用教程-深度学习框架 1. PyTorch简介 1.1-什么是PyTorch ​ PyTorch是一个广泛使用的开源机器学习框架&#xff0c;特别适合深度学习的应用。它以其动态计算图而闻名&#xff0c;允许在运行时修改模型&#xff0c;使得实验和调试更加灵活。PyTorch提供了强大的GPU加…

供应链管理、一件代发系统功能及源码分享 PHP+Mysql

随着电商行业的不断发展&#xff0c;传统的库存管理模式已经逐渐无法满足市场需求。越来越多的企业选择“一件代发”模式&#xff0c;即商家不需要自己储备商品库存&#xff0c;而是将订单直接转给供应商&#xff0c;由供应商直接进行发货。这种方式极大地降低了企业的运营成本…

Pr:音频过渡

Adobe Premiere Pro 自带一组共三个音频过渡 Audio Transitions效果。 对音频剪辑之间应用交叉淡化 Crossfade过渡&#xff0c;操作方式类似于应用视频过渡效果。 对于交叉淡化&#xff0c;要保证前剪辑的出点之后及后剪辑的入点之前有足够的预留内容&#xff08;也称“手柄”&…

前端页面开发步骤详解

目录 前言1. 页面搭建1.1 HTML 标签结构1.2 CSS 样式设计 2. 数据绑定与事件处理2.1 数据绑定2.2 表单校验 3. 调用后台接口3.1 接口文档与工具封装3.2 参数传递与接口调用 结语 前言 在前端开发过程中&#xff0c;从页面搭建到与后台接口对接是一个必不可少的完整流程。无论是…

A037-基于Spring Boot的二手物品交易的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

两行命令搭建深度学习环境(Docker/torch2.5.1+cu118/命令行美化+插件),含完整的 Docker 安装步骤

深度学习环境的配置过于繁琐&#xff0c;所以我制作了两个基础的镜像&#xff0c;希望可以帮助大家节省时间&#xff0c;你可以选择其中一种进行安装&#xff0c;版本说明&#xff1a; base 版本基于 pytorch/pytorch:2.5.1-cuda11.8-cudnn9-devel&#xff0c;默认 python 版本…

EXCEL延迟退休公式

如图&#xff1a; A B为手工输入 C2EOMONTH(A2,B2*12) D2EOMONTH(C2,IF(C2>DATEVALUE("2025-1-1"),INT((DATEDIF(DATEVALUE("2025-1-1"),C2,"m")4)/4),0)) E2EOMONTH(A2,B2*12IF(EOMONTH(A2,B2*12)>DATEVALUE("2025-1-1"),INT(…

区块链技术在数据安全中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 区块链技术在数据安全中的应用 区块链技术在数据安全中的应用 区块链技术在数据安全中的应用 引言 区块链技术基础 1.1 区块链的…

GIT 入门详解指南

前言&#xff1a; 注&#xff1a;本博客仅用于记录本人学习过程中对git的理解&#xff0c;仅供学习参考&#xff0c;如有异议请自行查资料求证 安装 使用git之前必须完成git的安装&#xff0c;Git 目前支持 Linux/Unix、Solaris、Mac和 Windows 平台上运行 git 安装教程 基本…

讯飞、阿里云、腾讯云:Android 语音合成服务对比选择

在 移动端 接入语音合成方面&#xff0c;讯飞和腾讯云等都是优秀的选择&#xff0c;但各有其特点和优势。咱们的需求是需要支持普通话/英语/法语三种语言&#xff0c;以下是对各个平台的详细比较&#xff1a; 一、讯飞语音合成介绍 与语音听写相反&#xff0c;语音合成是将一段…

HarmonyOS本地存储-Preferences(用户首选项)的使用

一&#xff0c;用户首选项简述 ohos.data.preferences (用户首选项) 用户首选项为应用提供Key-Value键值型的数据处理能力&#xff0c;支持应用持久化轻量级数据&#xff0c;并对其修改和查询。 数据存储形式为键值对&#xff0c;键的类型为字符串型&#xff0c;值的存储数据…

【机器学习】回归模型(线性回归+逻辑回归)原理详解

线性回归 Linear Regression 1 概述 线性回归类似高中的线性规划题目。线性回归要做的是就是找到一个数学公式能相对较完美地把所有自变量组合&#xff08;加减乘除&#xff09;起来&#xff0c;得到的结果和目标接近。 线性回归分为一元线性回归和多元线性回归。 2 一元线…

OceanBase 分区表详解

1、分区表的定义 在OceanBase数据库中&#xff0c;普通的表数据可以根据预设的规则被分割并存储到不同的数据区块中&#xff0c;同一区块的数据是在一个物理存储上。这样被分区块的表被称为分区表&#xff0c;而其中的每一个独立的数据区块则被称为一个分区。 如下图所示&…

【Android原生问题分析】夸克、抖音划动无响应问题【Android14】

1 问题描述 偶现问题&#xff0c;用户打开夸克、抖音后&#xff0c;在界面上划动无响应&#xff0c;但是没有ANR。回到Launcher后再次打开夸克/抖音&#xff0c;发现App的界面发生了变化&#xff0c;但是仍然是划不动的。 2 log初分析 复现问题附近的log为&#xff1a; 用户…