跳至內容

用戶: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 函數。