Linux 從core信息中找到TLS信息

背景

咱們在查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_TPTLS_DTV_AT_TP,在glibc2.20。x86_64上使用的是TLS_TCB_AT_TP。所以pd相對於mem的偏移就是固定的大小sizeof(struct pthread)數據結構

經過上面的描寫敘述,假設咱們可以知道某個線程所在內存段,那麼找到這個內存段的尾部,而後向前偏移sizeof(struct pthread)就可以找到struct pthread *的地址,進而找到specific_1stblockspecific的位置。函數

然而另外一個問題,就是怎麼肯定sizeof(struct pthread)的值?post

儘管一個結構體在編譯後的大小已經固定下來,但是看到glibc中複雜的定義,還有那麼多宏定義限制。我就僅僅能呵呵了。只是,我另外一招,就是直接從當前運行的一些程序中,肯定sizeof(struct pthread)的大小。ui

glibc提供的很是多函數中都會獲取TLS信息,比方pthread_selfspa

這個函數很是短:操作系統

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就是 0x‭7F6CC2D15710‬,再加上specific_1stblock的偏移量0x310,獲得‭0x7F6CC2D15A20‬。

最後一個,驗證拿到地址正確性:

(gdb) x/2xg 0x7F6CC2D15A20
0x7f6cc2d15a20: 0x0000000000000001      0x0000000012345678

大功告成。上面的結果,第一個數字是seq,第二個是data(這兩個是struct pthread_key_data的成員)。

儘管驗證的core文件正好是拿運行程序生成的,只是就是再運行一次生成一個新的core文件,這種方法同樣適用。

只是這也有受限的地方。最重要的緣由是以爲線程數據struct pthread就位於棧底,而棧在進程空間中是單獨的一個內存段。假設這個棧空間是由用戶建立線程時提供的。這種方法就可能不會適用。但願後面能找到更通用的方法,也許GDB會直接提供命令訪問線程變量。

總結

  1. 先找到struct pthread地址。

    可以經過gdb跟蹤正在運行的程序,查找進程棧內存空間,找到距離棧底的距離;

  2. 經過反彙編pthread_getspecific。找到specific_1stblock相對於struct pthread *的偏移量;
  3. 在core文件裏,經過棧寄存器rsp的地址,找到該線程所處內存段,依據上兩步的信息,計算出specific_1stblock的地址,進而打印出TLS變量的值。

NOTE: 此方法受限於GLIBC本身建立的內存棧空間和Linux X86_64環境。

相關文章
相關標籤/搜索