咱們在查core問題時,有時候需要查看某個TLS變量的值。但是GDB沒有提供直接的命令,或者我不知道。這篇文字的目的。就是想辦法從core文件裏找出某個線程存放TLS變量的內容。css
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變量的位置了。bash
依據上面的分析。咱們需要先找到struct pthread
的位置。先看一下struct pthread
在棧中的位置:markdown
/* 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)
的值?post
儘管一個結構體在編譯後的大小已經固定下來,但是看到glibc中複雜的定義,還有那麼多宏定義限制。我就僅僅能呵呵了。只是,我另外一招,就是直接從當前運行的一些程序中,肯定sizeof(struct pthread)
的大小。ui
glibc提供的很是多函數中都會獲取TLS信息,比方pthread_self
。spa
這個函數很是短:操作系統
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 *
的偏移量;specific_1stblock
的地址,進而打印出TLS變量的值。NOTE: 此方法受限於GLIBC本身建立的內存棧空間和Linux X86_64環境。