В начало!  
Сделай закладку этой страницы в Digg Сделай закладку этой страницы в Del.icoi.us Сделай закладку этой страницы в Slashdot Сделай закладку этой страницы в Technorati



Отладка AMD64 на уровне машинного кода с помощью отладчика dbx
Автор Нассер Нури \\ Перевод: Максим Карташев   
13.11.2007 г.

Содержание

Отладка на уровне машинного кода в среде командной строки dbx становится весьма полезной в том случае, если найти ошибку в программе оказывается сложно. Обычно программы пишутся на высокоуровневых языках, таких как C, C++, Fortran и большинство недоработок можно найти в среде dbx на уровне исходного кода.

Однако, знание системы, в которой исполняется программа, на уровне машинного кода и использование правильного инструмента, такого как dbx, поможет уменьшить время, потраченное на выявление причины возникновения ошибки, и позволит выработать оптимальное решение проблемы.

Эта статья рассказывает о том, как эффективно использовать dbx на архитектуре AMD64, как отображать содержимое памяти по заданному адресу, выводить машинные инструкции и т.п. Для отображения содержимого машинных регистров может использоваться команда regs, а для печати отдельных регистров команда print. Команды nexti, stepi, stopi и tracei используются для отладки на уровне машинного кода.

Архитектура AMD64

Для начала проведем краткий обзор архитектуры AMD64 и посмотрим на ее отличия от 32-битной архитектуры x86. Здесь будут рассмотрены только те области, которые имеют отношение к теме статьи. Для всестороннего обзора архитектуре AMD64 см. Amd64 manuals (http://developer.amd.com) и AMD64 Application Binary Interface (ABI) (http://www.x86-64.org).

Архитектура AMD64 имеет 16 64-битных регистров общего назначения: RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP, R8, R9, R10, R11, R12, R13, R14, и R15. У AMD64 есть восемь дополнительных по отношению к x86 регистров.

Регистры RAX, RBX, RCX, RDX, RBP, RSI, RDI и RSP используются как 32-битными, так и 64-битными приложениями. Однако, в 32-битном режиме используются только младшие 32 бита указанных регистров. В архитектуре x86 это регистры EAX, EBX, ECX, EDX, EBP, ESI, EDI и ESP.

64-bit регистры AMD32-bit регистры x86
RAXEAX
RBX EBX
RCX ECX
RDX EDX
RSI ESI
RDI EDI
RBP EBP
RSP ESP
R8 -
R9-
R10-
R11-
R12-
R13-
R14-
R15-
RFLAGS EFLAGS
RIP EIP

Таблица 1: Регистры общего назначения

Архитектура AMD64 предоставляет шестнадцать 128-битных XMM регистров. Регистры с XMM0 по XMM7 используются для передачи параметров типа float и double, параметры же типа long double передаются через память. Тип long double в архитектуре AMD64 занимает 16 байт, в отличие от x86, где тот же тип имеет длину в 12 байт. Тип long double реализован в соответствии с 80-битным расширенным (IEEE) стандартом.

128-bit XMM-регистры
XMM0
XMM1
XMM2
XMM3
XMM4
XMM5
XMM6
XMM7
XMM8
XMM9
XMM10
XMM11
XMM12
XMM13
XMM14
XMM15

Таблица 2: XMM-регистры

Архитектура AMD64 также предоставляет восемь 80-битных регистров для хранения чисел с плавающей точкой.

64-битные MMX-регистры / 80-битные регистры для вычислений с плавающей точкой
MMX0 / FPR0
MMX1 / FPR1
MMX2 / FPR2
MMX3 / FPR3
MMX4 / FPR4
MMX5 / FPR5
MMX6 / FPR6
MMX7 / FPR7

Таблица 3: MMX регистры и регистры для вычислений с плавающей точкой.

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

Типы bool, char, short, int, long, long long и указатели считаются принадлежащими классу целых. Для передачи параметров из класса целых типов используется первый свободный из следующей цепочки регистров: RDI, RSI, RDX, RCX, R8 и R9.

Регистры RBP, RBX, а также с R12 по R15 принадлежат вызывающей функции и вызванная функция обязана сохранить их значения (callee-save).

Регистр RIP является счетчиком инструкций. В 64-битном режиме RIP расширяется до 64 бит для поддержки 64-битной адресации. В 32-битной архитектуре x86 его аналогом является регистр EIP.

Возвращаемые значения функции подразделяются на классы согласно правилам, описанным в AMD64 ABI. Например, если возвращаемое значение нужно передавать в памяти, то вызывающая функция предоставляет место для этого значения и передает адрес выделенной области в регистре RDI, как если бы это был первый параметр функции. По возвращении, регистр RAX содержит адрес, переданный вызывающей функцией в регистре RDI.

Подобно этому, если возвращаемое значение имеет целый тип, используется первый доступный регистр из пары RAX, RDX.

В дополнение к регистрам у каждой функции есть рамка на стеке. Стек растет вниз от старших адресов к младшим. На таблице 4 приведена организация стека.

РасположениеСодержаниеРамка стека
8n+16 (%rbp)
...

32 (%rbp)
24 (%rbp)
16 (%rbp)
аргумент #n
...

аргумент #2
аргумент #1
аргумент #0
Старший адрес



Предыдущая рамка
8 (%rbp) Адрес возвратаТекущая рамка
0 (%rbp)Предыдущее значение %rbpТекущая рамка
-8 (%rbp)
-16 (%rbp)
...

0 (%rsp)
Локальная переменная #1
Локальная переменная #2
...

Локальная переменная #n
Текущая рамка



Младший адрес
-128 (%rsp)red zone 

Таблица 4: Рамка стека с указателем базы rbp

 

Регистр RSP это указатель стека (stack pointer), а RBP - указатель рамки функции (frame pointer). Операции со стеком неявно используют RSP и, в некоторых случаях, RBP. Значение RSP уменьшается при размещении новых значений на стеке и увеличивается при снятии элемента со стека. RBP указывает на самый младший адрес структуры данных, переданной от одной функции к другой.

Область в 128 байт, находящаяся за той точкой, на которую указывает RSP известна как red zone и считается зарезервированной. Функции могут использоваться эту область для временных данных, которые не нужно сохранять при вызовах. В частности, хвостовые функции могут использовать эту область в качестве своей рамки стека, а не корректировать указатель стека в своем прологе (см. Пример 1) и эпилоге (см. Пример 2).

prologue:
pushq %rbp / сохранить указатель рамки стека
movq %rsp,%rbp / установить новый указатель рамки
subq $48,%rsp / выделить место на стеке
movq %rbx,-16(%rbp) / сохранить регистры, которые
movq %r12,-24(%rbp) / нельзя менять вызванной
movq %r13,-32(%rbp) / функции
movq %r14,-40(%rbp)
movq %r15,-48(%rbp)

Пример 1: Пролог функции

При использовании области red zone нет необходимости корректировать регистр RSP. Другими словами, инструкция subq $48,%rsp не трубется.

epilogue:
movl -4(%rbp), %eax / сохранить возвращаемое значение
movq -16(%rbp),%rbx / восстановить сохраненные регистры
movq -24(%rbp),%r12
movq -32(%rbp),%r13
movq -40(%rbp),%r14
movq -48(%rbp),%r15
leave
ret
Пример  2: Эпилог функции

 

У языка C++ есть собственный Application Binary Interface (ABI). ABI С++ задает правила для передачи параметров и возвращаемых значений. Правила C++ ABI дополняют правила AMD64 ABI; компилятор C++ должен пользоваться правилами C++ ABI для передачи параметров функциям в дополнение к правилам AMD64 ABI.

Команды dbx

Приведенные ниже команды для отладки на уровне машинного языка описаны в руководстве Debugging a Program With dbx (http://docs.sun.com/doc/819-3683).

examine [ address ] [ / [ count ] [format ] ]

Отобразить count элементов, начиная с address в виде format (вместо examine можно также использовать x - прим. переводчика).

stepi

Совершить шаг на одну машинную инструкцию (останавливаясь в вызываемых функциях)

nexti

Совершить шаг на одну машинную инструкцию (не останавливаясь в вызываемых функциях)

listi

Вывести смешанный листинг (исходный и машинный код)

tracei

Трассировка на уровне машинного кода

stopi

Установить точку останова на уровне машинного кода

dis

Дизассемблировать 10 инструкций, начиная со значения `+' (то есть с адреса, следующего за последним отображенным - прим. переводчика)

print expession, ...

Напечатать значения выражения (выражений) expression, ...

regs [-f] [-F]

Напечатать значения регистров
-f: включая регистры плавающей точки (обычная точность)
-F: включая регистры плавающей точки (двойная точность)


Описание задачи

Для описания отладки в терминах машинного кода рассмотрим реальное сообщение об ошибке в 64-битной версии dbx на платформе AMD64.

Dbx выводит шестнадцатеричные значения вместо букв на AMD64 после вызова strchr:

(dbx) print strchr("hello", 'l') =  0xfffffd7fffdff742 "xdfxff^?xfdxffxff^D"     ===>> это ошибка в dbx

А вот тестовая программа:

main() {
char *b = "hello";
printf("%sn", b);
printf("%sn", strchr("hello", 'l'));
}

Программа написана верно, ошибка в dbx.

Ошибка в dbx

Для начала проследим нормальное исполнение программы в среде dbx используюя пошаговую отладку тестового кода.

 

% dbx a.out
Reading a.out
Reading ld.so.1
Reading libc.so.1
(dbx) stop in main
(2) stop in main
(dbx) run
Running: a.out
(process id 16245)
stopped in main at line 3 in file "1.c"
3 char *b = "hello";
(dbx) next
stopped in main at line 4 in file "1.c"
4 printf("%sn", b);
(dbx) next
hello
stopped in main at line 5 in file "1.c"
5 printf("%sn", strchr("hello", 'l'));
(dbx) next
llo
stopped in main at line 6 in file "1.c"
6 }
(dbx) next
execution completed, exit code is 4

В пятой строке программы вызывается strchr с двумя параметрами. Функция strchr осуществляет поиск по своему первому параметру, hello, и возвращает указатель на первое вхождение литеры l. В результате этого printf выводит строку llo.

Теперь воспроизведем ошибку, вызвав функцию strchr непосредственно из dbx с помощью команды print. Для вызова strchr из dbx таже может быть использована команда call.

 

% dbx a.out
Reading a.out
Reading ld.so.1
Reading libc.so.1
(dbx) stop in main
(2) stop in main
(dbx) run
Running: a.out (process id 14772)
stopped in main at line 3 in file "1.c"<
3 char *b = "hello";
(dbx) next
stopped in main at line 4 in file "1.c"
4 printf("%sn", b);
(dbx) next hello
stopped in main at line 5 in file "1.c"
5 printf("%sn", strchr("hello", 'l'));
(dbx) print strchr("hello", 'l')
strchr("hello", 'l') = 0xfffffd7fffdff742 "xdfxff^?xfdxffxff^D"

Dbx выводит неправильное значение в том случае, если strchr вызывается командой print. Dbx должен был вывести строку llo вместо шестнадцатеричных символов, так как функция strchr должна вернуть указатель на первое вхождение литеры l в символьную строку hello.

Сессия отладки

Запустим отладчик с a.out и остановим исполнение непосрдственно перед вызовом printf.

Функция strchr определена в библиотеке libc и наверняка скомпилирована без опции -g. Таким образом, отладочной информации нет и нам придется полагаться исключительно на ассемблерный код.

Команда stopi используется для установки точки останова на первую машинную инструкцию функции strchr.

% dbx a.out
Reading a.out
Reading ld.so.1
Reading libc.so.1
(dbx) stop in main
(2) stop in main
(dbx) run
Running: a.out (process id 15045)
stopped in main at line 3 in file "1.c"
3 char *b = "hello";
(dbx) next
stopped in main at line 4 in file "1.c"
4 printf("%sn", b);
(dbx) next
hello
stopped in main at line 5 in file "1.c"
5 printf("%sn", strchr("hello", 'l'));
(dbx) stopi at strchr
(3) stopi at &strchr
(dbx) print strchr("hello", 'l')
stopped in strchr at 0xfffffd7fff307910
0xfffffd7fff307910: strchr : movb& (%rdi),%dl

Dbx останавливается на первой инструкции strchr после того, как эта функция была вызвана из dbx посредством команды print.

Мы можем воспользоваться командой dis для просмотра первой части машинного когда функции strchr.

(dbx) dis strchr
0xfffffd7fff307910: strchr : movb (%rdi),%dl
0xfffffd7fff307912: strchr+0x0002: cmpb %dh,%dl
0xfffffd7fff307915: strchr+0x0005: je strchr+0x3f [0xfffffd7fff30794f, .+0x3a ]
0xfffffd7fff307917: strchr+0x0007: testb %dl,%dl
0xfffffd7fff307919: strchr+0x0009: je strchr+0x33 [0xfffffd7fff307943, .+0x2a ]
0xfffffd7fff30791b: strchr+0x000b: movb 0x0000000000000001(%rdi),%dl
0xfffffd7fff30791e: strchr+0x000e: mpb %dh,%dl
0xfffffd7fff307921: strchr+0x0011: je strchr+0x3c [0xfffffd7fff30794c, .+0x2b ]
0xfffffd7fff307923: strchr+0x0013: testb %dl,%dl
0xfffffd7fff307925: strchr+0x0015: je strchr+0x33 [0xfffffd7fff307943, .+0x1e ]

Первая инструкция strchr это movb (%rdi),%dl. Она записывает содержимое участка памяти, на который указывает регистр rdi в младшие восемь бит самого регистра rdi. Первой инструкцией является не pushq %rbp, что означает отсутствие пролога у функции strchr. Отсутствие пролога не является ошибкой.

Отладчик остановлен на первой инструкции, которая является подходящим местом программы для проверки правильности передачи входящих параметров strchr. У strchr два параметра. Первый параметр это указатель на участков памяти, содержащий строку hello, а второй параметр это литера l.

Согласно AMD64 ABI, первый и второй параметры распределены в регистры %rdi and %rsi соответственно.

Есть два способа отобразить содержимое регистров %rdi и %rsi.

  • Для того чтобы вывести содержимое отдельных регистров можно воспользоваться командой print. Опция -flx заставит dbx отобразить содержимое %rdi и %rsi в расширенном шестнадцатиричном формате.


    (dbx) print -flx $rdi
    $rdi = 0xfffffd7fffdff740
    (dbx) print -flx $rsi
    $rsi = 0x6c

  • Можно использовать команду regs, что приведет к отображению значений всех регистров AMD64.


    (dbx) regs
    current frame: [1]
    r15 0x0000000000000000
    r14 0x0000000000000000
    r13 0x0000000000000000
    r12 0x0000000000000000
    r11 0xfffffd7fff307910
    r10 0x0000000000000000
    r9 0x0000000000010000
    r8 0xfefeff6e6b6b6467
    rdi 0xfffffd7fffdff740
    rsi 0x000000000000006c
    rbp 0xfffffd7fffdff810
    rbx 0xfffffd7fff3fb190
    rdx 0x0000000000000000
    rcx 0x000000003f570d87
    rax 0x0000000000000000
    trapno 0x0000000000000003
    err 0x0000000000000000
    rip 0xfffffd7fff307910:strchr movb (%rdi),%dl
    cs 0x000000000000004b
    eflags 0x0000000000000282
    rsp 0xfffffd7fffdff738
    ss 0x0000000000000043
    fs 0x00000000000001bb
    gs 0x0000000000000000
    es 0x0000000000000000
    ds 0x0000000000000000
    fsbase 0xfffffd7fff3a2000
    gsbase& 0xffffffff80000000

Регистр %rdi содержит указатель на участок памяти 0xfffffd7fffdff740, находящийся на стеке (это можно узнать с помощью команды whereis -a 0xfffffd7fffdff740 ? прим. переводчика). При обычной работе программы этот регистр указывает на участок памяти в сегменте данных. Однако, когда dbx требуется вызвать функцию (strchr), dbx копирует участок памяти из сегмента данных на стек и записывает адрес на стеке в %rdi.

Содержимое участка памяти по адресу 0xfffffd7fffdff740 может быть проверено командой examine. Этот участок памяти должен содержать строку hello.

(dbx) examine 0xfffffd7fffdff740 / 2
0xfffffd7fffdff740: 0x6c6c6568 0x0000006f

Просмотрев таблицу символов ASCII, можно убедиться в том, что участок памяти по адресу 0xfffffd7fffdff740 содержит строку hello. Шестнадцатиричное число 68 является кодом литеры h, 65 ? литеры e, 6c ? литеры l, а 6f ? литеры o.

Командой examine можно отобразить содержание участка памяти по адресу 0xfffffd7fffdff740 в виде символьной строки, не сверяясь при этом с таблицей ASCII.

 

(dbx) examine 0xfffffd7fffdff740 / 6c
0xfffffd7fffdff740: 'h' 'e' 'l' 'l' 'o' '�'

Регистр %rsi содержит шестнадцатиричное число 6c, являющееся кодом литеры l.

Двумя другими важными регистрами являются %rsp (указатель стека) и %rbp (указатель рамки функции).

Регистр %rsp указывает на вершину стека и содержит значение 0xfffffd7fffdff738. Как можно заметить, это значение очень близко к значению регистра %rdi, который указывает на участок памяти, содержащий строку hello.

Регистр %rbp является указателем рамки функции и содержит значение 0xfffffd7fffdff81. Этот регистр не используется функцией strchr.

Содержимое стека может быть отображено с помощью команды examine.

 

(dbx) examine 0xfffffd7fffdff738 / 32lx
0xfffffd7fffdff738: 0xfffffd7fff220004 0x0000006f6c6c6568
0xfffffd7fffdff748: 0x0000000000000000 0x0000000000000000
0xfffffd7fffdff758: 0x0000000000000000 0xfffffd7fffdff7b0
0xfffffd7fffdff768: 0xfffffd7fff3c7e50 0x0000000000010000
0xfffffd7fffdff778: 0x0000000000000000 0x0000000000410c50
0xfffffd7fffdff788: 0x0000000000000000 0xfffffd7fffdff848
0xfffffd7fffdff798: 0x0000000000410c50 0x0000000000410c58
0xfffffd7fffdff7a8: 0xfffffd7fff3fb190 0x0000000000000000
0xfffffd7fffdff7b8: 0x0000000000000000 0xfffffd7fffdff810
0xfffffd7fffdff7c8: 0x000000000040099d 0x0000000000000000
0xfffffd7fffdff7d8: 0x0000000000000000 0x0000000000000000
0xfffffd7fffdff7e8: 0x0000000000000000 0x0000000000000000
0xfffffd7fffdff7f8: 0xfffffd7fff3fb190 0x0000000000410c50
0xfffffd7fffdff808: 0xfffffd7fffdff838 0xfffffd7fffdff820
0xfffffd7fffdff818: 0x000000000040080c 0x0000000000000000
0xfffffd7fffdff828: 0x0000000000000000 0x0000000000000001

Фактически, мы можем раскрутить стек, следуя принципам, изученным в предыдущем разделе (см. Таблицу 4) о рамке стека. Например, щестнадцатиричное число 0x40080c является адресом инструкции, следующей за callq. Функция main вызывается из функции _start с помощью инструкции callq.

Шестнадцатиричное число 0x40080c это адрес возврата, который помещается на стек перед вызовом функции main. Инструкция, находящаяся по адресу 0x40080c, push %rax, будет исполнена по завершении main. Другими словами, адрес 0x40080c будет загружен в счетчик команд, регистр %rip, как только функция main завершится.

Для просмотра содержимого текстового сегмента исполняемого файла можно воспользоваться командой objdump.

objdump -S a.out

00000000004007a0 <_start>:

4007a0: 6a 00 pushq $0x0
4007a2: 6a 00 pushq $0x0
4007a4: 48 8b ec mov %rsp,%rbp
4007a7: 48 8b fa mov %rdx,%rdi
4007aa: 48 c7 c0 80 0a 41 00 mov $0x410a80,%rax
...

400806: 59 pop %rcx
400807: e8 54 01 00 00 callq 400960
40080c: 50 push %rax
40080d: 50 push %rax
...

Первой инструкцией функции main является push %rbp. Следовательно, предыдущий указатель рамки функции (0xfffffd7fffdff820) помещен на стек сразу после адреса возврата.

Подобным образом адрес возврата (0x40099d) помещается на стек при вызове функции strchr из командной строки.

0000000000400960 <main>:
400960: 55 push
400961: 48 8b mov %rsp,%rbp
400964: 48 83 ec 40 sub $0x40,%rsp
...

40099d: b8 6c 00 00 00 mov $0x6c,%eax
4009a2: 0f be f0 movsbl %al,%esi
4009a5: 48 c7 c7 68 0c 41 00 mov $0x410c68,%rdi
4009ac: b8 00 00 00 00 mov $0x0,%eax

Однако, у strchr пролог отсутствует, так что содержимое регистра %rbp остается тем же, что и в момент вызова strchr из функции main. Регистр %rbp содержит значение 0xfffffd7fffdff810; в свою очередь, память по этому адресу содержит предыдущий указатель рамки стека 0xfffffd7fffdff820.

(dbx) examine 0xfffffd7fffdff810
0xfffffd7fffdff810: 0xfffffd7fffdff820

Следуя далее, проходим машинные инструкции, используя команду nexti до тех пор, пока не достигнем инструкции, которая возвращает значение в регистре %rax. Мы можем воспользоваться командой dis для того, чтобы отобразить последнюю часть инструкций функции strchr.

(dbx) dis
0xfffffd7fff307941: strchr+0x0031: jne strchr [0xfffffd7fff307910, .-0x31 ]
0xfffffd7fff307943: strchr+0x0033: xorl %eax,%eax
0xfffffd7fff307945: strchr+0x0035: ret
0xfffffd7fff307946: strchr+0x0036: incq %rdi
0xfffffd7fff307949: strchr+0x0039: incq %rdi
0xfffffd7fff30794c: strchr+0x003c: incq %rdi
0xfffffd7fff30794f: strchr+0x003f: movq %rdi,%rax
0xfffffd7fff307952: strchr+0x0042: ret
0xfffffd7fff307953: strchr+0x0043: addb %al,(%rax)
(dbx) nexti
stopped in strchr at 0xfffffd7fff307949
0xfffffd7fff307949:
strchr+0x0039:
incq %rdi
(dbx) nexti
stopped in strchr at 0xfffffd7fff30794c
0xfffffd7fff30794c: strchr+0x003c: incq %rdi
(dbx) nexti
stopped in strchr at 0xfffffd7fff30794f
0xfffffd7fff30794f: strchr+0x003f: movq %rdi,%rax
(dbx) nexti
stopped in strchr at 0xfffffd7fff307952
0xfffffd7fff307952: strchr+0x0042: ret

Согласно описанию функции strchr, она должна возвратить указатель на первое вхождение литеры l в строку hello. Мы можем проверить правильность работы функции strchr, рассмотрев значение регистра %rax.

(dbx) examine $rax / 4c
0xfffffd7fffdff742: 'l' 'l' 'o' '�'

Действительно, значение регистра %rax указывает на участок памяти 0xfffffd7fffdff742, который размещен на стеке и содержит строку llo.

Мы убедились, что функция strchr работает верно и возвращает указатель на строку llo в регистре %rax. Таким образом, проблема должна быть в том, что делает dbx после того, как завершит вызов функции strchr.

После вызова пользовательской функции, dbx всегда вызывает fflush для сброса потока вывода. Функция fflush принимает один параметр, указатель на структуру FILE.

fflush - flush a stream
#include <stdio.h>
int fflush(FILE *stream);

Воспользуемся командой dis для отображения машинных инструкций функции fflush.

(dbx) dis fflush

0xfffffd7fff33dca0: fflush: pushq %rbp
0xfffffd7fff33dca1: fflush+0x0001: movq %rsp,%rbp
0xfffffd7fff33dca4: fflush+0x0004: movq %rbx,0xfffffffffffffff0(%rbp)
0xfffffd7fff33dca8: fflush+0x0008: movq %r12,0xfffffffffffffff8(%rbp)
0xfffffd7fff33dcac: fflush+0x000c: subq $0x0000000000000010,%rsp
0xfffffd7fff33dcb0: fflush+0x0010: testq %rdi,%rdi
0xfffffd7fff33dcb3: fflush+0x0013: movq %rdi,%rbx

Посмотрим на пролог функции fflush:

pushq %rbp

Предыдущий указатель рамки стека сохраняется на стек.

movq %rsp, %rbp

Значение регистра %rsp, или предыдущего указателя стека, сохраняется в регистре %rbp. Это значение является новым указателем рамки функции fflush.

movq %rbx,0xfffffffffffffff0(%rbp)
movq %r12,0xfffffffffffffff8(%rbp)

Регистры %rbx и %r12 являются callee-save регистрами. Функция fflush обязана сохранить значения этих регистром на стеке для вызывавшей функции таким образом, чтобы они могли быть восстановлены позже в эпилоге функции непосредственно перед выходом из нее.

sub $0x0000000000000010,%rsp

Корректируется указатель стека для функции fflush.

Команда stopi используется для остановка на первой инструкции функции fflush.

(dbx) stopi at fflush
(dbx) cont
dbx: Call to 'strchr' completed. Going back to previous command
interpreter
stopped in fflush at 0xfffffd7fff33dca0
0xfffffd7fff33dca0: fflush: pushq %rbp

После ввода команды cont, dbx остановится на первой инструкции функции fflush.

Отобразим регистры %rdi, %rbp и %rsp. Регистр %rdi содержит указатель на структуру FILE.

(dbx) print -flx $rdi
$rdi = 0xfffffd7fff37f0a0
(dbx) print -flx $rbp
$rbp = 0xfffffd7fffdff810
(dbx) print -flx $rsp
$rsp = 0xfffffd7fffdff748

Пройдем пролог функции в пошаговом режиме и напечатаем значения регистров %rsp и %rbp снова.

(dbx) stepi
stopped in fflush at 0xfffffd7fff33dca1
0xfffffd7fff33dca1: fflush+0x0001: movq %rsp,%rbp
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dca4
0xfffffd7fff33dca4: fflush+0x0004: movq %rbx,0xfffffffffffffff0(%rbp)
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dca8
0xfffffd7fff33dca8: fflush+0x0008: movq %r12,0xfffffffffffffff8(%rbp)
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dcac
0xfffffd7fff33dcac: fflush+0x000c: subq $0x0000000000000010,%rsp
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dcb0
0xfffffd7fff33dcb0: fflush+0x0010: testq %rdi,%rdi
(dbx) print -flx $rbp
$rbp = 0xfffffd7fffdff740
(dbx) print -flx $rsp
$rsp = 0xfffffd7fffdff730

Как было отмечено в предыдущем разделе, стек растет вниз от старших адресов. После внимательного изучения регистра %rsp и сравнения его значения (0xfffffd7fffdff730) с последним значением регистра %rsp (0xfffffd7fffdff738) в функции strchr становиться очевидно, что пространство, выделенное на стеке для функции fflush, перекрывается с пространством, выделенным функции strchr.

Значение 0xfffffd7fffdff738 находится непосредственно между значениями регистра %rbp (0xfffffd7fffdff740) и %rsp (0xfffffd7fffdff730) функции fflush. Следовательно, функция fflush перезаписывает содержимое рамки стека функции strchr, что объясняет причину печати чепухи вместо строки llo командой print strchr("hello", 'l').

Исправление отладчика dbx заключается в сохранении содержимого стека вызовов непосредственно перед вызовом функции fflush и восстановлениии сразу перед возвратом к команде print.

Заключение

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

Статья Assembly Language Techniques for the Solaris OS, x86 Platform Edition дает более полное описание ассемблера x86. См. http://developers.sun.com/solaris/articles/x86_assembly_lang.html

Оригинал статьи: http://developers.sun.com/prodtech/cc/articles/x64_dbx.html.

Перевод: Максим Карташев, 2006 г.

 

Добавить комментарий


Защитный код
Обновить