Программа, скомпилированная с помощью -fPIC, аварийно завершает работу при переходе через локальную переменную потока в GDB
Это очень странная проблема, которая возникает только тогда, когда программа скомпилирована с -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
, 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
1 ответ
Проблема кроется в недрах 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
, и это так, потому что 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
имея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)
, ГАЗ встречает.loc 1 15 0
, оператор, который начинает следующую строку и запускает обработчикvoid dwarf2_directive_loc (int dummy ATTRIBUTE_UNUSED)
вdwarf2dbg.c
, К сожалению, обработчик не безоговорочно выдает отладочную информацию для текущего смещения внутри "фрагмента" (frag_now
) машинного кода, который он в настоящее время строит. Это можно было сделать, позвонивdwarf2_emit_insn(0)
, но.loc
обработчик в настоящее время делает так, только если он видит несколько.loc
Директивы последовательно. Вместо этого в нашем случае он переходит к следующей строке, оставляя отладочную информацию невыполненной.На следующей строке он видит
.byte 0x66
директива общей динамической последовательности. Это само по себе не является частью инструкции, несмотря на то, что представляетdata16
префикс инструкции в сборке x86. ГАЗ действует на него с обработчиком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 пытается перешагнуть, он восстанавливает байт инструкции, устанавливает для ПК адрес точки останова, и теперь он видит это:
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 перезапустил выполнение на один байт слишком далеко, процессор не декодирует fs:
префикс инструкции байта, и вместо этого выполняется mov (0x0),%rax
с сегментом по умолчанию, который ds:
(данные). Это немедленно приводит к чтению с адреса 0, нулевого указателя. SIGSEGV быстро следует.
Все благодарности Марку Плотнику за то, что он это сделал.
Решение, которое было сохранено, заключается в бинарном исправлении cc1
, gcc
фактический компилятор C, чтобы излучать data16
вместо .byte 0x66
, Это приводит к тому, что GAS анализирует комбинацию префикса и команды как единое целое, что дает правильное смещение в отладочной информации.