使用Windbg静态分析dump文件的一般步骤详解

目录

1、概述

2、静态分析dump文件的一般步骤

2.1、查看异常类型

2.2、使用.ecxr命令切换到发生异常的线程上下文,查看发生异常的那条汇编指令

2.3、使用kn/kv/kp命令查看异常发生时的函数调用堆栈

2.4、使用lm命令查看模块的时间戳,找到对应的pdb文件,设置到Windbg中

3、问题分析实例说明

4、使用Windbg详细分析dump文件,展现完整分析过程

4.1、查看异常类型和发生崩溃的汇编指令,初步分析

4.2、使用kn/kv/kp命令查看函数调用堆栈

4.3、找到pdb文件,设置到windbg中,查看完整的函数调用堆栈

4.4、可以将C++源代码路径设置到Windbg中,Windbg会自动跳转到源代码行号上

4.5、有时需要查看函数调用堆栈中函数的局部变量或C++类对象中变量的值

4.6、有时可能需要使用IDA去查看二进制文件的汇编代码上下文去辅助分析


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具案例集锦(正在更新中...)https://blog.csdn.net/chenlycly/category_12279968.html       有很多正在学习C++软件调试技术的朋友或者刚入门的人,希望我能详细讲讲使用Windbg静态分析dump文件的一般步骤,他们好对照这个步骤去练习去实操。所以今天就来讲讲这个主题内容,先概要性讲述Windbg静态分析dump文件的一般步骤,然后再以一个问题分析实例详细展现分析dump文件的完整过程(与本课程相关的免费视频教程,已经发布到B站,具体地址见本文的的评论区)。

1、概述

       基本大部分软件都内置了异常捕获模块,在软件发生异常闪退崩溃时,会弹出相关的提示框,比如PC版的微信在崩溃时,其内置的异常捕获模块会捕获到异常并生成日志及dump文件,同时会弹出如下的发送错误报告的提示框:

提示框的下方会自动带上崩溃相关的文件,其中最后一个文件就是我们要讲的dump文件,点击确定则会将这些文件发送到腾讯远端的后台服务器上。腾讯后台的运维人员会收到通知,然后到服务器上将dump等文件下载下去分析。

       有些软件可能没有上传崩溃日志到服务器的功能,捕捉到异常时会自动将dump文件保存到指定的路径中,事后可以到该路径中取到对应的dump文件。如果客户机器上遇到崩溃,可以和客户联系,让客户帮忙从对应的路径中取来出dump文件发过去。

       使用Windbg分析包含异常上下文的dump文件,属于静态分析,下面就来详细讲一下拿到dump文件之后如何使用Windbg去静态分析dump文件。

      关于Windbg的下载安装及详细介绍,可以查看我之前写的文章:
Windbg使用详解https://blog.csdn.net/chenlycly/article/details/120631007关于Windbg的命令汇总,可以查看我之前写的文章:

Windbg调试命令汇总https://blog.csdn.net/chenlycly/article/details/51711212

2、静态分析dump文件的一般步骤

        可以先打开Windbg,然后将dump文件拖到Windbg中。

2.1、查看异常类型

        打开dump文件,就会显示发生异常的类型,如下所示:

根据显示的异常类型(ExceptionCode值对应的含义,比如0xC0000005对应的就是Access violation内存访问违例)。

       通过这个异常类型可以初步判断出异常的种类,比如Access violation(内存访问违例)、Integer divided by zero(除0异常)、Stack overflow(线程栈溢出)等。对于Access violation内存访问违例的异常,原因比较多,很难一步确定引发异常的原因。但对于Integer divided by zero除0异常、Stack overflow线程栈溢出异常,则能给出直接的线索,可以快速地定位问题。

2.2、使用.ecxr命令切换到发生异常的线程上下文,查看发生异常的那条汇编指令

        在查看异常类型之后,我们输入.ecxr命令切换到发生异常的线程上下文,查看发生异常的那条汇编指令及相关寄存器的值:

通过查看汇编指令及各寄存器的值,可以初步判断当前发生异常的初步原因,比如可能是访问C++类空指针、野指针或者用户态的代码访问了内核态内存地址。

汇编代码能最直接、最本真的反映出崩溃的原因,程序运行时执行的都是二进制文件中的汇编代码(或者称为二进制机器码,二进制机器码和汇编代码是等价的)。

       下面讲两个典型的汇编指令中访问了不该访问的内存异常场景。

2.2.1、汇编指令中访问64KB小地址内存区,引发访问违例,导致崩溃

       在Windows系统中,64KB以内的内存地址是禁止访问的,如果程序访问这个范围内的内存,则会触发内存访问违例,系统会强行将进程终止掉。

2.2.2、汇编指令中访问了很大的内核态的内存地址,引发访问违例,导致崩溃

       对于32位程序,系统会给进程分配4GB的虚拟内存,一般情况下用户态和内核态会各占一半,即各占2GB,我们编写的代码基本都是运行在用户态的,用户态的代码时不能访问内核态内存地址的(内核态地址是供系统内核模块使用的),如果崩溃指令中访问了一个很大的内存地址,超过用户态的地址范围0-2GB,内存地址大于0x8000000,则会触发内存违例,因为用户态的代码是禁止访问内核态内存地址的。

2.3、使用kn/kv/kp命令查看异常发生时的函数调用堆栈

       接着,使用kn/kv/kp命令(三个命令,可以使用其中任何命令)去查看异常发生时的函数调用堆栈,看看是调用了什么函数触发的异常。根据函数调用堆栈,对照着源码去进行分析。    

       一般此时没有加载pdb文件,调用堆栈中不会显示具体的函数名和代码的行号:

因为Windbg没有加载包含函数及变量符号信息的pdb文件。要函数调用堆栈中显示具体的函数名和代码行号,则需要拿到pdb文件,然后将pdb文件的路径设置到Windbg中。然后Windbg记载到pdb符号库文件之后,重新输出函数调用堆栈,堆栈中就会显示出具体的函数名和代码行号了。

2.4、使用lm命令查看模块的时间戳,找到对应的pdb文件,设置到Windbg中

       函数调用堆栈在没加载pdb文件时,会显示代码所在的模块名称,我们使用lm命令查看这些模块编译生成的时间戳:

然后通过时间戳找来这些模块对应的pdb文件,然后将pdb文件的路径设置到windbg中。如何将pdb文件的路径设置到Windbg中,下面讲实例时会详细说明,此处就不再赘述了。

       Windbg加载pdb文件之后,使用kn/kv/kp命令再次输出函数调用堆栈,堆栈中就会显示具体的函数名及行号,这样对照着C++源代码去详细分析引发问题的最终原因了。

       有时候为了搞清楚发生异常的本质,我们还需要使用IDA查看相关二进制文件的汇编代码,查看一下发生异常的那条汇编指令的上下文,对照着C++源码,看看那条汇编指令为啥会出现异常。

3、问题分析实例说明

        我们为了方便展开讲解,我们特意使用VisualStudio创建了一个基于MFC对话框的exe测试程序,对话框中有个名为button1的按钮,如下所示:

我们在此按钮的响应中添加了一段引发崩溃的测试代码,故意让程序产生崩溃,测试然后拿到崩溃时的dump文件。

       具体的测试代码如下所示:

// 添加的一段测试代码
SHELLEXECUTEINFO *pShExeInfo = NULL;
int nVal = pShExeInfo->cbSize; // 通过空指针访问结构体成员,导致崩溃
 
CString strTip;
strTip.Format( _T("nVal=%d."), nVal );
AfxMessageBox( strTip );

代码中使用到的结构体SHELLEXECUTEINFO 定义如下:

typedef struct _SHELLEXECUTEINFOW
{
    DWORD cbSize;               // in, required, sizeof of this structure
    ULONG fMask;                // in, SEE_MASK_XXX values
    HWND hwnd;                  // in, optional
    LPCWSTR  lpVerb;            // in, optional when unspecified the default verb is choosen
    LPCWSTR  lpFile;            // in, either this value or lpIDList must be specified
    LPCWSTR  lpParameters;      // in, optional
    LPCWSTR  lpDirectory;       // in, optional
    int nShow;                  // in, required
    HINSTANCE hInstApp;         // out when SEE_MASK_NOCLOSEPROCESS is specified
    void *lpIDList;             // in, valid when SEE_MASK_IDLIST is specified, PCIDLIST_ABSOLUTE, for use with SEE_MASK_IDLIST & SEE_MASK_INVOKEIDLIST
    LPCWSTR  lpClass;           // in, valid when SEE_MASK_CLASSNAME is specified
    HKEY hkeyClass;             // in, valid when SEE_MASK_CLASSKEY is specified
    DWORD dwHotKey;             // in, valid when SEE_MASK_HOTKEY is specified
    union                       
    {                           
        HANDLE hIcon;           // not used
#if (NTDDI_VERSION >= NTDDI_WIN2K)
        HANDLE hMonitor;        // in, valid when SEE_MASK_HMONITOR specified
#endif // (NTDDI_VERSION >= NTDDI_WIN2K)
    } DUMMYUNIONNAME;           
    HANDLE hProcess;            // out, valid when SEE_MASK_NOCLOSEPROCESS specified
} SHELLEXECUTEINFOW, *LPSHELLEXECUTEINFOW;
 
 
#ifdef UNICODE
typedef SHELLEXECUTEINFOW SHELLEXECUTEINFO;
typedef LPSHELLEXECUTEINFOW LPSHELLEXECUTEINFO;
#else
typedef SHELLEXECUTEINFOA SHELLEXECUTEINFO;
typedef LPSHELLEXECUTEINFOA LPSHELLEXECUTEINFO;
#endif // UNICODE

       在测试代码中定义了SHELLEXECUTEINFO结构体指针pShExeInfo,并初始化为NULL,然后并没有给该指针赋一个有效的结构体对象地址,然后使用pShExeInfo访问结构体的cbSize成员的内存,因为pShExeInfo中的值为NULL,所以结构体cbSize成员的内存地址是结构体对象起始地址的偏移,因为结构体对象地址为NULL,cbSize成员位于结构体的首位,所以cbSize成员就是结构体对象的首地址,就是NULL,所以就访问64KB小地址内存块的异常,引发内存访问违例,导致程序发生崩溃闪退。

       至于如何在程序中设置异常捕获模块去捕获异常、自动生成dump文件,可以尝试使用google开源的CrashRpt库,我们产品很早就有了。大家如果想集成异常捕获库,可以使用开源CrashRpt(开源版本有一些缺陷,我们进行了一些优化和改进),也可以使用Google浏览器开源代码中的BreakPad,网上有很多版本,可以去研究一下。

4、使用Windbg详细分析dump文件,展现完整分析过程

4.1、查看异常类型和发生崩溃的汇编指令,初步分析

       使用Windbg打开dump文件(直接将dump文件拖入到Windbg),首先我们查看一下发生的异常类型,如下:

       然后使用.excr命令切换到发生异常的线程上下文,会自动将发生异常的那条汇编指令显示出来,如下:

       我们可以先看一下这条汇编指令以及崩溃时的各个寄存器的值,可能能初步估计出发生异常的原因。如果指令中访问了一个很小的地址或者访问了一个很大的地址,都会触发内存访问违例。
从崩溃的这条汇编指令来看,是访问了小地址0x00000000地址,这是访问了小于64KB小地址内存区,这个范围的地址是禁止访问的,所以引发了内存访问违例。这个异常很可能是访问了空指针引起的。

4.2、使用kn/kv/kp命令查看函数调用堆栈

       接着输入了kn命令,将函数调用堆栈打印出来:

函数调用堆栈是从下往上看的,最上面一行就是最后调用的一个函数,也是崩溃的那条汇编指令所在的函数。从函数调用堆栈的最后一帧调用的函数来看,程序的崩溃是发生在TestDlg.exe文件模块中,不是其他的dll模块。显示的函数地址是相对TestDlg.exe文件模块起始地址的偏移,为啥看不到模块中具体函数名称呢?那是因为Windbg找不到TestDlg.exe对应的pdb文件,pdb文件中包含对应的二进制文件中的函数名称及变量等信息,Windbg加载到pdb文件才能显示完整的函数名。

       查看函数调用堆栈的命令,除了kn,还有kv和kp命令,其中kv还可以看到函数调用堆栈中调用函数时传递的参数,如下所示:

我们需要取来pdb符号库文件,去查看具体的函数名及行号的,这样才好找到直接的线索的。下面就来看看如何获取到TestDlg.exe模块的pdb文件。

4.3、找到pdb文件,设置到windbg中,查看完整的函数调用堆栈

       如何才能找到TestDlg.exe文件对应的dpb文件?我们可以通过查看TestDlg.exe文件的时间戳找到文件的编译时间,通过编译时间找到文件对应的pdb文件。在Windbg中输入lm vm TestDlg*命令,可以查看到TestDlg.exe文件的详细信息,其中就包含文件的时间戳:(当前的lm命令中使用m通配符参数,所以在TestDlg后面加上了*号)

可以看到文件是2022年6月25日8点26分23秒生成的,就可以找到对应时间点的pdb文件了。

        一般在公司正式的项目中,通过自动化软件编译系统,每天都会自动编译软件版本,并将软件的安装包及相关模块的pdb文件保存到文件服务器中,如下所示:

这样我们就可以根据模块的编译时间找到对应版本的pdb文件了。

        我们找到了TestDlg.exe对应的pdb文件TestDlg.pdb,将其所在的路径设置到Windbg中。点击Windbg菜单栏中的File->Symbol File Path...,打开设置pdb文件路径的窗口,将pdb文件的路径设置进去,如下所示:

点击OK按钮之前,最好勾选上Reload选项,这样Windbg就会去自动加载pdb文件了。但有时勾选了该选项,好像不会自动去加载,我们就需要使用.reload /f TestDlg.exe命令去让Windbg强制去加载pdb文件(命令中必须是包含文件后缀的文件全名)。

        设置完成后,我们可以再次运行lm vm TestDlg*命令去看看pdb文件有没有加载进来:

如果已经加载进来,则会在上图中的位置显示出已经加载进来的pdb文件的完整路径,如上所示。

       关于pdb符号库文件的说明,可以查看我之前写的文章:
pdb符号库文件详解https://blog.csdn.net/chenlycly/article/details/125508858       加载到TestDlg.exe文件对应的pdb文件之后,我们再次执行kn命令就可以包含具体的函数名及及代码的行号信息了,如下:

 我们看到了具体的函数名CTestDlgDlg::OnBnClickedButton1,还看到了对应的代码行号312。通过这些信息,我们就能到源代码中找到对应的位置了,如下所示:

是访问了空指针产生的异常。当然上面的代码是我们故意这样写的,目的是为了构造一个异常来详细讲解如何使用Windbg进行动态调试跟踪的。

4.4、可以将C++源代码路径设置到Windbg中,Windbg会自动跳转到源代码行号上

       为了方便查看,我们可以直接在Windbg中设置C++源码路径,这样Windbg会自动跳转到源码对应的位置。点击Windbg菜单栏的File->Source File Path...,将源码路径设置进去:

然后在Windbg点击函数调用堆栈中的每一行记录前面的数字超链接,会自动跳转到对应的函数及行号上,如下所示:

上图中的函数调用堆栈中很多模块是系统库中的,比如mfc100u、User32等,这些库是系统库,是没有源码的。我们可以点击最下面的第23个链接,其位于我们应用程序的模块中,会自动跳转到对应的代码中,如下:

4.5、有时需要查看函数调用堆栈中函数的局部变量或C++类对象中变量的值

        有时我们通过查看变量的值,找到排查问题的线索,比如变量中值为0或者很大的异常值。这点我们在多次问题排查中使用到,确实能找到一些线索。可以查看函数中局部变量的值,也可以查看函数所在类对象的this指针指向的类对象中变量的值。我们要查看哪个函数,就点击函数调用堆栈中每一行前面的数字超链接,如下所示:

我们看到了局部变量pShExeInfo 的值:

struct _SHELLEXECUTEINFOW * pShExeInfo = 0x00000000

       我们可以点击this对象的超链接:

就能查看当前函数对应的C++类对象中成员变量的值,如下:

      有时函数中相关变量的值,对于分析当前的C++软件异常,可能是关键线索,比如我之前写过的两篇项目问题排查实例文章:

通过查看Windbg中的变量值去定位C++软件异常问题https://blog.csdn.net/chenlycly/article/details/125731044通过查看Windbg中汇编指令及内存中的值去定位软件崩溃问题https://blog.csdn.net/chenlycly/article/details/125793532       但有时不一定能查看变量的值,因为当前通过异常捕获模块自动生成的dump文件一般是minidump文件,文件也就几MB左右,不可能包含所有变量的值。所以要在minidump文件中查看变量的值,要看运气的,有时能查看到,有时是看不到的。这里要讲一下dump文件的分类,主要分为minidump文件和全dump文件。

      关于dump文件的分类与dump文件的生成方式,可以查看我之前写的文章:
dump文件类型与dump文件生成方法详解https://blog.csdn.net/chenlycly/article/details/127991002

4.6、有时可能需要使用IDA去查看二进制文件的汇编代码上下文去辅助分析

       有时通过函数调用堆栈和源码很难定位问题时,可能需要使用IDA打开二进制文件去查看汇编代码的上下文去辅助定位问题。关于如何使用IDA去辅助排查问题,可以参见我之前写的文章:

使用IDA查看汇编代码上下文去辅助排查C++软件异常问题https://blog.csdn.net/chenlycly/article/details/128942626

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

8个升级到ChatGPT Plus的理由,不升级你就out了

​关注文章下方公众号,可免费获取AIGC最新学习资料 导读:ChatGPT Plus 是 OpenAI 聊天机器人的高级付费版本。以每月 20 美元的价格,该服务为您提供访问 GPT-4,您可以享有令人难以置信的稳定性和更快的响应时间。 本文字数&#…

img[:, :, ::-1] 通俗理解

👨‍💻个人简介: 深度学习图像领域工作者 🎉工作总结链接:https://blog.csdn.net/qq_28949847/article/details/128552785 链接中主要是个人工作的总结,每个链接都是一些常用demo&#xff0c…

知识付费小程序搭建 为有价值的知识买单

以前我们学习写作遇到难题的时候,总喜欢上网搜一下参考资料,但是不知具体从何时起,很多平台内容查看都要钱了。这说明知识付费已经深入到我们的生活中了。再加上疫情爆发这几年,很多教育培训机构都抓住风口,开发了线上…

Web应用技术(第十四周/END)

本次练习基于how2j和课本,初步认识Spring。 以后我每周只写一篇Web的博客,所有的作业内容会在这篇博客中持续更新。。。 一、Spring基础1.Spring概述:2.Sring组成:3.BeanFactory:4.控制反转:5.依赖注入:6.JavaBean与S…

【六一 iKun】Happy LiuYi, iKuns

六一了,放松下。 Python iKun from turtle import * screensize(1000,1000) speed(6)#把衣服画出来,从右肩膀开始#领子 penup() goto(-141,-179) pensize(3) fillcolor("black") pencolor("black") begin_fill() pendown() left(1)…

30天从入门到精通TensorFlow1.x 第二天,变量 tf.Variable()

文章目录 一,接前一天(1).内容前先弄清楚 sess.run() 函数a. 该函数干嘛的b. 该函数有哪些参数c. 该函数的使用 (2).由库函数创建张量(3).由库函数创建张量 二、变量tf.Variable()(1…

Dart语法学习

最近在学习flutter相关方面的知识,里面用到了Dart语言,于是写下这篇博客记录学习的一门过程。如果你有其他编程语言的经验(尤其是Java和JavaScript),可以很快的上手Dart语言,Dart 在设计时应该是同时借鉴了…

AI注册流程

1、首先需要有一个OpenAI账号,如果有方法的,就可以自己先注册一下。如果没有方法的,还有一个付费版本的可以备选,亲测可用。 2、注册建议使用谷歌账号关联登录,最方便。微软账号太慢了,也可以使用。注册使用…

Git的安装和环境变量的配置

目录 前言一、下载Git二、安装Git三、检查是否安装成功四、 配置用户名和邮箱五、环境变量配置1. 获取git的安装路径2. 设置环境变量 前言 当我们第一次在新电脑上使用git命令的时候,会报错【git 不是内部或外部命令,也不是可运行的程序 或批处理文件】…

企业工程项目管理系统源码-专注项目数字化管理-Java工程管理-二次开发

工程项目各模块及其功能点清单 一、系统管理 1、数据字典:实现对数据字典标签的增删改查操作 2、编码管理:实现对系统编码的增删改查操作 3、用户管理:管理和查看用户角色 4、菜单管理:实现对系统菜单的增删改查操…

javaWeb 酒店民宿预定信息管理系统myeclipse开发mysql数据库MVC模式java编程计算机网页设计

一、源码特点 java ssh酒店民宿预定信息管理系统是一套完善的web设计系统(系统采用ssh框架进行设计开发),对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为T…

【十一】设计模式~~~结构型模式~~~代理模式(Java)

【学习难度:★★★☆☆,使用频率:★★★★☆】 6.1. 模式动机 在某些情况下,一个客户不想或者不能直接引用一个对 象,此时可以通过一个称之为“代理”的第三者来实现 间接引用。代理对象可以在客户端和目标对象之间起…

CTR预估之DNN系列模型:FNN/PNN/DeepCrossing

前言 在上一篇文章中 CTR预估之FMs系列模型:FM/FFM/FwFM/FEFM,介绍了FMs系列模型的发展过程,开启了CTR预估系列篇章的学习。FMs模型是由线性项和二阶交互特征组成,虽然有自动学习二阶特征组合的能力,一定程度上避免了人工组合特征…

Springboot中使用mail邮件

Springboot中使用mail邮件发送 1、配置邮箱的POP3/SMTP服务和IMAP/SMTP服务2、导入依赖和一些默认#配置新的3、发送邮件4、整合工具类 1、配置邮箱的POP3/SMTP服务和IMAP/SMTP服务 这里使用的是QQ邮箱,进入设置-账户,开启下服务。 开启后获取授权码,保存…

智能路由器开发之OpenWrt简介

智能路由器开发之OpenWrt简介 1. 引言 1.1 智能路由器的重要性和应用场景 智能路由器作为网络通信的核心设备,具有重要的地位和广泛的应用场景。传统的路由器主要提供基本的网络连接功能,但随着智能家居、物联网和大数据应用的快速发展,对于…

Typora+PicGo+阿里云OSS搭建博客图床

✅作者简介:大家好,我是Cisyam,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Cisyam-Shark的博客 💞当前专栏: 程序日常 ✨特色专栏&…

每日一题——删除字符串中的所有相邻重复项

每日一题 删除字符串中的所有相邻重复项 题目链接 思路 这是一道用栈解决的典型题目 我们先来看看栈的基本性质: 栈:是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素的操作。进行数据插入和删除操作的一端称为栈顶&#xff0c…

【PC迁移与管理】上海道宁为每个用户和每个 PC 传输和迁移场景提供解决方案——PCmover

PCmover 是一款 可以自动将所有选定文件、 文件夹、设置、用户配置文件 甚至应用程序 从旧PC传输、恢复和升级到 新PC或操作系统的软件 而且由于 大多数迁移的应用程序 都已安装在新PC上即可使用 通常无需查找旧CD 以前下载的程序 序列号或许可证代码 开发商介绍 La…

Zookeeper学习---2、客户端API操作、客户端向服务端写数据流程

1、客户端API操作 1.1 IDEA 环境搭建 前提&#xff1a;保证 hadoop102、hadoop103、hadoop104 服务器上 Zookeeper 集群服务端启动。 1、创建一个工程&#xff1a;Zookeeper 2、添加pom文件 <?xml version"1.0" encoding"UTF-8"?> <project …

Android Studio 2022.3 新版 flamingo 安装步骤及遇到的问题

下载地址: https://developer.android.google.cn/studio D盘中新建一个 Android 文件夹, 用来存储 Android studio 和 SDK 文件. 下载好之后, 运行 exe 文件, 点击 next 注意这个路径最好不要有空格,比如 program files这种目录,不然后面安装sdk的时候会有问题. 点击 instal…