Дамп ядра MIPS с ra и pc, равными 0000000

Я получаю периодические дампы ядра в одном из наших процессов. Все стеки потоков, кроме того, который рухнул, выглядят нормально и анализируются правильно.

Поток, в котором произошел сбой, имеет явно поврежденный стек вызовов. В стеке два фрейма, оба 0x00000000. Глядя на регистры, и PC, и RA равны 0 (что объясняет стек вызовов...). Регистр причины - 00800008.

  1. Есть ли способ получить больше информации о разбитом потоке?
  2. Почему сами регистры повреждены? (Или наоборот, в дампе ядра отладчик заполняет эти регистры на основе стека?)

Спасибо!


person Necnec    schedule 27.11.2012    source источник


Ответы (1)


Чтобы сначала ответить (2), потому что понимание того, что на самом деле произошло, важно для получения дополнительной информации об основной причине сбоя:

На самом деле сами регистры в машине во время выполнения равны 0; но дело не в том, что сами регистры были повреждены; скорее, память была повреждена, и эта поврежденная память затем была скопирована обратно в регистры, что в конечном итоге вызвало сбой.

Происходит что-то вроде этого: стек повреждается, в том числе (а) конкретно RA, пока он хранится в памяти стека, обнуляется. Затем, когда функция готова к возврату, она (б) восстанавливает регистр RA из стека, так что регистр RA теперь равен 0, а затем (в) переходит к возврату к RA , тем самым устанавливая ПК так же, чтобы он указывал на 0; следующая инструкция вызовет сбой, в то время как RA и PC равны 0.

Дело о хранении RA в стеке и последующем восстановлении из него объясняется, например, на http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm (выделено мной):

адрес возврата хранится в регистре $ra; если подпрограмма будет вызывать другие подпрограммы или является рекурсивной, адрес возврата должен быть скопирован из $ra в стек, чтобы сохранить его, поскольку jal всегда помещает адрес возврата в этот регистр и, следовательно, перезапишет предыдущее значение.

Вот пример программы, которая вылетает с PC и RA как 0, и которая хорошо иллюстрирует приведенную выше последовательность (точные числа могут быть изменены, в зависимости от системы):

#include <string.h>

int bar(void)
{
    char buf[10] = "ABCDEFGHI";
    memset(buf, 0, 50);
    return 0;
}

int foo(void)
{
    return bar();
}

int main(int argc, char *argv[])
{
    return foo();
}

И если мы посмотрим на дизассемблирование foo():

(gdb) disas foo
Dump of assembler code for function foo:
   0x00400408 <+0>:     addiu   sp,sp,-32
   0x0040040c <+4>:     sw      ra,28(sp)
   0x00400410 <+8>:     sw      s8,24(sp)
   0x00400414 <+12>:    move    s8,sp
   0x00400418 <+16>:    jal     0x4003a0 <bar>
   0x0040041c <+20>:    nop
   0x00400420 <+24>:    move    sp,s8
   0x00400424 <+28>:    lw      ra,28(sp)
   0x00400428 <+32>:    lw      s8,24(sp)
   0x0040042c <+36>:    addiu   sp,sp,32
   0x00400430 <+40>:    jr      ra
   0x00400434 <+44>:    nop
End of assembler dump.        

мы очень хорошо видим, что RA сохраняется в стеке в начале функции (<+4> sw ra,28(sp)), затем восстанавливается в конце (<+28> lw ra,28(sp)), а затем возвращается к (<+40> jr ra). Я показал foo(), потому что он короче, но точно такая же структура верна и для bar() -- за исключением того, что в bar() есть memset() посередине, который перезаписывает RA, пока он находится в стеке (это запись 50 байт в массив размером 10); а затем в регистр восстанавливается 0, что в конечном итоге приводит к сбою.

Итак, теперь мы понимаем, что первопричиной сбоя является какое-то повреждение стека, что возвращает нас к вопросу (1): есть ли способ получить больше информации об упавшем потоке?

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

  • Основная идея состоит в том, чтобы выяснить, что вызывает повреждение стека — скорее всего, это запись в какой-то локальный буфер, как в примере выше.
  • Постарайтесь как можно больше сосредоточиться на том, где в потоке происходит повреждение. Здесь может сильно помочь логирование: последний лог, который вы видите, явно произошел до сбоя (хотя и не обязательно до повреждения!) — добавьте больше логов в подозрительную область, чтобы сосредоточиться на месте сбоя. Конечно, если у вас есть доступ к отладчику, вы также можете пройтись по коду, чтобы выяснить, где происходит сбой.
  • После того, как вы найдете место сбоя, вам будет гораздо легче работать оттуда в обратном направлении: во-первых, перед сбоем ПК еще не был установлен на 0, и поэтому вы сможете увидеть обратную трассировку (хотя обратите внимание, что обратная трассировка сам "вычисляется" с использованием значений, хранящихся в стеке -- после того, как они повреждены, обратная трассировка не может быть вычислена за пределами повреждения. Но в данном случае это действительно полезно: это может сказать вам совершенно точно, где в памяти находится повреждение: точка, в которой обратная трассировка усекается, — это RA (в стеке), который был поврежден.)
  • Как только вы обнаружили, что повреждено, но все еще не знаете, что вызывает повреждение, используйте точки наблюдения: как только вы входите в функцию, которая помещает в стек RA, который в конечном итоге перезаписывается, установите для него точку наблюдения. Это должно привести к разрыву, как только произойдет повреждение...

Надеюсь это поможет!

person Dov Feldstern    schedule 05.02.2013