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/Linux
Red 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
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:
- a
mov
mit einem fs:
-Segmentüberschreibung und
- 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.
-
Beim zeilenweisen Analysieren in binutils-x.y.z/gas/read.c
Funktion void
read_a_source_file (char *name)
GAS-Begegnungen .loc 1 15 0
die 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.
-
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.
-
In der nächsten Zeile sieht es eine wahre Anweisung, leaq
die 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.
-
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.
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-pthread
meingcc 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=1
lautet 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.loc
wird es Zwergleitungstabelleninformationen ausgeben, wenn es die nächste Anweisung sieht, dh wenn es sie siehtleaq 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