zoukankan      html  css  js  c++  java
  • 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_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)的值?

    尽管一个结构体在编译后的大小已经固定下来,可是看到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就是 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环境。

  • 相关阅读:
    超实用的PHP代码片段
    推荐五款优秀的PHP代码重构工具
    PHP开发搜索引擎技术全解析
    怎样成为一名PHP专家?
    PHP中该怎样防止SQL注入?
    有关PHP 10条有用的建议
    fir.im Weekly
    可能是一场很 IN 的技术分享
    fir.im Weekly
    更新日志
  • 原文地址:https://www.cnblogs.com/zhchoutai/p/8386958.html
Copyright © 2011-2022 走看看