跳转到内容

User:Leonzhu211

维基百科,自由的百科全书

一次malloc,两次free[编辑]

源代码 && 分析[编辑]

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef struct _page{
    char *url;
}page;

#define EIS_ASSERT(c) assert(!(c))

page *page_new()
{
    page *me = (page*)calloc(1, sizeof(page));
    if(!me){
        EIS_ASSERT(1);
        return NULL;
    }
    return me;
}

void page_print(page *me)
{
    printf("%s\n", me->url);
}

void page_set_url(page *me, const char *url)
{
    if(me){
        me->url = strdup(url);
    }
    else{
        EIS_ASSERT(1);
    }
}

int page_delete(page *me)
{
    if(!me){
        EIS_ASSERT(1);
        return -1;
    }
    if(me->url){
        free(me->url);
    }
    free(me);
    return 0;
}

int page_clean(page *me)
{
    if(!me){
        EIS_ASSERT(1);
        return -1;
    }
    if(me->url){
        free(me->url);
    }
    /*
    这里缺少 me->url = NULL; 导致 me->url 会在 page_delete 中被再 free 一次。
    在 page_clean 与 page_delete 之间,me->url 可能正好被分配另一个模块。
    这样 page_delete 中第二次 free 相当于释放了另一个模块使用的指针。
    进一步导致另一个模块继续使用这个指针时,会出现死机。
    */
}

int main()
{
    page *t1 = page_new();
    page *t2 = page_new();
    page_set_url(t1, "http://www.eis/");

    /* 释放 t1->url,但 t1->url 未被置 NULL */
    page_clean(t1);

    /* t2 请求内存空间存放 url,假设这里请求到的空间正好是刚刚释放的 t1->url */
    page_set_url(t2, "http://www.eis/");

    /* 由于 page_clean 中没有置 t1->url 为 NULL,delete 中 free(me->url) 实际是释放了 t2->url */
    page_delete(t1);

    /* 打印 t2->url,但不幸已经被 free,如果内存管理在 free 时填充 0xFE,打印的内存会比较奇怪 */
    page_print(t2);
    page_delete(t2);
}

编译运行[编辑]

gcc 一次编译后输出为

$ gcc test1.c
$ ./a.exe
?�a?�aww.eis/

扩展[编辑]

类似的情况也出现在其他成对函数,例如 http_open 与 http_close。如果在 http_close 后没有将记录的 fd 清零,可能引起第二次 http_close。如果两次 http_close 中间,对应的 http fd 已经分配给其他模块使用,将会导致其他模块的 http 被莫名其妙关闭。

mantis bug[编辑]

参见:10535-33712: 播ocstream的过程中死机

局部变量未初始化[编辑]

源代码 && 分析[编辑]

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int test_stack(){
    /* 这个函数会导致 stack 中函数局部变量值为 -1 */
    int a = -1;
    return a;
}

int test1(int x)
{
    /* flag 定义,但未初始化,平台上一般是直接使用 stack 中残留内容,即 test_stack 导致的 -1 */
    int flag;
    
    /* 下面这一段 if,并不是所有分支都会对 flag 进行赋值 */
    if(x > 0){
        flag = 1;
    }
    /* 这里缺少 else 语句对 flag 进行赋值 */
    
    /* flag 未赋值,使用 stack 中残留内容,由于之前调用了 test_stack,导致值恰好为 -1,而不是想当然的 0 */
    if(flag == 0){
        return 0;
    }
    
    return 1;
}

int test2(int x)
{
    /* 与 test1 对比,这里初始化 */
    int flag = 0;
    
    /* 下面这一段 if,并不是所有分支都会对 flag 进行赋值 */
    if(x > 0){
        flag = 1;
    }
    /* 这里缺少 else 语句对 flag 进行赋值,但 flag 已初始化,不会导致异常 */
    
    /* flag 未赋值,使用函数初始化值 0 */
    if(flag == 0){
        return 0;
    }
    
    return 1;
}

int main()
{
    /* 调用这个函数,改变 stack 中函数局部变量的初始值 */
    test_stack();
    printf("test1: %d\n", test1(0));
    
    test_stack();
    printf("test2: %d\n", test2(0));
}


编译运行[编辑]

gcc 编译后输出为:

$ gcc test2.c
$ ./a.exe
test1: 1
test2: 0

mantis bug[编辑]

参见:926-30638: JVM 应用有时启动不了;开关打印表现有差异

lint(静态代码检查)[编辑]

对上述代码执行 lint 检查,会有下面的警告。与局部变量未初始化的警告信息见红色粗体文字

$ lint test2.c    
   Splint 3.1.1 --- 02 May 2003
   
   test2.c: (in function test1)
   
   test2.c:23:8: Variable flag used before definition
     An rvalue is used that may not be initialized to a value on some execution
     path. (Use -usedef to inhibit warning)
   
   test2.c: (in function main)
   test2.c:52:5: Return value (type int) ignored: test_stack()
     Result returned by function call is not used. If this is intended, can cast
     result to (void) to eliminate message. (Use -retvalint to inhibit warning)
   test2.c:55:5: Return value (type int) ignored: test_stack()
   test2.c:57:2: Path with no return in function declared to return int
     There is a path through a function declared to return a value on which there
     is no return statement. This means the execution may fall through without
     returning a meaningful result to the caller. (Use -noret to inhibit warning)
   test2.c:5:5: Function exported but not used outside test2: test_stack
     A declaration is exported, but not used outside this module. Declaration can
     use static qualifier. (Use -exportlocal to inhibit warning)
      test2.c:9:1: Definition of test_stack
   test2.c:11:5: Function exported but not used outside test2: test1
      test2.c:28:1: Definition of test1
   test2.c:30:5: Function exported but not used outside test2: test2
      test2.c:47:1: Definition of test2
   
   Finished checking --- 7 code warnings

越界写[编辑]

示例一(strcat)[编辑]

源代码 && 分析[编辑]

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef struct _t{
    char url[16];
    struct _t *next;
}t;

int test_url(char *url)
{
    /* 这里 strcat 导致 "hello" 超出 url 分配的内存,实际改写了 struct_t 中 next 成员 */
    strcat(url, "hello");
    return 0;
}

int test_t(t *t1)
{
    printf("start while\n");
    /* t1 的 next 被改写。非零,导致 while 条件满足 */
    while(t1){
        printf("t1: 0x%x\n", t1);
        
        /* 第二次进入循环, t1 不是一个合法的指针,导致 t1->next 时,死机。  */
        t1 = t1->next;
    }
    printf("end while\n");
    
}

int main()
{
    t *t1 = (t *)calloc(1, sizeof(t));
    strcpy(t1->url, "http://www.eis/");
    test_url(t1->url);
    printf("t1: 0x%x, t1->next: 0x%x\n", t1, t1->next);
    test_t(t1);
    free(t1);
}

编译运行[编辑]

gcc 上一次编译运行结果:

$ ./a.exe
   t1: 0x4b0008, t1->next: 0x6f6c6c65
   start while
   t1: 0x4b0008
   t1: 0x6f6c6c65
   Segmentation fault (core dumped)

mantis bug[编辑]

参见:775-32898: EPG应用切歌死机。 stop()

示例二(全局结构)[编辑]

源代码 && 分析[编辑]

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

/* 定义一个结构类型 */
typedef struct _s{
    int i;
    char name[256];
}s;

/* 定义两个不同类型指针,s1 中越界,可能会导致 sp1、ip1 被改写 */
s *sp1;
int *ip1;

/* 定义一个全局变量,是结构,而不是结构的指针 */
s s1;

/* 再定义两个不同类型指针,s1 中越界,也可能会导致 sp1、ip1 被改写 */
s *sp2;
int *ip2;

int main()
{
    s ss1, ss2;
    int ii1, ii2;
    char *name;
    sp1 = &ss1;
    sp2 = &ss2;
    ip1 = &ii1;
    ip2 = &ii2;
    
    printf("&s1: 0x%x\n", &s1);
    printf("&sp1: 0x%x\n", &sp1);
    printf("&sp2: 0x%x\n", &sp2);
    printf("&ip1: 0x%x\n", &ip1);
    printf("&ip2: 0x%x\n", &ip2);
    
    printf("\n");
    printf("sp1: 0x%x\n", sp1);
    printf("sp2: 0x%x\n", sp2);
    printf("ip1: 0x%x\n", ip1);
    printf("ip2: 0x%x\n", ip2);

    /* 下面一句,类型出错,导致 memset 实际越界 */
    memset(s1.name, 0, 256*sizeof(int));
    
    /* 越界破坏了哪些指针不确定,应该跟编译器及系统有关 */
    
    /* 从 linux gcc 编译后程序运行的结果看,上面的 memset 是改写了 sp1 和 sp2 中的内容 */
    printf("\n");
    printf("sp1: 0x%x\n", sp1);
    printf("sp2: 0x%x\n", sp2);
    printf("ip1: 0x%x\n", ip1);
    printf("ip2: 0x%x\n", ip2);
    
    /* 由于 sp1 中内容被改写,下面取 ->name 时,实际取到的是 name 成员在结构中的偏移,这里虽然出了问题,但不会死机 */
    name = sp1->name;
    printf("name: %x\n", name);
    
    /* 如果要写点什么,就死定了 */
    strcpy(sp1->name, "hello");
    
    /* 下面这句打印应该是没有机会看到了 */
    printf("after strcpy\n");
}

编译运行[编辑]

在 linux 上用 gcc 编译后运行:

root@linux ~/f/c>
$ gcc -g test5.c
root@linux ~/f/c>
$ ./a.out
&s1: 0x8049800
&sp1: 0x8049904
&sp2: 0x8049908
&ip1: 0x80497e4
&ip2: 0x80497e0

sp1: 0xbfffe410
sp2: 0xbfffe300
ip1: 0xbfffe2fc
ip2: 0xbfffe2f8

sp1: 0x0
sp2: 0x0
ip1: 0xbfffe2fc
ip2: 0xbfffe2f8
name: 4
Segmentation fault (core dumped)

从程序输出看,gcc 对全局变量,会按类型安排内存区域,与变量在源代码中出现的顺序无关。 这样,导致比较难找出越界写的指针。

mantis bug[编辑]

参见:10534-33441: 开机后进入UI界面,按遥控器,ipanel_proc出现段错误死机

建议[编辑]

不直接使用结构作全局变量,而改用指针作全局变量。这样,内存受 sdk xmem 统一管理,才有可能通过加打印确认是哪里越界。 直接使用结构,越界时破坏的目标内存,与编译器、平台关系太密切。在死机的模块,即被越界写坏内存的模块,很难反向猜测是哪个模块发生了越界写。

xmem查越界[编辑]

  1. 定义 DEBUG_MALLOC_FILE_LINE。
  2. 定义 HAVE_RO_FILENAME。
  3. 在 sdk 模块 XMalloc_malloc、XMalloc_malloc2、XMalloc_realloc 中返回有效 ptr 前,打印 filename、line、ptr。
  4. 在 sdk 模块 XMalloc_free 打印 ptr。
  5. 由调试工具(gdb or VC) 确定由 sdk xmem 分配的、被改写的内存。
  6. 由浏览器初始化时的 log 确定被改写内存所在的 segment 及 blocksize。
  7. 在 log 中找符合以下特点的内存:
    • 第一个
    • 位于被改写内存之前
    • 与被改写内存的偏移为 blocksize 整数倍
    • 已经被分配,但未被释放
  8. 根据该内存在分配置打印的 filename、line,检查相关代码,主要是被分配出的内存的使用情况,重点检查 memset, strcpy 等会改写内容的标准 C 函数。