Когда я раньше смотрел на сборку, я всегда смотрел на результаты GCC -S невооруженным глазом.Недостаток в том, что он очень неинтуитивен и не может видеть значение регистра в реальном времени, поэтому я изучил, как использовать GDB для отладки сборки. Конечно, более важной целью написания этой статьи является то, что я не вел блог уже полгода, а блог вот-вот зарастет травой. ^_^
Есть несколько вещей, которые мне нужны для отладки сборки:
- Возможность пошаговой отладки сборки.
- Изменения значений регистров можно увидеть в режиме реального времени.
- Можно увидеть взаимосвязь между исходным кодом и соответствующей сборкой.
Давайте поделимся реализацией трех вышеуказанных требований с GDB:
Приступайте к отладке сборки
Используйте си и ни. Разница между s и n заключается в следующем: s и n — это одноэтапная отладка на уровне языка C, а si и ni — одноэтапная отладка на уровне сборки.
Изменения значений регистров можно увидеть в режиме реального времени.
Добавьте параметр -tui при использовании gdb, откройте gdb и запуститеlayout regs
Заказ. Учтите, что лучше всего добавить -туй, иначе очень вероятно появление феномена Хуапина.
Может видеть взаимосвязь между исходным кодом и соответствующей сборкой
запустить в gdbset disassemble-next-line on
, указывающий код, который будет выполняться после автоматической дизассемблирования.
можно ясно видетьint c=sum(x,y);
Это соответствует инструкции по сборке в красной рамке ниже.
Если вы не хотите использовать такой примитивный метод, вы можете установить плагины для GDB или использовать emacs для достижения вышеуказанной цели.Рекомендуются две статьи:
- GDB превращается из полосатого в хорошо одетого
- Полное решение служебных плагинов GDB (peda, gef, gdbinit)
В заключение небольшой пример:
int sum(int x,int y){ return x+y; } int main(){ int x=10; int y=20; int c=sum(x,y); return 0; }
gcc версии 4.4.7, параметры оптимизации по умолчанию.
Пройдемся по сборке, соответствующей этому коду:
установить точку останова
Обратите внимание, что если вы хотите установить точку останова в начале функции уровня сборки, вы должны использоватьb *fun
вместоb func
, здесь мы устанавливаем точку останова наb *main
выделить кадр стека
0x0000000000400489 <main+0>: 55 push %rbp
0x000000000040048a <main+1>: 48 89 e5 mov %rsp,%rbp
0x000000000040048d <main+4>: 48 83 ec 10 sub $0x10,%rsp
%rbp и %rsp представляют нижнюю и верхнюю часть текущего фрейма стека. Где %rbp — это регистр, который должен сохранить вызываемый.sub $0x10,%rsp
Указывает на выделение пространства кадра стека для основной функции.
Обратите внимание, что здесь выделено 16 байт стекового пространства, а 4 байта использоваться не будут, лично я предполагаю, что оно генерируется сборкой gcc.cfi_def_cfa_offset 16
Связано, это не вникал.
int x=10
0x0000000000400491 <main+8>: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp)
положить значение x в стек
int y=20
0x0000000000400498 <main+15>: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
положить значение y в стек
Вызов функции суммы
0x000000000040049f <main+22>: 8b 55 f8 mov -0x8(%rbp),%edx
0x00000000004004a2 <main+25>: 8b 45 f4 mov -0xc(%rbp),%eax
0x00000000004004a5 <main+28>: 89 d6 mov %edx,%esi
0x00000000004004a7 <main+30>: 89 c7 mov %eax,%edi
0x00000000004004a9 <main+32>: e8 c6 ff ff ff callq 0x400474 <sum>
Присвойте x и y значениям %esi и %edi соответственно, где %edi и %esi указываются для передачи первого и второго параметров функции. (один вопрос, почему не напрямуюmov -0x8(%rbp),%esi
Шерстяная ткань? )
callq помещает адрес следующей инструкции в стек и переходит к первой инструкции функции суммы.
Введите функцию суммы
0x0000000000400474 <sum+0>: 55 push %rbp
0x0000000000400475 <sum+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400478 <sum+4>: 89 7d fc mov %edi,-0x4(%rbp)
0x000000000040047b <sum+7>: 89 75 f8 mov %esi,-0x8(%rbp)
Как и в случае с основной функцией, сначала сохраните %rbp, а затем возьмите параметры функции из %edi и %esi.
сумма
0x000000000040047e <sum+10>: 8b 45 f8 mov -0x8(%rbp),%eax
0x0000000000400481 <sum+13>: 8b 55 fc mov -0x4(%rbp),%edx
0x0000000000400484 <sum+16>: 8d 04 02 lea (%rdx,%rax,1),%eax
Добавьте x и y. Здесь используется инструкция lea. Ознакомление с инструкцией lea см.LEA instruction? , здесь повторяться не буду.
Поместите возвращаемое значение в %eax, а регистр %rax задает возвращаемое значение функции. Как и в языке GO, если функция может иметь несколько возвращаемых значений, возвращаемое значение помещается в стек.
Конец функции суммы
0x0000000000400487 <sum+19>: c9 leaveq
0x0000000000400488 <sum+20>: c3 retq
Давайте сначала посмотрим на текущий стек:
(я не знаю, почему здесь нет саба
xx, $rsp, я думаю gcc находит этот последний вызов функции, после этого роста стека не будет, а будет только откат стека, поэтому результаты использования %rsp и %rbp одинаковы. Простая проверка, должно быть так).
По окончании функции сначала необходимо восстановить кадр стека текущей функции, восстановить сохраненные регистры и восстановить значение %rip, то есть адрес возврата.
Инструкция leaveq эквивалентна:
mov %rbp,%rsp
pop %rbp
Роль состоит в освобождении (освобождении) кадра стека текущей функции и восстановлении значения сохраненного регистра. Из этого мы также можем видеть роль %rbp: помните, куда %rsp должен отступать, иначе %rsp не будет знать, куда отступать, когда функция завершится.
Инструкция req эквивалентна:
pop %rip
Восстановите адрес следующей инструкции callq, сохраненный выше, в %rip.
Получить возвращаемое значение функции
0x00000000004004ae <main+37>: 89 45 fc mov %eax,-0x4(%rbp)
Поместите значение %eax во фрейм стека основной функции.
return 0
0x00000000004004b1 <main+40>: b8 00 00 00 00 mov $0x0,%eax
То же, что и функция суммы выше.
конец основной функции
0x00000000004004b6 <main+45>: c9 leaveq
0x00000000004004b7 <main+46>: c3 retq
Если приведенные выше %rsp и %rbp указывают на одну и ту же область памяти, это не кажется интуитивно понятным, посмотрите на пространство стека, когда основная функция вот-вот завершится:
Так же, как объяснение функции суммы выше, оно не будет повторяться.
Программа успешно запускается и завершает работу.