使用-fPIC编译的程序在GDB中逐步执行线程局部变量时崩溃

Kar*_*and 17 c linux gcc gdb pthreads

这是一个非常奇怪的问题,只有在使用-fPIC选项编译程序时才会发生.

使用gdb我能够打印线程局部变量,但踩过它们会导致崩溃.

thread.c

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>

#define MAX_NUMBER_OF_THREADS 2

struct mystruct {
    int   x;
    int   y;
};

__thread struct mystruct obj;

void* threadMain(void *args) {
    obj.x = 1;
    obj.y = 2;

    printf("obj.x = %d\n", obj.x);
    printf("obj.y = %d\n", obj.y);

    return NULL;
}

int main(int argc, char *arg[]) {
    pthread_t tid[MAX_NUMBER_OF_THREADS];
    int i = 0;

    for(i = 0; i < MAX_NUMBER_OF_THREADS; i++) {
        pthread_create(&tid[i], NULL, threadMain, NULL);
    }

    for(i = 0; i < MAX_NUMBER_OF_THREADS; i++) {
        pthread_join(tid[i], NULL);
    }

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

使用以下内容编译它: gcc -g -lpthread thread.c -o thread -fPIC

然后在调试时: gdb ./thread

(gdb) b threadMain 
Breakpoint 1 at 0x4006a5: file thread.c, line 15.
(gdb) r
Starting program: /junk/test/thread 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff7fc7700 (LWP 31297)]
[Switching to Thread 0x7ffff7fc7700 (LWP 31297)]

Breakpoint 1, threadMain (args=0x0) at thread.c:15
15      obj.x = 1;
(gdb) p obj.x
$1 = 0
(gdb) n

Program received signal SIGSEGV, Segmentation fault.
threadMain (args=0x0) at thread.c:15
15      obj.x = 1;
Run Code Online (Sandbox Code Playgroud)

虽然,如果我编译它没有-fPIC那么这个问题不会发生.

在任何人问我为什么使用之前-fPIC,这只是一个简化的测试用例.我们有一个巨大的组件,它编译成一个so文件然后插入另一个组件.因此,fPIC是必要的.

由于它没有功能影响,只有调试几乎是不可能的.

平台信息:Linux 2.6.32-431.el6.x86_64 #1 SMP Sun Nov 10 22:19:54 EST 2013 x86_64 x86_64 x86_64 GNU/Linux,Red Hat Enterprise Linux Server 6.5版(圣地亚哥)

也可以重现以下内容

Linux 3.13.0-66-generic #108-Ubuntu SMP Wed Oct 7 15:20:27 
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
Run Code Online (Sandbox Code Playgroud)

Iwi*_*ist 6

问题深深地存在于GAS,GNU汇编器的肠胃中,以及它如何生成DWARF调试信息。

编译器GCC负责为与位置无关的线程本地访问生成特定的指令序列,该文档序列在文档ELF处理线程本地存储(第22页,第4.1.6节:x86-64)中进行了说明。通用动态TLS模型。该顺序是:

0x00 .byte 0x66
0x01 leaq  x@tlsgd(%rip),%rdi
0x08 .word 0x6666
0x0a rex64
0x0b call __tls_get_addr@plt
Run Code Online (Sandbox Code Playgroud)

之所以这样,是因为它占用的16个字节为后端/汇编程序/链接器优化留有空间。实际上,您的编译器会为以下生成以下汇编器threadMain()

threadMain:
.LFB2:
        .file 1 "thread.c"
        .loc 1 14 0
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movq    %rdi, -8(%rbp)
        .loc 1 15 0
        .byte   0x66
        leaq    obj@tlsgd(%rip), %rdi
        .value  0x6666
        rex64
        call    __tls_get_addr@PLT
        movl    $1, (%rax)
        .loc 1 16 0
        ...
Run Code Online (Sandbox Code Playgroud)

然后,汇编器GAS放宽此代码,其中包含一个函数调用(!),最多只能包含两个指令。这些是:

  1. 一个mov具有fs:-段覆盖,并
  2. 一种 lea

,在最终装配中。它们之间总共占据了16个字节,这说明了为何将“通用动态模型”指令序列设计为需要16个字节。

(gdb) disas/r threadMain                                                                                                                                                                                         
Dump of assembler code for function threadMain:                                                                                                                                                                  
   0x00000000004007f0 <+0>:     55      push   %rbp                                                                                                                                                              
   0x00000000004007f1 <+1>:     48 89 e5        mov    %rsp,%rbp                                                                                                                                                 
   0x00000000004007f4 <+4>:     48 83 ec 10     sub    $0x10,%rsp                                                                                                                                                
   0x00000000004007f8 <+8>:     48 89 7d f8     mov    %rdi,-0x8(%rbp)                                                                                                                                           
   0x00000000004007fc <+12>:    64 48 8b 04 25 00 00 00 00      mov    %fs:0x0,%rax
   0x0000000000400805 <+21>:    48 8d 80 f8 ff ff ff    lea    -0x8(%rax),%rax
   0x000000000040080c <+28>:    c7 00 01 00 00 00       movl   $0x1,(%rax)
Run Code Online (Sandbox Code Playgroud)

到目前为止,一切都已正确完成。现在,问题开始了,因为GAS会为您的特定汇编代码生成DWARF调试信息。

  1. 在解析线逐线binutils-x.y.z/gas/read.c,功能void read_a_source_file (char *name),GAS相遇.loc 1 15 0,那开始的下一行的声明,并运行处理程序void dwarf2_directive_loc (int dummy ATTRIBUTE_UNUSED)dwarf2dbg.c。不幸的是,处理程序不会无条件地发出针对当前frag_now正在构建的机器代码的“片段”()中的当前偏移量的调试信息。它可以通过调用来完成此操作dwarf2_emit_insn(0),但是.loc处理程序当前仅在.loc连续看到多个指令时才这样做。相反,在我们的情况下,它将继续到下一行,而忽略调试信息。

  2. 在下一行,它看到.byte 0x66了常规动态序列的指令。尽管这data16在x86汇编中表示指令前缀,但它本身并不是指令的一部分。GAS随处理程序对其执行操作cons_worker(),并且片段的大小从12个字节增加到13个字节。

  3. 在下一行中,它看到一条真实的指令,leaq该指令通过调用assemble_one()映射到void md_assemble (char *line)in 的宏进行解析gas/config/tc-i386.c。在该函数的output_insn()最后调用,它本身最终会调用dwarf2_emit_insn(0)并最终导致调试信息被发出。开始一个新的行号语句(LNS),该行声称第15行从function-start-address加上以前的片段大小开始,但是由于我们.byte在这样做之前就跳过了该语句,因此片段太大了1个字节,并且计算出的偏移量因此,第15行的第一条指令的地址为1个字节。

  4. 稍后,GAS将“全局动态序列”放宽为以开头的最终指令序列mov fs:0x0, %rax。由于两个指令序列均为16个字节,因此代码大小和所有偏移量均保持不变。调试信息不​​变,但仍然错误。


GDB读取“行号声明”时,被告知与序号threadMain()14(在其上找到其签名)关联的序言在第15行开始处结束。GDB尽职尽责地在该位置植入了一个断点,但不幸的是,它距离1个字节太远了。

在没有断点的情况下运行时,程序将正常运行,并看到

64 48 8b 04 25 00 00 00 00      mov    %fs:0x0,%rax
Run Code Online (Sandbox Code Playgroud)

。正确放置断点将涉及到将指令的第一个字节保存并替换为int3(opcode 0xcc),

cc                              int3
48 8b 04 25 00 00 00 00         mov    (0x0),%rax
Run Code Online (Sandbox Code Playgroud)

。然后,正常的跳越序列将涉及恢复指令的第一个字节,将程序计数器设置eip为该断点的地址,单步执行,重新插入断点,然后继续执行程序。

但是,当GDB将断点放置在错误的地址1个字节处太远时,程序会看到

64 cc                           fs:int3
8b 04 25 00 00 00 00            <garbage>
Run Code Online (Sandbox Code Playgroud)

这是一个奇怪但仍然有效的断点。这就是为什么您没有看到SIGILL(非法指令)的原因。

现在,当GDB尝试越过时,它将恢复指令字节,将PC设置为断点的地址,这就是现在看到的内容:

64                              fs:                # CPU DOESN'T SEE THIS!
48 8b 04 25 00 00 00 00         mov    (0x0),%rax  # <- CPU EXECUTES STARTING HERE!
# BOOM! SEGFAULT!
Run Code Online (Sandbox Code Playgroud)

由于GDB将执行重新开始的位置太远了一个字节,因此CPU不会解码fs:指令前缀字节,而是mov (0x0),%rax使用默认段ds:(数据)执行。这立即导致从地址0(空指针)进行读取。SIGSEGV及时跟进。

由于所有学分马克Plotnick的本质钉这一点。


这是保留的溶液是二进制补片cc1gcc的实际C编译器,以发射data16代替.byte 0x66。这导致GAS将前缀和指令组合解析为一个单元,从而在调试信息中产生正确的偏移量。