Соглашения о вызовах x86_64 и кадры стека

Я пытаюсь разобраться в исполняемом коде, который GCC (4.4.3) генерирует для машины x86_64, работающей под управлением Ubuntu Linux. В частности, я не понимаю, как код отслеживает кадры стека. В старые времена, в 32-битном коде, я привык видеть этот «пролог» почти в каждой функции:

push %ebp
movl %esp, %ebp

Затем, в конце функции, должен был быть «эпилог», либо

sub $xx, %esp   # Where xx is a number based on GCC's accounting.
pop %ebp
ret

или просто

leave
ret

который выполняет то же самое:

  • Установите указатель стека в верхнюю часть текущего кадра, чуть ниже адреса возврата.
  • Восстановите старое значение указателя кадра.

В 64-битном коде, как я вижу при дизассемблировании objdump, многие функции не следуют этому соглашению — они не передают %rbp, а затем сохраняют %rsp в %rbp. Как отладчик, такой как GDB, строит трассировку?

Моя реальная цель здесь состоит в том, чтобы попытаться выяснить разумный адрес, который следует рассматривать как вершину (самый высокий адрес) пользовательского стека, когда выполнение достигает начала произвольной функции дальше в программе, где, возможно, указатель стека переместился вниз. Для «верха», например, исходный адрес argv был бы идеальным, но у меня нет доступа к нему из произвольной функции, которую вызывает main. Сначала я подумал, что могу использовать старый метод обратной трассировки: отслеживание сохраненных значений указателя кадра до тех пор, пока сохраненное значение не станет равным 0, а затем следующее после этого может считаться наивысшим практическим значением. (Это не то же самое, что получить адрес argv, но сгодится, скажем, чтобы узнать значение указателя стека в _start или что-то еще, что вызывает _start [например, __libc_start_main].) Теперь я не знаю, как получить эквивалентный адрес в 64-битном коде.

Спасибо.


person Amittai Aviram    schedule 24.12.2011    source источник
comment
Хм действительно. И это не только с -fomit-frame-pointer.   -  person jørgensen    schedule 24.12.2011
comment
Вы пробовали -fno-omit-frame-pointer? Можете ли вы скомпилировать этот другой код с этим флагом?   -  person Firoze Lafeer    schedule 25.12.2011
comment
Исходный код libunwind может быть полезен.   -  person Nemo    schedule 25.12.2011
comment
Спасибо за все три комментария. Я думаю, что проблема здесь в том, что моя библиотека на самом деле является модифицированной версией GCC libgomp, поэтому я собираю ее, используя существующую систему сборки Gnu, и стараюсь по возможности избегать изменения значений по умолчанию. Я считаю, что GCC компилируется по умолчанию с -O2, который, я уверен, включает -fomit-frame-pointer. После публикации, но до того, как я увидел комментарий Фирозе, я просмотрел код glibc debug/backtrace.c, что привело меня к поиску __libc_stack_end, и именно так я нашел несколько разумное и общее решение.   -  person Amittai Aviram    schedule 25.12.2011
comment
sub $xx, %esp является частью пролога. Он резервирует место в стеке. Эпилог делает add $xx, %esp, чтобы вернуть указатель стека на то, что нужно извлечь. (Или в простых случаях leave включает mov %ebp, %esp, поэтому вы можете использовать его без предварительной настройки ESP. .)   -  person Peter Cordes    schedule 01.12.2016


Ответы (4)


Я думаю, что разница в том, что в amd64 просто больше поощряется отсутствие указателя кадра. В сноске на странице 16 abi говорится

Обычного использования %rbp в качестве указателя кадра для кадра стека можно избежать, используя %rsp (указатель стека) для индексации кадра стека. Этот метод сохраняет две инструкции в прологе и эпилоге и делает доступным один дополнительный регистр общего назначения (%rbp).

Я не знаю, что делает GDB. Я предполагаю, что при компиляции с -g объекты содержат волшебную отладочную информацию, которая позволяет GDB реконструировать то, что ему нужно. Я не думаю, что пробовал GDB на 64-битной машине без отладочной информации.

person Adrian Ratnapala    schedule 24.12.2011
comment
Мой опыт работы с x86-64 показал, что отладчик использует дополнительную информацию, чтобы узнать размер кадра стека, что сохраняет инструкции, но затрудняет отладку и раскручивание. - person Brian; 24.12.2011
comment
Ага, как я и подозревал. И все это не работает, когда исполняемый файл компилируется без отладочной информации? - person Adrian Ratnapala; 24.12.2011
comment
Спасибо. Эта рекомендация в ABI действительно объясняет, что происходит, но все еще заставляет меня задуматься, как я могу решить свою проблему. Мне нужно получить, грубо говоря, значение указателя стека, когда выполнение входит в main, из произвольной функции, которая идет после main в графе вызовов. Значение может быть выше, чем фактическое значение вершины основного кадра стека, если оно находится в пределах стека процесса, но чем ближе к вершине основного кадра стека, тем лучше. - person Amittai Aviram; 24.12.2011

GDB использует DWARF CFI для раскручивания. Для неразделенных двоичных файлов, скомпилированных с ключом -g, это будет в разделе .debug_info. Для удаленных двоичных файлов x86-64 в разделе .eh_frame есть информация о раскрутке. Это определено в x86-64 ABI, раздел 3.7, стр. 56. Обработать эту информацию самостоятельно довольно сложно, так как синтаксический анализ DWARF очень сложен, но я верю, что libunwind содержит его поддержку.

person Ted Mielczarek    schedule 24.08.2012
comment
Я почти уверен, что он всегда находится в разделе .eh_frame, поэтому почему он все еще там после удаления. Как вы это описываете, strip должен найти эту информацию в .debug_info и скопировать ее в .eh_frame, а раскрутка должна будет проверить оба местоположения... - person Peter Cordes; 01.12.2016
comment
.debug_info содержит дополнительную информацию о локальных переменных внутри фреймов стека, но .eh_frame всегда содержит достаточно информации, чтобы раскрутить стек. (т. е. размер каждого кадра стека и место сохранения регистров вызываемого абонента, но не то, какая переменная хранится где.) - person Peter Cordes; 01.12.2016

Если вам нужен адрес argv, почему бы просто не сохранить указатель на него в main?
Попытка раскрутить стек будет крайне непереносимой, даже если вы заставите ее работать.
Даже если вам удастся чтобы вернуться к стеку, не очевидно, что указатель фрейма первой функции будет NULL. Первая функция в стеке не возвращает значение, а вызывает системный вызов для выхода, поэтому ее указатель фрейма никогда не используется. Нет веской причины, по которой он будет инициализирован значением NULL.

person ugoren    schedule 24.12.2011
comment
Спасибо. Увы, нет, я не могу сохранить указатель в main. Я пишу пользовательскую библиотеку для компоновки произвольного кода, поэтому я не могу трогать исходный код (за исключением добавления #include) или предпочел бы не делать этого, если это вообще возможно. Что касается вашего второго пункта, у меня сложилось впечатление, что ядра, такие как ядро ​​​​Linux, действительно следуют соглашению об установке указателя кадра в NULL перед передачей управления пользовательскому процессу именно для этой цели. Но, возможно, это просто старое соглашение, которому следуют не все системы. - person Amittai Aviram; 24.12.2011

Предполагая, что я связываюсь с glibc (что я и делаю), похоже, что я могу решить эту проблему для практических целей с помощью глобального символа glibc __libc_stack_end:

extern void * __libc_stack_end;

void myfunction(void) {
  /* ... */
  off_t stack_hi = (off_t)__libc_stack_end;
  /* ... */
}
person Amittai Aviram    schedule 25.12.2011