第一回分析源码
程序流程一览图
感觉有点简单吧,ping的核心就是上面的图了,而其它都是对选项的处理,不仅仅是ping源码,其它开源代码一般都是一堆选项,如果一开始就被那些对分析程序核心没有帮助的选项所困扰,那么分析一个程序需要的时间和效率就可想而知。当然ping的很多选项都是值得分析的。
这个ping源码控制发包的时间间隔是通过main_loop()比较复杂的时间计算控制,因为它要控制诸如-l这样的选项,如果纯粹是控制时间,那么注册个SIGALRM信号处理函数,再加个1秒的定时器alarm(1)或者就直接sleep(1)就可以很轻松的实现。-w选项就是通过alarm完成的,SIGALRM信号处理函数是在main()->setup()中设置的,具体请看setup函数分析。
1,准备工作,获得ping源码并学会编译
会编译了看代码其实就是后续的活,网上找的方法,挺好用……
[root@xxx study_2]# type ping
ping is /bin/ping
[root@xxx study_2]# rpm -qf /bin/ping
iputils-20020927-46.el5
从而得知ping属于iputils包,去下iputils源码包即可,其它linux 命令源码获取方法类似。
[root@xxx iputils]# make
Please, set correct KERNEL_INCLUDE
make: *** [check-kernel] 错误 1
反正是一堆错误,改了这个来了那个,看了INSTALL说是被定制过的linux头文件可能会不支持,反正只要能编出ping就行,改了下Makefile,把有关KERNEL_INCLUDE的都屏蔽了,编译成功。
2,源码分析
只分析实现最简单的不带选项的ping程序。也花了一个星期的业余时间。
112main(int argc, char **argv)
113{
114 struct hostent *hp;
115 int ch, hold, packlen;
116 int socket_errno;
117 u_char *packet;
118 char *target, hnamebuf[MAXHOSTNAMELEN];
119 char rspace[3 + 4 * NROUTES + 1]; /* record route space */
120#ifdef DO_IPSEC
121 char *policy_string = NULL;
122#endif
123
124 icmp_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); //创建icmp套接字
125 socket_errno = errno;
126
127 uid = getuid();//设置进程有效用户ID为实际用户uid,访问权限方面的知识。
128 setuid(uid);
129
130 source.sin_family = AF_INET;
131
132 preload = 1;//这个参数默认为1,可由-l选项控制,一次发多个包而不管对方是否回应,其本质还是通过计算时间来实现,具体可看main_loop().没兴趣不用管。
133 while ((ch = getopt(argc, argv, COMMON_OPTSTR "bRT:P:")) !=EOF) {
134 switch(ch) {//getopt得好好学会。
213 default:
214 usage();
215 }
216 }
217 argc -= optind;//可参考GETOPT(3) ,是个全局变量
218 argv += optind;
219
//这里对选项的处理,不管,option为0。
220 printf("options :%d\n", options );
221 if (argc == 0)
222 usage();
223 if (argc > 1) {
224 if (options & F_RROUTE)
225 usage();
226 else if (options & F_TIMESTAMP) {
227 if (ts_type != IPOPT_TS_PRESPEC)
228 usage();
229 if (argc > 5)
230 usage();
231 } else {
232 if (argc > 10)
233 usage();
234 options |= F_SOURCEROUTE;
235 }
236 }
//处理传给ping的参数
237 while (argc > 0) {
//我只传一个目标IP,所以argc=1,这个循环只做一回,把IP地址给了target
238 target = *argv;
239 printf("target:%s\n", target);
240
241 bzero((char *)&whereto, sizeof(whereto));
242 whereto.sin_family = AF_INET;
243 if (inet_aton(target, &whereto.sin_addr) == 1) {//如果是IP地址
244 hostname = target;
245 if (argc == 1)
246 options |= F_NUMERIC;
247 } else {//如果是其它域名
248 hp = gethostbyname(target);
249 if (!hp) {//如果解析域名不成功,返回
250 fprintf(stderr, "ping:unknown host %s\n", target);
251 exit(2);
252 }
253 memcpy(&whereto.sin_addr, hp->h_addr, 4);
254 strncpy(hnamebuf, hp->h_name,sizeof(hnamebuf) - 1);
255 hnamebuf[sizeof(hnamebuf) - 1] = 0;
256 hostname = hnamebuf;
257 }
258 if (argc > 1)
259 route[nroute++] = whereto.sin_addr.s_addr;
260 argc--;
261 argv++;
262 }
//这部分只是对目标IP的一个connect尝试,或者判断是否是广播地址,connect失败退出,
//如果成功再获取本机struct sockaddr结构到source,
264 if (source.sin_addr.s_addr == 0) {
265 int alen;
266 struct sockaddr_in dst = whereto;
267 int probe_fd = socket(AF_INET, SOCK_DGRAM, 0);
268
269 if (probe_fd < 0) {
270 perror("socket");
271 exit(2);
……
……
……
322 alen = sizeof(source);
323 if (getsockname(probe_fd, (struct sockaddr*)&source, &alen) ==-1) {
324 perror("getsockname");
325 exit(2);
326 }
327 source.sin_port = 0;
328 close(probe_fd);
329 } while (0);
330
//如果目标IP为0.0.0.0,则赋值目标IP为127.0.0.1,即ping本地回环地址。
331 if (whereto.sin_addr.s_addr == 0)
332 whereto.sin_addr.s_addr = source.sin_addr.s_addr;
333
//如果icmp_sock套接字创建失败,返回。
334 if (icmp_sock < 0) {
335 errno = socket_errno;
336 perror("ping: icmp open socket");
337 exit(2);
338 }
339
//device是加-I选项选择出口接口,这回没加-I选项。
338 if (device) {
……
……
349 }
……
//这部分都是对一些选项处理, 省略。
……
//,
446
447 /* Estimate memory eaten by single packet. It is rough estimate.
448 * Actually, for small datalen's it depends on kernel side a lot. */
449 hold = datalen + 8;
450 hold += ((hold+511)/512)*(optlen + 20 + 16 + 64 + 160);
451 sock_setbufs(icmp_sock, hold);//设置此套接字接收和发送缓冲区大小。
515 printf("PING %s (%s) ", hostname, inet_ntoa(whereto.sin_addr));
516 if (device || (options&F_STRICTSOURCE))
517 printf("from %s %s: ", inet_ntoa(source.sin_addr), device ?:"");
518 printf("%d(%d) bytes of data.\n", datalen,datalen+8+optlen+20);
519
520 setup(icmp_sock);
521
522 main_loop(icmp_sock, packet, packlen);
//到这里main就结束了。现在来分析setup和main_loop。
//setup()函数
……
478 set_signal(SIGINT, sigexit);
479 set_signal(SIGALRM, sigexit);
480 set_signal(SIGQUIT, sigstatus);
481
482 gettimeofday(&start_time, NULL);
483
484 if (deadline) {
485 struct itimerval it;
486
487 it.it_interval.tv_sec = 0;
488 it.it_interval.tv_usec = 0;
489 it.it_value.tv_sec = deadline;
490 it.it_value.tv_usec = 0;
491 setitimer(ITIMER_REAL, &it, NULL);
492 }
……
set_signal(SIGALRM, sigexit);
static void sigexit(int signo)
{
exiting = 1;
}
主要是注册了几个信号量。为-w选项设置定时器,如果超时,置exiting为1,而在main_loop()的主循环中一开始就对这个变量做了判断,如果exiting为真跳出循环,程序退出。
for (;;) {
/* Check exit conditions. */
if (exiting)
break;
……
……
}
//main_loop()函数,关键东西都在这里。
504 void main_loop(int icmp_sock, __u8*packet, int packlen)
505 {
506 char addrbuf[128];
507 char ans_data[4096];
508 struct iovec iov;
509 struct msghdr msg;
510 struct cmsghdr *c;
511 int cc;
512 int next;
513 int polling;
514
515 iov.iov_base = (char *)packet;
516
517 for (;;) {
518 /* Check exit conditions. */
519 if (exiting)
520 break;
521 if (npackets && nreceived + nerrors >= npackets)
522 break;
523 if (deadline && nerrors)
524 break;
525 /* Check for and do special actions. */
526 if (status_snapshot)
527 status();
528
529 /* Send probes scheduled to this time. */
530 do {
531 next = pinger();//发送ICMP包
532 next = schedule_exit(next);
533 } while (next <= 0);
534
535 /* "next" is time to send next probe, if positive.
536 * If next<=0 send now or assoon as possible. */
537
538 /* Technical part. Looks wicked. Could be dropped,
539 * if everyone used the newest kernel. :-)
540 * Its purpose is:
541 * 1. Provide intervals less than resolution of scheduler.
542 * Solution: spinning.
543 * 2. Avoid use of poll(), when recvmsg() can provide
544 * timed waiting (SO_RCVTIMEO).*/
545 polling = 0;
546 if ((options & (F_ADAPTIVE|F_FLOOD_POLL)) ||next 547 int recv_expected = in_flight(); 548 549 /* If we are here, recvmsg() isunable to wait for 550 * required timeout. */ 551 if (1000*next <=1000000/(int)HZ) { 552 /* Very shorttimeout... So, if we wait for 553 * something, we sleep forMININTERVAL. 554 * Otherwise, spin! */ 555 if (recv_expected) { 556 next = MININTERVAL; 557 } else { 558 next = 0; 559 /* When spinning, noreasons to poll. 560 * Use nonblockingrecvmsg() instead. */ 561 polling = MSG_DONTWAIT; 562 /* But yield yet. */ 563 sched_yield(); 564 } 565 } 566 567 if (!polling && 568 ((options &(F_ADAPTIVE|F_FLOOD_POLL)) || interval)) { 569 struct pollfd pset; 570 pset.fd = icmp_sock; 571 pset.events = POLLIN|POLLERR; 572 pset.revents = 0; 573 if (poll(&pset, 1, next)< 1 || 574 !(pset.revents&(POLLIN|POLLERR))) 575 continue; 576 polling = MSG_DONTWAIT; 577 } 578 } 579 580 for (;;) {//接收ICMP_ECHOREPLY回应包 581 struct timeval *recv_timep = NULL; 582 struct timeval recv_time; 583 int not_ours = 0; /* Raw socketcan receive messages 584 * destined to otherrunning pings. */ 585 586 iov.iov_len = packlen; 587 msg.msg_name = addrbuf; 588 msg.msg_namelen = sizeof(addrbuf); 589 msg.msg_iov = &iov; 590 msg.msg_iovlen = 1; 591 msg.msg_control = ans_data; 592 msg.msg_controllen =sizeof(ans_data); 593 594 cc = recvmsg(icmp_sock, &msg,polling);//接收 595 polling = MSG_DONTWAIT; 596 597 if (cc < 0) { 598 if (errno == EAGAIN || errno== EINTR) 599 break; 600 if (!receive_error_msg()) { 601 if (errno) { 602 perror("ping: recvmsg"); 603 break; 604 } 605 not_ours = 1; 606 } 607 } else { 608 609 #ifdef SO_TIMESTAMP 610 for (c = CMSG_FIRSTHDR(&msg); c; c = CMSG_NXTHDR(&msg, c)) { 611 if (c->cmsg_level !=SOL_SOCKET || 612 c->cmsg_type !=SO_TIMESTAMP) 613 continue; 614 if (c->cmsg_len 615 continue; 616 recv_timep = (structtimeval*)CMSG_DATA(c); 617 } 618 #endif 619 620 if ((options&F_LATENCY) ||recv_timep == NULL) { 621 if((options&F_LATENCY) || 622 ioctl(icmp_sock, SIOCGSTAMP,&recv_time)) 623 gettimeofday(&recv_time, NULL); 624 recv_timep =&recv_time; 625 } 626 627 not_ours =parse_reply(&msg, cc, addrbuf, recv_timep);//处理回应包 628 } 629 630 /* See? ... someone runs anotherping on this host. */ 631 if (not_ours) 632 install_filter(); 633 634 /* If nothing is in flight,"break" returns us to pinger. */ 635 if (in_flight() == 0) 636 break; 637 638 /* Otherwise, try to recvmsg()again. recvmsg() 639 * is nonblocking after the firstiteration, so that 640 * if nothing is queued, it will receiveEAGAIN 641 * and return to pinger. */ 642 } 643 } 644 finish();//收尾 645 } //pinger() int pinger(void) { staticint oom_count; staticint tokens; inti; /*Have we already sent enough? If we have, return an arbitrary positive value. */ if(exiting || (npackets && ntransmitted >= npackets &&!deadline)) return 1000; /*Check that packets < rate*time + preload */ if(cur_time.tv_sec == 0) {//这部分对时间的控制 gettimeofday(&cur_time, NULL); tokens = interval*(preload-1);//preload就是通过-l选项送进来的,默认为1。 }else { long ntokens; struct timeval tv; gettimeofday(&tv, NULL); ntokens = (tv.tv_sec -cur_time.tv_sec)*1000 + (tv.tv_usec-cur_time.tv_usec)/1000; if (!interval) { /*Case of unlimited flood is special; * if we see no reply, they are limited to100pps */ if(ntokens < MININTERVAL && in_flight() >= preload) return MININTERVAL-ntokens; } ntokens += tokens; if (ntokens > interval*preload) ntokens= interval*preload; if (ntokens < interval) returninterval - ntokens; cur_time = tv; tokens = ntokens - interval; } resend: i= send_probe();//发送数据包的核心。 if(i == 0) {//发送数据包成功。 oom_count = 0; advance_ntransmitted(); if (!(options & F_QUIET) &&(options & F_FLOOD)) { /*Very silly, but without this output with * high preload or pipe size is very confusing.*/ if((preload < screen_width && pipesize < screen_width) || in_flight() < screen_width) write(STDOUT_FILENO, ".", 1); } return interval - tokens; } //发送失败,错误处理。 /*And handle various errors... */ if(i > 0) { /* Apparently, it is some fatal bug. */ abort(); }else if (errno == ENOBUFS || errno == ENOMEM) { …… …… } returnSCHINT(interval); } //send_probe() int send_probe() { structicmphdr *icp;//icmp报文头部结构。 intcc; inti; icp= (struct icmphdr *)outpack; icp->type= ICMP_ECHO; //request请求类型。 icp->code= 0; icp->checksum= 0; icp->un.echo.sequence= htons(ntransmitted+1);//数据包序列。可以把htons去掉抓包对比。 icp->un.echo.id= ident; /* ID */ //进程ID。 CLR((ntransmitted+1)% mx_dup_ck); if(timing) { if (options&F_LATENCY) { staticvolatile int fake_fucked_egcs = sizeof(struct timeval); structtimeval tmp_tv; gettimeofday(&tmp_tv,NULL);//获得当前时间发送给对方,为计算来回时间差RTT /*egcs is crap or glibc is crap, but memcpy does not copy anything, if len is constant!*/ memcpy(icp+1,&tmp_tv, fake_fucked_egcs); } else { memset(icp+1,0, sizeof(struct timeval)); } } cc= datalen + 8; /* skips ICMPportion */ /*compute ICMP checksum here */ icp->checksum= in_cksum((u_short *)icp, cc, 0); //数据校验,不分析了,《计算机组成原理》貌似讲过原理,可以参考TCPv1,TCPv2或者其它书籍。 if(timing && !(options&F_LATENCY)) { static volatile int fake_fucked_egcs =sizeof(struct timeval); struct timeval tmp_tv; gettimeofday(&tmp_tv, NULL); /* egcs is crap or glibc is crap, butmemcpy does not copy anything, if len is constant! */ memcpy(icp+1, &tmp_tv,fake_fucked_egcs); icp->checksum = in_cksum((u_short*)(icp+1), fake_fucked_egcs, ~icp->checksum); } do { static struct iovec iov = {outpack, 0}; static struct msghdr m = { &whereto,sizeof(whereto), &iov, 1, &cmsg, 0, 0 }; m.msg_controllen = cmsg_len; iov.iov_len = cc;//可选数据为56字节再加8字节ICMP报头=64字节。所以整个icmp包就是64字节,最终产生64+20字节IPv4首部=84字节的IP数据包。 i = sendmsg(icmp_sock, &m, confirm);//终于发出去了。 confirm = 0; }while (0); return(cc == i ? 0 : i); } //parse_reply(),最后一部分对回应包的处理,这也是核心,需要对icmp包和ip数据包很好了解。 parse_reply(struct msghdr *msg, int cc,void *addr, struct timeval *tv) { structsockaddr_in *from = addr; __u8*buf = msg->msg_iov->iov_base; structicmphdr *icp; structiphdr *ip; inthlen; intcsfailed; /*Check the IP header */ ip= (struct iphdr *)buf; hlen= ip->ihl*4;//这个为什么*4可看TCPv1,它其实就是IP首部以字节为单位的长度。 if(cc < hlen + 8 || ip->ihl < 5) { if (options & F_VERBOSE) fprintf(stderr,"ping: packet too short (%d bytes) from %s\n", cc, pr_addr(from->sin_addr.s_addr)); return 1; } /*Now the ICMP part */ cc-= hlen; icp= (struct icmphdr *)(buf + hlen);//指针往后移ip首部长度就是ICMP头部了。 csfailed= in_cksum((u_short *)icp, cc, 0);//数据校验。 if(icp->type == ICMP_ECHOREPLY) {//如果是回应报 if (icp->un.echo.id != ident)//判断是否是自己发的请求包。如果不是返回。 return1; /* 'Twas not our ECHO */ if (gather_statistics((__u8*)(icp+1), cc, ntohs(icp->un.echo.sequence), ip->ttl, 0, tv, pr_addr(from->sin_addr.s_addr))) //计算来回时间差,并打印。 return0;//返回到main_loop,继续循环做时间计算-->发包-->收包。 }else {//如果不是,做其它处理。。。。 return0; } 4,总结。 本次ping源码分析知识点: 源码获得方法、 Linu网络编程、 Linux系统编程信号使用、 Linux时间相关系统调用、 getopt对选项处理过程(可以看下功能更强大的getopt_long) ICMP报文组成理解、 本地和网络字节序理解、 抓包分析能力 附常用抓包命令: tcpdump抓包常用命令: tcpdump -ni any icmp tcpdump -ni any host 192.168.147.1 tcpdump -ni any port \(500 or 4500\) tcpdump -ni any arp -s0 -w arp.cap 当然windows一下的wireshark也是非常不错的抓包分析工具。