专栏首页编程珠玑记64位地址截断引发的挂死问题

记64位地址截断引发的挂死问题

前言

最近要将整个项目的代码从原先的只支持32位变成同时支持32位和64位,这个过程中遇到一个很不容易定位的挂死问题,花了不少时间才定位解决,因此分享给大家。

32位和64位代码区别

在分享之前,需要了解一下32位和64位程序代码有何区别,它的主要区别体现在某些数据类型的占用字节大小的不同:

数据类型

32位

64位

long

4字节

8字节

unsigned long

4字节

8字节

指针

4字节

8字节

size_t

4字节

8字节

ssize_t

4字节

8字节

这些是主要的差别。

那么为什么要切64位呢?原因也很简单,32位寻址范围有限,能使用的最大内存也是非常有限的,因此需要使其能够支持64位,这个过程需要修改编译工程,编译第三方库为64位,修改代码等等。当然这些都不是本文的重点,本文仅介绍遇到的这个典型的问题。

问题描述

由于项目本身涉及的系统比较复杂,因此简单分享一下定位过程,下一节将通过简洁的示例程序来说明。

问题现象:向服务器发送一条操作指令后直接挂死

分析解决过程简化为如下步骤:

  • 查看日志以及coredump信息,初步定位挂死的位置
  • 发现挂死在停止定时器的位置
  • 32位程序正常,而64位异常,因此和32位与64位的差别有关
  • 怀疑传入定时器数据有问题,编写小demo,排除传入数据问题
  • 编译可调试版本,加入-g参数
  • 跟踪调试,发现最终挂在了一个动态库中
  • 设置gdb源码路径,以便调试跟踪动态库
  • 通过gdb观察传入指针,在访问指针时,出现错误,提示访问非法内存
  • 打印传入定时器指针地址,发现异常,地址开头4字节为全f,不正常,因此怀疑该指针最开始就已经出问题
  • 跟踪启动定时器部分,动态库接口返回的地址值,就已经异常了。但是跟踪到动态库接口内部,发现返回的结果是正常的8字节地址值,排除定时器接口的问题
  • 最终可以确定,在调用动态库接口时,虽然返回的是8字节地址,但是赋给外部变量时,就被截断
  • 换项目中的另外一个进程调试demo发现,编译时出现错误,提示函数没有声明
  • 于是加上声明之后编译通过,但并没有出现挂死的问题
  • 随即继续跟踪原项目出问题的进程,发现同样这些接口都没有外部声明,再加上另外一个进程的警告信息,提示有int往指针强转,因此怀疑和函数的声明有关。

最终确实如此。

具体是为什么呢?

简化示例

示例代码分别放在main.c和test.c中,main.c内容如下:

//main.c
//公众号编程珠玑
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    void *p = NULL;
    //打印p的地址
    printf("%p\n",&p);
    //为p赋值
    p = testFun();
    printf("%p\n",p);
    //释放内存
    free(p);
    p = NULL;
    return 0;
}

test.c的内容如下:

//test.c
#include<stdio.h>
#include<stdlib.h>
void *testFun()
{
   //申请内存需要足够大,能方便达到本文的示例效果
    void *p = (void*)malloc(1024*1024*10);
    if(NULL == p)
    {
        printf("malloc failed\n");
    }
    printf("malloc success,p = %p\n",p);
    return p;
}

上面两段代码再简单不过,testFun在函数中申请一段内存,并返回。而main函数通过调用testFun,将地址值返回给p,并打印p的地址值。

编译运行:

$ gcc -o main main.c test.c
$ ./main
0x7ffef59d4230
malloc success,p = 0x7f193ec5f010
0x3ec5f010
Segmentation fault (core dumped)

从运行结果中,我们可以发现以下几个事实:

  • 64位程序地址为8字节
  • testFun内部申请到的内存地址值是占用8字节的值
  • main函数中的p的地址值为4字节
  • 返回值被截断了

也就是和我们预期的结果完全不一样。我们逐步分析,到底是为什么。

特别说明: 如果赋值那一行改成下面这样

p = (void*)testFun();

运行结果中如下:

0x7ffd5a75dbe0
malloc success,p = 0x7fc6fb5ac010
0xfffffffffb5ac010
Segmentation fault (core dumped)

其实看到8字节的前面4字节都是f,就可以判断这个地址是非法的了。为什么?(提示:程序地址空间分布)。

为什么coredump?

这个问题很明显,因为申请内存得到的地址值与释放内存的地址不是同一个,因此导致coredump(coredump的查看可参考《linux常用命令-开发调试篇》中的gdb部分)。

为什么地址值被截断?

在解释这个之前,我们先看一个简单的示例程序:

//testReturn.c
#include<stdio.h>
test()
{
    printf("test function\n");
    return 0;
}
int main(void)
{
    test();
    return;
}

编译:

$ gcc -o test testReturn.c
testReturn.c:2:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
 test()
 ^

我们在编译的时候出现了一个警告,提示test函数没有返回值,会默认返回值为int。

也就是说,如果函数实际有返回值,但是函数返回值类型却没有指明,编译器会将其默认为int

实际上前面的示例程序在编译的时候就有警告:

main.c: In function ‘main’:
main.c:11:9: warning: implicit declaration of function ‘testFun’ [-Wimplicit-function-declaration]
     p = testFun();
         ^
main.c:11:7: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     p = testFun();
       ^

两个警告的意思分别为:

  • testFun没有声明
  • 尝试从整形转换成指针

第一个警告很容易理解,虽然定义了testFun函数,但是在main函数中并没有声明。因此对mian函数来说,它在编译阶段(关于编译阶段,可参考《hello程序是如何变成可执行文件的》),“看不到”testFun,因此会默认为其返回值为int。而正因如此,就有了第二个警告,提示从整型转换成指针。

到此其实也就真相大白了。既然testFun的返回值被编译器默认为int,返回一个8字节的指针类型,而返回值却是int,自然就会被截断了。

如何解决

既然知道原因所在,那么如何解决呢?这里提供两种方式。

  • extern声明
  • 在头文件中声明,调用者包含该头文件

按照第一种方式,在main.c中增加一行声明:

extern void *testFun();

运行结果:

0x7fffee1bd7b0
malloc success,p = 0x7fcafef2e010
0x7fcafef2e010

第二种方式,增加test.h,内容为testFun的声明:

//test.h
#include<stdio.h>
#include<stdlib.h>
void *testFun();

main.c包含test.h头文件:

//main.c
//公众号编程珠玑
#include"test.h"
int main(void)
{
    void *p = NULL;
    //打印p的地址
    printf("%p\n",&p);
    //为p赋值
    p = testFun();
    printf("%p\n",p);
    //释放内存
    free(p);
    p = NULL;
    return 0;
}

test.c修改如下:

//test.c
#include"test.h"
void *testFun()
{
    void *p = (void*)malloc(1024*1024*10);
    if(NULL == p)
    {
        printf("malloc failed\n");
    }
    printf("malloc success,p = %p\n",p);
    return p;
}

以上两种方式都可解决前面的问题。

而32位程序为什么正常?相信你已经有了答案。

总结

由于对出现问题的程序代码不熟悉,加上其编译工程充斥着大量的警告而没有处理,以及涉及动态库,导致这个引起挂死问题的罪魁祸首没有提前暴露处出来。而问题的根本原因我们也清楚了,就是因为调用函数前没有声明。本文总结如下:

  • 不要忽略任何一个警告,除非你非常清楚地知道自己在做什么
  • 在头文件中声明函数,并提供给调用者
  • 函数使用前进行声明
  • 问题长期定位不出来时,休息一下
  • 尽量编写通用性代码
  • 非必要时不强转
  • 使用void *指针格外小心

思考

  • 为什么32位的时候运行正常,而64位程序会挂死
  • 32位和64位程序用户空间地址范围分别是多少
  • 如何在调试中设置程序源码路径
  • 程序完整编译经历那几个阶段

本文分享自微信公众号 - 编程珠玑(shouwangxiansheng),作者:守望先生

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 动态库的制作与两种使用方式你掌握了吗?

    在《如何制作属于自己的静态库》中简单介绍了静态库的制作方法,但实际上动态库的使用更为广泛,至于原因,在《静态库和动态库的区别》一文中已有说明。本文介绍动态库的制...

    编程珠玑
  • 每周小题-Linux命令,编译链接

    编程珠玑
  • 理一理字节对齐的那些事

    字节对齐是我们初学C语言就会接触到的一个概念,但是到底什么是字节对齐?对齐准则又是什么?为什么要字节对齐呢?字节对齐对我们编程有什么启示?本文将简单理一理字节对...

    编程珠玑
  • 华裔女孩“美国梦”:从清洁工到领军谷歌回中国做AI,她实现人生逆袭

    最实用、最超前的美国移民和投资动态 12月13日,Google宣布Google AI 中国中心(Google AI China Center)于北京正式成立,大...

    企鹅号小编
  • Linux下环境变量配置方法梳理(.bash_profile和.bashrc的区别)

    在linux系统下,如果下载并安装了应用程序,在启动时很有可能在键入它的名称时出现"command not found"的提示内容。如果每次都到安装目标文件夹内...

    洗尽了浮华
  • Linux系统相关配置

    为了使程序在崩溃时产生core文件,我们经常在终端使用命令ulimit -c unlimited 来设置。但是当前设置只能在当前会话有效,当关闭当前会话,打开新...

    Dabelv
  • 未来可期:物联网未来十年6大发展趋势

    未来十年里,随着物联网设备得到更广泛的应用,以及各行业找到新的方法来使物联网技术适应特定行业需求,我们将看到物联网市场的重大变化与发展趋势。

    i策划
  • MongoDB 入门篇

      一般而言,数据缺乏组织及分类,无法明确的表达事物代表的意义,它可能是一堆的杂志、一大叠的报纸、数种的开会记录或是整本病人的病历纪录。数据描述事物的符号记录,...

    惨绿少年
  • 物联网将如何为远程工作和疫情危机后的常态提供动力

    how-iot-will-power-remote-work-and-the-new-post-crisis-normal-1536x944_副本.jpg

    用户4122690
  • AI技术在智能海报设计中的应用

    在视觉设计领域中,设计师们往往会因为一些简单需求付出相当多的时间,比如修改文案内容,设计简单的海报版式,针对不同机型、展位的多尺寸拓展等。这些工作需要耗费大量的...

    美团技术团队

扫码关注云+社区

领取腾讯云代金券