Программа, скомпилированная с -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: общая динамическая модель TLS x86-64. Эта последовательность:

0x00 .byte 0x66
0x01 leaq  [email protected](%rip),%rdi
0x08 .word 0x6666
0x0a rex64
0x0b call [email protected]

и так оно и есть, потому что 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    [email protected](%rip), %rdi
        .value  0x6666
        rex64
        call    [email protected]
        movl    $1, (%rax)
        .loc 1 16 0
        ...

Ассемблер, GAS, затем расслабляет этот код, который содержит вызов функции (!), вплоть до двух инструкций. Это:

  • a mov с fs: -сегментным переопределением и
  • a 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 последовательно. Вместо этого в нашем случае он переходит к следующей строке, в результате чего отладочная информация не работает.

  • В следующей строке он видит директиву .byte 0x66 общей динамической последовательности. Это не само по себе часть инструкции, несмотря на представление префикса инструкции data16 в сборке x86. 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 начиналась с функции-start-address плюс размер предыдущего фрагмента, но, поскольку мы передали оператор .byte, прежде чем это сделать, фрагмент имеет размер 1 байт слишком большой и вычисленное смещение для первой команды строки 15, следовательно, 1 байт выключено.

  • Спустя некоторое время GAS расслабляет глобальную динамическую последовательность до конечной последовательности команд, которая начинается с mov fs:0x0, %rax. Размер кода и все смещения остаются неизменными, так как обе последовательности команд составляют 16 байт. Отладочная информация не изменяется и все еще ошибочна.


GDB, когда он читает строки с номерами, сообщает, что пролог threadMain(), который связан с линией 14, на которой найден его подпись, заканчивается там, где начинается строка 15. GDB покорно закладывает точку останова в этом месте, но, к сожалению, он слишком длинный.

При запуске без точки останова программа работает нормально и видит

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 перезапустил выполнение за один байт слишком далеко, CPU не декодирует байтовый префикс инструкции fs:, а вместо этого выполняет mov (0x0),%rax с сегментом по умолчанию, который является ds: (data). Это немедленно приводит к чтению с адреса 0, нулевого указателя. SIGSEGV быстро следует.

Все кредиты Mark Plotnick за то, что они по сути прибивают.


Решение, которое было сохранено, это бинарный-patch cc1, gcc фактический компилятор C, чтобы испустить data16 вместо .byte 0x66. Это приводит к тому, что GAS анализирует префикс и комбинацию команд как единое целое, что дает правильное смещение в отладочной информации.