2016-01-22 2 views
4

Я унаследовал некоторый умный x64 машинный код для GNU/Linux, который создает оболочку машинного кода вокруг вызова функции c. Я предполагаю, что в более высоких языковых терминах код можно назвать декоратором или закрытием. Код функционирует хорошо, но с неудачным артефактом, который когда вызывается оболочка, он поглощает трассировку стека в gdb.Как использовать gdb stacktrace с заданным временем машинного кода?

Из того, что я узнал из сети, gdb использует https://en.wikipedia.org/wiki/DWARF в качестве руководства для разделения кадров стека в стеке. Это хорошо работает для статического кода, но, очевидно, код, сгенерированный и вызываемый во время выполнения, не зарегистрирован в структуре DWARF.

Вопрос в том, есть ли способ спасти трассировку стека в этой ситуации?

Вот такой же c-код, который показывает проблему.

typedef int (*ftype)(int x); 
int wuz(int x) { return x + 7; } 
int wbar(int x) { return wuz(x)+5; } 
int main(int argc, char **argv) 
{ 
    const unsigned char wbarcode[] = { 
    0x55 ,       // push %rbp 
    0x48,0x89,0xe5 ,     // mov %rsp,%rbp 
    0x48,0x83,0xec,0x08 ,    // sub $0x8,%rsp 
    0x89,0x7d,0xfc ,     // mov %edi,-0x4(%rbp) 
    0x8b,0x45,0xfc ,     // mov -0x4(%rbp),%eax 
    0x89,0xc7 ,      // mov %eax,%edi 
    0x48,0xc7,0xc0,0xf6,0x04,0x40,00, // mov $0x4004f6,%rax 
    0xff,0xd0,      // callq *%rax 
    0x83,0xc0,0x05 ,     // add $0x5,%eax 
    0xc9 ,       // leaveq 
    0xc3        // retq 
    }; 

    int wb = wbar(5); 
    ftype wf = (ftype)wbarcode; 
    int fwb = wf(5); 
} 

Собирать по:

gcc -g -o mcode mcode.c 
execstack -s mcode 

и запустить его в БГД по:

$ gdb mcode 
(gdb) break wuz 

Если разбирать wbar мы получаем что-то очень похожее на последовательности байтов в wbarcode[]. Единственное различие заключается в том, что я изменил соглашение о вызове для вызова wuz().

(gdb) disas/r wbar 
Dump of assembler code for function wbar: 
    0x0000000000400505 <+0>: 55  push %rbp 
    0x0000000000400506 <+1>: 48 89 e5  mov %rsp,%rbp 
    0x0000000000400509 <+4>: 48 83 ec 08  sub $0x8,%rsp 
    0x000000000040050d <+8>: 89 7d fc  mov %edi,-0x4(%rbp) 
    0x0000000000400510 <+11>:  8b 45 fc  mov -0x4(%rbp),%eax 
    0x0000000000400513 <+14>:  89 c7 mov %eax,%edi 
    0x0000000000400515 <+16>:  e8 dc ff ff ff callq 0x4004f6 <wuz> 
    0x000000000040051a <+21>:  83 c0 05  add $0x5,%eax 
    0x000000000040051d <+24>:  c9  leaveq 
    0x000000000040051e <+25>:  c3  retq 
End of assembler dump. 

Если мы сейчас запустим программу, она будет остановлена ​​дважды в wuz(). В первый раз через наш c-вызов, и мы можем запросить трассировку стека через bt.

Breakpoint 3, wuz (x=5) at mcode.c:2 
=> 0x00000000004004fd <wuz+7>: 8b 45 fc mov -0x4(%rbp),%eax 
    0x0000000000400500 <wuz+10>: 83 c0 07 add $0x7,%eax 
    0x0000000000400503 <wuz+13>: 5d pop %rbp 
    0x0000000000400504 <wuz+14>: c3 retq 
(gdb) bt 
#0 wuz (x=5) at mcode.c:2 
#1 0x000000000040051a in wbar (x=5) at mcode.c:3 
#2 0x00000000004005b0 in main (argc=1, argv=0x7fffffffe528) at mcode.c:20 

Это нормальный трассировки стека показывает, что мы получили от main()wbar()wuz().

Но если мы теперь продолжаем мы достигаем wuz() во второй раз, и мы снова запрос трассировки стека:

(gdb) c 
Continuing. 

Breakpoint 3, wuz (x=5) at mcode.c:2 
=> 0x00000000004004fd <wuz+7>: 8b 45 fc mov -0x4(%rbp),%eax 
    0x0000000000400500 <wuz+10>: 83 c0 07 add $0x7,%eax 
    0x0000000000400503 <wuz+13>: 5d pop %rbp 
    0x0000000000400504 <wuz+14>: c3 retq 
(gdb) bt 
#0 wuz (x=5) at mcode.c:2 
#1 0x00007fffffffe419 in ??() 
#2 0x0000000500000001 in ??() 
#3 0x00007fffffffe440 in ??() 
#4 0x00000000004005c6 in main (argc=0, argv=0xffffffff) at mcode.c:22 
Backtrace stopped: frame did not save the PC 

Несмотря на то, что мы сделали те же две иерархические вызовы, мы получаем трассировки стека, что содержит неправильные фреймы. В моем первоначальном унаследованном коде обертки ситуация была еще хуже, так как трассировка стеки закончилась после того, как 5 кадров с верхним уровнем, имеющими адрес 0.

Таким образом, вопрос опять же, есть ли дополнительный код, который может быть добавлен до wbarcode[], что приведет к тому, что gdb выдаст действительную стеклу? Или есть ли какая-либо другая техника времени выполнения, которая может быть использована для создания gdb распознавания фреймов стека?

+0

Хм, динамически генерируемая функция 'wbarcode' делает традиционный стек кадров (нажатие'% rbp'). Я думаю, это не поможет, если вызывающий был скомпилирован с помощью '-fomit-frame-pointer' по умолчанию, хотя, поскольку вызывающий' wbarcode', вероятно, не сохранит его '% rsp'-on-entry в' % rbp'. ** Что произойдет, если вы скомпилируете все это с помощью '-fno-omit-frame-pointer' **? IDK, если все еще будет раздел '.eh_frame_hdr', который gdb все равно захочет использовать ... –

+0

Не уверен, что это умный код, так как он использует жесткий кодированный адрес в кодированном ассемблере. –

+0

@MichaelPetch: он сказал, что это всего лишь MCVE, а не то, что на самом деле это похоже на его фактическую динамически генерируемую функцию. –

ответ

2

На некоторых архитектурах вы можете просто сделать фрейм с макетом, который, как ожидается, будет развязан по умолчанию для gdb. Однако это не доступно для всех архитектур. Читая порт x86-64 (см. gdb/amd64-tdep.c, в частности, функция amd64_frame_cache_1), я думаю, что здесь gdb хочет знать границы функций, поэтому он может попытаться проанализировать пролог. Но границы функций исходят из таблицы символов ELF, поэтому вам там не повезло.

Однако есть еще способ.Из-за недавнего (в условиях gdb) повышения JIT-компиляторов gdb предоставляет три других способа решения этой проблемы.

Один из способов заключается в том, что ваша программа может выделять специальный объект ELF (действительно любой формат объекта, который gdb понимает, IIRC) в памяти, и вызывать крюк времени выполнения для информирования gdb о его существовании. gdb прочитает этот объект, включая любую содержащуюся в нем отладочную информацию. Этот подход довольно тяжелый, но дает доступ к большинству возможностей gdb - вы можете указать не только разматывание, но и типы, локальные переменные и т. Д.

Второй способ несколько похож. Ваша программа по-прежнему вызывает специальный крючок. Однако вы также предоставляете плагин, загружаемый gdb. Этот плагин может читать символы и другую информацию из нижнего, но в этом случае символы и информация для размотки не должны быть в каком-либо конкретном формате.

Последний способ (новый в gdb 7.10) состоит в том, что вы можете написать разматыватель в Python. При работе с my JIT unwinder я выбрал этот подход, потому что он легко отлаживается, прост в развертывании, достаточно гибкий и не требует каких-либо конкретных изменений в нижнем.

Эти методы - все documented in the gdb manual. Однако в некоторых случаях документация оставляет желать лучшего. Возможно, вам придется найти какой-то примерный код или выкопать в источники gdb, чтобы действительно понять, как он должен работать.

Смежные вопросы