概述
最近在调试rpc,没法子,翻译了一下windbg这篇调试RPC的文档,后面可能还有其他内容,也就弄个系列吧
算是自己的笔记,有看客的话,可以多多指出问题,提提建议,不吝赐教!
微软的远程过程调用(RPC)可以轻松越过进程和机器的界限并且进行数据通信。这种网络通信标准是微软Window网络通信如此强大的原因(….太绕了,翻不来,也不重要)。
然而,因为RPC对进程隐藏了网络调用,所以隐藏了计算机之间的交互细节。这使得用户很难确认线程为什么这么做,正在做什么,为什么在支持的功能上失败。所以,调试和解决RPC错误非常困难。另外,大部分RPC错误的问题实际上出现在配置问题,网络连接问题,其他组件问题上。
Windows有个调试工具是DbgRpc,是RPC相关的调试器扩展。这些扩展能够用来分析Windows Xp以及更新版本系统的各种RPC问题。
这些Windows版本可以配置来保存RPC实时状态信息。可以保存不同数量的状态信息;这可以让你获得需要的信息,而不用防止一个重的负担在你的电脑上了(significant burden,什么东西)。细节请看Enabling RPC State Information
之后这些信息就可以被调试器或者DbgRpc访问了。在每种情况下,一个集合的查询都是可以用的。细节请看Displaying RPC State Information
在大部分情况下,你可以通过使用Common RPC Debugging Techniques.中的技术找到问题。
如果你想探索一下机器是怎么保存这些细腻的,或者你想设计自己的状态信息分析的技术,可以看看RPC State Information Internals.
这些工具和技术在Windows2000中不能使用
激活RPC状态信息
可以收集两种不同的RPC运行时状态信息:服务端信息和完整信息。必须要在调试器或者DbgRpc使用之前激活状态信息的收集。
只有Windows XP以及以后的系统可以收集RPC状态信息。
收集服务端状态信息是比较轻量级。每次RPC调用大概需要100条机器指令,甚至在性能测试中都几乎不可察觉已经被加载了(…)。但是收集这些信息会耗费内存(每个RPC服务端大概4KB),所以不推荐内存有压力的机器使用。服务端信息包括数据,endpoints,线程,连接对象和服务调用对象(SCALL)。这些足够调试大部分RPC问题了。
收集全部状态信息更加heavyweight。它收集了所有的信息,包括服务端信息,另外还有客户端调用对象(CCALL)。全部状态信息通常是不需要的。
在电脑中运行Group Policy Editor(Gpedit.msc)可以激活收集RPC状态信息的功能。在本地电脑策略中,找到Computer Configuration/Administrative Templates/System/Remote Procedure Call。在这节下面可以看到RPC Troubleshooting State Information,当你编辑它的属性时,
1 | 本地计算机策略-计算机配置-管理模板-系统-远程过程调用-维护RPC疑难解答状态信息(默认未启用,启用之后配置下面5中状态) |
可以看到5中可能的状态:
None:不维持任何状态信息。除非你电脑内存压力很大,不推荐这种配置。
Server:收集服务端状态信息。推荐在个人电脑中设置这个。
Full:收集全部状态信息。
Auto1:在内存小于64MB的电脑中,相当于None配置。在大于64MB的电脑中相当于Server配置。
Auto2:在内存小于128MB的运行Windows Server2003的电脑,或者运行Windows XP的电脑,相当于None配置。在大于128MB的Windows Server 2003中,相当于Server。这个也是默认配置。
如果你想同时配置一个局域网中的电脑的这些状态,使用Group Policy Editor卷起(roll out)机器策略到首选的机器中。策略引擎会监视你配置的策略传到首选的机器中。在这种情况下,Auto1和Auto2是特别有用的,因为不同机器的操作系统和内存大小是不一样的。
如果网络中包括运行了比Windows XP更老的系统,这些电脑会忽略这些配置。
显示RPC状态信息
各种各样的RPC调试扩展在Rpcexts.dll中导出。
这些用来显示RPC状态信息RPC扩展只能在用户模式中运行。他们可以在CDB(或者NTSD)或者用户模式的Windbg中使用。
用户模式的调试器必须有一个目标程序,但是这个目标跟RPC扩展又是没有关系的(??)。如果调试器还没有运行,你可以简单打开它调试一个毫不相干的目标(比如windbg notepad或者cdb winmine)。接着在CDB中CTRL+C,或者Windbg中Debug|Break来停止目标进程,这样可以使用调试器的命令窗口。
如果你需要分析一个远程电脑的RPC状态信息,你需要在目标电脑中运行一个用户模式的调试器,然后使用Remote Debugging。
通过调试器访问RPC状态信息在一个stress环境中是特别有用的,或者当一个调试器已经运行了。
使用RPC调试扩展
各种各样的RPC调试扩展在Rpcexts.dll中导出。
这些用来显示RPC状态信息RPC扩展只能在用户模式中运行。他们可以在CDB(或者NTSD)或者用户模式的Windbg中使用。
用户模式的调试器必须有一个目标程序,但是这个目标跟RPC扩展又是没有关系的(??)。如果调试器还没有运行,你可以简单打开它调试一个毫不相干的目标(比如windbg notepad或者cdb winmine)。接着在CDB中CTRL+C,或者Windbg中Debug|Break来停止目标进程,这样可以使用调试器的命令窗口。
如果你需要分析一个远程电脑的RPC状态信息,你需要在目标电脑中运行一个用户模式的调试器,然后使用Remote Debugging。
通过调试器访问RPC状态信息在一个stress环境中是特别有用的,或者当一个调试器已经运行了。
使用DbgRpc工具
DbgRpc工具(DbgRpc.exe)放在windbg安装目录中,必须使用命令提示窗口打开它。
双击是不能启动这个工具的。
命令提示窗口必须运行在本地电脑的administrator权限的账户下,或者域管理员权限。
DbgRpc不会对系统服务产生任何调用(比如LSASS)。 这个对调试时非常有用的,只要内核还在运行,即便在系统服务已经崩溃了。
在远程电脑中使用DbgRpc
DbgRpc也可以用来检查远程电脑的信息。为了让这个可以正常工作,远程电脑需要可以接受远程连接和远程认证用户。如果远程电脑的RPCSS服务已经崩溃,DbgRpc将不能工作。远程电脑中也需要Administrative或者域管理员权限。
-s参数用来指定服务端名字,-p指定传输协议。TCP和命名管道都可以使用。推荐使用TCP协议,它几乎可以在每种情况下使用。
1 | G:\>dbgrpc -s MyServer -p ncacn_ip_tcp -l -P 1e8 -L 0.1 |
DbgRpc命令行
可以查看详细的DbgRpc命令描述
获取RPC Cell信息
详细的cell信息通过!rpcexts.getdbgcell显示,或者使用DbgRpc的-l开关。
需要指定进程id已经cell number。
下面的例子,进程id是0x278,cell number是0000.0002
1 | D:\wmsg>dbgrpc -l -P 278 -L 0.2 |
获取RPC Endpoint信息
Endpoint信息通过!rpcexts.getendpointinfo显示,或者DbgRpc的-e开关。
如果指定了endpoint number,就会显示它的信息。如果忽略endpoint number,系统中所有进程的endpoint都会显示。
下面是显示所有endpoints的例子,通常包含进程id和cell number作为额外的参数是很有用的方式。
1 | D:\wmsg>dbgrpc -e |
For details on the optional parameters, see DbgRpc Command-Line Options.
For a similar example using the RPC debugger extensions, see !rpcexts.getendpointinfo.
获取RPC线程信息
使用显示!rpcexts.getthreadinfo线程信息,或者DbgRpc的-t开关。
必须指定进程pid。也可以指定线程ID,如果忽略,显示进程的所有线程。
例子,进程ID 0x278,忽略了线程ID
1 | D:\wmsg>dbgrpc -t -P 278 |
获取RPC调用信息
服务端调用信息通过!rpcexts.getcallinfo显示,DbgRpc的-c开关
有4个可选的参数。其中三个CallID,IfStart,ProcNum是用来跟中RPC调用来标记信息的。第四个参数是ProcessID是服务端的Pid。你可以使用你知道的参数值来缩小搜索。
如果没有参数指定,系统中所有可知的SCALLs都会显示。例子:
1 | D:\wmsg>dbgrpc -c |
For details on the optional parameters, see DbgRpc Command-Line Options.
For a similar example using the RPC debugger extensions, see !rpcexts.getcallinfo.
获取RPC客户端调用信息
使用!rpcexts.getclientcallinfo获取客户端调用信息,或者DbgRpc的-a开关。
也有四个参数可选。其中三个CallID,IfStart,ProcNum是用来跟中RPC调用来标记信息的。第四个参数ProcessID是属于这个调用的进程的Pid。你可以使用你知道的参数值来缩小搜索。
如果没有参数指定,系统中所有可知的CCALLs都会显示。例子:
1 | D:\wmsg>dbgrpc -a |
For details on the optional parameters, see DbgRpc Command-Line Options.
For a similar example using the RPC debugger extensions, see !rpcexts.getclientcallinfo.
注意:
只有在全部状态信息都收集的时候,才有CCALLS的信息。
常用的RPC调试技术
下面将介绍4中常见的RPC问题。RPC状态信息可以用来检查这些问题。
DbgRpc和RPC调试扩展命令都可以使用。
分析一个Stuck(卡)调用问题
当一个进程直接或间接的进行一次RPC调用时,等待(holding)一个critical section或者资源时通过会出现这个问题。在这种情况下,RPC调用会到另一个进程或者机器,然后派遣到管理接口(服务接口)中,这个会等待很久。这导致调用方会出现等待超时。
当通过调试器检查时,RPC是这个等待线程的最高层,但是不清楚究竟在等待什么。
下面是一个这种堆栈的例子,有很多可能性。
1 | 0:002> ~1k |
下面是怎么检查这个问题。
Troubleshooting a stuck call problem
1- 保证调试器正在调试有这个stuck cell的进程。(是那个可能在等待RPC的线程所属的进程)
2- 或者线程的堆栈指针。堆栈就像上面例子中显示的那样,这个例子的堆栈指针是0x0068FBA0
3- 获取这个线程的调用信息。通过!rpcexts.rpcreadstack加上堆栈指针作为参数来获取。
1 | 0:001> !rpcexts.rpcreadstack 68fba0 |
显示的这些信息可以让你跟踪这个调用。
4- 网络地址是空的,标明是本地机器。Endpoint是1120。需要确认哪个进程拥有这个endpoint。通过!rpcexts.getendpointinfo加上endpoint作为参数来获取 //应该是客户端
1 | 0:001> !rpcexts.getendpointinfo 1120 |
5- 根据前面的信息,可以看到进程0x278拥有这个endpoint,可以通过!rpcexts.getcallinfo
获取到这个call的所有信息,需要加上四个参数CallID, IfStart, and ProcNum(3步骤已经知道)和进程pid 0x278
1 | 0:001> !rpcexts.getcallinfo 1 19bb5061 0 278 |
6- 第5步的信息非常有用,但是有些信息太少了。第二列给出的cell id是0000.0004。如果
!rpcexts.getdbgcell加上这个cell id,可以显示更易读的cell信息:
1 | 0:001> !rpcexts.getdbgcell 278 0.4 |
信息显示这个调用已经”dispatched”,表示已经离开了RPC运行时。最后更新时间是470.25,通过!rpcexts.rpctime可以看到现在的时间。
1 | 0:001> !rpcexts.rpctime |
表示这次call的最后联系在5533秒之前了,接近92分钟,因此这个肯定是一个stuck call。
7- 在挂载到服务端进程之前,你可以使用Servicing thread identifier找到当前处理这个call的线程信息。也就是另一个cell number,第6步中的0x0.2,可以像下面一样使用:
1 | 0:001> !rpcexts.getdbgcell 278 0.2 |
现在你知道你要找的是0x278进程的0x1A4线程。
可能这个线程已经在做其他的RPC调用了,你又必要重复这个过程跟踪这个call。
##跟踪服务端进程的Contention(争用)
为了能够处理发来的请求,RPC报了一个工作线程集合。理论上这个线程数量很小。然后理想的情况只存在实验室环境下,这些服务管理函数非常小心和谐(。。。)。在真实情况下,线程的数量决定于服务端的工作量,不过不管怎么样都在1-50的范围内。
如果工作线程数量超过了50,可能服务端进程有过多的竞争。通过引起这个的是heap的胡乱使用,内存的压力,或者服务端大部分的活动都通过一个单独的临界区。
使用!rpcexts.getthreadinfo获取服务进程的线程数量,或者DbgRpc的-t选项。需要指定进程ID,如下面的0xC4
1 | D:\wmsg>dbgrpc -t -P c4 |
在这个例子中,只有7个工作线程,是合理的。
如果有超过100个线程,就需要加载调试器看看问题了。
Note Running queries such as dbgrpc -t remotely is expensive to the server and the network. If you use this query in a script, you should make sure this command is not run too often.
检查Struct的线程
RPC需要它的工作线程来完成正常的工作,通常有个问题,在同一个进程中的组件会因为等待一个公共的临界区死锁(比如,loader lock或者heap lock)。这将导致很多线程暂停,很有可能也有RPC工作线程。
如果出现了这种情况,RPC服务器不会再给外界响应。RPC调用将返回RPC_S_SERVER_UNAVAILABLE或者RPC_S_SERVER_TOO_BUSY
一个很小的问题可能会硬气有问题的驱动阻止IRPs完成,到达RPC服务器。
如果你怀疑可能出现了这个问题,使用DbgRpc –t或者!rpcexts.getthreadinfo需要进程PID作为参数。下面的列子是0xC4:
1 | D:\wmsg>dbgrpc -t -P c4 |
TID那一列给出了每个线程的ID。LATSTIME列包含每个线程最近状态改变的时间戳。
只要服务器收到一个请求,至少有一个线程会改变状态,时间戳就会更新。因此,一个RPC请求失败了,但是没有任何一个线程的时间戳改变,表示这个请求没有到达RPC运行时中。你需要在研究是什么引起的。
在服务端标明调用者
有些时候需要确认谁发送的RPC请求,虽然你只有这次调用的服务线程信息。
这个非常有用-比如,找到谁传递了不合法的参数给RPC调用。
根据某些特别依赖于协议序列的调用,你可以或者不同程度的细节,而有些协议根本没有这些信息(比如NetBiso)
Identifying the caller from the server thread
1- 打开用户模式调试器,挂载到目标服务线程中
2- 通过|命令获取到进程id
1 | 0:001> | |
3- !rpcexts.getcallinfo获取到进程中存在的calls。需要指定进程ID 0x3D4
1 | 0:001> !rpcexts.getcallinfo 0 0 FFFF 3d4 |
查找状态时2或1(dispatched或active)的调用。在这个例子中,只有一个call,如果有更多的,你可以使用!rpcexts.getdbgcell加上cell number(THRDCELL列)来获取线程IDs,从而你可以决定哪个是你感兴趣的调用了
4- 在知道你感兴趣的call之后,看看CONN/CLN所在的cell number,这个是连接对象的cell ID。这里是0000.0003。使用!rpcexts.getdbgcell加上这个id
1 | 0:001> !rpcexts.getdbgcell 3d4 0.3 |
这个命令显示了这个连接的客户端的所有信息。实际的信息会有很多不同,跟使用的transport有关系。
在这个例子中,使用的本地命令管道通信,调用者的进程对象地址也显示了。如果你挂载了内核调试器(或者启动一个本地调试器),你可以使用!process看到看看这个地址的信息。
如果使用LRPC通信,会显示进程ID和线程ID。
如果使用TCP通信,会显示调用者的IP地址。
如果使用了远程命名管道,不会显示任何信息。
转载请注明出处,谢谢!