使用 -fPIC 编译的程序在跨过 GDB 中的线程局部变量时崩溃

2024-03-11

这是一个非常奇怪的问题,只有当程序编译时才会出现-fPIC option.

Using 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;
}

使用以下命令编译它: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;

虽然,如果我不编译它-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,红帽企业 Linux 服务器版本 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

问题在于 GAS、GNU 汇编器的内部,以及它如何生成 DWARF 调试信息。

编译器 GCC 负责为位置无关的线程本地访问生成特定的指令序列,这在文档中进行了记录线程本地存储的 ELF 处理 http://www.akkadia.org/drepper/tls.pdf,第 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

,之所以如此,是因为它占用的 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
        ...

然后,汇编器 GAS 将这段包含函数调用 (!) 的代码放宽为只有两条指令。这些都是:

  1. a mov有一个fs:-段覆盖,以及
  2. a 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)

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

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

  2. 在下一行中,它看到.byte 0x66通用动态序列的指令。尽管代表了指令,但它本身并不是指令的一部分data16x86 汇编中的指令前缀。 GAS 与 handler 一起作用于它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 行从函数起始地址加上先前的片段大小开始,但由于我们忽略了.byte在执行此操作之前声明,该片段太大了 1 个字节,因此第 15 行第一条指令的计算偏移量少了 1 个字节。

  4. 一段时间后,GAS 将全局动态序列放宽到以以下开头的最终指令序列mov fs:0x0, %rax。代码大小和所有偏移量保持不变,因为两个指令序列都是 16 字节。调试信息没有变化,仍然是错误的。


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

当没有断点运行时,程序正常运行,并看到

64 48 8b 04 25 00 00 00 00      mov    %fs:0x0,%rax

。正确放置断点需要保存指令的第一个字节并将其替换为int3(操作码0xcc),离开

cc                              int3
48 8b 04 25 00 00 00 00         mov    (0x0),%rax

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

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

64 cc                           fs:int3
8b 04 25 00 00 00 00            <garbage>

这是一个奇怪但仍然有效的断点。这就是为什么你没有看到 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!

因为 GDB 重新开始执行一个字节的时间太远,所以 CPU 不会解码fs:指令前缀字节,而是执行mov (0x0),%rax与默认段,这是ds:(数据)。这会立即导致从地址 0(空指针)读取。 SIGSEGV 紧随其后。

所有应得的学分马克·普洛特尼克 https://stackoverflow.com/questions/33429912/program-compiled-with-fpic-crashes-while-stepping-over-thread-local-variable-in/33557963#comment54798247_33429912基本上解决了这个问题。


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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 -fPIC 编译的程序在跨过 GDB 中的线程局部变量时崩溃 的相关文章

  • 内联函数/方法

    声明 内联函数必须在调用之前定义 这个说法正确吗 EDIT 该问题最初是德语 内联功能穆森 弗 伊赫雷姆 奥夫鲁夫定义 sein 也许它对任何人都有帮助 是的 它是正确的 但只是部分正确 它可能正确地重新构建如下 内联函数必须在每个翻译单位
  • 将字节数组转换为托管结构

    更新 这个问题的答案帮助我编写了开源项目GitHub 上的 AlicanC 现代战争 2 工具 https github com AlicanC AlicanC s Modern Warfare 2 Tool 你可以看到我是如何阅读这些数据
  • System.IO.IOException:由于意外>数据包格式,握手失败?

    有谁知道这意味着什么 System Net WebException 底层连接已关闭 发送时发生意外错误 gt System IO IOException 由于意外 握手失败 数据包格式 在 System Net Security SslS
  • 为什么大多数平台上没有“aligned_realloc”?

    MSVC有自己的非标准函数 aligned malloc aligned realloc and aligned free C 17和C11引入了 std aligned alloc 其结果可以是de分配有free or realloc B
  • 选择列表逻辑应位于 ASP.NET MVC、视图、模型或控制器中的什么位置?

    我觉得我的问题与这个问题很接近 但我想对这样的代码应该放在哪里进行更一般的讨论 Asp Net MVC SelectList 重构问题 https stackoverflow com questions 2149855 asp net mv
  • 从 C 结构生成 C# 结构

    我有几十个 C 结构 我需要在 C 中使用它们 典型的 C 结构如下所示 typedef struct UM EVENT ULONG32 Id ULONG32 Orgin ULONG32 OperationType ULONG32 Size
  • mprotect 之后 malloc 导致分段错误

    在使用 mprotect 保护内存区域后第一次调用 malloc 时 我遇到分段错误 这是执行内存分配和保护的代码片段 define PAGESIZE 4096 void paalloc int size Allocates and ali
  • 如何使用MySqlCommand和prepare语句进行多行插入?(#C)

    Mysql 给出了如何使用准备语句和 NET 插入行的示例 http dev mysql com doc refman 5 5 en connector net programming prepared html http dev mysq
  • 带 If 的嵌套 For 循环的时间复杂度

    void f int n for int i 1 i lt n i if i int sqrt n 0 for int k 0 k lt pow i 3 k do something 我的思考过程 执行if语句的次数 sum i 1 to
  • 从 Code::Blocks 运行程序时出现空白控制台窗口 [重复]

    这个问题在这里已经有答案了 当我尝试在 Code Blocks 中构建并运行新程序时 控制台窗口弹出空白 我必须单击退出按钮才能停止它 它对我尝试过的任何新项目 包括 Hello world 都执行此操作 奇怪的是 它对于我拥有的任何旧项目
  • C# 委托责任链

    为了我的理解目的 我实现了责任链模式 Abstract Base Type public abstract class CustomerServiceDesk protected CustomerServiceDesk nextHandle
  • 使用 WF 的多线程应用程序的错误处理模式?

    我正在写一个又长又详细的问题 但只是放弃了它 转而选择一个更简单的问题 但我在这里找不到答案 应用程序简要说明 我有一个 WPF 应用程序 它生成多个线程 每个线程执行自己的 WF 处理线程和 WF 中的错误 允许用户从 GUI 端进行交互
  • C++ 错误 - “成员初始值设定项表达式列表被视为复合表达式”

    我收到一个我不熟悉的 C 编译器错误 可能是一个非常愚蠢的错误 但我不能完全指出它 Error test cpp 27 error member initializer expression list treated as compound
  • C# 中的常量和只读? [复制]

    这个问题在这里已经有答案了 可能的重复 const 和 readonly 之间有什么区别 https stackoverflow com questions 55984 what is the difference between cons
  • 二叉树中的 BFS

    我正在尝试编写二叉树中广度优先搜索的代码 我已将所有数据存储在队列中 但我不知道如何访问所有节点并消耗它们的所有子节点 这是我的 C 代码 void breadthFirstSearch btree bt queue q if bt NUL
  • 从 R 到 C 处理列表并访问它

    我想使用从 R 获得的 C 列表 我意识到这个问题与此非常相似 使用 call 在 R 和 C 之间传递数据帧 https stackoverflow com questions 6658168 passing a data frame f
  • 如何引用解决方案之外的项目?

    我有一个 Visual Studio C 解决方案 其中包含一些项目 其中一个项目需要引用另一个不属于解决方案的项目 一开始我引用了dll
  • 在 C++17 中使用 成员的链接错误

    我在 Ubuntu 16 04 上使用 gcc 7 2 并且需要使用 C 17 中的新文件系统库 尽管确实有一个名为experimental filesystem的库 但我无法使用它的任何成员 例如 当我尝试编译此文件时 include
  • 为什么 Linux 对目录使用 getdents() 而不是 read()?

    我浏览 K R C 时注意到 为了读取目录中的条目 他们使用了 while read dp gt fd char dirbuf sizeof dirbuf sizeof dirbuf code Where dirbuf是系统特定的目录结构
  • 以 UTF8 而不是 UTF16 输出 DataTable XML

    我有一个 DataTable 我正在使用 WriteXML 创建一个 XML 文件 尽管我在以 UTF 16 编码导出它时遇到问题 并且似乎没有明显的方法来更改它 我了解 NET 在字符串内部使用 UTF 16 这是正确的吗 然后 我通过

随机推荐