转载请注明来源:http://blog.csdn.net/imred/article/details/50756016
历时半年,厚厚的CSAPP终于快要啃完了。但是就在这时,敲书上一个示例代码时却出现一个令人困惑的问题。
出现问题的代码是code/netp/hostinfo.c,代码如下:
#include "csapp.h"
int main(int argc, char **argv)
{
char **pp;
struct in_addr addr;
struct hostent *hostp;
if (argc != 2) {
fprintf(stderr, "usage: %s \n" , argv[0]);
exit(0);
}
if (inet_aton(argv[1], &addr) != 0)
hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET);
else
hostp = Gethostbyname(argv[1]);
printf("official hostname: %s\n", hostp->h_name);
for (pp = hostp->h_aliases; *pp != NULL; pp++)
printf("alias: %s\n", *pp);
for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
addr.s_addr = ((struct in_addr *)*pp)->s_addr;
printf("address: %s\n", inet_ntoa(addr));
}
exit(0);
}
因为当时手上没有csapp.h,所以所有的头文件名都是我手敲进去的,编译时一切正常,0 error,0 warning。
但是运行时出故障了:
[admin@localhost gethost]$ ./hostinfo www.hust.edu.cn
official hostname: www.hust.edu.cn
Segmentation fault (core dumped)
经过跟踪,发现程序是在最后一个for循环语句的printf语句core掉的,调试时出现了很奇怪的事情。在出现问题的哪一行我设置了断点,运行到哪一行以后,我先检查了inet_ntoa(addr)的返回值,没有任何异常,然后运行下一条语句,结果又core掉了:
(gdb) print inet_ntoa(addr)
$1 = 0x7ffff7fe2718 "202.114.0.245"
(gdb) n
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a62a94 in _IO_vfprintf_internal (s=, format=, ap=ap@entry=0x7fffffffe3f8) at vfprintf.c:1635
是在进入printf函数后出现了问题,但是明明inet_ntoa(addr)的返回值没有任何问题啊。
我又给程序加了一个中间变量char *temp,inet_ntoa(addr)的返回值先传给temp,然后再传给printf,这次编译时给出了以下警告:
hostinfo_bug.c: In function ‘main’:
hostinfo_bug.c:38:8: warning: assignment makes pointer from integer without a cast [enabled by default]
temp = inet_ntoa(addr);
^
把整型变量传给指针?经过检查,inet_ntoa()返回值是char *没错啊,那么这个warning是怎么出现的呢?当时先没有理会,再跟踪一次试试。结果出现了以下的状况:
(gdb) print inet_ntoa(addr)
$1 = 0x7ffff7fe2718 "202.114.0.245"
(gdb) print temp
$2 = 0xfffffffff7fe2718 0xfffffffff7fe2718 out of bounds>
上面的print是在给temp赋值后输入的,当时确实有点懵了,毕竟这样的事情从来没有遇到过,只是一个简单的赋值语句而已,为什么值会变了。
想了一会儿没明白怎么回事,然后就去吃饭了。吃完饭回来才发现竟然忘了包含arpa/inet.h头文件了。改完代码赶快又跟踪了一次:
(gdb) print inet_ntoa(addr)
$1 = 0x7ffff7fe2718 "202.114.0.245"
(gdb) print temp
$2 = 0x7ffff7fe2718 "202.114.0.245"
问题就这样解决了。
但是为什么呢,只是多包含了一个头文件就让程序正常运行了。我就比较了一下修改前后程序汇编成的汇编代码:
[admin@localhost gethost]$ diff hostinfo.s hostinfo_bug.s
1c1
< .file "hostinfo.c"
---
> .file "hostinfo_bug.c"
43a44
> movl $0, %eax
95a97
> movl $0, %eax
96a99
> cltq
有问题的代码多了3条指令,我又在gdb中把有问题的程序反汇编了一下,多出来的cltq语句出现在了这里:
0x00000000004007f5 <+261>: mov $0x0,%eax
0x00000000004007fa <+266>: callq 0x400580 <inet_ntoa@plt>
0x00000000004007ff <+271>: cltq
0x0000000000400801 <+273>: mov %rax,-0x18(%rbp)
然后就百度了一下cltq,发现原来许多前辈都遇到过同样的问题。比如这篇“一条cltq指令引发的血案”(http://blog.csdn.net/runboying/article/details/7211741)。
我来还原一下血案现场:由于没有arpa/inet.h这个头文件,所以编译器没得到inet_ntoa函数的声明,因此假设该函数返回4字节的int,由于这个返回值存在eax中,因此在把它赋给一个8字节的寄存器之前要先进行符号扩展,也就是使用cltq指令。但是实际上函数的返回值是一个8字节的指针,因此rax高4字节也是有用信息。这样一来,rax高4字节的有用信息就被0或者1覆盖了,地址被改写,也就引发了segmentation fault血案。
由此得到的教训是头文件不能少,函数声明不能少。
转载请注明来源:http://blog.csdn.net/imred/article/details/50756016