一、被调试任务所有so文件如何枚举
在前一篇博客中,大致说明了gdb是通过一个动态库提供的回调函数(_dl_debug_state)处埋伏断点,然后通过约定好的_r_debug全局变量来得到exe程序对应的link_map,然后以该结构为队列头来遍历被调试任务中所有的so文件。当时也说了这个地方比较模糊,只是说了一个思路,所以这里再试图把这个实现相对详细的描述一下。
二、定义被调试任务(debuggee)的link_map地址
同样是在gdb-6.5\gdb\solib-svr4.c文件中,其中包含了专门用来定位这个文件位置的函数:
static CORE_ADDR
elf_locate_base (void)
{
struct bfd_section *dyninfo_sect;
int dyninfo_sect_size;
CORE_ADDR dyninfo_addr;
gdb_byte *buf;
gdb_byte *bufend;
int arch_size;
/* Find the start address of the .dynamic section. */
dyninfo_sect = bfd_get_section_by_name (exec_bfd, ".dynamic");通过名字找到被调试程序的动态库节(节名为.dynamic)
if (dyninfo_sect == NULL)
return 0;
dyninfo_addr = bfd_section_vma (exec_bfd, dyninfo_sect);找到该节被加载入内存之后的地址,这是一个动态地址。
/* Read in .dynamic section, silently ignore errors. */
dyninfo_sect_size = bfd_section_size (exec_bfd, dyninfo_sect);动态节大小。
buf = alloca (dyninfo_sect_size);
if (target_read_memory (dyninfo_addr, buf, dyninfo_sect_size))将动态节所有内容读入调试器内存中。
return 0;
/* Find the DT_DEBUG entry in the the .dynamic section.
For mips elf we look for DT_MIPS_RLD_MAP, mips elf apparently has
no DT_DEBUG entries. */
arch_size = bfd_get_arch_size (exec_bfd);
if (arch_size == -1) /* failure */
return 0;
if (arch_size == 32) 32bits系统处理。
{ /* 32-bit elf */
for (bufend = buf + dyninfo_sect_size;
buf < bufend;
buf += sizeof (Elf32_External_Dyn))遍历动态节中的每个tag。
{
Elf32_External_Dyn *x_dynp = (Elf32_External_Dyn *) buf;
long dyn_tag;
CORE_ADDR dyn_ptr;
dyn_tag = bfd_h_get_32 (exec_bfd, (bfd_byte *) x_dynp->d_tag);
if (dyn_tag == DT_NULL)
break;
else if (dyn_tag == DT_DEBUG)如果某个tag标识为DT_DEBUG,返回该TAG的值。注意,这个是实现的核心。
{
dyn_ptr = bfd_h_get_32 (exec_bfd,
(bfd_byte *) x_dynp->d_un.d_ptr);
return dyn_ptr;
}
我们随便找个可执行程序来看一下它的动态节
[tsecer@Harry linux-2.6.37.1]$ readelf -d `which cat`
Dynamic section at offset 0xa5e4 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x8048cdc
0x0000000d (FINI) 0x805066c
0x6ffffef5 (GNU_HASH) 0x804818c
0x00000005 (STRTAB) 0x8053da4
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 795 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0 TAG对应内容为零,因为它是在运行时由动态链接器初始化的。
0x00000003 (PLTGOT) 0x80536dc
三、DT_DEBUG何时初始化
glibc-2.7\elf\rtld.c
static void
dl_main (const ElfW(Phdr) *phdr,
ElfW(Word) phnum,
ElfW(Addr) *user_entry)
{
……
/* Initialize _r_debug. */
struct r_debug *r = _dl_debug_initialize (GL(dl_rtld_map).l_addr,
LM_ID_BASE);
……
/* Set up debugging before the debugger is notified for the first time. */
#ifdef ELF_MACHINE_DEBUG_SETUP
/* Some machines (e.g. MIPS) don‘t use DT_DEBUG in this way. */
ELF_MACHINE_DEBUG_SETUP (main_map, r);
ELF_MACHINE_DEBUG_SETUP (&GL(dl_rtld_map), r);
#else
if (main_map->l_info[DT_DEBUG] != NULL)
/* There is a DT_DEBUG entry in the dynamic section. Fill it in
with the run-time address of the r_debug structure */
main_map->l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r;
/* Fill in the pointer in the dynamic linker‘s own dynamic section, in
case you run gdb on the dynamic linker directly. */
if (GL(dl_rtld_map).l_info[DT_DEBUG] != NULL)
GL(dl_rtld_map).l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r;
#endif
……
}
所以此时的方法是调试器在主程序(注意:不是动态链接器)的DT_DEBUG节中填充上程序的_r_debug变量的地址。我们看一下找个结构的定义
glibc-2.7\elf\link.h
struct r_debug
{
int r_version; /* Version number for this protocol. */
struct link_map *r_map; /* Head of the chain of loaded objects. */
}
四、动态库布局的一些问题
[tsecer@Harry linux-2.6.37.1]$ sleep 1234 &
[1] 17451
[tsecer@Harry linux-2.6.37.1]$ cat /proc/17451/maps
001e8000-00206000 r-xp 00000000 fd:00 1280 /lib/ld-2.11.2.so
00206000-00207000 r--p 0001d000 fd:00 1280 /lib/ld-2.11.2.so 这里横亘一个只读数据区,比较特殊,从何而来?
00207000-00208000 rw-p 0001e000 fd:00 1280 /lib/ld-2.11.2.so
0020a000-0037c000 r-xp 00000000 fd:00 1282 /lib/libc-2.11.2.so
0037c000-0037d000 ---p 00172000 fd:00 1282 /lib/libc-2.11.2.so 这个地方还有一个更惨无人道的不可访问数据区。
0037d000-0037f000 r--p 00172000 fd:00 1282 /lib/libc-2.11.2.so
0037f000-00380000 rw-p 00174000 fd:00 1282 /lib/libc-2.11.2.so
00380000-00383000 rw-p 00000000 00:00 0
00bef000-00bf0000 r-xp 00000000 00:00 0 [vdso]
08048000-0804e000 r-xp 00000000 fd:00 49195 /bin/sleep
0804e000-0804f000 rw-p 00005000 fd:00 49195 /bin/sleep
09d16000-09d37000 rw-p 00000000 00:00 0 [heap]
b7686000-b7886000 r--p 00000000 fd:00 100518 /usr/lib/locale/locale-archive
b7886000-b7887000 rw-p 00000000 00:00 0
b789c000-b789d000 rw-p 00000000 00:00 0
bfafc000-bfb11000 rw-p 00000000 00:00 0 [stack]
[tsecer@Harry linux-2.6.37.1]$ readelf -l /lib/ld-2.11.2.so
Elf file type is DYN (Shared object file)
Entry point 0x1e8850
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x001e8000 0x001e8000 0x1d58c 0x1d58c R E 0x1000
LOAD 0x01dc60 0x00206c60 0x00206c60 0x00bc0 0x00c80 RW 0x1000
DYNAMIC 0x01defc 0x00206efc 0x00206efc 0x000c8 0x000c8 RW 0x4
NOTE 0x000114 0x001e8114 0x001e8114 0x00024 0x00024 R 0x4
GNU_EH_FRAME 0x01aee0 0x00202ee0 0x00202ee0 0x005e4 0x005e4 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x01dc60 0x00206c60 0x00206c60 0x003a0 0x003a0 R 0x1
[tsecer@Harry linux-2.6.37.1]$ readelf -l /lib/libc-2.11.2.so
Elf file type is DYN (Shared object file)
Entry point 0x220d10
There are 10 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x0020a034 0x0020a034 0x00140 0x00140 R E 0x4
INTERP 0x13fc90 0x00349c90 0x00349c90 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x0020a000 0x0020a000 0x171bcc 0x171bcc R E 0x1000
LOAD 0x1721c0 0x0037d1c0 0x0037d1c0 0x027bc 0x057a8 RW 0x1000
DYNAMIC 0x173d7c 0x0037ed7c 0x0037ed7c 0x000f8 0x000f8 RW 0x4
NOTE 0x000174 0x0020a174 0x0020a174 0x00044 0x00044 R 0x4
TLS 0x1721c0 0x0037d1c0 0x0037d1c0 0x00008 0x00040 R 0x4
GNU_EH_FRAME 0x13fca4 0x00349ca4 0x00349ca4 0x06d5c 0x06d5c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x1721c0 0x0037d1c0 0x0037d1c0 0x01e40 0x01e40 R 0x1
1、各个内存区属性设置位置
glibc-2.7\elf\dl-load.c
struct link_map *
_dl_map_object_from_fd (const char *name, int fd, struct filebuf *fbp,
char *realname, struct link_map *loader, int l_type,
int mode, void **stack_endp, Lmid_t nsid)
其中有一个循环,就是处理program header中的各个节,其中代码为
case PT_LOAD:这里使我们最为常见的两个映射,也就是对应上面“r-xp”对应的代码段,rw-p对应的数据段。
/* A load command tells us to map in part of the file.
We record the load commands and process them all later. */
……
case PT_GNU_STACK:
stack_flags = ph->p_flags;
break;
case PT_GNU_RELRO:这里是我们不太常见,但是能够从maps文件中体现出来的RELRO节。
l->l_relro_addr = ph->p_vaddr;
l->l_relro_size = ph->p_memsz;
break;
2、不可访问数据区由来
0037c000-0037d000 ---p 00172000 fd:00 1282 /lib/libc-2.11.2.so 这个地方还有一个更惨无人道的不可访问数据区。
我们看一下glibc的两个DT_LOAD节
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0020a000 0x0020a000 0x171bcc 0x171bcc R E 0x1000
LOAD 0x1721c0 0x0037d1c0 0x0037d1c0 0x027bc 0x057a8 RW 0x1000
第一个节结束于0x0020a000 + 0x171bcc=0x37BBCC,第二个节开始于0x0037d1c0 ,前者向上以页面为单位取整(0x1000)为0x37c000,后者向下取整为0x0037d000 ,中间相差了一个页面,然后动态连接器毫不客气的把这个区间设置为了不可访问,对应代码为
/* Determine whether there is a gap between the last segment
and this one. */
if (nloadcmds > 1 && c[-1].mapend != c->mapstart)
has_holes = true;
……
if (has_holes)
/* Change protection on the excess portion to disallow all access;
the portions we do not remap later will be inaccessible as if
unallocated. Then jump into the normal segment-mapping loop to
handle the portion of the segment past the end of the file
mapping. */
__mprotect ((caddr_t) (l->l_addr + c->mapend),
loadcmds[nloadcmds - 1].mapstart - c->mapend,
PROT_NONE);
3、只读数据由来
void internal_function
_dl_protect_relro (struct link_map *l)
{
ElfW(Addr) start = ((l->l_addr + l->l_relro_addr)
& ~(GLRO(dl_pagesize) - 1));
ElfW(Addr) end = ((l->l_addr + l->l_relro_addr + l->l_relro_size)这里的l_relro_addr和l_relro_size同样是之前对DT_RELRO节的读取,对于libc来说,这个值为0x1721c0 0x0037d1c0 0x0037d1c0 0x01e40 0x01e40 R 0x1,即地址为0x0037d1c0 、大小为0x01e40 。
& ~(GLRO(dl_pagesize) - 1));
if (start != end
&& __mprotect ((void *) start, end - start, PROT_READ) < 0)
{
static const char errstring[] = N_("\
cannot apply additional memory protection after relocation");
_dl_signal_error (errno, l->l_name, NULL, errstring);
}
}
上面的流程处理比较诡异,其实地址和结束地址都是向下取整,所以对于这只读区间,其保护范围为
0x0037d1c0向下取整0x0037d000,结束地址37F000,所以这个只读区大小为两个页面,对应内存为
0037d000-0037f000 r--p 00172000 fd:00 1282 /lib/libc-2.11.2.so
五、和nptl线程库比较
其实这个so的枚举和线程的枚举有很多类似的地方,之前说的对vfork clone之类的跟踪并不能解决线程枚举问题,因为gdb有时候需要在一个程序运行起来之后 attach到一个线程,在attach之后,它只能逐个枚举线程(而不是靠拦截clone系统调用),它有和动态库相似的模式,只是现在的gdb还没有使用,但是线程库操作始终是一个重要问题,大家可以看一下nptl_db文件夹下实现,好像应该对应的文件为pthread_db库,它包含了很多对线程库调试相关的内容。
原文地址:https://www.cnblogs.com/tsecer/p/10486357.html