赞
踩
目录
1.注册表设置 要调试的exe的 debugger 为 x86的windbg
msdn上的文档:
使用 WinDbg 进行调试 - Windows drivers | Microsoft Docs
《格蠹汇编》这里的“汇编”意思不是说汇编语言,是说一个汇总编辑的一个意思。
Windbg是微软开发的免费源码级调试工具。Windbg可以用于Kernel模式调试和用户模式调试,可以挂上正在运行的程序进行调试。还可以调试Dump文件。
使用的版本
以下的windbg相关的命令可以在文档中找到:
边看 《格蠹汇编》 边熟悉windbg的使用,了解如何解决一个问题。
中间是每一章的问题的解决过程和思路以及知识点(还没看完,持续更新中)
最后会总结用的windbg的命令
待解决:ntdll The system cannot find the file specified
>.sympath+ 一个文件路径
设置符号文件的路径。加一个符号路径,扩大,然后执行.reload,刷新模块信息。
>.srcpath 一个文件路径
设置源文件的路径
(我一般都是直接在界面中设置符号路径和源路径,比较形象 不用记命令)
>!heap —— 查看堆数量
>!address —— 查看用户态空间的所有区域——!address 具体地址
s:查看内存命令
-u:Unicode字符
10000 L8000000 符号范围
“查找内容”
可以用来看比如栈回溯中,函数的某个参数如果是字符串的话,dU 某地址。
The d* commands display the contents of memory in the given range.
db | Byte values and ASCII characters. Each display line shows the address of the first byte in the line, followed by up to 16 hexadecimal byte values. The byte values are immediately followed by the corresponding ASCII values. The eighth and ninth hexadecimal values are separated by a hyphen (-). All nonprintable characters, such as carriage returns and line feeds, are displayed as periods (.). The default count is 128 bytes. |
默认显示128个字节
显示字节内容
The e* commands enter into memory the values that you specify.
eb | Byte values. 后面的是字节值 |
ff fe 是小端序 utf16 的标识符 详解windows记事本的4种编码方式_u012138730的专栏-CSDN博客_windows记事本默认编码格式
The .writemem command writes a section of memory to a file.
.writemem FileName Range
查看寄存器的值。
1)在栈中进行编辑,先用栈顶寄存器的值找到栈顶的位置。esp 汇编指令和寄存器_u012138730的专栏-CSDN博客
2)前面用过eb 写的是字节,ew写的word。输入中文。同理dw看的就是看的是word,2个字节
ew | Word values (2 bytes). |
Bug Check 0xC000021A WINLOGON_FATAL_ERROR - Windows drivers | Microsoft Docs
在一台电脑上启动windbg调试器(我们称这台为主机),然后用一个串口电缆链接这个和蓝屏的电脑系统(我们称这台为目标机器)。
启动那台会蓝屏的电脑,按F8,选择debugging mode 方式启动 (如何选择debugging mode)。
当检测到bug check的时候,在有调试器的情况下,bug check会触发系统中断到调试器;没有调试器的情况下,就会蓝屏和重启。
主机启用windbg内核调试:
估计应该是选COM?串口电缆链接的话,待验证
目标机器开启内核调试模式:
(如果此时不能进入目标机器了,就用wer机制修改——wer机制后面会说)
(以管理员身份执行cmd,直接执行bcdedit可以看到当前的启动加载项,应该能看到current)
bcdedit /copy {current} /d "Kernel Debug"——" Kernel Debug "是新启动入口的名字。(这个启动入口会在重新启动以后显示出来,试过了)
C:\WINDOWS\system32>bcdedit /copy {current} /d "Kernel Debug"
已将该项成功复制到 {795bcaaf-1f5b-11eb-a1fe-d2cde0ec311b}。
bcdedit /debug {795bcaaf-1f5b-11eb-a1fe-d2cde0ec311b} on——对新启动入口启用内核调试。
设置目标机与主机之间的通信参数:比如使用串行口1号,波特率115200
bcdedit /set {某GUID} debugtype serial
bcdedit /set {某GUID} debugport 1
bcdedit /set {某GUID} baudrate 115200
感觉是在注册表上创建了一个东西:
步骤:(一个串口电缆链接)
1)目标机器启用内核调试以后,在选择入口的界面上停留
2)主机启动windbg调试。(处于等待状态的windbg会反复向外发送复位包)
3)目标机器选择启动内核调试的入口进入。(内核调试引擎收到复位包,进行回复复位包,windbg收到版本信息,并打印出来,就是连接成功了)
1)内核创建的第一个用户态 进程:启动SMSS.exe(SessionManager管理器,所以这个是用户态的进程,session会话,会话管理器。)
2)SMSS.exe 先检查注册表的一个选项 如下,进行逐一删除或重命名(延迟删除 Windows延迟删除原理_mizac32的文章收藏-CSDN博客)
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager\PendingFileRenameOperations 和PendingFileRenameOperations2
3)SMSS.exe 再启动 CSRSS.exe 和 WinLogon.exe。这两个如果启动失败的话,会触发bug check。
其中CSRSS.exe : Client Server Runtime Subsystem——这个进程启动完成就说明完成Windows子系统的初始化,也就是内核初始化,执行体初始化等,大部分的驱动程序的加载工作
和 WinLogon.exe :是负责 创建登录桌面 和 系统服务管理进程 Services.exe(这个系统服务进程后面会讲到)
如果说WinLogon.exe 在系统启动过程中被删除导致一直蓝屏重启,可能是:
1)WinLogon.exe 被加入到了上述的 PendingFileRenameOperations当中去了
2)某个驱动程序(比如杀毒软件的驱动程序)根据读取一个配置文件,在启动WinLogon.exe之前把配置文件中的应用(比如有 WinLogon.exe)给删除了。(这时候普通的进程没机会启动,只有驱动程序才有几乎运行做这个操作)
即explorer.exe,是操作系统的外壳,在登录了到系统以后,理论上会自动启动这个进程。
(如果平时在用的过程中,资源管理器突然退出了看不见最下面的那些进程图标,使用快捷键打开cmd,键入explorer.exe,重新启动就好了。)
1.WinLogon将用户名密码发送给LSASS(一个负责安全认证的系统进程。)(LSASS 验证通过,返回访问令牌对象)
2.用户名密码验证通过以后,WinLogon会启动 UserInit表键下的程序,默认是userinit.exe。
(UserInit键下面可以注册多个程序,应该会依次执行,如果执行成功了就不走下一个了,如果每一个程序都没法正常启动,那么系统就会强制log off登出。)
3.UserInit表键下的程序会执行登录和初始化操作,userinit.exe启动shell表键中的程序,是explorer.exe,即资源管理器。
问题出现的原因:
就是 UserInit表键下的值被修改了成了其他值,而不是应该是的如下:
C:\Windows\system32\userinit.exe,
windows启动模式:
普通模式:
安全模式:选择safe mode
调试模式:选择debugging mode
winER模式(winer机制):是简化的windows,选择Repair your computer。也叫WER机制。1)可以用来拷贝出目标系统的dump文件,2)可以给目标系统设置增加新入口,新入口开启内核调试模式。3)对目标系统设置启用JIT。遇到的时候会介绍怎么设置。
错误发生在用户态,但是仍然可以通过内核方式设置断点。或者等待发生未处理异常的时候中断到内核调试器。
后续会用三种方法分析这个问题。
应用程序错误对话框,简称AE(ApplicationError)。当应用程序出现了严重的错误(比如非法访问内存,触发cpu的保护模式异常),而应用程序自己又没有处理时,会被系统的未处理异常函数接管——弹出AE,终止应用程序等操作。
(但是有的应用程序的一些错误 不能唤起JIT。估计还是要有特定错误路线走向才能唤起JIT。后续看看)
计算机\HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug
~~
管理员模式启动cmd,然后输入 windbg.exe(所在路径) -I (就会在注册表中生成了 如上图)
在winER下,cmd启动register,然后加载目标系统的注册表文件的software子健(目标系统的盘符:Windows\System32\config 下的),加载以后然后设置同方法一。
设置完jit之后,打开例子中的程序,崩溃以后会直接弹出windbg了,断到崩溃的地方。同时源代码也会读取出来,因为路径设置好了:
-p
-e
-g
崩溃的时候执行的汇编指令:(ACSVio是模块,main是函数)
(00401076就是eip的值,cs:ip 就是当前执行的指令的位置)
(不懂,为什么002B:760034c0 会等于 0f9425ff,难道这个是物理地址?)
对应的源代码: 重点就在*,*ProcAdd指向的内存地址是一个函数所在的地址,当然是只读的区域,不可以更改的,所以执行这句话就会崩溃。
根据源代码中看这个应该是Kernel32.dll模块(也是一个系统库,是运行在用户态的)中的一个地址
也可以在windbg打开相应的窗口看。
一般也叫栈回溯。
图略
图略
1.看函数原型
2.将该地址上的东西翻译成此类型: dt 类型 地址
下面会采用三个方法来解析这个问题。分析dump文件,使用内核调试,使用JIT调试。
很多重要的服务,比如日志服务,PnP服务,电源服务,DCOM等都host在SvcHost.exe中。
(什么叫做服务)
而SvcHost.exe这个进程是运行在不可见的session 0中的。
GS机制是在可能发生溢出的函数所使用的栈帧起始处(EBP-4)存放一个整数,在函数返回值检查这个Cookie是否完整。(ebp的介绍 可以看另外一篇文章的函数调用)
如果返回的时候检查这个Cookie不完整那就说明是溢出了。
部署和检查cookie的代码,都是编译器在编译时加入的。(如何用windbg命令反汇编出一个模块U的函数U呢 后面有讲到 uf lsm!CUtils::StartServiceW )
当触发了GS机制的时候,会调用__report_gsfailure函数(在 gs_report.c中搜此函数),函数内容
1)调用UnhandledExceptionFilter
2)结束进程TerminateProcess
如果没有GS机制,当函数的返回地址被破坏也能继续运行,而如果函数是会返回到一个精心设计的地方,那么就会开始执行恶意代码了。
UnhandledExceptionFilter函数 的对栈溢出异常(0xC0000409)的处理:
在没有内核调试的情况下,没有挂着调试器的情况下,UnhandledExceptionFilter 会调用BaseReportFault,进而调用WerpReportFault函数,也就是WER(windows error report)的错误报告函数。(估计其他异常也会这么走)
在有调试器下,会执行了INT 3指令了,出发断点异常0x80000003(只有栈溢出异常会走特殊处理吧)。接下去执行的话,也是跟上面一样的逻辑,也就说会调用到WerpReportFault。
windows本地调试中的软件断点功能,是依赖于cpu的断点指令(对于x86,即int 3)。cpu执行到断点指令的时候,会中断下来,并以异常的方式报告给操作系统,再由操作系统分发给调试器(简称 产生断点异常 )。
这是cpu提供的的调试支持。此外cpu还以下直接提供:
标志寄存器
调试寄存器
调试异常
任务状态段
分支记录机制
性能监视
JTAG支持:
此外,cpu还间接支持
异常机制
保护模式
心而能监视
WER系统服务 如何收到错误 以及 如何处理:
前面说到UnhandledExceptionFilter 会调用到WerpReportFault, WerpReportFault会进一步调用WerpReportFaultInternal。
WerpReportFaultInternal内部创建一个事件对象来同步。
(什么是事件对象?)
然后通过SendMessageToWERService函数 ,向WER系统服务报告错误:
WER系统服务 收到报告后,创建错误报告进程WerpFault.exe(那个AE就是werpfault.exe?)
WerpFault.exe启动后,为发起调用的出错的进程,产生转储文件dump文件。
打开服务控制面板services.msc,修改启动类型
如果打不开桌面了,就用WER模式启动,打开注册表,加载目标系统的system注册表(同之前说的software):
然后选择修改Start键值,2为4(2是自动,3是手动启动,4是禁止)3是手动启动,也包括那种被其他服务启动的模式。不是说用户主动去双击进程启动。
Select中的Current为1,就是指示着,CurrentControlSet就是指向ControlSet001的。
所有的系统服务运行在会话0
(不是和普通程序运行在一个会话中,普通程序应该是运行在会话1)。但是在任务管理器里面还是可以看到那个服务进程的。
会话0有自己的桌面和窗口系统,默认不可见。让其可见需要做:
1)打开services.msc 修改登录属性。
或者 修改注册表:上上个图中 Power中看到有个Type建值 为0x20 (代表这个服务与其他服务共享一个宿主进程.ps如果想要独占一个,就0x10),把0x20改成0x120(即与0x100与一下)。
2)找到 Interactive Services Detection服务,启动之。(但是我的win10找不到这个)负责检测有没有服务的交互窗口的。
做了这两步以后,当某个服务在会话0有界面显示的时候,就会弹出来一个窗口,问要不要进入会话0,选择要就行了。
现在是单位5000ms Control下面的一个键值。看样子是对所有的服务都生效的。
1.注册表设置 要调试的exe的 debugger 为 x86的windbg
打开注册表,在[运行] (win + R)中输入 regedit,找到下面这个路径:
比如想要调试test.exe, 就新建一个test.exe 文件夹,并新建 一个 字符串值的键,名称(Key)为 Debugger,值(Value)为 windbg所在的路径
2.打开windbg,需要设置这三个路径或者环境变量
但是这个设置都是单次有效,退出就失效了。再次打开不会保留上次的设置。
所以可以用环境变量进行设置
Path名分别是 _NT_SYMBOL_PATH (map文件所在目录) 、 _NT_SOURCE_PATH (源文件所在目录)、 _NT_EXECUTABLE_IMAGE_PATH (exe文件所在目录)
General Environment Variables - Windows drivers | Microsoft Docs
当你设置了这个以后,每次打开都会是这个了
如果有修改的就每次打开了以后再设置一下上上图的具体要改的路径,然后选中reload。
3.设置好了以后,关闭windbg。
打开test.exe 就会自动打开windbg了
dir *.mdmp /s
拷贝出dmp文件
开头就提示了是栈溢出。
所以看之前的调用,反汇编之前的调用,看到某一可疑函数的 ebp-4 ebp ebp+4 感觉像是ascii码,所以怀疑是此函数栈溢出了。
nLength>512导致的。输入了一个超长的文件名,函数里面没有做检查。然后看当时的栈空间可以知道路径名是什么
连接上以后目标系统监测到有调试器的存在,并主动触发断点异常。目标系统主动中断到调试器。
(int 3 就是cpu的断点指令。cpu运行到断点指令,中断出来,报告操作系统,操作系统查找调试器接管)
ps:如果7588cebd 是断点指令的地址,从这个地址可以看到这个是在用户态空间的(如果都是小于2G(0x80000000),那么都是用户空间的)。
后面执行k指令,可以看一些返回地址和栈帧基地址,是不是都是在用户态空间的地址。
因为是内核调试,所以调试器能看到的是系统中运行的所有的进程。先确定这是哪个进程断进来的,使用如下命令查看:
>!process (好像是内核模式才能用的指令)
会依次列出该进程的参数
0)PROCESS:进程地址。进程的EPROCESS结构的地址。
1)SessionID:会话ID,表示的是该进程所在的Windows会话的ID号。Windows会为每个登录用户创建一个会话,每个会话有自己的WorkStation和Desktop。0表示系统服务使用,仅仅允许系统服务运行在session 0,系统启动后会自动创建会话0,没有界面显示。用户登录以后,会创建会话1。所以,我们可以看到有两个windows子系统CSRSS在运行。
2) CId:ClientID 即进程ID,标识进程的一个整数。用户态的函数经常用cid作为标识进程的参数。(内核态的代码,一般用的EPROCESS指针标识进程)
ParentCid:创建该进程的那个进程的cid。
3)DirBase:进程的顶级页表(二级页表结构,就是页目录表)的位置=也是cr3中存的值(cr3的内容看 段机制(段描述符)和页机制(内存分页))。也叫页目录基地址。
(在经典的32分页下,cr3的高20位,也就是dirbase的高20位,cr3和dirbase的低12位总是0,高20位是页帧编号(PFN),即dirbase=0x1f350000,那么pfn=0x1f350。在IA-32e模式中低12位就不是0了,因cr4的pcide位17位而不同)
(页帧编号代表的是物理内存页的标号,再加上页面偏移地址,就是物理地址了。)
4)ObjectTable:该进程的内核对象和句柄 的 表格。通过这个表格,将句柄,翻译成指向内核对象的指针。
windbg的 !handle 以及 !object 命令
5)HandleCount:句柄数,也就是ObjectTable的表项数目。
6)Image:进程的可执行映像文件名称
通过上面的指令,看输出的Image,正好是SvcHost.exe,说明是 从某个服务的宿主进程 中中断进来的。
因为是内核调试,所以要先加载该进程的用户态模块,然后再用kn看栈回溯,要不然都没有符号信息。
>.reload /user
>.symfilepath c:\symbols ===>设置符号路径,并保证主机端可以访问微软的符号服务器。
>.symfix c:\symbols ===>也是设置符号路径,不知有何区别
然后使用kn看栈回溯:就是下面画红线的,电源服务线程导致的。同第5章分析的一样。看栈空间的值推测出来是栈溢出等等——
自己测试一下
>?? (unsigned int)(-1)
比如上面例子中断在UnhandledExceptionFilter 中,那么执行u,
>u
!error 错误码 ===> 比如 !error cC000409
使用JIT调试查找问题的原因,这也是三个方法中最曲折的,但是作为学习的目的,作者还是一步一步带着我们解决了。
当尝试对目标机器使用JIT调试以后,启动系统后黑屏,无法看到调试器!!——首先要解决这个问题(而且是花了大短时间解决这个问题),所以先启动了内核调试。
所以要先启用内核调试,解决这个问题。
这里等到黑屏出现的时候再启动主机端的调试器。
思路:
看上文中有关——windows系统启动过程——的描述,先看看是不是该有的进程都起了。
(!process 好像只有内核模式才能看 反正我本地生成的dump是执行不了)
上面的命令会列出所有的进程。
我们不仅看到了该有的进程都有了,而且还看到了windbg.exe的进程(不过也能猜测到,黑屏应该就是跟windbg设置为JIT了导致的,要不然不应该是这个现象)
在所有进程中找到windbg.exe的相关描述。记住其进程地址—— PROCESS:进程地址
上面的命令会列举这个进程的详细信息。
有两个线程。
一个是UI线程,在等待消息输入
一个是调试器的工作线程,在执行调试器的工作循环(EnglineLoop)
windbg.exe 已经启动完毕,说明已经中断到了某个线程,但是是黑屏状态。
是不是正是因为程序被windbg中断了,导致某个线程阻塞了,从而导致让windows的启动过程受阻了。
所以需要看看启动的关键进程 winlogon.exe 进程的相关信息
根据提示执行一个g命令
然后用之前说过 执行 .reload /user 加载该进程的用户态模块
会列出线程情况。第一个为主线程。
所有线程都在等待状态
除了主线程(列出的第一个线程),其他都处于没有需要执行的空闲状态。
WaitForSingleObject 等待的是一个事件对象—— 938024f8 下面看其详细信息
这个是终端服务已经准备就绪的一个同步事件
再看WaitForSingleObject 的上一帧是 WaitForLsmStart函数。
大概是说Winlogon.exe在等待lsm.exe的一个事件。
(Lsm.exe 我本地是找不到)
书中例子是说lsm.exe已经启动了,为什么winlogon.exe还在等待lsm启动呢?
然后切换到lsm.exe进程看详细情况,看进程情况
看看其主线程
执行了Start和StartServiceW方法用来启动某个服务以后,开始SleepEx
留意上面的第7行的 WaitStartTickCount 和 Ticks
接下去切到该进程的主线程看看
找到StartServiceW栈帧的第一参数值:
dU 第一个参数的地址——看启动的是什么服务。显示出来是 RpcSs
找到SleepEx栈帧的第一参数值:
?? 一个参数值——看要sleep多久,显示3600000(ms),即1个小时。
上图的Ticks等待的时间,打算一直等待够1个小时,看看接下去执行什么
00bf114d 是 SleepEx函数 的返回地址。(虽然感觉有点小奇怪)
上面一步下了断点以后,等着被执行到。
然后使用p进行单步调试。
在这个StartServiceW这个函数里面继续执行,但是当调用到CloseServiceHandle这个API进去以后就没返回了。
主动点击break,将目标中断。看看此时的堆栈信息。
切换到lsm进程,看主线程的栈回溯,发现确实是断在了CloseServiceHandle函数中了(倒数第8行)。
CloseServiceHandle 后面是 RPC调用,然后转化成本地过程调用 ALPC。
(ALPC是windows系统中一种常用的通信机制,是LPC的改进版本。经常用作内核与用户态的通信,或者不同系统之间的通信)
这个线程正在等待针对 9322a230 消息的答复。看这个消息的具体结构内容,命令为
>!aplp /m 9322a230
(为什么可以这么看呢)
得到如下图:
QueuePort:这个消息所发送到ALPC的端口号是8c3f1a88 —— !aplc /p 8c3f1a88 可以看这个端口的详细情况
QueuePortOwnerProcess:这个端口的宿主进程是 services.exe (服务管理器进程,之前有提到过)
ServerThread:服务线程是84b74538
所以切换到 services.exe 进程,然后看线程 !thread 84b74538 观察栈回溯,发现这个线程也陷入了等待,且已经等待了很久了(看 WaitStartTickCount 和 Ticks )
梳理一下流程,总结黑屏的原因就是:
1)电源服务进程的挂起过程
电源服务进程启动,主线程从umpo!UmpoMain开始执行,会调用RegisterServiceCtrlHandleExW 向服务管理器(services.exe)注册服务控制函数。
然后调用自己的umpo!UmpoAlpcInit函数创建一个新的线程(),线程的入口为umpo!UmpoAlpcListenForProMessages,用于监听由全局变量mpo!UmpoAlpcPoPort所标识的ALPC端口。
主线程继续调用UmpoGroupPolicyInit函数,调用UmpoNotifyKernelAllPowerPolicyChanged函数通知电源策略变化,但是函数中发生了栈溢出,当崩溃后,由于注册了JIT调试器,系统自动启动JIT附加到这个进程,使得电源服务进程挂起。
2)
服务管理器进程,在得到电源服务进程启动得消息后,进程中得scext模块企图通过ALPC消息注册电源事件回调,(意思是 服务管理器进程进程给电源服务进程发消息了,等待他得回复吗,通过alpc端口?),但是由于电源服务进程挂起,注册过程受阻,导致服务管理器进程de服务线程被阻塞。(估计就是一直在等待回复de状态)
3)
LSM进程,启动rpcss服务时与服务管理器进行通信,但是由于服务管理器进程de服务线程被阻塞,导致这个进程也在等待状态也被阻塞。
4)
Winlogon进程的主线程(系统启动的关键路径),会等待一个termsevreadyevent事件(应该是由 LSM进程 发出的?),但是一直没有收到,所以电脑就一直黑屏了。
大概就是Winlogon进程等待着 LSM进程, LSM进程等待着服务管理器进程,服务管理器进程等待着电源服务进程,但是电源服务进程被挂起,所以就一直等待着了就是黑屏了。
所以最后执行.kill 9381e870 和 g 命令,将windbg进程杀掉,黑屏就重见天日了。
【服务是后台进程的意思吗?】
所以解决黑屏的方法是延迟 电源服务的启动,修改为禁止启动。等待进入了桌面以后,再去手动启动,使之崩溃。
根据前面知识点4)中 设置操作可以进入会话0的操作。
但是当对电源服务进行了上述交互式的设置以后,没有弹出要不要进去会话0,猜测原因跟 之前 黑屏一样,电源服务的栈溢出以后的某些逻辑阻止了 Interactive Services Detection 这个进程的工作。
所以在电源服务启动(宿主进程svchost.exe)的时候就启动调试器,让早一点检测到,方法如文中最后所说(如何在启动一个进程的时候就挂上调试器)。
操作以后便出现了正常弹出窗口,进入会话0,看到windbg的界面,马上点击g命令,让进行运行,然后就断到了方法一(第五章)中所示的dump一样的堆栈信息,具体分析方法看方法一(第五章)。
之前猜测Interactive Services Detection 弹出交互框这个进程受阻了,导致弹出不了进入会话0的对话框。这里可以尝试另一种方法“进入”,使用另一个windbg远程会话0的windbg。
例子中当jit在会话0中被启动的时候,是在系统的其他服务都启动后的。网络和命名管道应该都是已经可以用的,所以尝试修改JIT设置中的AeDebug的JIT的设置路径为:
windbg的exe -p %ld -e %ld -QY -c ".server tcp:port=2000" -logo 日志路径
不能有-g开关:如果有的话,会跳过电源服务进程的断点异常了,所以不加。
.server tcp:port=2000:让windbg创建调试服务,监听tcp网络接口连接。
同时去掉上文中 在电源服务启动(宿主进程svchost.exe)的时候就启动调试器 所作的设置。
然后启动电源服务进程,然后在任务管理器里面看到了这个windbg进程起来以后,在启动一个windbg实例,并且操作菜单使用远程会话,设置参数:
点击ok以后,就能正在连接会话0的JIT调试器。
接下来就可以输入命令开始调试了,
~*k列出所有的线程
~s切换到那个线程
接下来的分析就一样了。
Adplus 抓取Crash Dump - GrayGuo - 博客园
Adplus -pn powerpnt.exe -pn wincmd32.exe -hang -o c:\test
给涉及到的两个进程,产生dmp,日志,和报告文件。
(我自己保存一个别的好像没有生成dmp文件)
使用windbg:
利用 windbg 或 adplus 生成 dump 文件 - 布丁嫩 - 博客园
如果有模块的符号表,就可以使用kPL命令了。如果没有就不能用。
(具体命令是啥呢?)
栈回溯——根据保存在栈上的信息,可以生成一个线程的函数调用过程,从当前的执行点反向追溯到父函数。(查看另外一篇文章的 函数调用过程可以更清楚的理解原理。)
对于几乎所有的WindowsGUI程序,编号为0的初始线程就是UI线程。
界面没有响应,其实就是负责消息处理的界面更新的UI线程阻塞了。
使用~0s切换到0号线程:0表示0号线程:
eip寄存器是cpu下一次要执行的指令地址(eip保存的地址上的值现在是c3(机器码),对应就是ret(汇编语言)。)。看到下一条要执行返回ret。但是还没执行到,一直停着了。
可以看到模块是ntdll! 就是内核模块在用户模块的一个领事馆,意思就是进入马上要进入到了内核模块去执行了,通过KiFastSystemCallRet函数,而且进入内核态以后至今尚未返回。
使用kn 100看100个深度的栈回溯:
od帧 那个 POWERPNT+地址,是微软的符号服务器没有包含Office程序的符号文件(pdb),所以显示不出来的符号名。
OLE是对象链接与嵌入,03帧的CDdeObject 动态数据交换相关,跟拖动脚本到ppt这个过程有关。
第1帧是发起一个系统服务调用,NtUserMessagesCall
第0帧以快速系统调用方式进入了内核态了。
1)如果挂的进程是64位的,就用64位的windbg。(挂错了就提示 NTSTATUS 0xC00000BB)
2)windbg需要用管理员模式启动,否则会提示拒绝访问
1)使用windbg挂到挂死的ppt进程上,看栈回溯,也就是上图。
2)查看最近几个函数的原型(09帧的OleCreateFromFileEx),参数值。
使用MSDN中的文档查看OleCreateFromFileEx,其函数的第二个参数就是文件名(EBP+C),验证一下是否是拖进去的那个文件的路径,执行完下面这条命令了以后,确实是拖进去那个文件路径。
>dU poi(0013a988+c)
poi是MASM表达式支持的特殊运算符,poi的含义是从指定地址取指针长度的数据
3)所以就是ppt在接受这个文件的时候,意外挂死了。在接收的过程中最后调用了内核服务NtUserMessageCall,同样看看这个函数的函数原型,然后分析一下传入的参数。使用以下命令看前两个栈帧的有关参数:
>kb 2
第一个参数是要发送消息的目标窗口句柄,0xffffffff是特殊值,表示用来广播消息的(HWND_BROADCAST)
第二个参数是发送消息,0x3e0表示的消息类型后面再看看,先对这个数值有印象。
4)再开启一个wdb实例,进入本地内核调试会话(设置内核调试的方法前面的内容中提到过)
打开windbg,File>Kernel Debug>Local
然后执行如下命令找到PowerPoint进程的进程地址
>!process 0 0 powerpnt.exe
然后执行如下命令看PowerPoint进程的线程信息
>!PROCESS PowerPoint进程的进程地址 2
然后执行如下命令看UI线程的栈回溯
>!THREAD PowerPoint的第一个线程地址UI线程地址
显示如下:
一般来说SendMesseage的第一个参数都是窗口句柄(看上图,好几个都是bc74ed08 ),试着看看其值是多少
>dd bc74ed08 l1
其值是 bc74ed08 地址的值为 001506C4。
我们验证看这个值是否是窗口句柄值,可以使用spy++工具来查看,也可以通过skywing 编写的sdbgext模块中的hwnd扩展命令来查看:
如上所示,确实是窗口句柄。
根据最后一行其ProcessID(1c68)在任务管理器里面找到这个进程XXX。
到此为止,导致PPT挂死的原因应该就是,这个进程的这个线程没有回复消息给PPT,导致PPT阻塞了。
5)再开启一个wdb实例,附加到上述找到的那个进程XXX,查看原因为什么不恢复消息呢
>~*————显示所有的线程,看到上述的那个线程值(000016d8)的线程号是什么
>~线程号s——切换过去
>kn 100查看栈回溯
然后执行lm,列出进程的所有模块,找到某个服务来自何方。然后再系统服务管理器中停止这个服务以后,【这里没看懂 68页】
找到之前挂PowerPoint进程的windbg,然其detach(Debug>detach),PowerPoint然后恢复了。
所以问题的原因就是 ppt发送了一个广播消息,但是有个第三方的服务没有回复消息。
许多使用OLE技术的程序都有这个问题
很多挂死的问题都是因为双方或者多方协作除了问题。
之前的3e0的消息就是WM_DDE_INITIATE消息,要求系统中每一个窗口都相互谦让。
但是这个问题在Win7中没有发生挂死了。
关键在于之前图中的 CPackagerMoniker::BindToObject方法的 06号栈帧
再win7中BindToObject没有在再调用DdeBindToObject,原因是:
BindToObject 的内部实现是 调用 CoCreateInstance函数。
CoCreateInstance的第一个参数,要创建的对象的组件的ID是 {一个GID值},
到注册表里面去查找这个组件,CoCreateInstance就会返回成功,
于是BindToObject就会调用这个组件对象来执行而不是调用DdeBindToObject了(继续调用DdeBindToObject就会出现挂死问题了)。
(xp也是这个,但是xp的系统里面找不到这个组件的注册信息,BindToObject 就会调用DdeBindToObject了)
而这个组件是在packager.dll中的,如下搜组件的GID
Packager.dll就是Windwos Server 2003引入的模块,被称为Object Packager 2,即第二代的对象打包器。
(所以在win7系统下,如果将注册表该项目的 packager.dll改名,或者直接在调试器中修改调用CoCreateInstance函数的返回值,就可以重现这个崩溃了。)
问题:邮件中打开pdf附件以后,pdf阅读器无响应了。鼠标点击pdf阅读器窗口无反应。
(windows子系统发现pdf阅读器长时间不处理窗口消息,在标题条打上不响应的标签。)
(Windows处理窗口消息函数是 GetMessage PeekMessage 等)
1)粘滞在内核态:ppt的例子,因为UI线程调用SendMessageAPI广播DDE消息进入内核态一去不回。
2)死锁:这个例子
3)死循环:后面讲
.dump /mfh c:\dumps\pdf\acrobat.dmp
m——代表minidump。但不一定是真的mini。只不过windows下的用户态转储文件一遍都是使用minidumpwritedump这个api。
f——代表full。打印完整的用户态转储。转储文件中包含用户态空间的所有内存数据。会比较大的。
h——代表包含句柄信息。对于调试程序挂死非常有价值。
. 表示 调试会话中的当前线程
#表示 中断到调试器,产生转储时的当前线程、
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、多个命令的组合:看线程命令+栈回溯命令
~* kv1 看每个线程的栈回溯,只看第一个栈帧 (k 看栈回溯 v 有参数 1 栈回溯深度为1)
~ns 切换到n号线程。
~n k 看n号线程的栈回溯。
NtWait ZwWait ,这些要么以nt开始,要么以zw开始。表示已经调用系统服务而进入到了内核态了。
他们调用的事等待函数。那么他们在等谁,要等多久。
比如 NtWaitForSingleObject API签名:(通过kv 就可以看到参数了,就是下面这三个参数具体是什么值)
第一个参数:等待的对象
第三个参数:等待的时间
可以看到好几个都是0,就是NULL。这意味着线程在无线等待第一个参数指定的内核对象。
1)等待内核对象。比如NtWaitForSingleObject ,等待内核对象改变(比如临界区)
2)是线程池的工作函数(看栈回溯的上面几层可以看出来),在等待事情做。
相当于看 dword ptr [ebx] 的值
dd:汇编指令和寄存器_u012138730的专栏-CSDN博客
看 dword ptr [ebx+28h] ——dd @ebx+28h l1
等待往往和锁有关。因为要进入的区域被加锁了,所以要等待。
通过上述得出的信息:
1)关键区的地址 都位于 CoolType模块中。
2)有3个处于锁定状态,即有3个线程进入了这三个关键区。OwningThread就是拥有这个关键区的线程号。观察图,其实是两个线程号,后面两个属于同一个线程号25f4.
3)总有541个关键区,关键区的概念。(ps:上面的图没有下面的图说的LockSemaphore的字段的记录)
(ps:用!cs -l 命令弥补没有LockSemaphore的字段的记录)
ps:但是我本地输入!locks 出现错误,以后再看吧 debugging - Why isn't !locks working for me? - Stack Overflow
2460拥有的那个关键区的对应的事件(LockSemaphore) 是 2e0。
(ID是 25f4 线程号6,等待的对象正是 2e0。图9.2)
25f4 拥有的第一个关键区的对应的事件(LockSemaphore) 是 2DC。
(ID是 2460 线程号3,等待的对象正是 2DC。图9.2)
25f4 想进入2460所拥有的关键区,2460想进入25f4所拥有的关键区。
如何避免这样的死锁:如果多个线程同时需要获取多个锁,那么获取的顺序一定要一致。下面是错误的示意图,不同的顺序获取,导致了死锁。
可以给你分析是什么类型的错误,错误码是什么,断在那句话了,哪个模块等等可以自己挨句话看。
停止码。0x7f:内核态错误,超出了系统所能处理的范围。
最后四行:第一行,类型 8 表示双误异常(double fault)
cpu报告了一个比较严重的异常以后,异常处理过程还没有结束,又发生了第二个比较严重的异常。
(注意是比较严重。比如连续两次页错误异常,连续两次通用保护错误(General Fault)异常。但是断点异常,调试异常等有益的异常不算是严重异常)
IA-32cpu中 是一种异常,向量号位8,属于终止类异常,一旦发生,不可恢复运行,不可处理异常。
(关于上面说的 各种异常,向量号 在 汇编语言 这篇文章中有详细描述)
CR3寄存器就是用来存放当前进程的页目录基地址的。
执行>!process ————可以看到当前进程 DirBase 就是页目录基地址的值 0ff2f000 。
而 执行 >r cr3 ——确实是0003000 (r就是看寄存器 )
为什么会不一样呢,这是双误异常发生后的典型症状。
当发生双误异常以后,cpu会动用硬件的线程切换机制(应该是一般不会动用硬件切换),把CR3寄存器切换到系统进程所使用的页目录基地址(这里是00030000),在新的线程中开始执行双误代码,让系统崩溃和保存转储信息。
但此时操作系统没有来得及更新当前进程。所以产生了,操作系统没有更新当前进程,但是CR3已经是新的了。
即cpu眼中的进程是新的,操作系统眼中的是旧的。
在x86架构上,nt会使用FS寄存器来指向kpcr这个段的地址。借助这个fs,cpu可以很方便的访问到自己的信息。
FS寄存器所指向的段中保存着CPU的当前线程结构地址(KTHREAD):
在线程结构中,有这个线程所属进程的进程结构,即EPROCESS。
PsGetCurrentProcessId函数就是利用类似的方法得到的:
>u nt!PsGetCurrentProcessId
看到 PsGetCurrentProcessId 的反汇编
1)第一行 获得 fs:[00000124h]的地址的内容,这个地址的内容其实就是当前线程结构的地址。即fs的[00000124h]偏移就是当前线程 的地址(可以反汇编 uf nt!PsGetCurrentThread 验证)。
2)第二行 获得线程地址偏移214h处的取内容,就是存的进程id的值。
150h处存的就是该线程的进程地址的地址
看寄存器的值就用命令r
大多数的Windows线程都是有两个栈。一个是内核态的,一个是用户态的。用户态的栈默认大小是1MB,通常不会溢出。
而内核态的栈通常是十几千字节,到几十千字节之间。基于不同的处理器的Windows系统默认的内核态栈大小:
1)x86 cpu系统中,内核态栈初始大小是12K
2)x64cpu(英特尔64和AMD64)中,是24K
3)安腾处理器中,是36K
因为windbg去寻找tss信息找到另外一个栈,所以能将两个不同的栈连接起来了
还可以从返回地址看出这是用户态还是内核态的地址空间((如果都是小于2G(0x80000000),那么都是用户空间的))
看到只有SMSS和CSRSS,WinLogon进程还没有出现(看上文的Windows启动过程)
1)分析出来是一个双误异常,也就是现在处于双误线程的执行中。
2)在转去执行双误异常之前断在push ebp,函数开头,然后就cpu就转去执行双误异常的KiTrap08例程了。
3)当前进程CSRSS的页目录基地址和CR3寄存器的内容不同。正常情况下应该是一样的,不一致说明cpu已经切换了CR3寄存器的内容,但是操作系统还没有切换当前进程。
(FPO:TSS 28:0) 是所谓的任务门。(任务门,是指的是,登记在IDT表中的一种特殊入口。IDT表:中断向量表吗?)
CPU可以根据这个任务门,所指向的任务状态段(TSS,这里指向的任务状态段是 28 ),来切换到指定的任务(线程)
比如发生双误的时候,cpu找到登记在IDT表中的8号(双误异常是8号)表项的任务门,来切换到处理双误异常的新线程。
通过
>!pcr ——显示cpu的状态。 pcr全称处理器控制域信息(Processor Control Region)。显示的是KPCR数据结构的信息。每个CPU对应一个KPCR结构,可以在命令中指定要显示的CPU序号,不指定,就显示当前CPU的PCR信息。
在启动早期,操作系统会为每个cpu建立一个pcr区域,一般为2个内存页大小8k。
>dt _RTSS TSS的地址——显示当前线程的TSS的内容
下图是例子中的内容
显示了:
1)cpu自己的编号: processor 3
2)cpu自己的关键资产:IDT GDT TSS等等的位置
3)要执行的线程的位置信息:当前,下一个,idle线程
4)DPC(Deferred Procedure Call)延迟过程调用
TSS(任务状态段)中有字段:
CR3:cr3寄存器的内容,案例中的是0x30000,我这个是最后亲自动手的例子中的。
Backlink:previous task link field。指的是cpu所执行的前一个任务,这里是0x28
也就是说在双误线程之前,前一个线程的TSS的段选择子是0x28.
回到上一个段选择子的现场。可以看到当时的寄存器的数值。
可以看到,当前要执行的指令是 push ebp(一般来说eip指向的都是是下一条要执行的指令,但是这里通过后面分析,应该是已经执行过这条指令,然后又重新指向这条指令的)
(
当cpu执行到错误类异常的时候,比如页错误,那么他会把程序指针重新指向这条导致异常的指令开始处(页错误就是这么设计的,之前文章讲到过,缺页时候的处理)。
而这个例子,反汇编 pusb ebp的前一条指令,不太可能会出现异常,因为是前一条计算指令,对esp进行减法的指令,没有对内存进行读写。而push ebp这条指令,对栈空间进行push,很有可能导致异常,比如要存的地址,栈无效了)
)
通过,esp寄存器的值(f655cfb48),计算当时要push的内存地址,也就是esp-4=f655cfb4,存入ebp的值(f655d7dc)。手动写一下如下,果然是行不通呢。
>ed f655cfb4 f655d7dc
那么,看看这个地址空间上的数据能不能读取吧。如下:
>dd f655cfb4
问号的可能性:1)可能是这段内存没有包含在转储文件中,2)也有可能是那段内存根本就是没有分配,不存在。怎么判断内存是不是没有分配对应的物理内存呢,就是通过!pte命令
>!pte f655cfb4 ——页表项没有指向物理内存,所以此地址就是就是发生了要写的内存无效而产生的页错误异常。
>!pte f655d000——页表项指向PFN=100e1的物理内存页
确实是的,可以看出
1)f6560000-f655d000=00003000=12K 栈空间大小,而esp-4=f655cfb8,都已经超出了f655d000, 72个字节了
2)当前进程是csrss.exe
那为什么esp会指向无效的地址呢,是当调用到此函数的时候,函数使用了FPO(帧指针省略,《软件调试》第22章)优化。没有建立栈帧基地址ebp,直接开始调整栈指针,为当前函数分配栈帧空间,而栈空间又溢出了,导致的。
可以用r cr2验证是不是正好是访问那个无效地址的时候导致的页错误,cr2确实是=f655cfb8
因为函数开头是一个对esp的减408的操作(这个是看了之前push ebp 之前的那条指令,就是对esp减408),所以把esp还原回去。然后就可以看当时的栈空间了
>dd f655cfb8+408
返回地址是 aebc6eac(之前文章说过函数调用过程中栈的存放,一般都是从右到左压入栈,最后是函数返回地址。嗯,应该是这样,待确认)
通过ln命令,看这个地址对应的符号是什么,得到函数名。
>ln aebc6eac ——是xxx文件的大概多少行
作者在源代码中找到这个函数调用,在函数开头是局部变量的定义。而栈就是分配局部变量的主要空间,除了适合分配在寄存器上的少数变量外,大多数局部变量是分配在栈上的。
所以之前的对esp减408的操作就是因为函数开头定义的局部变量。
而这里的局部变量导致的栈溢出,是由于栈空间不够了。由于这个是驱动程序的代码。
因为驱动程序是运行在内核态的,大小通常只有几千到几十千字节之间。(例子中就是只有12K)
那为什么会栈空间用完了呢?继续使用k看栈回溯,看看是怎么把这12K空间用完的:
除了前两个,后面有的是系统的函数,有的是显卡驱动程序的函数。
大概是WIndows子系统在初始化视频显示(InitVideo,倒数第10行),Windows子系统的用户态模块调用内核态驱动,内核态驱动调用显卡驱动,显卡驱动在访问注册表的时候,触发了有问题驱动的注册表挂钩函数,而导致了溢出。
后台程序具体的定义是啥不清楚。一般接触到的:
linux下:daemon程序
windows下:有系统服务 SystemService
Sysfs:设备驱动程序文件系统,是一种使用内存,作为存储媒介的,特殊文件系统。是linux下,用户态模块和内核态模块相互通信的常用技术。
利用Sysfs,应用程序(用户态)便可以用读写文件的方式,与驱动程序交换数据(内核态)。
应用程序,可以从驱动程序那里读取来自硬件设备的数据,读取驱动程序的状态,配置驱动程序。
驱动程序创建虚拟文件的内核函数如下:
(每个驱动程序可以创建多个虚拟文件。虚拟文件系统,会将这些文件都映射到sys/设备名/vfile)
sysfs_create_file()
sysfs_create_bin_file()
sysfs_create_group()
输入参数是:struct device_attribute 结构体:
一些c标准库函数
printk:驱动开发中常用的打印函数
dmsg | grep " "
在之前的文章中说到过文件描述符。
默认的文件描述符 0 1 2分别是标准输入。后续再打开文件的话,就从3开始。
如果把0 1 2这三个文件描述符都关掉的话,再次打开新的文件,就会从0开始分配了。
针对某个应用程序的错误,可以看看事件记录里面的详细信息,有没有报告什么错误代码以及提供什么信息。
MsiInstaller:应该就是msi的安装程序吧。
启用以后,生成的日志在系统的临时目录下,MSI日志文件以msiXXX.log结尾。
cmd中,输入 cd %temp%
或者“运行”的那个程序,输入 %temp%
其实就是 c:\users\用户名\AppData\Local\Temp
1.比如要看在调用了CreateFileW函数的时候,创建了哪些文件:
>bp KERNELBASE!CreateFileW+0x5 "dU /c 50 poi(@ebp+8);gc"
bp 位置:设置断点的位置,不知道为啥要+0x5,其他数字应该也行吧
"dU /c 50 poi(@ebp+8);gc":断点执行到那了以后的输入的命令
dU /c 50 poi(@ebp+8) :打印 ebp+8 处的内存内容,Unicode字符显示 ——poi 和 @ 之前都有用过——也就是CreateFileW的第一个参数,是一个字符串。
gc:继续执行的意思吧?
>bp KERNELBASE!CreateFileW+0x5 "dU /c 50 poi(@ebp+8);gu;r eax;gc"
gu:不知道啥意思,猜测是不是这个函数执行完?
r eax:看eax的值,看函数的返回值,为了看函数的返回值有没有异常。
>bp KERNELBASE!CreateFileW+0x5 "dU /c 50 poi(@ebp+8);gu;r eax;.if (@eax<0){.echo hit} .else{gc}"
.if (@eax<0){.echo hit} .else{gc}:看eax的值,看函数的返回值,如果小于0,.echo hit,并停止运行了。否则继续运行。——中断了以后就是返回值小于0。可以查看k 看不正常的时候的栈信息了
2.比如要看在调用了OpenFileMappingW函数的时候,创建了哪些文件:
>bp KERNELBASE!OpenFileMappingW+0x5 "kv10;dU /c 30 poi(@ebp+10)"
bp 位置:设置断点的位置
"kv10;dU /c 30 poi(@ebp+10)":断点执行到那了以后的输入的命令
dU /c 30 poi(@ebp+10) :打印 ebp+10 处的内存内容,Unicode字符显示 ——也就是OpenFileMappingW的第三个参数就是字符串类型的,是文件映射对象名称——@ebp+4 返回地址 @ebp+8第一个参数 @ebp+c第二个参数 @ebp+10第三个参数
这里没有gc了,就会断在那里了,需要手动按g让继续执行了,因为这里需要观察一下,有没有异常,有异常的话就分析一下
.restart —— 重新启动
.restart /f——让目标复位,啥意思?
单步跟踪命令——p
手动中断,快捷键Ctrl+Break
用ln 和 !address 没怎么看懂其实。
1)解压到临时目录
- Setup.exe:补丁安装程序。
- SetupEngine.dll:通用引擎模块。
- ParameterInfo.xml: 补丁包的参数信息文件。
最上面两行补丁的详细信息:核心补丁文件名,大小,补丁的guid
ApplicableIf 节:判断补丁的适用性。 MsiPatch描述补丁,TargetProduct描述的是修补的目标(可以看看这些目标,在注册表中是否存在)。
- MSP文件:补丁内容和执行补丁动作。
- 资源文件
2)分析补丁的适用性,确定补丁应用顺序
3)调用MsiExec,解析和执行MSP文件中补丁逻辑:在这一步中才会进行执行文件替换,注册表更新等修补动作,才会产生MSI日志文件。
有msi文件,msp文件,以及一些子目录
这些文件是用来卸载和修复已经安装的软件的。
是以前安装软件是安装程序故意留下的,可以想象这些为MSI本地包。
注册表里面用来记录他们的路径的表键叫 LocalPackage。
本篇文章开头提了一下是否应该抛出异常的代码争论
文章的正文是讲一个程序,由于没有恰当的或者是即使的处理抛出的异常而引发的卡壳。也是告诉我们如果抛出异常,没有在最佳时机编写捕捉异常的代码,就会出现意向不到的结果。
真正安装的程序是什么 64位程序的话 就需要64位 的windbg。
32位程序好像是随便的。64位和x86的都行
使用如下命令查看每个线程的id 和 每个线程执行后的LastError(相当于GetLastError的API)
>~*e?@$tid;!gle
跟lasterror类似的还有laststatus
lasterrorvalue和laststatusvalue 保存在哪里:TEB(线程环境块) 中。
使用dt命令 观察他们:
>dt _TEB -y Last
lasterrorvalue: 表示有dos错误码。比如: 5拒绝访问 2 找不到指定的文件
(比如一个值是8,到winerror.h中可以找到,类型是ERROR_NOT_ENOUGH_MEMORY
ERROR_NOT_ENOUGH_MEMORY 8L
)
laststatusvalue: 表示nt操作系统所使用的状态码。比如 C0000005 访问违例 C000034 代表对象名不存在
调用window api的时候会修改,windows api内部会修改这两个变量或一个:
具体的流程可以截图看看例子。
这两个值理论上应该是代表的一个意思,但是往往都是一个代表成功一个代表错误,有可能就是某些api只修改了一个。根据经验看lasterrorvalue比较准。
注意观察显示的信息,以及看命令,比如切换线程:
用操作系统给线程的序号切换: ~[14d8]s用调试器给线程的序号切换: ~9s
当.Net运行时模块时 会出现 mscorwks模块
出现这个模块很有可能是JIT即时编译出的托管代码
由此,我们可以推断这个程序时.Net开发语言开发的
所以才会有很多栈帧中没有函数名
这时候可以使用解析托管语义的 SOS扩展 命令 模块(我觉得是这样断句的 )
>.loadby sos mscorwks————从mscorwks模块所在的位置加载 sos扩展
>!clrstack————观察托管方法的经过使用sos的 >!thread 观察线程信息,可以看到最近发生的异常的信息。可以看到一个文件没有找到的异常信息。
使用sos的 >!do(dumpobject) 地址 —————— 【上面显示出来的异常的地址】 看异常对象的详情 看异常对象的文字描述 ,发现确实是要找的文件在本地没有出现,好像是使用的sdk版本不对,本机的电脑是x64的,但是他却要找ia64 安腾的。
使用sos的 >!printexception ——————— 打印当前线程的异常信息,跟使用!do 差不多。(应该是加载了sos扩展以后,用的就是sos的那些命令了?)
所以到目前为止,知道了一个事实,就是在下载文件过程中,在获取文件大小的时候一个处理时,抛出了一个IO异常。那抛出异常又为什么会导致卡壳呢。这就是要分析栈回溯的最后,为什么会处于哪个函数了。
sos的栈回溯的最后的函数是A
本地的栈回溯的最后的函数是B
所以猜测是A调用的B
验证结果 把B栈帧的返回地址C 传入sos,让帮忙解析是位于托管的那个函数里面的
>!ip2md 地址C
显示结果 果然是位于A函数中的。
所以就是托管的函数A调用的B
发现就是Dispose函数(A) 调用的Sleep函数(B)。
因为看sos栈回溯和异常信息分析出是下载文件的时候 进入了死循环 卡死了
那想看源代码哪里写错了怎么办
1)如果有源代码更好
2)没有源代码,看反汇编,有源程序
3)如果是.Net程序,使用 ILDASM.exe (中间语言反汇编 ,直接电脑上搜这个 然后把程序拖进去就可以)进行反汇编成中间语言 就可以看到 。
所以看托管的函数A 使用 ILSDASM
这样就可以深入了解某个函数的具体的实现了
看Dispose函数的IL(A函数的IL),然后写成源代码
一直死循环,应该是get_IsBusy一直为true。
确认是否为true,m_isBusy字段。使用
>!do 或者 !dso 地址。(为什么是这个地址呢,就是m_isBusy的数据地址)
然后判断查找一个变量的函数为什么是true的 然后导出IL中间文件 搜索这个变量
然后猜测为什么没有执行
结合之前的栈回溯
是调用一个文件长度的函数的时候发现文件不存在,然后抛出了一个异常,然后被最外层的finally catch到了
然后 finally里面会调用Dispose
但是此时 变量是true的 所以就是一直进入死循环了。
IL代码其实挺好看懂的额
如何看IL代码: IL指令时基于栈的。
官方文档 ECMA-335 - Ecma International
wiki :https://en.wikipedia.org/wiki/List_of_CIL_instructions
某人的文章
https://blog.csdn.net/icebergliu1234/article/details/81322455
https://blog.csdn.net/icebergliu1234/article/details/81260175
使用windbg打开dump文件的信息有以下几点 :
1)给出目标系统的概况
2)给出错误信息 (不知道是第一次还是第二次引发的异常 first/second chance not available)
查看错误原因:
>!error c0000008 —————— 无效的句柄。
3)显示断点处要执行的下一条的指令:
ntdll!NtGetContextThread 用来获取线程上下文结构的内核函数的用户态桩(stub)函数。后面的ret表示这个线程进入到内核态去执行尚未返回。
这里直接执行k观察栈回溯,观察不到。要执行.ecxr
一个异常从发生那一刻起 就有专门的数据结构为其建立档案。在异常分发和处理中,会一直伴随。并写入到转储文件中。
Windows系统的用户态转储中定义了记录异常信息的数据块—————异常数据流————MINIMUMP_EXCEPTION_STREAM结构
而当windbg分析转储文件的时候,.ecxr就是专门访问这个数据块。使其回到异常现场。然后再使用k,就能显示栈回溯了。观察寄存器的值,看看有没有巧合,rax=rbx=c0000008。各个寄存器可能还残留着之前的痕迹。(就是跟你在运行程序的时候,挂着调试器,然后抛出异常了,点击中断,看到的栈信息是一样的)
看到最后一个调用的函数是 RtlRaiseStatus 这个函数就是抛出的异常的,那为什么会抛出异常呢
抛出异常就是投出了一颗手雷,必须紧急处理。研究一下为什么会抛出异常呢。看栈回溯。
如何找到1号栈帧的函数名。
使用反向反汇编
>ub 01'40007d7a 1号栈帧的返回地址
使用宏汇编表达式中的poi运算符来取指函数指针指向的内容 然后再用ln命令 查找对应的名称
通过地址--》查到地址里面的内容--》通过地址里面的内容查到对应的函数名字
为什么1号栈帧没有显示出来RtlLeaveCriticalSection这个真正的名字,这是编译器后使用优化工具优化将不热门的代码移到边远地带的副作用————流放代码————导致1号栈帧是乱七八糟的函数名
查找RtlLeaveCriticalSection为什么会执行到RtlRaiseStatus
>uf RtlLeaveCriticalSection
SetEvent 这个API
设置指定的事件对象 参数 传递事件对象的句柄
(句柄:内核空间中内核对象的一个用户态代号 )
猜测是传给这个函数的参数句柄值不对。那怎么找传给这个函数的句柄值是个什么呢?
通过函数原型,在windbg中打印输入参数的具体数据结构
>dt 命令
知识点:
当线程A需要进入关键区,(就是获取这个关键区),但是关键区里现在线程B,需要等待。线程A就挂起了。
这个时候,系统会创建一个事件对象,供这个线程来等待。并且把这个事件句柄保存在关键区的某个字段(LockSemaphore)里面。(LockSemaphore就是关键区使用的事件对象句柄)
当线程B离开了关键区,系统检查某个字段(LockSemaphore),如果不是0或者-1,就会调用 SetEvent 函数去唤醒等待的线程。LockSemaphore这个得值是什么,就去SetEvent 这个。
所以就是RtlLeaveCriticalSection输入参数中的句柄值,应该就是SetEvent 中的句柄值。
出题就出在这个句柄是一个无效的句柄值。
接下去就是要找这个无效的句柄值是什么?
RtlLeaveCriticalSection输入参数保存的值
根据他是怎么一步一步传进来的。
其实也就是关键区是哪一个。
还有是谁在拥有这个关键区呢
因为寄存器内容容易变,如果能发现这个值存在到栈上过就好了,特别是尚未返回函数所使用的栈帧
windows系统的句柄跟踪功能
>?? 0xdfdf-0x4323
>ub 01'40007d7a1 反向反汇编
>!cs 列出进程中所有的关键区 (可以看到关键区结构的地址)
dp 地址 l1
dt _RTL_CRITICAL_SECTION 地址
dt 地址 _RTL_CRITICAL_SECTION_DEBUG 所以最后两个参数可以呼唤的
所以要使用dt 就要知道 类型名字 + 地址
如果系统挂死了,想产生转储文件。可以按上面的热键。
但是前提是修改一个注册表,使这个操作生效。添加一个建值:
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\i8042prt\Parameters
热键触发的蓝屏崩溃的操作码是0xe2(MANUALLY_INITIATED_CRASH),在转储文件中可以看到。
这样的话,键盘驱动程序接收到 这个热键操作以后,就会进行中断处理程序,调用蓝屏的那个函数KeBugCheckEx.
03:系统分发键盘中断
02:键盘驱动的中断处理程序
01:调用I8xProcessCrashDump发起崩溃转储。
使用pcr来查看cpu的状态,关于pcr和fs寄存器等上面有介绍过,搜即可。
发现奇怪的现象:既然nextthread不是空,但是为什么currentthread会在执行idleThread呢?
那么在idleThread的时候cpu是不是什么事情都不做呢?不是的。
说明在第2步卡住了,要不然应该就会执行第三步。
先切换到空闲进程,看看栈回溯
>.thread 80551d20
>kn
关注04帧:忙等待函数,让cpu空转一段时间。通常是不得已而为止
关注05帧: rh:roothub。 portreset:端口复位。 反汇编这个函数:
这个函数就是读取一个寄存器的地址的值,然后把某一一个位置清零,如果清零成功了,就跳出,否则就重新进行读取清零操作。
这里如果一直清零不成功,就会造成死循环了,无法进行dpc队列的处理,dpc对象也会越堆积越多。
如何获得上面那个函数的第一个参数的数值 ebp+8
>dd ebp+8 l8
得到的是线性地址,使用vtop转化成物理地址
>!vtop 0 线性地址 。
得到了物理地址以后,找到属于那一块物理地址,倒数第二行
usbehci驱动——英特尔南桥数据手册
usb ehci 章节
portsc寄存器:专门用来描述usb端口状态。每个usb端口都有这样一个寄存器。包含32个二进制位。
最后也米有说为什么复位不成功
使用f10 进行单步调式,也就是p
使用pc,进入到下一个函数中
~ns 切换到线程号为n的线程。
~nk 看n号线程的栈回溯。
(!process 只有内核模式才能用)
!process 0 0 ——看所有的进程
!process /i 地址 ——切换到某个进程
!process 地址 ——看某个进程,如果是当前进程就 !process就行咯
!process ——看当前进程,.process 也是看当前进程
!process cid ——看cid=4的进程
280是进程号,2a4是线程号。(再使用 ~ 查看线程,~*查看所有线程)
比如umpo! 指的是umpo模块(可以使用 lmvm umpo 看这个模块的信息)
比如0a帧的最后一列,是执行到函数UmpoAlpcSendPowerMessage的第0x88字节,即进入了__report_gsfailure函数了(看上一个帧09的最后)。
而0a帧的第一列,就是函数UmpoAlpcSendPowerMessage的栈帧基址,即EBP为009afb30。
而0a帧的第二列,是返回地址,即UmpoAlpcSendPowerMessage这个函数被调用的地方,输入命令:dd 009afb30(后面可以用l+数字长度 来指定显示的长度,不加默认就是128),显示出来的依次是旧的EBP值(感觉显示出来的旧的EBP值不对啊),这个函数的返回地址值,这个函数的输入参数的值(看 虚拟内存以及进程的虚拟内存分布(第六章)_u012138730的专栏-CSDN博客)
3:kd > 前面的3是3号cpu的意思。 目标系统有多个cpu,当前是3号。
观察下图:
其实就是n是帧号显示,默认就有了。v是参数显示。v比b更多。最后的数字是栈回溯的深度。
还有个kPL 是显示函数原型和参数值
1.按系统环境分类:Windows,Linux,Dos
2.按目标代码的执行方式
1)脚本(脚本解释器,解释执行)
2)代码:
托管代码(是编译成了中间代码,到运行的时候再动态编译成cpu能够执行的目标代码,C#的.Net程序)————托管调试
本地代码(直接编译成目标代码)——————本地调试
3.目标代码的执行模式:用户态,内核态
4.软件所处于的阶段:开发期,产品期
5.调试器和调试目标的相对位置:本机调试,远程调试
6.调试目标的活动性:转储文件的调试,活动目标的调试
7.调试工具:使用调试器(软件调试),不使用调试器(日志,调试信息)
好像打开windbgx64 和 windbgx86都没事
直接把dump文件拖进来就可以了
当然也要设置上面说的三个路径
在窗口中输入 :.ecxr 回车
view 中 打开堆栈窗口,看执行到哪里了
下次再查崩溃可以好好看下下面的文章先
dump后,如何用Windbg进行分析呢?_iwilldoitx的博客-CSDN博客
Windbg查看调用堆栈(k*)_xumaojun的专栏-CSDN博客
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。