`
freesoftman
  • 浏览: 318780 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

调试手段及原理

阅读更多

作者联系方式:http://blog.csdn.net/absurd

作者联系方式:李先静 <xianjimli at hotmail dot com>

 

知其然也知其所以然,是我们《大内高手》系列一贯做法,本文亦是如此。这里我不打算讲解如何使用boundscheckerpurifyvalgrind或者gdb,使用这些工具非常简单,讲解它们只是多此一举。相反,我们要研究一下这些工具的实现原理。

 

本文将从应用程序、编译器和调试器三个层次来讲解,在不同的层次,有不同的方法,这些方法有各自己的长处和局限。了解这些知识,一方面满足一下新手的好奇心,另一方面也可能有用得着的时候。

 

从应用程序的角度

 

最好的情况是从设计到编码都扎扎实实的,避免把错误引入到程序中来,这才是解决问题的根本之道。问题在于,理想情况并不存在,现实中存在着大量有内存错误的程序,如果内存错误很容易避免,JAVA/C#的优势将不会那么突出了。

 

对于内存错误,应用程序自己能做的非常有限。但由于这类内存错误非常典型,所占比例非常大,所付出的努力与所得的回报相比是非常划算的,仍然值得研究。

 

前面我们讲了,堆里面的内存是由内存管理器管理的。从应用程序的角度来看,我们能做到的就是打内存管理器的主意。其实原理很简单:

 

对付内存泄露。重载内存管理函数,在分配时,把这块内存的记录到一个链表中,在释放时,从链表中删除吧,在程序退出时,检查链表是否为空,如果不为空,则说明有内存泄露,否则说明没有泄露。当然,为了查出是哪里的泄露,在链表还要记录是谁分配的,通常记录文件名和行号就行了。

 

对付内存越界/野指针。对这两者,我们只能检查一些典型的情况,对其它一些情况无能为力,但效果仍然不错。其方法如下(源于《Comparing and contrasting the runtime error detection technologies)

 

l         首尾在加保护边界值

Header

Leading guard(0xFC)

User data(0xEB)

Tailing guard(0xFC)

 

在内存分配时,内存管理器按如上结构填充分配出来的内存。其中Header是管理器自己用的,前后各有几个字节的guard数据,它们的值是固定的。当内存释放时,内存管理器检查这些guard数据是否被修改,如果被修改,说明有写越界。

 

它的工作机制注定了有它的局限性: 只能检查写越界,不能检查读越界,而且只能检查连续性的写越界,对于跳跃性的写越界无能为力。

 

l         填充空闲内存

空闲内存(0xDD)

 

内存被释放之后,它的内容填充成固定的值。这样,从指针指向的内存的数据,可以大致判断这个指针是否是野指针。

 

它同样有它的局限:程序要主动判断才行。如果野指针指向的内存立即被重新分配了,它又被填充成前面那个结构,这时也无法检查出来。

 

从编译器的角度

 

boundscheckerpurify的实现都可以归于编译器一级。前者采用一种称为CTI(compile-time instrumentation)的技术。VC的编译不是要分几个阶段吗?boundschecker在预处理和编译两个阶段之间,对源文件进行修改。它对所有内存分配释放、内存读写、指针赋值和指针计算等所有内存相关的操作进行分析,并插入自己的代码。比如:

Before

    if (m_hsession) gblHandles->ReleaseUserHandle( m_hsession );

    if (m_dberr) delete m_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);

        delete m_dberr;

    }

 

Purify则采用一种称为OCI(object code insertion)的技术。不同的是,它对可执行文件的每条指令进行分析,找出所有内存分配释放、内存读写、指针赋值和指针计算等所有内存相关的操作,用自己的指令代替原始的指令。

 

boundscheckerpurify是商业软件,它们的实现是保密的,甚至拥有专利的,无法对其研究,只能找一些皮毛性的介绍。无论是CTI还是OCI这样的名称,多少有些神秘感。其实它们的实现原理并不复杂,通过对valgrindgccbounds checker扩展进行一些粗浅的研究,我们可以知道它们的大致原理。

 

gccbounds checker基本上可以与boundschecker对应起来,都是对源代码进行修改,以达到控制内存操作功能,如malloc/free等内存管理函数、memcpy/strcpy/memset等内存读取函数和指针运算等。Valgrind则与Purify类似,都是通过对目标代码进行修改,来达到同样的目的。

 

Valgrind对可执行文件进行修改,所以不需要重新编译程序。但它并不是在执行前对可执行文件和所有相关的共享库进行一次性修改,而是和应用程序在同一个进程中运行,动态的修改即将执行的下一段代码。

 

Valgrind是插件式设计的。Core部分负责对应用程序的整体控制,并把即将修改的代码,转换成一种中间格式,这种格式类似于RISC指令,然后把中间代码传给插件。插件根据要求对中间代码修改,然后把修改后的结果交给corecore接下来把修改后的中间代码转换成原始的x86指令,并执行它。

 

由此可见,无论是boundscheckerpurifygccbounds checker,还是Valgrind,修改源代码也罢,修改二进制也罢,都是代码进行修改。究竟要修改什么,修改成什么样子呢?别急,下面我们就要来介绍:

 

管理所有内存块。无论是堆、栈还是全局变量,只要有指针引用它,它就被记录到一个全局表中。记录的信息包括内存块的起始地址和大小等。要做到这一点并不难:对于在堆里分配的动态内存,可以通过重载内存管理函数来实现。对于全局变量等静态内存,可以从符号表中得到这些信息。

 

拦截所有的指针计算。对于指针进行乘除等运算通常意义不大,最常见运算是对指针加减一个偏移量,如++pp=p+np=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; /* return an invalid pointer */

    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("invalid pointer");

}

 

 

重载内存管理函数:

void *__bound_malloc(size_t size, const void *caller)

{

    void *ptr;

   

    /* we allocate one more byte to ensure the regions will be

       separated by at least one byte. With the glibc malloc, it may

       be in fact not necessary */

    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("freeing invalid region");

 

    libc_free(ptr);

}

 

 

重载内存操作函数:

void *__bound_memcpy(void *dst, const void *src, size_t size)

{

    __bound_check(dst, size);

    __bound_check(src, size);

    /* check also region overlap */

    if (src >= dst && src < dst + size)

        bound_error("overlapping regions in memcpy()");

    return memcpy(dst, src, size);

}

 

从调试器的角度

 

现在有OS的支持,实现一个调试器变得非常简单,至少原理不再神秘。这里我们简要介绍一下win32linux中的调试器实现原理。

 

Win32下,实现调试器主要通过两个函数:WaitForDebugEventContinueDebugEvent。下面是一个调试器的基本模型(源于: Debugging Applications for Microsoft .NET and Microsoft Windows)

 

lef

分享到:
评论

相关推荐

    大内高手调试手段及原理-50页.pdf

    【大内高手调试手段及原理】这篇文章探讨了高级调试技巧,旨在深入了解调试工具的运作机制,而不仅仅是如何使用它们。作者没有详细介绍boundschecker、purify、valgrind或gdb等常见调试工具的使用方法,因为这些工具...

    大内高手--调试手段及原理

    相反,我们要研究一下这些工具的实现原理。 本文将从应用程序、编译器和调试器三个层次来讲解,在不同的层次,有不同的方法,这些方法有各自己的长处和局限。了解这些知识,一方面满足一下新手的好奇心,另一...

    GDB基本调试原理及三种调试方式

    **GDB调试原理** GDB,全称GNU Debugger,是一个强大的源代码级调试工具,用于检查和调试C、C++等编程语言编写的程序。它的主要功能在于帮助开发者找到并修复程序中的错误(bug),以及深入理解程序的执行流程和...

    gdb 用法指导 调试 gdb的基本工作原理

    总结起来,gdb作为一款强大的调试工具,通过ptrace系统调用和信号机制,为开发者提供了灵活的调试手段。熟练掌握gdb的用法和原理,对于提高程序调试效率、理解程序行为有着至关重要的作用。无论是本地调试还是远程...

    ARM-JTAG-调试原理

    ARM架构在嵌入式系统中广泛使用,而JTAG(Joint Test Action Group)调试技术则是针对这类处理器进行故障排查和开发调试的重要手段。本文将详细介绍ARM JTAG调试的基本原理,涵盖TAP(Test Access Port)和BOUNDARY-...

    CloseHandle反调试_检测调试器原理

    总的来说,"CloseHandle反调试"是一种技术手段,通过关闭异常句柄来干扰调试器的工作,提高程序的防逆向工程能力。而检测调试器的原理则涉及多个层次的技术,包括对进程状态、异常处理、内存、API调用等多个方面的...

    ARM JTAG 调试原理.pdf

    边界扫描技术是一种用于测试和调试集成电路的重要手段。它基于IEEE 1149.1标准,该标准由Joint Test Action Group (JTAG) 组织提出并由IEEE正式发布。边界扫描技术的核心思想是在芯片的输入/输出引脚附近增加一系列...

    嵌入式系统交叉调试原理和方法

    在这种情况下,必须通过其他手段将固件烧写到外存的固件区,然后使用基于JTAG的片上调试方法进行调试。通常的调试流程包括:在遇到问题时停止调试,修改程序后重新编译链接;将操作系统、所有应用程序、UI资源文件、...

    记录一种简单的反调试手段(调试随手笔记)1

    【标题】:“记录一种简单的反调试手段(调试随手笔记)1” 在计算机程序开发和逆向工程中,反调试技术是一种常见的策略,用于防止或阻碍他人对程序进行调试。本篇笔记将介绍一种简单的反调试手段,通过自校验来...

    MTK调试(MTK的调试过程解析)

    在MTK平台上,常用的手机调试手段之一是TRACE函数。例如,`kal_prompt_trace`函数是系统提供的,主要用于在catcher中调试错误。当`kal_prompt_trace`不适用时,可以使用`system_print`或`dbg_printf`函数,它们可以...

    微机原理与接口技术:05第2章 习题课+汇编语言程序调试 习题1&2.doc

    微机原理与接口技术第 5 章习题课+汇编语言程序调试习题 1&2 微机原理与接口技术是一门专业基础课,旨在指导学生熟练掌握微机原理相关的知识和技能。本章节主要讲授微机原理的基本概念、微机原理与接口技术的应用、...

    JTAG4.0及ARM JTAG调试原理

    ARM架构广泛应用于各种嵌入式系统中,JTAG调试是其重要的开发和故障排查手段。ARM处理器内建了一个JTAG接口,允许外部设备通过TAP访问处理器的寄存器和内存。以下是ARM JTAG调试的基本步骤: 1. **建立连接**:通过...

    msp430调试器原理图

    MSP430 系列是一个 16 位的、具有精简指令集的、超低功耗的混合型单片机,在 1996 年问世,由于它具有极低的功耗、丰富的片内外设和方便灵活的开发手段,已...这是msp430调试器原理图,我自己亲自用过的,没问题的。

    内核Panic和Oops现象调试手段

    【内核Panic和Oops现象调试手段】 在Linux操作系统中,内核Panic和Oops是两种严重的错误状态,它们通常发生在内核遇到无法恢复的错误时。了解这些现象的调试手段对于系统的稳定性和故障排查至关重要。 **内核Oops*...

    各种反调试技术原理与实例VC版.pdf

    反调试技术是指软件开发者或恶意软件设计者为了防止其软件被调试分析而采取的一系列技术手段。这些技术通常涉及检测调试器的存在,并在检测到调试器时采取措施阻止调试过程继续进行。本书主要围绕Windows平台上的...

    ARM-JTAG-调试原理.

    总结,ARM-JTAG调试原理涉及硬件接口、数据传输、断点设置、内存访问等多个方面,它是嵌入式系统开发中不可或缺的调试手段。了解并掌握这一技术,能有效提高开发效率,解决复杂的系统问题。在实际应用中,结合各种...

    易语言反调试手段,检测自身是否被断点

    思路也很简单,就是第一次初始化时将代码段的开始地址到结束的地址所有的字节累加起来,然后再写个死循环重新累加对比初始化时的字节累加总数,不同就是被下了断点,改下初始化逻辑能应付下破J新手,其原理是当设置...

Global site tag (gtag.js) - Google Analytics