Linux 从core信息中找到TLS信息
2016-08-09 16:59
134 查看
背景
我们在查core问题时,有时候需要查看某个TLS变量的值,但是GDB没有提供直接的命令,或者我不知道。这篇文字的目的,就是想办法从core文件中找出某个线程存放TLS变量的内容。依据
Linux的glibc库创建线程时,使用mmap创建一块内存空间,作为此线程的栈空间。并将一个叫做
struct pthread的数据结构放在栈的顶端(参考glibc代码
allocate_stack@allocatestack.c),而TLS的数据结构就在
struct pthread中:
struct pthread { // ... struct pthread_key_data { uintptr_t seq; void *data; } specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE]; struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE]; // ... };
其中
specific_1stblock数组是第一层的TLS变量,
PTHREAD_KEY_2NDLEVEL_SIZE是一个宏定义,在glib2.20中的大小是32。如果TLS变量超过了这个值,就会使用
specific来存储。从这里可以看出来,只要我们找到了
specific_1stblock的位置,就能找到TLS变量的位置了。
根据上面的分析,我们需要先找到
struct pthread的位置。先看一下
struct pthread在栈中的位置:
/* Place the thread descriptor at the end of the stack. */ #if TLS_TCB_AT_TP pd = (struct pthread *) ((char *) mem + size - coloring) - 1; #elif TLS_DTV_AT_TP pd = (struct pthread *) ((((uintptr_t) mem + size - coloring - __static_tls_size) & ~__static_tls_align_m1) - TLS_PRE_TCB_SIZE); #endif
pd的定义是
struct pthread *pd;。代码中的
mem是使用
mmap创建的内存首地址。
coloring根据宏定义
COLORING_INCREMENT来决定是否是一个变化的值。在我看的代码版本和使用的操作系统(Redhat 6.5)安装的glibc中,都是0,也就是说
coloring是一个常量0。这里还有两个宏定义条件,
TLS_TCB_AT_TP和
TLS_DTV_AT_TP,在glibc2.20,x86_64上使用的是
TLS_TCB_AT_TP,因此
pd相对于
mem的偏移就是固定的大小
sizeof(struct pthread)。
通过上面的描述,如果我们可以知道某个线程所在内存段,那么找到这个内存段的尾部,然后向前偏移
sizeof(struct pthread)就可以找到
struct pthread *的地址,进而找到
specific_1stblock和
specific的位置。
然而还有一个问题,就是怎么确定
sizeof(struct pthread)的值?
虽然一个结构体在编译后的大小已经固定下来,但是看到glibc中复杂的定义,还有那么多宏定义限制,我就只能呵呵了。不过,我还有一招,就是直接从当前运行的一些程序中,确定
sizeof(struct pthread)的大小。
glibc提供的很多函数中都会获取TLS信息,比如
pthread_self。这个函数很短:
pthread_t __pthread_self (void) { return (pthread_t) THREAD_SELF; }
代码中
THREAD_SELF的定义是
# define THREAD_SELF \ ({ struct pthread *__self; \ asm ("mov %%fs:%c1,%0" : "=r" (__self) \ : "i" (offsetof (struct pthread, header.self))); \ __self;})
这个代码只是拿到
fs段寄存器加上固定的偏移量的值。其实我本来想过直接用
fs寄存器的值,可惜这个值不管在正在运行的程序中还是在
core文件中,gdb都是看不到的。好吧,做了这么多白搭了。
不过幸运的是,gdb在调试正在执行的程序的时候,是可以直接执行函数的,我把
pthread_self()函数的返回值拿出来,然后跟这个线程所在段的内存做对比,就可以知道
struct pthread *相对于栈底的偏移量了。
费了九牛二虎之力拿到了
sizeof(struct pthread),回头看一看,才完成了任务的一半。还得知道
specific_1stblock相对于
struct pthread *的偏移量。不过还好,这个是比较容易做的,看看
pthread_getspecific的汇编代码就一目了然了:
Dump of assembler code for function pthread_getspecific: 0x0000003bcd40c470 <+0>: cmp $0x1f,%edi 0x0000003bcd40c473 <+3>: push %rbx 0x0000003bcd40c474 <+4>: ja 0x3bcd40c4ba <pthread_getspecific+74> 0x0000003bcd40c476 <+6>: mov %edi,%eax 0x0000003bcd40c478 <+8>: shl $0x4,%rax 0x0000003bcd40c47c <+12>: mov %fs:0x10,%rdx 0x0000003bcd40c485 <+21>: lea 0x310(%rdx,%rax,1),%rdx 0x0000003bcd40c48d <+29>: mov 0x8(%rdx),%rax 0x0000003bcd40c491 <+33>: test %rax,%rax 0x0000003bcd40c494 <+36>: je 0x3bcd40c4ac <pthread_getspecific+60> .....
对比一下glibc中的代码:
struct pthread_key_data *data; /* Special case access to the first 2nd-level block. This is the usual case. */ if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE)) data = &THREAD_SELF->specific_1stblock[key]; else
THREAD_SELF就是当前线程的
struct pthread *。C代码跟汇编代码对比着看,就很容易找到
specific_1stblock的偏移量。汇编中的
edi寄存器就是传入的参数
pthread_key_t key。
mov %fs:0x10,%rdx这一行代码使用了
fs寄存器,跟上面看到的
pthread_self函数的方法一样,这就可以确定是获取
struct pthread *的地址。
那么接下来的一行
lea 0x310(%rdx,%rax,1),%rdx自然就是获取
specific_1stblock的值了。这一行中
rdx寄存器存放
struct pthread*,
rax存放
key * sizeof(struct pthread_key_data),最后把
rdx + (rax * 1) + 0x310的值放入了
rdx中,很明显,0x310就是
specific_1stblock的偏移量(0x310)。
到目前为止,已经准备好了所有获取TLS变量的条件,
sizeof(struct pthread)和
specific_1stblock的偏移量。下面就开始动手测试验证。
测试
写一个使用TLS的测试代码这个代码创建了一个线程变量和一个线程,创建出来的线程设置了线程变量的值。
#include <pthread.h> #include <unistd.h> pthread_key_t key; void *thread_func(void *arg) { pthread_setspecific(key, (const void *)0x12345678); // 设置一个特殊的值方便检测测试结果 sleep(100); // 睡眠一段时间用来生成core文件 return NULL; } int main(int argc, char **argv) { pthread_key_create(&key, NULL); pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); pthread_join(tid, NULL); return 0; }
编译
g++ -lpthread test.cpp
默认生成
a.out。直接执行,会在
sleep中暂停一段时间,用gdb attach上去。
执行
info thread
(gdb) info thread 2 Thread 0x7f6cc2d15710 (LWP 15000) 0x0000003bcd0a6a8d in nanosleep () from /lib64/libc.so.6 * 1 Thread 0x7f6cc2d17720 (LWP 14999) 0x0000003bcd40803d in pthread_join () from /lib64/libpthread.so.0 (gdb)
我们来看Thread 2,就是创建出来的线程。
执行
thread 2切换到线程2。
执行
call pthread_self(),结果却得到
(gdb) call pthread_self() $8 = -1026468080
改成十六进制打印
(gdb) p/x $8 $9 = 0xc2d15710
明显还是不对,相当无语,gdb的call指令只打印了4个字节。不过稍微注意一下就发现了
info thread输出的结果,有一个数据和这里一样:
2 Thread 0x7f6cc2d15710 (LWP 15000) 0x0000003bcd0a6a8d in nanosleep () from /lib64/libc.so.6
Thread后面的数字,就是
pthread的地址,不过这个数据在调试core文件时并没有打印:
(gdb) info thread 2 Thread 14999 0x0000003bcd40803d in pthread_join () from /lib64/libpthread.so.0 * 1 Thread 15000 0x0000003bcd0a6a8d in nanosleep () from /lib64/libc.so.6
虽然执行的结果与预期不符,但是还好拿到了pthread的地址。接下来找到这个线程所在的内存段,就是栈区间。进程的数据段信息可以从/proc/
pid/maps文件中看到,其中
pid是进程号。
这是我测试出来的进程中的内存信息:
7f6cc2315000-7f6cc2316000 ---p 00000000 00:00 0 7f6cc2316000-7f6cc2d1d000 rw-p 00000000 00:00 0 7fff4c321000-7fff4c337000 rw-p 00000000 00:00 0 [stack] 7fff4c35a000-7fff4c35b000 r-xp 00000000 00:00 0 [vdso]
很明显,0x7f6cc2d15710属于这一段:
7f6cc2316000-7f6cc2d1d000 rw-p 00000000 00:00 0
这就是线程2的栈空间,由于栈是从上往下增长的,那么栈底就是7f6cc2d1d000。它与0x7f6cc2d15710的距离是0x78f0。
在gdb中用
gcore命令生成一个core文件,用gdb打开core文件验证测试,并找出TLS的值。
gdb a.out core
打印出core文件记录的程序内存段
(gdb) info files Symbols from "/data01/usergrp/wangyl11/a.out". Local core dump file: `/data01/usergrp/wangyl11/core.14999', file type elf64-x86-64. 0x0000000000400000 - 0x0000000000400000 is load1 0x0000000000600000 - 0x0000000000601000 is load2 0x00000000006d1000 - 0x00000000006f2000 is load3 ............................. 0x0000003bcde83000 - 0x0000003bcde84000 is load24 0x00007f6cc2316000 - 0x00007f6cc2d1d000 is load25 0x00007fff4c321000 - 0x00007fff4c337000 is load26 0x00007fff4c35a000 - 0x00007fff4c35b000 is load27 0xffffffffff600000 - 0xffffffffff601000 is load28 ........
一大堆内存段,哪个才是自己要找的线程呢?
线程所处的空间是一个栈空间,那只要找到某个线程的栈上的变量或者其它信息,再根据这个信息就可以找到对应的内存段。有一个很容易查看的栈信息就是栈寄存器
rsp。
看下线程的栈寄存器:
(gdb) thread 1 [Switching to thread 1 (Thread 15000)]#0 0x0000003bcd0a6a8d in nanosleep () from /lib64/libc.so.6 (gdb) info reg rsp rsp 0x7f6cc2d14c90 0x7f6cc2d14c90
这样就找到了这个段:
0x00007f6cc2316000 - 0x00007f6cc2d1d000 is load25
这一段也是刚才看到的线程栈空间。拿栈底的地址就是 0x00007f6cc2d1d000,减去pthread偏移0x78f0就是 0x7F6CC2D15710,再加上
specific_1stblock的偏移量0x310,得到0x7F6CC2D15A20。
最后一个,验证拿到地址正确性:
(gdb) x/2xg 0x7F6CC2D15A20 0x7f6cc2d15a20: 0x0000000000000001 0x0000000012345678
大功告成,上面的结果,第一个数字是
seq,第二个是
data(这两个是
struct pthread_key_data的成员)。
虽然验证的core文件正好是拿执行程序生成的,不过就是再运行一次生成一个新的core文件,这个方法一样适用。
不过这也有受限的地方,最重要的原因是认为线程数据
struct pthread就位于栈底,而栈在进程空间中是单独的一个内存段。如果这个栈空间是由用户创建线程时提供的,这个方法就可能不会适用。希望后面能找到更通用的方法,或许GDB会直接提供命令访问线程变量。
总结
先找到struct pthread地址。可以通过gdb跟踪正在执行的程序,查找进程栈内存空间,找到距离栈底的距离;
通过反汇编
pthread_getspecific,找到
specific_1stblock相对于
struct pthread *的偏移量;
在core文件中,通过栈寄存器rsp的地址,找到该线程所处内存段,根据上两步的信息,计算出
specific_1stblock的地址,进而打印出TLS变量的值。
NOTE: 此方法受限于GLIBC自己创建的内存栈空间和Linux X86_64环境。
相关文章推荐
- Linux 从core信息中找到TLS信息
- Linux 从core信息中找到TLS信息
- Linux 如何使用gdb 查看core堆栈信息
- Linux中gdb 查看core堆栈信息 gdb core 调试
- Linux中gdb 查看core堆栈信息
- Linux 如何使用gdb 查看core堆栈信息
- Linux中gdb 查看core堆栈信息
- 编译Linux/Android 输出很多信息很难找到错误/警告信息怎么办?
- Linux中gdb 查看core堆栈信息
- Linux中生成Core Dump系统异常信息记录文件的教程
- linux下打开core功能,以便于通过gdb查看出错堆栈信息
- Linux 如何使用gdb 查看core堆栈信息
- Linux中gdb 查看core堆栈信息
- Linux中gdb 查看core堆栈信息
- Linux中gdb 查看core堆栈信息
- Linux中gdb 查看core堆栈信息
- linux下找到apache的安装目录,配置文件,域名等信息命令
- linux 系统调用 出错信息 调试 strerror errno
- Linux下获取binary中的rpath信息
- Linux查看内核、CPU等信息的常用命令