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