Nano Hash - криптовалюты, майнинг, программирование

Программа, скомпилированная с параметром -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
30.10.2015

  • Вы используете какую платформу для этого, пожалуйста? 30.10.2015
  • @alk Информация о платформе = Linux vm-kartika-vnc 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 30.10.2015
  • @alk Red Hat Enterprise Linux Server версии 6.5 (Сантьяго) 30.10.2015
  • Обновите gdb до последней версии (при необходимости соберите из исходников). Если проблема не устранена, сообщите об ошибке. Вы также можете попытаться получить поддержку от RH, если вы являетесь их платным клиентом. 30.10.2015
  • Такое же поведение в Debian Wheezy (Linux debian-stable 3.2.0-4-amd64 #1 SMP Debian 3.2.68-1+deb7u5 x86_64 GNU/Linux) с использованием gcc 4.7.2. 30.10.2015
  • После исправления -lpthread как -pthread мой gcc 4.8.4/gdb 7.7.1 запускает эту программу без каких-либо проблем. 30.10.2015
  • @EOF, но разве это не должно быть -lpthread? 30.10.2015
  • @KartikAnand Нет, это не должно быть -lpthread. См. stackoverflow.com/a/1665110/50617. 30.10.2015
  • @EmployedRussian Если это неправильно, программа даже не скомпилируется. Программа компилируется корректно и работает корректно. Я указываю библиотеку pthread так, как она используется в gcc, используя -lpthread. Я не вижу здесь ничего плохого. 30.10.2015
  • @KartikAnand Я не вижу ничего плохого - очень плохо: в вашей командной строке множество ошибок. В дополнение к разнице между -pthread и -lpthread, ваш -lpthread также находится в неправильном месте в командной строке. компиляция и правильная работа -- это случайность. Многие программы с ошибками похоже работают правильно, пока не перестают работать. 30.10.2015
  • @EOF: Интересно, попытка исправить -lpthread как -pthread была первой, которую я сделал, когда тестировал это на своей платформе (gcc 4.7.2, gdb 7.4.1), и это не помогите. 31.10.2015
  • @EmployedRussian и @EOF Я все еще получаю тот же SIGSEGV даже после компиляции с -pthread. И снова только -fPIC имеют значение 31.10.2015
  • Такое же поведение здесь с gdb 7.9.1 (с использованием -pthread), вероятно, нет смысла пробовать более новую версию. 31.10.2015
  • @KartikAnand: я не уверен, но вы пробовали gcc -g thread.c -o thread -fPIC -pthread ? (Я заметил в паре случаев, что у меня были небольшие ошибки в зависимости от порядка/размещения связанных библиотек) 02.11.2015
  • Похоже, gdb неправильно находит конец пролога threadMain. При компиляции в Ubuntu 15, gcc 4.9.2 (с -pthread, а не -lpthread) gdb 7.10 ставит точку останова на 0x4007e3, но инструкция (mov %fs:0x0,%rax) фактически начинается с 0x4007e2. 02.11.2015
  • Я немного разделил пополам, и похоже, что gdb сломался где-то между версиями 7.4.1 и 7.5. Если есть возможность, понизьте версию до 7.4.1. 02.11.2015
  • @ ks1322 У меня есть доступ к gdb 7.2, проблема все еще существует. 03.11.2015
  • @MarkPlotnick Есть ли обходной путь или любой другой отладчик с открытым исходным кодом, который может здесь помочь? 03.11.2015
  • у меня отлично работает с clang 3.7.0 / gdb 7.7.1, а при компиляции с gcc 4.9.2 или gcc 5.1.0 у меня возникает та же проблема. Может ли ошибка быть связана с компилятором? 03.11.2015
  • Я могу воспроизвести это с помощью gcc 4.8.1 / gdb 7.6.50, а компиляция с gcc -g thread.c -o thread -pthread -fPIC и gcc -g thread.c -o thread -lpthread -fPIC дает один и тот же SHA1 по сравнению с двоичным файлом, поэтому это не связано с -(l)pthread. 03.11.2015
  • @IwillnotexistIdonotexist да, я думал о том же. Проблема возникает только с -fPIC 03.11.2015
  • Таблица строк gdb (gdb.selected_frame().find_sal().symtab.linetable()) согласуется с выводом objdump -WL, но они все еще отличаются на один байт. Вывод gcc -g -S выглядит правильно, но последующая обработка инструкций TLS+fPIC остальной частью цепочки инструментов, похоже, является ошибкой. Так что использование clang, вероятно, пока является обходным путем. 03.11.2015
  • В моей Ubuntu, добавляя следующие фрагменты кода, я получаю предупреждение при компиляции #ifndef TLS #warning TLS is not enabled #endif 03.11.2015
  • @MarkPlotnick проблема в том, что будет очень сложно интегрировать clang в существующую архитектуру. Весь наш набор зависит от сборки gcc. И я не думаю, что они захотят перейти на clang только из-за одного компонента. 03.11.2015
  • В моем ограниченном исследовании я обнаружил, что проблема не в отладчике. Двоичный файл генерируется с неверной отладочной информацией, которая ошибочно идентифицирует начало строки 15 в 0x4007fd, на один байт дальше. В результате адреса для всех следующих строк также отстают на один байт. GDB просто помещает BP туда, где было обещано начало инструкции, и попадает в BP с переопределением fs: сегмента. Но когда происходит переход, поскольку перезапуск ПК установлен после переопределения сегмента, ЦП не декодирует его, обращается к неправильной памяти (ds:) и segfaults. 03.11.2015
  • @IwillnotexistIdonotexist Но тогда как работает временное решение, предоставленное Марком над вашим комментарием? Не будет ли там такой же проблемы? 03.11.2015
  • Я не утверждаю, что точно понимаю, почему информация о DWARF была сгенерирована именно так; Я только утверждаю, что точно понимаю, что происходит в отладчике, учитывая неверную информацию о DWARF, которую он предоставляет. Я лично подозреваю, что есть ошибка в определении длины пролога, когда первая инструкция после пролога имеет префикс fs: и компилируется -fPIC. Эта ошибка существует на уровне ассемблера или выше; Вывод отладочной информации ассемблера неверен, и компоновщик оставляет его нетронутым. 03.11.2015
  • Я отказался от своего обходного пути, заключающегося в размещении вызова ничего не делающей функции в начале каждой строки, которая ссылается на переменную TLS. Он работает с ошибкой таблицы строк в вашей примерной программе и не исправит ситуацию, но я не знаю, будет ли она работать с ошибкой во всех случаях в более сложных программах. 03.11.2015
  • Хорошо, я думаю, что корень проблемы вот в чем: для obj.x=1 ассемблерный код, выдаваемый gcc, равен .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). (Большая часть этой последовательности инструкций заменяется позже - загрузчиком? - до того, как будет создан исполняемый файл.) Когда газ увидит .loc, он выдаст информацию о таблице карликовых строк, когда увидит следующую инструкцию, т. Е. Когда он увидит leaq obj@tlsgd(%rip), %rdi. Но gcc, очевидно, намеревался, чтобы газ выдал информацию о номере строки, как только увидит директиву .byte 0x66. 03.11.2015
  • @MarkPlotnick Эта последовательность точно соответствует Обработка ELF для локального хранилища потока, см. стр. 22 4.1.6 Общая динамическая модель TLS x86-64. Таким образом, проблема заключается в GAS, когда он ослабляет General Dynamic до Local Exec или аналогичного (посмотрите тот же документ, страницы 51 и далее) и не может сгенерировать для него правильную отладочную информацию DWARF. 03.11.2015
  • @MarkPlotnick Я думаю, ты попал в точку. Я заменил .byte 0x66 \n на data16, собрал его, и он сгенерировал правильную информацию о строке отладки DWARF. 03.11.2015
  • И если вам очень срочно нужно это исправить, то, как бы банально это ни звучало, откройте /usr/lib64/gcc/.../cc1 в шестнадцатеричном редакторе, найдите 11-байтную бинарную строку .byte\t0x66\n (в моем компиляторе C она только одна) и замените ее на эквивалентную 11-байтовая строка data16 (обратите внимание на 5 одиночных пробелов в конце). 03.11.2015
  • @IwillnotexistIdonotexist У меня нет доступа, чтобы напрямую писать в этой области. Что я могу сделать, так это скопировать cc1 в свой домашний регион и попробовать исправить. Есть ли способ указать другой путь cc1 к gcc ? 04.11.2015
  • @KartikAnand Передайте параметр -B/path/to/cc1/directory в GCC. 04.11.2015
  • @IwillnotexistIdonotexist Это работает!. Но я получаю следующее предупреждение /tmp/ccP6tWZY.s: Сообщения ассемблера: /tmp/ccP6tWZY.s:30: Предупреждение: автономный префикс `data16' 04.11.2015
  • @KartikAnand, вы обязательно заменили последнюю новую строку в этой 11-байтовой строке пробелом? 04.11.2015
  • @IwillnotexistIdonotexist Да, я забыл. Сейчас работает без предупреждений. Так это уже существующая ошибка в GCC или нам нужно зарегистрировать новую? 04.11.2015
  • Ошибка заключается в GAS, поскольку эквивалентный ввод ассемблера выдает другую и неправильную отладочную информацию DWARF. Этот хак только изменяет GCC, чтобы предоставить альтернативный, эквивалентный ассемблер, для которого GAS создаст правильный DWARF. На вашем месте я бы сообщил об ошибке в GAS. 04.11.2015
  • Если ваш код скомпилирован с -fPIC, он подходит для включения в библиотеку - библиотека должна иметь возможность перемещаться из предпочтительного места в памяти на другой адрес, может быть другая уже загруженная библиотека по адресу, который предпочитает ваша библиотека. Проверьте, происходит ли это с -lpthread 04.11.2015
  • @IwillnotexistIdonotexist Можете ли вы опубликовать свои комментарии в качестве ответа, чтобы я мог его принять. Спасибо за помощь! 05.11.2015

Ответы:


1

Проблема лежит глубоко в недрах GAS, ассемблера GNU и того, как он генерирует отладочную информацию DWARF.

Компилятор, GCC, отвечает за создание определенной последовательности инструкций для независимого от позиции локального доступа к потоку, который задокументирован в документе Обработка ELF для локального хранилища потока, стр. 22, раздел 4.1.6: Общая динамическая модель TLS x86-64 . Эта последовательность:

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. 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 для вашего конкретного ассемблерного кода.

  1. При построчном анализе в 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. Вместо этого в нашем случае он переходит к следующей строке, оставляя отладочную информацию неотправленной.

  2. В следующей строке он видит директиву .byte 0x66 общей динамической последовательности. Это само по себе не является частью инструкции, несмотря на то, что в ассемблере x86 представлен префикс инструкции data16. GAS воздействует на него обработчиком 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, фактического компилятора C gcc, чтобы он выдавал data16 вместо .byte 0x66. Это приводит к тому, что GAS анализирует комбинацию префикса и инструкции как единое целое, давая правильное смещение в отладочной информации.

06.11.2015
Новые материалы

Кластеризация: более глубокий взгляд
Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

Как написать эффективное резюме
Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

Частный метод Python: улучшение инкапсуляции и безопасности
Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

Как я автоматизирую тестирование с помощью Jest
Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..


© 2024 nano-hash.ru, Nano Hash - криптовалюты, майнинг, программирование