使用WinDBG调试OnDO Server

一、问题

在OnDo服务端和PJSIP客户端配置好IPv6,发现电话机可以向服务器注册成功,但使用话机A拨打话机B时,OnDo服务器返回500 (Server Internal Error) 错误。抓的包如下:

 

通过仔细对比与IPv4下INVITE的请求,没有发现明显差异。服务器的嫌疑比较大。

二、分析

要分析这个问题,首先需要定位服务器是何时发送500错误的,为此需要跟踪服务器的执行过程。

在这里我们使用WinDBG来调试跟踪,WinDBG是微软提供的在Windows上强大的调试工具,下载页面在这里:
http://msdn.microsoft.com/en-us/windows/hardware/gg463009.aspx
如果页面过期,可以直接在MSDN中搜索 WinDBG 即可找到相关的页面。基本的调试命令这里也不解释了,文档非常详细,网络上相关的资料也非常多。

运行WinDBG,点击 File -> Attach to process ,我们要附加到OnDo Server的进程,然后才能进行调试。那么怎么找到这个进程?如果你对OnDo Server非常熟悉,知道它是一个运行于Java平台上的一个组件,那么你会非常容易找到那个合适的java.exe进程。如果不熟悉的话,我们可以这样做:
首先,打开Windows的控制台,输入这个命令:

wmic process list full

 

这个命令会详细列出正在运行的每个进程的详细信息,仔细找里面的一个(可以重定向到一个文件中,搜索ondo即可)

CommandLine="C:\Program Files\Java\jre7\bin\java" ... com.brekeke.ondo.sv ...
Description=java.exe
Handle=2368
Name=java.exe
...

在这里省略了一些大部分信息,只列出了其中几个属性,但对我们已经足够了。从CommandLine属性,我们知道我们没有看错人(进程);现在只需去找PID为2368,名称为java.exe的进程就可以了。

使用WinDBG附加到这个进程上。

然后,我们要设置断点,理想的断点是当OnDo产生500错误的时候(一定有会一个判断语句,从这个语句进入了这个分支),但这似乎并不现实,如果我们知道它如何产生的500错误,就不用如此费力的分析原因了。

所以,我们可以找到它发送500错误的时候。我们知道,在Windows平台上,网络通信最终使用的基本都是WinSock——当然也可以不是,但这种情况并不多见,我们先做这样的假设,OnDo使用的是WinSock,如果后面进行不下去了,我们再回头来分析其他情况,不过谢天谢地,这件事没有发生,我们的假设是正确的。使用WinSock,发送消息的函数并不多,就四个,下面列出了四个函数的声明:

int send(
  _In_  SOCKET s,
  _In_  const char *buf,
  _In_  int len,
  _In_  int flags
);

int sendto(
  _In_  SOCKET s,
  _In_  const char *buf,
  _In_  int len,
  _In_  int flags,
  _In_  const struct sockaddr *to,
  _In_  int tolen
);

int WSASend(...);    // 参数省略,详见MSDN

int WSASendTo(...);  // 参数省略,详见MSDN

 更详细的信息可以查阅MSDN,WinSock Functions:

http://msdn.microsoft.com/en-us/library/windows/desktop/ms741394%28v=vs.85%29.aspx
从MSDN我们还可以知道,这些函数都在ws2_32.dll中,因此我们就可以很方便的设置断点了(实际上我只设置了两个,而只用到了一个):

0:018> bp ws2_32!send
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\system32\WS2_32.dll -
0:018> bp ws2_32!sendto
0:018> bl
 0 e 71a24c27     0001 (0001)  0:**** WS2_32!send
 1 e 71a22f51     0001 (0001)  0:**** WS2_32!sendto

粗体部分就是我使用的命令。

现在我们可以打电话了。我们设置的断点,会截获所有调用send和sendto的地方,因此在调试过程中,我们可以看到,OnDo首先给客户端回复100 Trying 消息,然后尝试转发INVITE消息(并失败),然后再给客户端回复500 Server Internal Error 消息。这个过程很清晰,我们可以很明确的看到,转发INVITE消息的sendto调用,返回值是-1 (0xFFFFFFFF),也就是SOCKET_ERROR。

这个事情让人很疑惑,我的两个话机是在同一个ONT上的,IP地址是一样的,唯一不同的就是注册的号码和绑定的端口号,sendto显然不会涉及到注册的号码,那么为什么给同一个IP地址的两个端口发送数据会导致两种截然不同的结果呢?我们还记得两部话机的注册都是好的,500错误也可以收到,那么是什么导致这样的差异呢?对此,我们可以详细比较一下转发INVITE和发送100通知(或者500错误)的两次sendto调用的参数。

下图是发送100 Trying 消息时调用 sendto() 的堆栈(从esp寄存器看就可以了)。

 

下图是转发INVITE 消息时调用 sendto() 的堆栈。

 

这样看还不够明显。我们知道 sendto() 的 6 个参数,因此逐个对比一下:

参数

100 Trying

INVITE

Socket

60 0d 00 00

60 0d 00 00

Buffer

44 f1 54 03

44 f1 64 03

Buffer length

81 01 00 00

89 04 00 00

Flags

00 00 00 00

00 00 00 00

Socket address

28 f1 54 03

28 f1 64 03

Socket address length

1c 00 00 00

1c 00 00 00

第二、三、四个参数基本可以忽略。我们看到,两次发送,连套接字使用的都是相同的。似乎INVITE更加没有理由发送失败了。现在唯一不同就是发送的目的地 (struct sockaddr 结构) 了,我们再到那块内存中继续寻找线索。

这个内存在截图中已经体现出来了(就是在发送的buffer前面的28个字节)。这个结构的定义是这样的:

struct in6_addr {
    union {
        u_char  Byte[16];
        u_short Word[8];
    } u;
};

struct sockaddr_in6 {
    short   sin6_family;         // 2
    u_short sin6_port;           // 2
    u_long  sin6_flowinfo;       // 4
    struct  in6_addr sin6_addr; // 16
    u_long  sin6_scope_id;       // 4   
};

应该明确一下,IPv4和IPv6使用的结构是不同的,这也是sendto() 最后一个参数的用途,我们这里自然只需关心IPv6的结构。为了更加明显的进行对比,请看下表:

成员

100 Trying

INVITE

sin6_family

17 00

17 00

sin6_port

13 d8

13 da

sin6_flowinfo

00 00 00 00

00 00 00 00

sin6_addr

fe c0 00 00 00 00 00 00

c3 0c ab ff 13 6f 22 1c

fe c0 00 00 00 00 00 00

c3 0c ab ff 13 6f 22 1c

sin6_scope_id

01 00 00 00

00 00 00 00

其中差异已经用红蓝两种颜色表示出来了。前面我说过,我的两个话机是在同一个ONT上,使用不同的端口注册的,所以他们共享同一个IP。从这里你可以明确的看出两个端口分别是5080(0x13d8)和5082(0x13da),因此这个差异是预料之中的。那么剩下的就只有 sin6_scope_id 了。为什么一个是1,另一个是0呢?其实首先应该问,是这个差异导致的 sendto() 失败进而服务器返回 500 错误吗?答案是肯定的。当我们人为的把这个0改为1的时候,奇迹(其实不是奇迹,而是我们预期的现象)出现了,INVITE发送成功,另一个话机响铃了。现在要问,为什么一个是1,另一个是0呢?我不知道。是的,我不知道。同样的配置,在Windows 7 系统上完全没有问题,而在 Windows XP 上就戏剧性的出现了上面的一幕。在OnDO的配置上,Configuration -> System -> Network,我们只设置了IPv6地址,而没有设置 scope 。

 

因此,OnDO在打算从这个 interface 发送 INVITE 请求时,使用了某种方法获取 scope ID,而显然它没有获取到(或者获取到了错误的值),后面的事情我们都知道了。因此,假如我们在这里明确指定 scope :

 

结果就正确了,所有的数据包都正常发送,电话可以通了。友情提示,这里的 scope 可以在命令行输入 ipconfig 查看,请不要猜测。

到这里,问题已经解决了。现在我依然很诧异OnDO在Windows XP和Windows 7系统的表现上的差异究竟来源于何处。

三、结论

归根结底,还是OnDO 服务器的配置问题,至少在 Windows XP 系统上,如果使用IPv6,需要把 scope 同时填写到地址上。这一点在Brekeke官方的Wiki上并没有体现(或许是他隐含的意思?),这就导致了我们兜了一个大圈。

posted @ 2013-06-20 08:37  西北望长安  阅读(648)  评论(0编辑  收藏  举报