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