分类: 2011 February

thttpd源码小分析之Reactor pattern

这篇文章非常粗略地讨论thttpd的工作pattern: Reactor.关于pattern和model的区别,这里不多解释,参考这个说法: M(odel)-VC pattern.关于pattern的更多资料,参考 http://www.enterpriseintegrationpatterns.com/ .

什么是Reactor

Reactor又称Dispatcher,用于同步IO,它逆置了常见的函数调用机制,也就是说, Application将一个service对应的handler注册到Reactor,当这个service需要被处理时(一般通过定时器来触发),Reactor主动调用handler.浅白一点说, ‘Don’t call us, we’ll call you.’ — Hollywood principle.

在前面对Reactor的简单解释中,可以看到Reactor至少有下面几个组件:

  1. handle: service(read, write)对应的句柄,Linux下通常是file description.
  2. handler: service对应的回调函数,如 handle_read(), handle_write().
  3. demultiplexer: 多路复用机制,Linux下可以用select/(e)poll
  4. reactor: 注册/移除handler的统一界面,如: Reactor::register_handler(), Reactor::remove_handler().

废话少说,看一下Reactor pattern大概的样子:

int fd[MAX_FD]; //要素1

typedef int EventType;
typedef int HANDLER;

enum
{
	READ_EVENT = 1;
	WRITE_EVENT = 2;
	...
};

class CEventHandler //要素2
{
public:
	virtual void handle_read(HANDLER handle) = 0;
	virtual void handle_write(HANDLER handle) = 0;
	virtual HANDLER get_handle() const = 0;
	...
};

class CReactor //要素4
{
public:
	virtual void register_handler(HANDLER handle, CEventHandler *ceh, EventType et) = 0;
	virtual void remove_handler(HANDLER handle, CEventHandler *ceh, EventType et) = 0;
	void handle_events(struct timeval *tv);
	...
};

thttpd的Reactor pattern

thttpd是事件驱动(event-driven)的,它有效的避开了多线程附带的复杂不易维护(尤其是临界区)和上下文切换,将CPU从事件源中解放出来,无须block.关于Event driven,这里不多说.

现在看看thttpd很寒酸的Reactor pattern:

thttpd自身既是master,又是worker,matser代码是main(), worker代码是handle_read(),handle_write()等函数.thttpd使用Reactor pattern,但弃用了Reactor 要素4的reactor,也就是说,代码中根本就没有CReactor界面,这直接导致了要素2被散乱的硬编码到thttpd.c文件中,无需注册,也没有办法移除.对于thttpd这种HTTP服务器来说,要素1很自然的就是fd,而要素3 demultiplexer,在我的电脑上,configure thttpd代码时被选定为select.

以上,确实很粗略,哈哈哈. 其实thttpd里面有很多不错的小技巧,比如说watchdog,总结出来应该是很实用的 :p

thttpd源码小分析

情人节在家很无聊,很随便地就找了这么一个号称 安全 的web服务器:thttpd,闲的蛋疼,于是看了下代码.

主要数据结构

thttpd代码量很小,wc -l看了一下,11,000行左右,除去1k行左右的几个cgi演示性质的代码,大概有15个源文件,主要的数据结构如下:

各结构体的作用简单描述如下: TimerStruct(typedef成Timer)主要作为一个定时器,它包含了一个TimerProc*元素作为它到时之后的处理函数.结构体httpd_server表示一个绑定到某网址的server,而结构体httpd_conn就是一个从客户端(浏览器)到服务器(thttpd)的连接信息,这些连接信息也就是客户端看到的HTTP协议下指定的GET查询,POST更改操作对应的HTTP请求信息和响应信息,以及服务器看到的accept()返回的connfd信息,显然,结构体connecttab表示的就是被定时器绑定的连接信息和对应.

下面,分别介绍一下这些结构体对应的数据结构和算法:

Timer

由上图可知,Timer有两个指针,分别是prev和next,这样设计,就可以像timers.c下的 static Timer* free_timers; 一样,创建一个双向链表,元素的个数可以用一个int元素表示.这是低阶的实现,更高级的应用在这一行代码: Timer *timers[HASH_SIZE]; ,这里定义了一个数组,每个数组元素却是由多个Timer元素组成的双向链表,数组的下标根据Timer元素值的hash指定.

这么设计有什么作用呢? 实际上,在thttpd的实现中,timers不外乎就是拿来插入和删除一个Timer元素的,稍微有点不一样的是,作者对这个数组的每一个元素(也就是链表)做了排序.对双向链表排序看起来有点无厘头,因为它是顺序取值的,又不是数组那样可以随机读写.实际上,这是作者对Timer应用的思考: 不管用什么数据结构来组织,既然Timer是一个计时器,那么就总是需要从一堆Timer里面找出离某个时间最近的那个Timer的. 在thttp的实现中,因为与客户端的连接有很多,也就是accept()返回的connfd数量有很多,根据这个最近的Timer,就可以从这一堆connfd中select出已经ready(可读可写或异常)的那个connfd,然后对此connfd操作(写或读).好吧,既然关键点出来了,那么将链表排序也就是可以理解了的: 要找出最小的那个Timer,只要从times[]数组里面找出头元素最小的那个链表的下标,然后在此下标对应的链表中顺序比较找出最小的那个就可以了,这么一来,就省去了所有元素都比较一遍的麻烦.转化成代码如下:

Timer* tmr_mstimeout( )
{
    register Timer* min = timers[0];

    for ( h = 1; h < HASH_SIZE; ++h )
    {
        //timers在插入或删除元素时自然是要用心维护好顺序的,这里不打出来
        if ( timers[h] < min )
            min = timers[h];
    }
    return min;
}

至于为什么不直接设计成一个由很多Timer组成的数组呢(即: 元素是Timer而不是由Timer组成的链表,也就是 Timer timers[HASH_SIZE]) ,这么设计就可以直接用二分查找了.我觉得,这应该是因为作者认为哈希后每个链表的元素很少的缘故.

至于前面提到的 static Timer* free_timers; ,它实际上起的是类似于内存池的作用,当需要一个Timer元素时,从free_timers分配,当timers里需要cancel一个Timer时,由free_timers接收,省去了分配内存和释放内存的时间(C语言还好,C++的话分配是需要三步骤的,明显的吃力不讨好.).分配操作转换成代码如下:

Timer* tmr_create( )
{
    Timer* t;

    //free_timers相当于内存池
    if ( free_timers != (Timer*) 0 )
    {
        t = free_timers;
        free_timers = t->next;
        --free_count;
    }
    else
    {
        t = (Timer*) malloc( sizeof(Timer) );
        if ( t == (Timer*) 0 )
            return (Timer*) 0;
        ++alloc_count;
    }

    t->hash = hash( t );
    //将新分配的元素插入timers
    l_add( t );
    ++active_count;

    return t;
}

文件mmc.c下结构体MapStruct还有thttpd.c下结构体connecttab实现思想上和Timer都差不多,不加赘述.

fdwatch.c

fdwatch.c下select poll devpoll kqueue操作维护了各自的数据结构(保存文件描述符fd)和几种操作(add_fd,del_fd等),作者把这些操作封装成界面通用的宏,成功的隐藏了内部细节.比如说,fdwatch.c的用户(主要是thttp.c文件)只要用一句DEL_FD宏就可以实现添加fd的功能,而无需考虑背后fd是添加到select poll devpoll kqueue对应的哪个数据结构中,这里不多说.

下面看看select,很明显,它需要维护一个数据结构,用来存储一堆文件描述符fd,这个数据结构需要支持add和del操作.第一选择是一维数组,add时就是push(),del时也不过排序后二分查找即可.作者采用的是另外一种思路,开两个数组,维持 a[i]=j, b[j]=i 的关系.这样,要在a[]数组中找到j,可以很简单先到辅助数组b[]中找出j在a[]数组中的下标,省去了排序和二分查找.这是小技巧,直接看一下add和del的代码:

//nselect_fds维持为数组select_fds最后那个元素的下标
static void select_add_fd (int fd)
{
    select_fds[nselect_fds] = fd;
    select_fdidx[fd] = nselect_fds;

    ++nselect_fds;
}

static void select_del_fd (int fd)
{
    int idx = select_fdidx[fd];

    --nselect_fds;
    select_fds[idx] = select_fds[nselect_fds];
    select_fdidx[select_fds[idx]] = idx;
    select_fds[nselect_fds] = -1;
    select_fdidx[fd] = -1;
}

thttpd.c

这个没什么好说的,看一下main()函数流程就是了:

int main(int argc, char * argv[])
{
	...
	//很无聊的初始化各种数据结构(timer,connecttab,httpd_server等等),读配置,转daemon,注册信号处理函数等操作

	...

    //真正开始干活了.
    while(...)
    {

	//看一下有多少个文件描述符是ready了的.
        num_ready = fdwatch( tmr_mstimeout( &tv ) );

	//找到需要处理的connection
        while ( c = (connecttab*) fdwatch_get_next_client_data() )
        {
			switch ( c->conn_state )
			{
			case CNST_READING:
				handle_read( c, &tv );
				break;
			case CNST_SENDING:
				handle_send( c, &tv );
				break;
			case CNST_LINGERING:
				handle_linger( c, &tv );
				break;
			}
        }

		//所有计时器都跑的很欢快.
		tmr_run(...);
    }

    //主循环结束
    exit(0);
}

唉,人家都是分析架构的,我只看得懂一点点代码.杯具.

调试器是怎样工作的: Part 3 – 调试信息

一点声明
原文作者: http://eli.thegreenplace.net/
原文链接: http://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information/

translated by Arthur1989, 个人主页: http://godorz.info

这是本系列(调试器是如何工作的?)的第三篇.在阅读这篇文章之前,请确保你已经看过了第一篇和第二篇.

In this part

我会解释调试器是怎样在机器代码中找到C函数和变量,以及用来映射C代码和机器代码的数据.

Debugging information

现代编译器通过自己精致的缩进和嵌套控制结构,以及任何类型的变量,可以很好的将高级语言代码编译成机器代码,这么做唯一的目的就是让程序在目标CPU上跑的越快越好.绝大部分的C代码会被转换成各种机器指令,而变量将被塞入栈,寄存器,或者是被优化得无影无踪.结构和代码甚至压根就不在输出代码中 — 它们作为一种抽象,会被转换成相对于内存缓存硬编码的偏移量.

那么,当被要求在某个函数暂停时,调试器是如何知道对应的入口地址呢?当被要求显示某个变量的值时,调试器又是如何知道应该去哪里寻找变量呢? 答案便是–调试信息(debugging information).

调试信息是由编译器在编译机器代码时一道生成的.它是可执行文件和源代码之间关系的一种描述.调试信息根据预定义的格式被编码到机器代码中.在过去的年代,对应于各种架构,有很多格式被发明了出来.因为本文的目的不在于研究这些格式的历史,而是研究它们是如何工作的,所以我们最好选定一种格式,它就是DWARF,在Linux平台和类Unix(Unix-y)平台上,DWARF被用来描述ELF格式可执行文件的调试信息,可以说,它无处不在.

The DWARF in the ELF

根据维基百科,DWARF是和ELF一起被设计的,不过它可以被嵌入到其他目标格式中[1].

DWARF很复杂,它建立于对其他格式的多年研究经验之上,这些格式可以运用于各种架构.DWARF必须是复杂的,因为它需要解决一个很难办的问题–向调试器展示任何高级语言代码的调试信息,为各种架构和ABIs(application binary interface)提供支持.鄙文不足以详尽地阐释它,老实说,我对DWARF的各种阴暗面都还没有透彻的了解[2].在这片文章中,我采用动手的方式,来展示调试信息在实践中是如何被使用的.

Debug sections in ELF files

首先,让我们看看DWARF信息在ELF文件内部何处.ELF定义了目标文件中的各种可选section,而section头表(section header table)则定义了存在哪些sections已经这些sections的名字.不同的工具以特殊的方式处理不同的sections -– birshuo,链接器读取某些sections,而调试器则读取另外的sections.

作为实验,我们把下面的C程序编译成tracedprog2:

#include <stdio.h>

void do_stuff(int my_arg)
{
    int my_local = my_arg + 2;
    int i;

    for (i = 0; i < my_local; ++i)
        printf("i = %d\n", i);
}

int main()
{
    do_stuff(2);
    return 0;
}

用objdump -h把ELF文件的section头部打印出来.注意以.debug_开头的section –- 它们就是DWARF调试sections:

26 .debug_aranges 00000020  00000000  00000000  00001037
                 CONTENTS, READONLY, DEBUGGING
27 .debug_pubnames 00000028  00000000  00000000  00001057
                 CONTENTS, READONLY, DEBUGGING
28 .debug_info   000000cc  00000000  00000000  0000107f
                 CONTENTS, READONLY, DEBUGGING
29 .debug_abbrev 0000008a  00000000  00000000  0000114b
                 CONTENTS, READONLY, DEBUGGING
30 .debug_line   0000006b  00000000  00000000  000011d5
                 CONTENTS, READONLY, DEBUGGING
31 .debug_frame  00000044  00000000  00000000  00001240
                 CONTENTS, READONLY, DEBUGGING
32 .debug_str    000000ae  00000000  00000000  00001284
                 CONTENTS, READONLY, DEBUGGING
33 .debug_loc    00000058  00000000  00000000  00001332
                 CONTENTS, READONLY, DEBUGGING

这些调试section的第1个数字表示该段大小,最后一个数字表示它在ELF文件中的偏移量.调试器使用这些信息从可执行文件读取section.

现在,让我们看看在DWARF中找出有用信息的几个例子.

Finding functions

调试器最基本的功能就是在函数中设置断点,让调试器刚好在进入函数时暂停.为了实现这个功能,调试器必须了解高级语言中的函数与机器代码中这个函数的起始地址的映射信息.

这个信息可以从DWARF中的.debug_info获得.在更进一步之前,先介绍一点背景.DWARF的最基本描述个体称为Debugging Information Entry (DIE).每个DIE有自己的标签 –- 它的类型, 一系列的属性.DIEs通过兄弟和儿子互联,属性值可以指向其他的DIE.

运行命令:

objdump --dwarf=info tracedprog2

输出很长,我们把注意力集中于这几行[3]:

<1><71>: Abbrev Number: 5 (DW_TAG_subprogram)
    <72>   DW_AT_external    : 1
    <73>   DW_AT_name        : (...): do_stuff
    <77>   DW_AT_decl_file   : 1
    <78>   DW_AT_decl_line   : 4
    <79>   DW_AT_prototyped  : 1
    <7a>   DW_AT_low_pc      : 0x8048604
    <7e>   DW_AT_high_pc     : 0x804863e
    <82>   DW_AT_frame_base  : 0x0      (location list)
    <86>   DW_AT_sibling     : <0xb3>

<1>: Abbrev Number: 9 (DW_TAG_subprogram)
       DW_AT_external    : 1
       DW_AT_name        : (...): main
       DW_AT_decl_file   : 1
       DW_AT_decl_line   : 14
       DW_AT_type        : <0x4b>
       DW_AT_low_pc      : 0x804863e
       DW_AT_high_pc     : 0x804865a
       DW_AT_frame_base  : 0x2c     (location list)

注意有两个标签为DW_TAG_subprogram的DIE,分别代表do_stuff和main,DW_TAG_subprogram作为DWARF的术语,是指一个函数.每个section有各种属性,其中最值得探讨的是DW_AT_low_pc.这是函数起始地址对应的EIP值.对do_stuff来说,它是0x8048604.现在让我们看看这个地址在反汇编出来的代码中表示什么,objdump -d:

08048604 :
 8048604:       55           push   ebp
 8048605:       89 e5        mov    ebp,esp
 8048607:       83 ec 28     sub    esp,0x28
 804860a:       8b 45 08     mov    eax,DWORD PTR [ebp+0x8]
 804860d:       83 c0 02     add    eax,0x2
 8048610:       89 45 f4     mov    DWORD PTR [ebp-0xc],eax
 8048613:       c7 45 (...)  mov    DWORD PTR [ebp-0x10],0x0
 804861a:       eb 18        jmp    8048634 
 804861c:       b8 20 (...)  mov    eax,0x8048720
 8048621:       8b 55 f0     mov    edx,DWORD PTR [ebp-0x10]
 8048624:       89 54 24 04  mov    DWORD PTR [esp+0x4],edx
 8048628:       89 04 24     mov    DWORD PTR [esp],eax
 804862b:       e8 04 (...)  call   8048534 
 8048630:       83 45 f0 01  add    DWORD PTR [ebp-0x10],0x1
 8048634:       8b 45 f0     mov    eax,DWORD PTR [ebp-0x10]
 8048637:       3b 45 f4     cmp    eax,DWORD PTR [ebp-0xc]
 804863a:       7c e0        jl     804861c 
 804863c:       c9           leave
 804863d:       c3           ret

确实,0x8048604就是do_stuff的起始地址,所以调试器可以将函数映射到它在可执行文件中的位置.

Finding variables

假设进程已经中断在do_stuff,我们想要调试器显示出变量my_local 的值.调试器怎么知道它在哪里呢?事实上,这比找出函数更有难度.变量可以存在于全局变量区中,栈中,甚至是寄存器中.而且,相同的名字的变量在不同的作用域中可以有不同的值,调试信息必须能够反映出所有的这些变量,而DWARF确实做到了这一点.

我不会讲解所有的可能(变量在全局变量区或者栈或者寄存器),就看看调试器怎么找出do_stuff中的my_local吧.找到.debug_info,看一下do_stuff对应的DIE,留意它的子DIE:

<1><71>: Abbrev Number: 5 (DW_TAG_subprogram)
    <72>   DW_AT_external    : 1
    <73>   DW_AT_name        : (...): do_stuff
    <77>   DW_AT_decl_file   : 1
    <78>   DW_AT_decl_line   : 4
    <79>   DW_AT_prototyped  : 1
    <7a>   DW_AT_low_pc      : 0x8048604
    <7e>   DW_AT_high_pc     : 0x804863e
    <82>   DW_AT_frame_base  : 0x0      (location list)
    <86>   DW_AT_sibling     : <0xb3>
 <2><8a>: Abbrev Number: 6 (DW_TAG_formal_parameter)
    <8b>   DW_AT_name        : (...): my_arg
    <8f>   DW_AT_decl_file   : 1
    <90>   DW_AT_decl_line   : 4
    <91>   DW_AT_type        : <0x4b>
    <95>   DW_AT_location    : (...)       (DW_OP_fbreg: 0)
 <2><98>: Abbrev Number: 7 (DW_TAG_variable)
    <99>   DW_AT_name        : (...): my_local
    <9d>   DW_AT_decl_file   : 1
    <9e>   DW_AT_decl_line   : 6
    <9f>   DW_AT_type        : <0x4b>
       DW_AT_location    : (...)      (DW_OP_fbreg: -20)
<2>: Abbrev Number: 8 (DW_TAG_variable)
       DW_AT_name        : i
       DW_AT_decl_file   : 1
       DW_AT_decl_line   : 7
       DW_AT_type        : <0x4b>
       DW_AT_location    : (...)      (DW_OP_fbreg: -24)

注意每个DIE中第一个尖括号里的序号.这说明在第几层 — 在这个例子中,<2>DIE是<1>DIE的儿子.可以看到,变量my_local(标签为DW_TAG_variable)是函数do_stuff的儿子.调试器对变量类型也很感兴趣,因为这样她才能正确的显示变量值,这里变量my_local的类型指向另外一个DIE – <0x4b>.查找objdump的输出,可以看到它表示4字节长的有符号整数.

为了在进程内存映像中定位变量,调试器会查看DW_AT_location属性. 对my_local来说,它是DW_OP_fbreg: -20.这意味着变量存储在函数栈帧底部(DW_AT_frame_base)偏移-20的位置.

do_stuff的DW_AT_frame_base属性为0x0 (location list),这说明值需要到location list section中查找.命令如下:

$ objdump --dwarf=loc tracedprog2

tracedprog2:     file format elf32-i386

Contents of the .debug_loc section:

    Offset   Begin    End      Expression
    00000000 08048604 08048605 (DW_OP_breg4: 4 )
    00000000 08048605 08048607 (DW_OP_breg4: 8 )
    00000000 08048607 0804863e (DW_OP_breg5: 8 )
    00000000 
    0000002c 0804863e 0804863f (DW_OP_breg4: 4 )
    0000002c 0804863f 08048641 (DW_OP_breg4: 8 )
    0000002c 08048641 0804865a (DW_OP_breg5: 8 )
    0000002c 

我们感兴趣的location信息是第一个[4].它指出了当前的栈底地址,变量在这个栈底地址的偏移量被当成是离寄存器值的偏移量计算.对x86来说,bpreg4指esp,bpreg5指ebp.

看看do_stuff的前几条指令:

08048604 :
 8048604:       55          push   ebp
 8048605:       89 e5       mov    ebp,esp
 8048607:       83 ec 28    sub    esp,0x28
 804860a:       8b 45 08    mov    eax,DWORD PTR [ebp+0x8]
 804860d:       83 c0 02    add    eax,0x2
 8048610:       89 45 f4    mov    DWORD PTR [ebp-0xc],eax

ebp在第2条指令之后才变得有意义,一旦ebp是有效的,我们就可以计算到它的偏移量,因为ebp一直保持不变,而esp随着数据的入栈和出栈而增减.

那么my_local到底在哪里呢?我们只对0x8048610指令后my_local的值有兴趣 (在eax中计算后,my_local值被放回内存中),所以调试器将利用DW_OP_breg5: 8来寻找它. 回忆一下,my_local的DW_AT_location属性是DW_OP_fbreg: -20.让我们做一点计算吧: 栈底偏移-20,而栈底是ebp+8,得出地址是ebp – 12.看一下反汇编出来的代码 –- 确实,my_local就是存储在ebp-12中.

Looking up line numbers

好吧,我承认,在谈到找出调试信息中的函数时,我小小的作弊了.当我们调试C代码,在函数中设置断点时,我们经常对相关机器指令的第一句是没有什么兴趣的[5].我们真正感兴趣的是,C代码中函数的第一行.

这就是为什么DWARF保存中C代码行号和可执行文件中机器指令的完全映射信息的原因.这些信息由.debug_line记录,我们可以用如下命令查看:

$ objdump --dwarf=decodedline tracedprog2

tracedprog2:     file format elf32-i386

Decoded dump of debug contents of section .debug_line:

CU: /home/eliben/eli/eliben-code/debugger/tracedprog2.c:
File name           Line number    Starting address
tracedprog2.c                5           0x8048604
tracedprog2.c                6           0x804860a
tracedprog2.c                9           0x8048613
tracedprog2.c               10           0x804861c
tracedprog2.c                9           0x8048630
tracedprog2.c               11           0x804863c
tracedprog2.c               15           0x804863e
tracedprog2.c               16           0x8048647
tracedprog2.c               17           0x8048653
tracedprog2.c               18           0x8048658

第5行指向do_stuff入口地址 -– 0x8040604.第6行是断点在do_stuff时,调试器需要暂停的真实地址,它指向0x804860a,这是函数真正工作的开始.这些line信息允许调试器很容易地在行号和地址之间做双向映射:

* 当被要求在某行设置断点时,调试器找出需要设置中断的地址(能想起上篇文章提到的int 3吗?)
* 当一个指令产生段错误时,调试器很容易找出代码所在行.

libdwarf – Working with DWARF programmatically

尽管同来获取DWARF信息的一些命令行工具很有用,但是它们还不太令人满意.作为程序员,我们想要编写自己的程序,它可以读取格式,提取出我们想要的信息.

当然,一种途径就是掌握DWARF细节,然后开始造轮子.但是,记得人们千叮咛万嘱咐,一定要用库,而不是自己手工写一个HTML解析器吗? 好吧,对DWARF来说,更该如此,它比HTML复杂多了.文中展示的只是冰山一角,更糟糕的是,很多信息是被很紧凑地压缩在目标文件中的[6].

既然如此,我们就选择另外一条道路吧,我们可以使用DWARF库.主要有两种:
1. BFD (libbfd) — 它被GNU binutils选中,包括文中大放异彩的objdump, ld(链接器) 和 as (汇编器).
2. libdwarf –- 它和libelf被拿来开发了Solaris和FreeBSD上的很多工具

我选择libdwarf而不是BFD,因为我觉得没那么晦涩,而且它的授权也比较宽松(LGPL vs. GPL).

因为libdwarf本身就是很复杂的,所以我们需要编写很多代码来使用它.我不打算展示这些代码,你可以在这里下载源文件.要编译代码,你需要安装libelf和libdwarf,然后向链接器传递-lelf和-ldwarf标志.

下载的代码编译出来的程序需要一个参数作为目标文件,它分析出目标文件的函数和对应的入口地址,就像这样:

$ dwarf_get_func_addr tracedprog2
DW_TAG_subprogram: 'do_stuff'
low pc  : 0x08048604
high pc : 0x0804863e
DW_TAG_subprogram: 'main'
low pc  : 0x0804863e
high pc : 0x0804865a

libdwarf文档很不错,通过一点努力,我们很容易就可以找出DWARF sections的其他信息.

Conclusion and next steps

调试信息理论上是一个很简单的概念.它的实现细节可能错综复杂,但是,重要的是现在我们知道了调试器如何找出可执行文件中的相关信息.有了这些调试信息,调试器架起了一道用户和可执行文件之间的桥梁,用户从源代码和数据结构的角度思考,而可执行文件是一堆机器指令加上内存或寄存器中的数据.

这篇文章,和它之前的两篇文章,总结了一份概括性的介绍,解释了调试器的内部理论.通过这些知识和一些编程的努力,我们可以编写一个简单但是用的Linux下的调试器.

至于下一步呢,我还没有确定.也许这个系列在这里就结束了,也许我会介绍一些高级话题,比如说回溯(backtraces),或者是Windows下的调试.读者也可以通过评论或者email,提供进一步的话题或者是相关的资料,

References

* objdump man page
* ELFDWARF 的维基页面.
* Dwarf Debugging Standard主页 –- 在这里你可以获得DWARF的绝佳指南(by Michael Eager),还有DWARF标准本身.你可能需要版本2,因为gcc用的就是这个版本.
* libdwarf主页 -– 包含了libdwarf详尽说明的下载页面
* BFD文档

[1] DWARF 是一个开放的标准,由DWARF standards committee发布在这里.
[2] 在文章的最后,我提供了一些资料,也许你可以从DWARF tutorial开始.
[3] 为了缩短篇幅,这里我用(…)代替那些无谓的信息.
[4] 因为do_stuff的DW_AT_frame_base属性值包含了location list的偏移量0x0.注意main的相同属性包含了0x2c,它是到第二个location表达式(location expression)的偏移量.
[5] 这时函数序言(function prologue,译注,指堆栈的创建,和函数尾声(function epilogue)相对.)刚被执行,变量值无效
[6] 有些信息(比如说location数据和行号数据)被编码成专用虚拟机的指令.