Чтобы сначала ответить (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