C语言:未包含头文件引起的奇怪错误

转载请注明来源: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 <domain name or dotted-decimal>\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=<optimized out>, format=<optimized out>, 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 <Address 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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值