大内高手调试手段及原理.docx
- 文档编号:4400378
- 上传时间:2022-12-01
- 格式:DOCX
- 页数:13
- 大小:35.63KB
大内高手调试手段及原理.docx
《大内高手调试手段及原理.docx》由会员分享,可在线阅读,更多相关《大内高手调试手段及原理.docx(13页珍藏版)》请在冰豆网上搜索。
大内高手调试手段及原理
大内高手--调试手段及原理
知其然也知其所以然,是我们《大内高手》系列一贯做法,本文亦是如此。
这里我不打算讲解如何使用boundschecker、purify、valgrind或者gdb,使用这些工具非常简单,讲解它们只是多此一举。
相反,我们要研究一下这些工具的实现原理。
本文将从应用程序、编译器和调试器三个层次来讲解,在不同的层次,有不同的方法,这些方法有各自己的长处和局限。
了解这些知识,一方面满足一下新手的好奇心,另一方面也可能有用得着的时候。
从应用程序的角度
最好的情况是从设计到编码都扎扎实实的,避免把错误引入到程序中来,这才是解决问题的根本之道。
问题在于,理想情况并不存在,现实中存在着大量有内存错误的程序,如果内存错误很容易避免,JAVA/C#的优势将不会那么突出了。
对于内存错误,应用程序自己能做的非常有限。
但由于这类内存错误非常典型,所占比例非常大,所付出的努力与所得的回报相比是非常划算的,仍然值得研究。
前面我们讲了,堆里面的内存是由内存管理器管理的。
从应用程序的角度来看,我们能做到的就是打内存管理器的主意。
其实原理很简单:
对付内存泄露。
重载内存管理函数,在分配时,把这块内存的记录到一个链表中,在释放时,从链表中删除吧,在程序退出时,检查链表是否为空,如果不为空,则说明有内存泄露,否则说明没有泄露。
当然,为了查出是哪里的泄露,在链表还要记录是谁分配的,通常记录文件名和行号就行了。
对付内存越界/野指针。
对这两者,我们只能检查一些典型的情况,对其它一些情况无能为力,但效果仍然不错。
其方法如下(源于《Comparingandcontrastingtheruntimeerrordetectiontechnologies》):
● 首尾在加保护边界值
Header
Leadingguard(0xFC)
Userdata(0xEB)
Tailingguard(0xFC)
在内存分配时,内存管理器按如上结构填充分配出来的内存。
其中Header是管理器自己用的,前后各有几个字节的guard数据,它们的值是固定的。
当内存释放时,内存管理器检查这些guard数据是否被修改,如果被修改,说明有写越界。
它的工作机制注定了有它的局限性:
只能检查写越界,不能检查读越界,而且只能检查连续性的写越界,对于跳跃性的写越界无能为力。
● 填充空闲内存
空闲内存(0xDD)
内存被释放之后,它的内容填充成固定的值。
这样,从指针指向的内存的数据,可以大致判断这个指针是否是野指针。
它同样有它的局限:
程序要主动判断才行。
如果野指针指向的内存立即被重新分配了,它又被填充成前面那个结构,这时也无法检查出来。
从编译器的角度
boundschecker和purify的实现都可以归于编译器一级。
前者采用一种称为CTI(compile-timeinstrumentation)的技术。
VC的编译不是要分几个阶段吗?
boundschecker在预处理和编译两个阶段之间,对源文件进行修改。
它对所有内存分配释放、内存读写、指针赋值和指针计算等所有内存相关的操作进行分析,并插入自己的代码。
比如:
Before
if(m_hsession)gblHandles->ReleaseUserHandle(m_hsession);
if(m_dberr)deletem_dberr;
After
if(m_hsession){
_Insight_stack_call(0);
gblHandles->ReleaseUserHandle(m_hsession);
_Insight_after_call();
}
_Insight_ptra_check(1994,(void**)&m_dberr,(void*)m_dberr);
if(m_dberr){
_Insight_deletea(1994,(void**)&m_dberr,(void*)m_dberr,0);
deletem_dberr;
}
Purify则采用一种称为OCI(objectcodeinsertion)的技术。
不同的是,它对可执行文件的每条指令进行分析,找出所有内存分配释放、内存读写、指针赋值和指针计算等所有内存相关的操作,用自己的指令代替原始的指令。
boundschecker和purify是商业软件,它们的实现是保密的,甚至拥有专利的,无法对其研究,只能找一些皮毛性的介绍。
无论是CTI还是OCI这样的名称,多少有些神秘感。
其实它们的实现原理并不复杂,通过对valgrind和gcc的boundschecker扩展进行一些粗浅的研究,我们可以知道它们的大致原理。
gcc的boundschecker基本上可以与boundschecker对应起来,都是对源代码进行修改,以达到控制内存操作功能,如malloc/free等内存管理函数、memcpy/strcpy/memset等内存读取函数和指针运算等。
Valgrind则与Purify类似,都是通过对目标代码进行修改,来达到同样的目的。
Valgrind对可执行文件进行修改,所以不需要重新编译程序。
但它并不是在执行前对可执行文件和所有相关的共享库进行一次性修改,而是和应用程序在同一个进程中运行,动态的修改即将执行的下一段代码。
Valgrind是插件式设计的。
Core部分负责对应用程序的整体控制,并把即将修改的代码,转换成一种中间格式,这种格式类似于RISC指令,然后把中间代码传给插件。
插件根据要求对中间代码修改,然后把修改后的结果交给core。
core接下来把修改后的中间代码转换成原始的x86指令,并执行它。
由此可见,无论是boundschecker、purify、gcc的boundschecker,还是Valgrind,修改源代码也罢,修改二进制也罢,都是代码进行修改。
究竟要修改什么,修改成什么样子呢?
别急,下面我们就要来介绍:
管理所有内存块。
无论是堆、栈还是全局变量,只要有指针引用它,它就被记录到一个全局表中。
记录的信息包括内存块的起始地址和大小等。
要做到这一点并不难:
对于在堆里分配的动态内存,可以通过重载内存管理函数来实现。
对于全局变量等静态内存,可以从符号表中得到这些信息。
拦截所有的指针计算。
对于指针进行乘除等运算通常意义不大,最常见运算是对指针加减一个偏移量,如++p、p=p+n、p=a[n]等。
所有这些有意义的指针操作,都要受到检查。
不再是由一条简单的汇编指令来完成,而是由一个函数来完成。
有了以上两点保证,要检查内存错误就非常容易了:
比如要检查++p是否有效,首先在全局表中查找p指向的内存块,如果没有找到,说明p是野指针。
如果找到了,再检查p+1是否在这块内存范围内,如果不是,那就是越界访问,否则是正常的了。
怎么样,简单吧,无论是全局内存、堆还是栈,无论是读还是写,无一能够逃过出工具的法眼。
代码赏析(源于tcc):
对指针运算进行检查:
void *__bound_ptr_add(void *p, int offset)
{
unsigned long addr =(unsigned long)p;
BoundEntry *e;
#if defined(BOUND_DEBUG)
printf("add:
0x%x%d\n",(int)p, offset);
#endif
e = __bound_t1[addr >>(BOUND_T2_BITS + BOUND_T3_BITS)];
e =(BoundEntry *)((char *)e +
((addr >>(BOUND_T3_BITS - BOUND_E_BITS))&
((BOUND_T2_SIZE -1)<< BOUND_E_BITS)));
addr -= e->start;
if (addr > e->size){
e = __bound_find_region(e, p);
addr =(unsigned long)p - e->start;
}
addr += offset;
if (addr > e->size)
return INVALID_POINTER; /*returnaninvalidpointer*/
return p + offset;
}
static void __bound_check(const void *p, size_t size)
{
if (size ==0)
return;
p = __bound_ptr_add((void *)p, size);
if (p == INVALID_POINTER)
bound_error("invalidpointer");
}
重载内存管理函数:
void *__bound_malloc(size_t size, const void *caller)
{
void *ptr;
/*weallocateonemorebytetoensuretheregionswillbe
separatedbyatleastonebyte.Withtheglibcmalloc,itmay
beinfactnotnecessary*/
ptr = libc_malloc(size +1);
if (!
ptr)
return NULL;
__bound_new_region(ptr, size);
return ptr;
}
void __bound_free(void *ptr, const void *caller)
{
if (ptr == NULL)
return;
if (__bound_delete_region(ptr)!
=0)
bound_error("freeinginvalidregion");
libc_free(ptr);
}
重载内存操作函数:
void *__bound_memcpy(void *dst, const void *src, size_t size)
{
__bound_check(dst, size);
__bound_check(src, size);
/*checkalsoregionoverlap*/
if (src >= dst && src < dst + size)
bound_error("overlappingregionsinmemcpy()");
return memcpy(dst, src, size);
}
从调试器的角度
现在有OS的支持,实现一个调试器变得非常简单,至少原理不再神秘。
这里我们简要介绍一下win32和linux中的调试器实现原理。
在Win32下,实现调试器主要通过两个函数:
WaitForDebugEvent和ContinueDebugEvent。
下面是一个调试器的基本模型(源于:
《DebuggingApplicationsforMicrosoft.NETandMicrosoftWindows》)
void main ( void )
{
CreateProcess (..., DEBUG_ONLY_THIS_PROCESS ,...);
while (1== WaitForDebugEvent (...))
{
if (EXIT_PROCESS)
{
break ;
}
ContinueDebugEvent (...);
}
}
由调试器起动被调试的进程,并指定DEBUG_ONLY_THIS_PROCESS标志。
按Win32下事件驱动的一贯原则,由被调试的进程主动上报调试事件,调试器然后做相应的处理。
在linux下,实现调试器只要一个函数就行了:
ptrace。
下面是个简单示例:
(源于《Playingwithptrace》)。
#include
#include
#include
#include
#include
etc.*/
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs;
long ins;
if(argc !
=2){
printf("Usage:
%s
argv[0], argv[1]);
exit
(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL,®s);
ins =ptrace(PTRACE_PEEKTEXT, traced_process,
regs.eip, NULL);
printf("EIP:
%lxInstructionexecuted:
%lx\n",
regs.eip, ins);
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
由于篇幅有限,这里对于调试器的实现不作深入讨论,主要是给新手指一个方向。
以后若有时间,再写个专题来介绍linux下的调试器和ptrace本身的实现方法。
~~~end~~
转载时请注明出处和作者联系方式:
作者联系方式:
李先静
更新时间:
2007-7-9
转载时请注明出处和作者联系方式:
作者联系方式:
李先静
更新时间:
2007-7-9
发表于@2006年07月25日 22:
07:
00 | 评论( 11 ) | 举报| 收藏
Spring 发表于WedJul26200623:
06:
00GMT+0800(ChinaStandardTime) 举报回复
领悟ing~
辛苦了!
多谢
absurd
发表于SunJul30200616:
21:
00GMT+0800(ChinaStandardTime) 举报回复
toSpring:
谢谢鼓励
天堂的隔壁 发表于WedAug30200621:
01:
00GMT+0800(ChinaStandardTime) 举报回复
对付内存泄露。
重载内存管理函数,在分配时,把这块内存的记录到一个链表中,在释放时,从链表中删除吧,在程序退出时,检查链表是否为空,如果不为空,则说明有内存泄露,否则说明没有泄露。
当然,为了查出是哪里的泄露,在链表还要记录是谁分配的,通常记录文件名和行号就行了。
===============================
如何记录文件名和行号?
处理这些链表显然会在重载的内存管理函数等里面,而这个时候__FILE__,__LINE__都等于这个实现重载的文件而不是调用的那个
天堂的隔壁 发表于WedAug30200621:
46:
00GMT+0800(ChinaStandardTime) 举报回复
to楼上:
非常感谢:
)
看来下午解决这个问题果然使用了一个非常傻的办法(正在往偶那贴ing),不过总是还是搞定了呵呵
jcy 发表于TueApr17200720:
42:
41GMT+0800(ChinaStandardTime) 举报回复
谢谢
liufeng_cp 发表于MonMar03200818:
01:
11GMT+0800(ChinaStandardTime) 举报回复
重载内存管理函数,在分配时,把这块内存的记录到一个链表中,在释放时,从链表中删除吧,在程序退出时,检查链表是否为空,如果不为空,则说明有内存泄露,否则说明没有泄露。
当然,为了查出是哪里的泄露,在链表还要记录是谁分配的,通常记录文件名和行号就行了。
这个方法真的好,实现起来也很简单(再加个编译开关,在release版把相关监视代码去掉),我以前怎么就没尝试过呢!
?
hoholine2007 发表于TueMay06200819:
01:
28GMT+0800(ChinaStandardTime) 举报回复
学习。
虚心。
dig_ge 发表于FriSep12200810:
09:
04GMT+0800(ChinaStandardTime) 举报回复
试着用Valgrind和mtraceglib写的几个小东西进行调试,发现报告了n多处内存泄漏,为了找到泄漏的地方,写了一些小demo,发现,只要用到GList的函数,都会报告内存泄漏,比如
#include
#include
intmain(intargc,char**argv)
{
mtrace();
GList*list=NULL;
list=g_list_append(list,(gpointer)10);
g_list_free(list);
muntrace();
return0;
}
会报告有8处内存泄漏,后来,自己构建了和GList一样的结构,实现append和free功能,就没有内存泄漏了。
能不能请李先生指点一下究竟是什么原因造成的,是glib库的问题吗?
ps:
我用的glib库是2.18.0
absurd 发表于MonSep15200819:
44:
08GMT+0800(ChinaStandardTime) 举报回复
回复dig_ge:
原因是glib对内存管理做了优化,相当中间加了个cache,g_list_free它并没有真正释放内存,所以valgrind就认为是leak了。
dig_ge 发表于FriSep12200810:
16:
21GMT+0800(ChinaStandardTime) 举报回复
/**FILENAME:
test_list.c**/
#include
#ifdef__GLIB__
#include
#include
voidfree_element(gpointerdata,gpointeruser_data)
{
if(data)g_free(data);
}
#else
#include
#include
#include
#include
typedefstruct_mylist{
void*data;
struct_mylist*prev;
struct_mylist*next;
}mylist;
mylist*list_append(mylist*list,void*data)
{
mylist*node=(mylist*)malloc(sizeof(mylist));
if(!
node)returnlist;
memset((void*)node,0,sizeof(mylist));
node->data=data;
node->next=NULL;
node->prev=NULL;
/**firstnode**/
if(!
list){
list=node;
}
else{
mylist*last=list;
while(last->next)last=last->next;
node->prev=last;
last->next=node;
}
returnlist;
}
voidlist_free_with_data(mylist*list)
{
if(!
list)return;
while(list){
mylist*p=list;
if(p->data)free(p->data);
list=list->next;
free(p);
}
return;
}
#endif
intmain()
{
mtrace();
#ifdef__GLIB__
GList*list=NULL;
gchar*str1=(gchar*)g_malloc(10);
gcha
ciahi 发表于TueJul27201023:
38:
04GMT+0800(ChinaStandardTime) 举报回复
如果只记录文件和行号,有时候还是不够。
比如说通用容器,别的程序会经常调用这个容器的代码,申请、释放内存,即使打印出内存泄露的文件及行号,都是
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 大内 高手 调试 手段 原理