Программа, скомпилированная с помощью -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, ослабляет этот код, который содержит вызов функции (!), До двух инструкций. Это:

  1. mov имея fs:переопределение сегмента и
  2. 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) в dwarf2dbg.c, К сожалению, обработчик не безоговорочно выдает отладочную информацию для текущего смещения внутри "фрагмента" (frag_now) машинного кода, который он в настоящее время строит. Это можно было сделать, позвонив dwarf2_emit_insn(0), но .loc обработчик в настоящее время делает так, только если он видит несколько .loc Директивы последовательно. Вместо этого в нашем случае он переходит к следующей строке, оставляя отладочную информацию невыполненной.

  2. На следующей строке он видит .byte 0x66 директива общей динамической последовательности. Это само по себе не является частью инструкции, несмотря на то, что представляет data16 префикс инструкции в сборке x86. ГАЗ действует на него с обработчиком cons_worker()размер фрагмента увеличивается с 12 байтов до 13.

  3. На следующей строке он видит истинную инструкцию, 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 байт выключено.

  4. Некоторое время спустя 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 анализирует комбинацию префикса и команды как единое целое, что дает правильное смещение в отладочной информации.

Другие вопросы по тегам