Что означают адреса в трассировках стека DMD?
Я компилирую файл stacktrace.d:void main(){assert(false);}
с выключенным ASLR и при запуске я получаю:
core.exception.AssertError@stacktrace.d(2): Assertion failure
----------------
??:? _d_assertp [0x55586ed8]
??:? _Dmain [0x55586e20]
objdump -t stacktrace|grep _Dmain
дает
0000000000032e0c w F .text 0000000000000019 _Dmain
И если я бегу gdb -q -nx -ex start -ex 'disas /rs _Dmain' -ex q stacktrace
:
...
Dump of assembler code for function _Dmain:
0x0000555555586e0c <+0>: 55 push %rbp
0x0000555555586e0d <+1>: 48 8b ec mov %rsp,%rbp
=> 0x0000555555586e10 <+4>: be 02 00 00 00 mov $0x2,%esi
0x0000555555586e15 <+9>: 48 8d 3d 44 c0 02 00 lea 0x2c044(%rip),%rdi # 0x5555555b2e60 <_TMP0>
0x0000555555586e1c <+16>: e8 47 00 00 00 callq 0x555555586e68 <_d_assertp>
0x0000555555586e21 <+21>: 31 c0 xor %eax,%eax
0x0000555555586e23 <+23>: 5d pop %rbp
0x0000555555586e24 <+24>: c3 retq
Таким образом, даже если первые два байта 0x55 были просто обрезаны, 0x...86e20, заданное в трассировке стека, не соответствует началу инструкции.
1 ответ
Хорошо, я только что нашел часть исходного кода, которая подтверждает мои интуитивные ощущения из комментария.
Вот обвинение в том, когда он был добавлен: https://github.com/dlang/druntime/blame/bc940316b4cd7cf6a76e34b7396de2003867fbef/src/core/runtime.d#L756
Увы, сообщение коммита не суперинформативно, но сам код вместе с моей памятью меня очень убедил.
Так что это файл core/runtime.d
в druntime
библиотека. На момент написания статьи он находится на линии 756
enum CALL_INSTRUCTION_SIZE = 1; // it may not be 1 but it is good enough to get
// in CALL instruction address range for backtrace
callstack[numframes++] = *(stackPtr + 1) - CALL_INSTRUCTION_SIZE;
Обратите внимание, что callstack
переменная там делает копию текущих вызовов при возникновении исключения. При запросе на фактическую запись принтер трассировки будет смотреть на этот массив, чтобы определить, что писать. (Смотрите, ДЕЙСТВИТЕЛЬНО МЕДЛЕННО искать отладочную информацию, чтобы напечатать номера файлов / строк и имена функций, поэтому он делает это только тогда, когда это необходимо, чтобы сохранить нормальное использование исключений - когда оно генерируется и перехватывается позже - быстрее.)
Во всяком случае, я помню, когда использовалась обратная трассировка для печати неправильной строки. Это напечатало бы строку кода, содержащую следующую инструкцию - которая может быть довольно далеко в исходном тексте от фактического утверждения assert/throw, делая печать менее полезной. Если вы посмотрите на эту ссылку git blame, вы увидите старый код, используемый для буквального копирования адресов прямо из стека.
call
Инструкция работает, помещая адрес возврата в стек, а затем переходя к адресу подпрограммы. Адрес возврата находится сразу после инструкции вызова, поэтому, когда ЦП вернется туда, он больше не будет выполнять вызов. Вот почему старый код будет показывать неправильный номер строки, неправильно обвиняя в следующей инструкции.
Новый код немного перематывает этот адрес, чтобы вернуть его обратно к самой инструкции вызова - таким образом, помещая напечатанную функцию в строку, к которой она принадлежит. Но на x86 есть несколько разных инструкций вызова, и я даже не уверен, что можно правильно перемотать назад - вы можете определить действительный размер инструкции, только взглянув на код операции, и вы только знаете, где находится код операции если вы знаете размер инструкции или читаете код в прямом порядке, как это делает сам процессор. Более того, на других процессорных архитектурах размер будет другим.
Как говорится в комментарии в этой строке, мы не должны быть идеальными. Цель этого обратного следа - просто заставить пользователя смотреть в нужном месте. Отладочная информация использует своего рода ограничивающий прямоугольник - если вы находитесь по начальному адресу этой функции или строки источника или после него, но еще не по начальному адресу следующей функции / строки, она считает вас там. Он не знает и не заботится о дробных строках кода.
Таким образом, это значительно упрощает реализацию, просто предполагая, что размер равен 1 - достаточно хорош, чтобы вернуть его в эту границу.
Бьюсь об заклад, GDB делает что-то аналогично внутри, просто его принтер скрывает это, показывая адрес возврата из стека непосредственно в его обратной трассировке. (Кстати забавный совет: пройти --DRT-trapExceptions=no
к аргументам командной строки вашей программы при запуске его внутри GDB. Затем он будет удерживаться в точке выброса при работающей программе вместо того, чтобы напечатать сообщение и сказать, что программа завершилась с кодом 1!)
Код печати druntime может также +1 вернуться к нему перед печатью, чтобы скрыть этот внутренний взлом реализации... но ме. Обратный адрес также не там, где на самом деле был звонок, вам нужно посмотреть выше в вашем дизассемблере. И даже GDB на самом деле не показывает адрес вызова (по крайней мере, не моя старая версия этого, может быть, новые). Но было бы неплохо, если бы в разборке было значение для разборки, независимо от... Если вы хотите сделать пиар в пьяное время, я бы поддержал вас в этом (заметьте, у меня нет полномочий, но я могу помочь с комментариями).
Но это, по крайней мере, окончательно объясняет статус-кво.