Linux 从core信息中找到TLS信息
背景
我们在查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 *
的偏移量。只是还好,这个是比較easy做的,看看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代码跟汇编代码对照着看,就非常easy找到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
........
一大堆内存段。哪个才是自己要找的线程呢?
线程所处的空间是一个栈空间,那仅仅要找到某个线程的栈上的变量或者其他信息,再依据这个信息就能够找到相应的内存段。有一个非常easy查看的栈信息就是栈寄存器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信息的更多相关文章
- Python之向日志输出中添加上下文信息
除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如:远程客户端的IP地址和用户名.这里我们 ...
- 【转】Python之向日志输出中添加上下文信息
[转]Python之向日志输出中添加上下文信息 除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定 ...
- Python 日志输出中添加上下文信息
Python日志输出中添加上下文信息 除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如: ...
- 最简单的方法是使用标准的 Linux GUI 程序之一: i-nex 收集硬件信息,并且类似于 Windows 下流行的 CPU-Z 的显示。 HardInfo 显示硬件具体信息,甚至包括一组八个的流行的性能基准程序,你可以用它们评估你的系统性能。 KInfoCenter 和 Lshw 也能够显示硬件的详细信息,并且可以从许多软件仓库中获取。
最简单的方法是使用标准的 Linux GUI 程序之一: i-nex 收集硬件信息,并且类似于 Windows 下流行的 CPU-Z 的显示. HardInfo 显示硬件具体信息,甚至包括一组八个的流 ...
- 【Linux】 linux中的进程信息相关的一些内容
_ linux进程信息 ■ top top命令用于动态地查看系统的进程和其他一些资源的信息.开启top的时候可以加上-t <sec>来设置top更新的频率高低.进入top界面之后,可以输入 ...
- 『.NET Core CLI工具文档』(二).NET Core 工具遥测(应用信息收集)
说明:本文是个人翻译文章,由于个人水平有限,有不对的地方请大家帮忙更正. 原文:.NET Core Tools Telemetry 翻译:.NET Core 工具遥测(应用信息收集) .NET Cor ...
- 修改.net mvc中前端验证信息的显示方式
最近一直在学习.net core的用法.想法是通过写一个新闻系统来熟悉一下这个最新的技术.其实,我以前一直对.net技术都是浅尝辄止,最主要原因是没有动力.平时写企业站因为时间原因,不是使用php的框 ...
- linux查看cpu、内存、版本信息
1. 查看物理CPU的个数#cat /proc/cpuinfo |grep "physical id"|sort |uniq|wc –l 2. 查看逻辑CPU的个数#cat ...
- PowerShell_零基础自学课程_6_PS中获取帮助信息详解、管道、格式化输
前些文章陆续的说了一些关于这些主题,但是讨论的都不够深入,今天我们深入的了解一下获取帮助信息.管道以及格式化输出的内容. 一.获取帮助信息 在PS中获取帮助信息,最常用的有: -? .get-comm ...
随机推荐
- 使用form-create动态生成vue组件
使用form-create动态生成vue自定义组件和嵌套表单组件 [github] | [说明文档] 示例 let rule = [ { type:'row', children:[ { type:' ...
- PHP定时执行任务
ignore_user_abort();//关掉浏览器,PHP脚本也可以继续执行. set_time_limit(0);// 通过set_time_limit(0)可以让程序无限制的执行下去 $int ...
- 【VC++学习笔记五】SDI|MDI的全屏显示
一.Mainframe中添加一个记录是否全屏状态的变量BOOL m_bFullScreen. 二.工具栏添加一个按钮,进行全屏的操作,响应事件函数写在Mainframe中. 三.在响应函数中,添加如下 ...
- python + eclipse + django + postgresql 开发网站(二)
引用与参考 http://www.cnblogs.com/lanxuezaipiao/p/3283932.html python2.7 django1.6 1.新建Django项目
- 【Henu ACM Round#15 A】 A and B and Chess
[链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 统计大写和小写的个数. 比较答案.输出即可. [代码] #include <bits/stdc++.h> using n ...
- 我的RTOS 之二 --Threadx在skyeye上仿真測试(基于2410)
对于RTOS 移植来说,移植平台至少要提供双方面的设备. 1.OS执行时,须要tick,所以须要提供Timer定时器 2.OS执行时,须要调度,就是挂起当前线程,把控制权交给系统,所以须要訪问系统各个 ...
- 第8章7节《MonkeyRunner源代码剖析》MonkeyRunner启动执行过程-小结
最后我们对MonkeyRunner启动的过程做一个总结,当然,当中也包括启动Monkey,尽管它不属于启动过程的一部分: monkeyrunner这个shell脚本会先设置一些执行环境的系统属性保存到 ...
- 关于exports 和 module.exports
本文来源为node.js社区附上链接 http://cnodejs.org/topic/5231a630101e574521e45ef8 require 用来加载代码,而 exports 和 modu ...
- js中常用的对象—Math的属性和方法
js中有一个内置对象——Math对象,在有的时候非常有用,我们来分析一下: Math常用属性(注意要大写): E 返回算术常量 e,即自然对数的底数(约等于2.718)LN2 返回 2 的自然对数(约 ...
- BZOJ 2037 区间DP
跟POJ 3042是一个类型的http://blog.csdn.net/qq_31785871/article/details/52954924 思路: 先排个序 (把初始位置也插进去) f[i][j ...