草庐IT

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

coder 2023-06-16 原文

这是一个非常奇怪的问题,仅当使用-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;
}

使用以下命令进行编译: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 Handling for Thread-Local Storage,第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放宽此代码,其中包含一个函数调用(!),最多只能包含两个指令。这些是:
  • 具有mov -segment覆盖的fs:
  • 一个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调试信息。
  • 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指令时才这样做。相反,在我们的情况下,它将继续到下一行,而忽略调试信息。
  • 在下一行中,它将看到General Dynamic序列的.byte 0x66指令。尽管这在x86汇编中表示data16指令前缀,但它本身并不是指令的一部分。 GAS使用处理程序cons_worker()对其执行操作,该片段的大小从12个字节增加到13个字节。
  • 在下一行,它会看到一个真实的指令leaq,该指令是通过调用映射到assemble_one()中的void md_assemble (char *line)的宏gas/config/tc-i386.c进行解析的。在该函数的最后,将调用output_insn(),它本身最终将调用dwarf2_emit_insn(0)并最终发出调试信息。开始一个新的行号语句(LNS),声称第15行从函数开始地址加上先前的片段大小开始,但是由于我们在这样做之前跳过了.byte语句,因此该片段太大了1个字节,因此,第15行的第一条指令的偏移量为1个字节。
  • 不久之后,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
    

    。正确放置断点将涉及到将指令的第一个字节保存并替换为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及时跟进。

    所有应归功于Mark Plotnick实质上是在钉牢这个问题。

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

    关于c - 使用-fPIC编译的程序在GDB中跳过线程局部变量时崩溃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33429912/

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

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

      我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

    3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

      很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

    5. ruby - 在 Ruby 中使用匿名模块 - 2

      假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

    6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

      我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

    7. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

      我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

    8. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    9. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

      我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

    10. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

      我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

    随机推荐