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