一个不易察觉的逻辑错误

作者在 2010-01-04 12:40:54 发布以下内容
    前段时间写网络程序练笔,写了个sniffer小程序,其中用到ip头:
typedef struct tagIPHDR {
    u_char  vhl;      // version and length of header
    u_char  tos;      // type of service
    u_short totlen;   // total length
    u_short id;       // identification
    u_short flag_off; // flags and fragment offset;
    u_char  ttl;      // time to live
    u_char  protocol; // protocol
    u_short checksum; // checksum
    in_addr src;      // internet address - source
    in_addr dst;      // internet address - destination
} IPHDR,* PIPHDR;
    然后打印收到的ip包中的源地址和目的地址:
    PIPHDR pip = (PIPHDR)buf;
    os<<">From: "<<inet_ntoa(pip->src)
        <<", To: "<<inet_ntoa(pip->dst)
        <<", Size: "<<len<<"B"<<endl;
    问题就出现了,输出的两个地址总是一样的!!!
    一开始总搞不懂,怎么会一样呢?难道是ip头写错了?还是数据包中地址搞错了?
    搞不懂,于是看汇编代码:
    PIPHDR pip = (PIPHDR)buf;
00411643  mov         eax,dword ptr [buf]
00411646  mov         dword ptr [pip],eax
    os<<">From: "<<inet_ntoa(pip->src)
        <<", To: "<<inet_ntoa(pip->dst)
        <<", Size: "<<len<<"B"<<endl;
00411649  mov         esi,esp
0041164B  mov         eax,dword ptr [__imp_std::endl (41C3FCh)]
00411650  push        eax 
00411651  push        offset string "B" (418820h)
00411656  mov         edi,esp
00411658  mov         ecx,dword ptr [len]
0041165B  push        ecx 
0041165C  push        offset string ", Size: " (418814h)
00411661  mov         ebx,esp
00411663  mov         edx,dword ptr [pip]
00411666  mov         eax,dword ptr [edx+10h]
00411669  push        eax 
0041166A  call        dword ptr [__imp__inet_ntoa@4 (41C548h)]
00411670  cmp         ebx,esp
00411672  call        @ILT+540(__RTC_CheckEsp) (411221h)
00411677  push        eax 
00411678  push        offset string ", To: " (41880Ch)
0041167D  mov         ebx,esp
0041167F  mov         ecx,dword ptr [pip]
00411682  mov         edx,dword ptr [ecx+0Ch]
00411685  push        edx 
00411686  call        dword ptr [__imp__inet_ntoa@4 (41C548h)]
0041168C  cmp         ebx,esp
0041168E  call        @ILT+540(__RTC_CheckEsp) (411221h)
00411693  push        eax 
00411694  push        offset string ">From: " (418800h)
00411699  mov         eax,dword ptr [this]
0041169C  mov         ecx,dword ptr [eax]
0041169E  push        ecx 
0041169F  call        std::operator<<<std::char_traits<char> > (4111C7h)
004116A4  add         esp,8
004116A7  push        eax 
004116A8  call        std::operator<<<std::char_traits<char> > (4111C7h)
004116AD  add         esp,8
004116B0  push        eax 
004116B1  call        std::operator<<<std::char_traits<char> > (4111C7h)
004116B6  add         esp,8
004116B9  push        eax 
004116BA  call        std::operator<<<std::char_traits<char> > (4111C7h)
004116BF  add         esp,8
004116C2  push        eax 
004116C3  call        std::operator<<<std::char_traits<char> > (4111C7h)
004116C8  add         esp,8
004116CB  mov         ecx,eax
004116CD  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (41C404h)]
004116D3  cmp         edi,esp
004116D5  call        @ILT+540(__RTC_CheckEsp) (411221h)
004116DA  push        eax 
004116DB  call        std::operator<<<std::char_traits<char> > (4111C7h)
004116E0  add         esp,8
004116E3  mov         ecx,eax
004116E5  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (41C400h)]
004116EB  cmp         esi,esp
004116ED  call        @ILT+540(__RTC_CheckEsp) (411221h)
     可以看到,整个operator<<()语句的参数是从后往前进栈的!很奇怪,这完全违背了源代码的本意,这是为什么呢?
     仔细想想,是可以想明白的,因为编译器在这里做了一步优化,operator<<()是一个函数,第一个参数是ostream对象,返回值也是ostream类型的对象,返回值再作为下一个<<操作的第一个参数,以此类推,会发现这其实是函数的多重迭代。加入operator<<()函数用f代替,原来的代码就等价于:
     f( f( f( f( f( f( f( f( os, ">From: " ), inet_ntoa(pip->src) ), ", To: " ), inet_ntoa(pip->dst) ),", Size: " ),len ), "B" ), endl() )
     也就是说这样一个函数调用的汇编代码如上,参数进栈方式是从右往左,这样就理所当然了,c标准函数调用,参数自右向左进栈,于是先是endl()首地址进栈,然后左边的参数是另一f函数的返回值,所以得运算f函数,再次自右向左压参数,于是"B"串的首地址进栈,以此类推。
     但这只能说明那段汇编代码的写法情有可原,到底是什么让源地址、目的地址相同呢?
     这就涉及到socket编程中的一点知识了,看看inet_ntoa函数:
       char* FAR inet_ntoa( __in struct in_addr in );
     返回的是一个字符串首地址,而这个地址是有效的,说明是socket库自动分配的内存空间,MSDN上说:
      The string returned by inet_ntoa resides in memory that is allocated by Windows Sockets. The application should not make any assumptions about the way in which the memory is allocated. The string returned is guaranteed to be valid only until the next Windows Sockets function call is made within the same thread.
     也就是说这两次返回的地址,如果不是NULL,那么就是相同的,而operator<<()函数调用时,这个地址已经被计算出来了,即所有的参数都已经确定,多以打印两个地址的操作引用的是同一个地址,即源地址字符串地址。
     话说回来,这也不是什么重大发现,但你会发现operator<<()这个函数调用的规则 需要特别注意,如果出现错误就很难发现,还有那个socket函数,这其实是个基础知识,也需尤其留心。
编程杂记 | 阅读 984 次
文章评论,共0条
游客请输入验证码