Mit -fPIC kompiliertes Programm stürzt ab, während Thread-lokale Variable in GDB übersprungen wird

Lesezeit: 9 Minuten

Benutzer-Avatar
Kartik Anand

Dies ist ein sehr seltsames Problem, das nur auftritt, wenn das Programm mit kompiliert wird -fPIC Möglichkeit.

Verwenden gdb Ich kann lokale Thread-Variablen drucken, aber wenn ich sie überschreite, kommt es zum Absturz.

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

Kompilieren Sie es wie folgt: gcc -g -lpthread thread.c -o thread -fPIC

Dann beim Debuggen: 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;

Obwohl, wenn ich es ohne kompiliere -fPIC dann tritt dieses Problem nicht auf.

Bevor mich jemand fragt, warum benutze ich -fPIC, dies ist nur ein reduzierter Testfall. Wir haben eine riesige Komponente, die in a kompiliert wird so Datei, die dann in eine andere Komponente eingesteckt wird. Deswegen, fPIC ist notwendig.

Es gibt dadurch keine funktionalen Auswirkungen, nur dass das Debuggen nahezu unmöglich ist.

Plattforminformationen: 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/LinuxRed Hat Enterprise Linux Server Version 6.5 (Santiago)

Auch auf folgendem reproduzierbar

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

  • Aktualisieren Sie auf die neueste gdb (bei Bedarf aus den Quellen erstellen). Wenn das Problem weiterhin besteht, melden Sie einen Fehler. Sie können auch versuchen, Unterstützung von RH zu erhalten, wenn Sie deren zahlender Kunde sind.

    – n. 1.8e9-wo-ist-meine-Aktie m.

    30. Oktober 2015 um 6:54 Uhr


  • Nach dem Fixieren der -lpthread sein -pthreadmein gcc 4.8.4/gdb 7.7.1 läuft dieses Programm ohne Probleme.

    – EÖF

    30. Oktober 2015 um 14:27 Uhr

  • @KartikAnand Nein, ist es nicht soll sein -lpthread. Siehe stackoverflow.com/a/1665110/50617

    – Angestellter Russe

    30. Oktober 2015 um 15:27 Uhr

  • @EOF: Interessant, versucht “Befestigung der -lpthread sein -pthread” war er der erste, den ich gemacht habe, als ich das auf meiner Plattform (gcc 4.7.2, gdb 7.4.1) getestet habe, und es hat funktioniert nicht Hilfe.

    – alk

    31. Oktober 2015 um 8:25 Uhr


  • OK, ich denke, die Wurzel des Problems ist folgende: z obj.x=1lautet der von gcc ausgegebene Assembler-Code .loc 1 14 0 \n .byte 0x66 \n leaq obj@tlsgd(%rip), %rdi \n .value 0x6666 \n rex64 \n call __tls_get_addr@PLT \n movl $1, (%rax). (Ein Großteil dieser Anweisungssequenz wird später ersetzt – durch den Lader? – bevor die ausführbare Datei erstellt wird.) Wenn Gas das sieht .locwird es Zwergleitungstabelleninformationen ausgeben, wenn es die nächste Anweisung sieht, dh wenn es sie sieht leaq obj@tlsgd(%rip), %rdi. Aber gcc hat offensichtlich beabsichtigt, dass Gas Zeilennummerninformationen ausgibt, sobald es die sieht .byte 0x66 Richtlinie.

    – Markus Plotnick

    3. November 2015 um 19:25 Uhr


Benutzer-Avatar
Ich werde nicht existieren Ich werde nicht existieren

Das Problem liegt tief in den Eingeweiden von GAS, dem GNU-Assembler, und wie er DWARF-Debug-Informationen generiert.

Der Compiler GCC hat die Aufgabe, eine bestimmte Befehlsfolge für einen positionsunabhängigen Thread-lokalen Zugriff zu generieren, die im Dokument dokumentiert ist ELF-Handhabung für Thread-lokale SpeicherungSeite 22, Abschnitt 4.1.6: x86-64 allgemeines dynamisches TLS-Modell. Diese Reihenfolge ist:

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

, und das ist so, weil die 16 Bytes, die es belegt, Platz für Backend-/Assembler-/Linker-Optimierungen lassen. Tatsächlich generiert Ihr Compiler den folgenden Assembler für 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
        ...

Der Assembler GAS entspannt dann diesen Code, der einen Funktionsaufruf (!) enthält, auf nur noch zwei Anweisungen. Diese sind:

  1. a mov mit einem fs:-Segmentüberschreibung und
  2. a lea

, in der Endmontage. Sie belegen untereinander insgesamt 16 Bytes, was zeigt, warum die Anweisungssequenz des allgemeinen dynamischen Modells so ausgelegt ist, dass sie 16 Bytes erfordert.

(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)

Bisher wurde alles richtig gemacht. Das Problem beginnt nun damit, dass GAS DWARF-Debug-Informationen für Ihren speziellen Assembler-Code generiert.

  1. Beim zeilenweisen Analysieren in binutils-x.y.z/gas/read.cFunktion void
    read_a_source_file (char *name)
    GAS-Begegnungen .loc 1 15 0die Anweisung, die die nächste Zeile beginnt und den Handler ausführt void dwarf2_directive_loc (int dummy ATTRIBUTE_UNUSED) in dwarf2dbg.c. Leider gibt der Handler nicht bedingungslos Debug-Informationen für den aktuellen Offset innerhalb des “Fragments” aus (frag_now) des Maschinencodes, den es gerade erstellt. Es hätte dies durch einen Anruf tun können dwarf2_emit_insn(0)aber die .loc Der Handler tut dies derzeit nur, wenn er mehrere sieht .loc Direktiven nacheinander. Stattdessen wird in unserem Fall mit der nächsten Zeile fortgefahren, wobei die Debug-Informationen nicht ausgegeben werden.

  2. In der nächsten Zeile sieht es die .byte 0x66 Direktive der Allgemeinen Dynamischen Sequenz. Dies ist an und für sich nicht Teil einer Anweisung, obwohl es die darstellt data16 Anweisungspräfix in der x86-Assembly. GAS handelt mit dem Hundeführer darauf ein cons_worker()und das Fragment wird von 12 Bytes auf 13 Bytes größer.

  3. In der nächsten Zeile sieht es eine wahre Anweisung, leaqdie durch Aufrufen des Makros analysiert wird assemble_one() das abbildet void md_assemble (char *line) in gas/config/tc-i386.c. Ganz am Ende dieser Funktion output_insn() gerufen wird, der sich schließlich selbst ruft dwarf2_emit_insn(0) und bewirkt, dass Debug-Informationen zuletzt ausgegeben werden. Eine neue Zeilennummeranweisung (LNS) wird begonnen, die behauptet, dass Zeile 15 bei Funktionsstartadresse plus vorheriger Fragmentgröße begonnen hat, aber seit wir die .byte Anweisung vor, ist das Fragment 1 Byte zu groß, und der berechnete Offset für die erste Anweisung von Zeile 15 ist daher 1 Byte daneben.

  4. Einige Zeit später entspannt GAS die globale dynamische Sequenz auf die letzte Anweisungssequenz, die mit beginnt mov fs:0x0, %rax. Die Codegröße und alle Offsets bleiben unverändert, da beide Befehlsfolgen 16 Bytes groß sind. Die Debug-Informationen sind unverändert und immer noch falsch.


GDB wird beim Lesen der Line Number Statements mitgeteilt, dass der Prolog von threadMain(), die der Zeile 14 zugeordnet ist, auf der sich ihre Signatur befindet, endet dort, wo Zeile 15 beginnt. GDB setzt an dieser Stelle pflichtbewusst einen Haltepunkt, aber leider ist er 1 Byte zu weit entfernt.

Wenn es ohne Haltepunkt ausgeführt wird, läuft das Programm normal und sieht

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

. Das korrekte Platzieren des Haltepunkts würde das Speichern und Ersetzen des ersten Bytes einer Anweisung durch beinhalten int3 (Opcode 0xcc), Verlassen

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

. Die normale Stepover-Sequenz würde dann das Wiederherstellen des ersten Bytes der Anweisung beinhalten, wobei der Programmzähler gesetzt wird eip zur Adresse dieses Haltepunkts, Einzelschritt, erneutes Einfügen des Haltepunkts, dann Fortsetzen des Programms.

Wenn GDB jedoch den Haltepunkt an der falschen Adresse 1 Byte zu weit setzt, sieht das Programm stattdessen

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

Das ist ein seltsamer, aber immer noch gültiger Haltepunkt. Deshalb haben Sie SIGILL (illegale Anweisung) nicht gesehen.

Wenn GDB nun versucht, zu wechseln, stellt es das Befehlsbyte wieder her, setzt den PC auf die Adresse des Haltepunkts und sieht jetzt Folgendes:

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

Da GDB die Ausführung ein Byte zu weit neu gestartet hat, dekodiert die CPU die fs: Anweisungspräfix-Byte und wird stattdessen ausgeführt mov (0x0),%rax mit dem Standardsegment, das ist ds: (Daten). Dies führt sofort zu einem Lesen von Adresse 0, dem Nullzeiger. Der SIGSEGV folgt prompt.

Alle fälligen Kredite an Mark Plotnick, der dies im Wesentlichen auf den Punkt gebracht hat.


Die Lösung, die beibehalten wurde, ist ein Binär-Patch cc1, gcc‘s eigentlicher C-Compiler auszugeben data16 Anstatt von .byte 0x66. Dies führt dazu, dass GAS die Kombination aus Präfix und Anweisung als eine einzelne Einheit parst und den korrekten Offset in den Debug-Informationen liefert.

  • Wissen wir, ob dies offiziell gepatcht wurde? Wenn ja, welche gcc-Version enthält den Fix? Wenn nicht, gibt es irgendwo einen öffentlichen Issue-Tracker, den wir im Auge behalten können?

    – Alex Jansen

    10. November 2021 um 10:23 Uhr

  • Nun, zumindest funktioniert die Lösung, die Sie im Fragenkommentar-Thread beschrieben haben. Falls jemand anderes darauf stößt und möchte, dass ein “Befehl” ausgeführt wird, hat Folgendes für mich in gcc-Version 4.8.5 20150623 (Red Hat 4.8.5-44) funktioniert: cc1_loc="$(gcc -print-prog-name=cc1)" && cp "$cc1_loc" cc1.original && yum install -y vim && xxd -p "$cc1_loc" | sed 's/2e6279746509307836360a/6461746131362020202020/g' | xxd -r -p - > cc1.modified && chmod 755 cc1.modified && chmod +x cc1.modified && cp -f cc1.modified "$cc1_loc" Fühlen Sie sich frei, das zu bereinigen und es einer formellen Antwort hinzuzufügen, wenn Sie möchten.

    – Alex Jansen

    10. November 2021 um 23:30 Uhr


  • @AlexJansen Eigentlich sieht es dort so aus ist eine Lösung, und es verlinkt sogar zurück zu Mark Plotnicks Kommentar. GCC-SVN r262006Git fd082a66f8be44616584164672eeb8e2779c5593. Fix landete in GCC 9.1.

    – Ich werde nicht existieren Ich werde nicht existieren

    11. November 2021 um 4:26 Uhr

1149840cookie-checkMit -fPIC kompiliertes Programm stürzt ab, während Thread-lokale Variable in GDB übersprungen wird

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy