Любой программист хоть раз заглядывавший в отладчик знаком с понятием точки останова (aka бряк, breakpoint). Казалось бы нет ничего проще, чем поставить точку останова пара кликов мышкой в графическом интерфейсе или команда в консоли отладчика, но не всегда жизнь системного программиста столь проста и иногда возникает необходимость выставлять точки останова автоматически — изнутри самой программы.
[про этот вид точек останова оказывается достаточно подробно писали два года назад, поэтому я кратко привел лишь общие соображения]
Предположим мы пишем JIT компилятор и хотим выставлять точки останова внутри генерируемого им кода. Оказывается для этого достаточно вставить в поток кода всего лишь одну инструкцию
Кстати сами отладчики тоже пользуются этой инструкцией: просто подменяют ей инструкции в памяти, когда мы просим их поставить бряк. Именно поэтому
Как мы видим ничего сложного в ручной расстановке instruction breakpoints нет. Можно даже реализовывать их интерактивное включение-выключение: достаточно запоминать их позиции и заменять ненужные на пустую инструкцию
Гораздо интереснее дело обстоит с другим типом точек останова — точками останова на доступ к памяти.
Допустим мы отлаживаем повреждение памяти в среде исполнения с копирующим сборщиком мусора, который постоянно компактифицирует кучу и всякими другими способами тасует объекты в памяти. Мы смогли выяснить приблизительно какое поле в каком объекте повреждается, но просто загрузить программу в отладчик и поставить им точку останова на доступ к этому полю не получается, потому что GC постоянно путается под ногами своими перемещениями. Поэтому у нас возникает разумное желание, чтобы сам добряк Сборщик Мусора и выставлял/обновлял этот бряк.
Иными словами мы хотим предоставить ему в распоряжение функцию
Здесь нам на помощь приходят добры молодцы отладочные регистры
Пользоваться ими достаточно просто: в один из регистров
Вооружившись этими функциями можно не зная брода бросится в воду и попробовать реализовать
Однако эта попытка обречена на провал: доступ к отладочным регистрам возможен только из нулевого кольца защиты, т.е. из ядра. Однако, регистры эти плюсплюс-полезны (любой современный отладчик ими пользуется), поэтому ОС обычно предоставляют API для доступ к этим регистрам. Например, на Mac OS X читать и писать эти регистры можно через функции
Вот и все! Теперь добрый дядька Дворник-Сборщик Мусора может сам управлять точками останова. Кто не верит, может написать маленькую тестовую программку:
и запустив её под отладчиком убедится, что исполнение каждый прерывается в правильных местах — т.е. на инструкции следующей за той, которая доступается к области памяти:
Instruction breakpoints
[про этот вид точек останова оказывается достаточно подробно писали два года назад, поэтому я кратко привел лишь общие соображения]
![](https://habrastorage.org/storage/habraeffect/21/a9/21a957a026e35a9da3ccb1433f494b7b.png)
int 3
в тех местах, в которых мы хотели бы остановить исполнение программы. Когда процессор натолкнется на эту инструкцию, то сгенерирует соответсвующее прерывание, которое ядро ОС обработает и превратит, например, под Linux в сигнал SIGTRAP. Запущенная в свободная плаванье программа просто упадет натолкнувшись на int 3
, но вот отладчик поймает этот сигнал, остановит программу и позволит исследовать её состояние.Кстати сами отладчики тоже пользуются этой инструкцией: просто подменяют ей инструкции в памяти, когда мы просим их поставить бряк. Именно поэтому
int 3
кодируется одним байтом (0xCC
) а не двумя, как остальные инструкции генерации программого прерывания int X
(0xCD imm8
) — иначе бы int 3
не годилась бы для подмены однобайтовых инструкций.Как мы видим ничего сложного в ручной расстановке instruction breakpoints нет. Можно даже реализовывать их интерактивное включение-выключение: достаточно запоминать их позиции и заменять ненужные на пустую инструкцию
nop
(0x90
). Гораздо интереснее дело обстоит с другим типом точек останова — точками останова на доступ к памяти.
Access breakpoints
![](https://habrastorage.org/storage/habraeffect/f3/14/f31489d6e7b837ea33c626f42e6c533a.jpg)
Иными словами мы хотим предоставить ему в распоряжение функцию
void SetAccessBreak(void* addr);
Здесь нам на помощь приходят добры молодцы отладочные регистры
dr0
, dr1
, dr2
, dr3
и их дядька Черномор dr7
, содержащий контрольные флаги.Пользоваться ими достаточно просто: в один из регистров
dr0
— dr3
загружаем адрес, за которым нужно следить, а в dr7
выставляем соответствующие флажки определяющие активирована или нет точка останова соответствующего регистра, событие за которым она следит (выполнение/ чтение/чтение-или-запись по этому адресу), размер данных (1 байт, 2 байта, 4 байта, 8 байт). Дабы не тратить место на невнятные словесные объяснения правил кодировки флагов, я сразу приведу код двух утилитных функций: MakeFlags
, которая кодирует флаги для заданного отладочного регистра в формате использующемся в dr7
, и MakeMask
, которая для заданного регистра вычисляет битовую маску, покрывающую все флаги относящиеся к этому регистру (подобная маска нужна, если мы хотим сбросить все флаги).enum DebugRegister { <br/>
kDR0 = 0,<br/>
kDR1 = 2,<br/>
kDR2 = 4,<br/>
kDR3 = 6<br/>
};<br/>
<br/>
enum BreakState { <br/>
kDisabled = 0, // disabled - 00<br/>
kEnabledLocally = 1, // task local - 01<br/>
kEnabledGlobally = 2, // global - 10<br/>
kBreakStateMask = 3 // mask 11<br/>
};<br/>
<br/>
enum Condition { <br/>
kWhenExecuted = 0, // on execution - 00 <br/>
kWhenWritten = 1, // on write - 01<br/>
kWhenWrittenOrReaden = 3, // on read or write - 11<br/>
kConditionMask = 3 // mask 11<br/>
};<br/>
<br/>
enum Size { <br/>
kByte = 0, // 1 byte - 00<br/>
kHalfWord = 1, // 2 bytes - 01<br/>
kWord = 3, // 4 bytes - 11<br/>
kDoubleWord = 2, // 5 bytes - 10<br/>
kSizeMask = 3 // mask 11<br/>
}; <br/>
<br/>
<br/>
uint32_t MakeFlags(DebugRegister reg, BreakState state, Condition cond, Size size) {<br/>
return (state | cond << 16 | size << 24) << reg;<br/>
}<br/>
<br/>
<br/>
uint32_t MakeMask(DebugRegister reg) {<br/>
return MakeFlags(reg, kBreakStateMask, kConditionMask, kSizeMask);<br/>
}<br/>
Вооружившись этими функциями можно не зная брода бросится в воду и попробовать реализовать
SetAccessBreak
с помощью простого встроенного ассемблера:bool SetAccessBreak(void* addr,<br/>
DebugRegister reg,<br/>
Condition cond,<br/>
Size size) {<br/>
const uint32_t control = MakeFlags(reg, kEnabledLocally, cond, size);<br/>
__asm__("movl %0, %%dr0\n"<br/>
"movl %1, %%dr7\n" : : "r"(addr), "r"(control) : );<br/>
}
Однако эта попытка обречена на провал: доступ к отладочным регистрам возможен только из нулевого кольца защиты, т.е. из ядра. Однако, регистры эти плюсплюс-полезны (любой современный отладчик ими пользуется), поэтому ОС обычно предоставляют API для доступ к этим регистрам. Например, на Mac OS X читать и писать эти регистры можно через функции
thread_get_state
/thread_set_state
. Получив через них доступ к нужным регистрам, мы легко реализуем SetAccessBreak
:bool SetAccessBreak(pthread_t target_thread,<br/>
void* addr,<br/>
DebugRegister reg,<br/>
Condition cond,<br/>
Size size) {<br/>
x86_debug_state dr;<br/>
mach_msg_type_number_t dr_count = x86_DEBUG_STATE_COUNT;<br/>
<br/>
// Извлечем из POSIX потока нижлежащий MACH поток.<br/>
mach_port_t target_mach_thread = pthread_mach_thread_np(target_thread);<br/>
<br/>
// Запросим состояние отладочных регистров потока.<br/>
kern_return_t rc = thread_get_state(target_mach_thread,<br/>
x86_DEBUG_STATE,<br/>
reinterpret_cast<thread_state_t>(&dr),<br/>
&dr_count);<br/>
<br/>
// Попытка получить текущее состояние отладочных регистров провалилась<br/>
if (rc != KERN_SUCCESS) return false;<br/>
<br/>
// Загрузим адрес, за которым нужно следить с указанный регистр.<br/>
switch (reg) {<br/>
case kDR0: dr.uds.ds32.__dr0 = reinterpret_cast<unsigned int>(addr); break;<br/>
case kDR1: dr.uds.ds32.__dr1 = reinterpret_cast<unsigned int>(addr); break;<br/>
case kDR2: dr.uds.ds32.__dr2 = reinterpret_cast<unsigned int>(addr); break;<br/>
case kDR3: dr.uds.ds32.__dr3 = reinterpret_cast<unsigned int>(addr); break;<br/>
}<br/>
<br/>
// Сбросим все флаги относящиеся к указанному регистру.<br/>
dr.uds.ds32.__dr7 &= ~MakeMask(reg);<br/>
<br/>
// Установим новое значение флагов.<br/>
dr.uds.ds32.__dr7 |= MakeFlags(reg, kEnabledLocally, cond, size);<br/>
<br/>
// Обновим состояние отладочных регистров.<br/>
rc = thread_set_state(target_mach_thread,<br/>
x86_DEBUG_STATE,<br/>
reinterpret_cast<thread_state_t>(&dr),<br/>
dr_count);<br/>
<br/>
// Обновление регистров провалилось.<br/>
if (rc != KERN_SUCCESS) return false;<br/>
<br/>
// Точка установа успешно выставлена.<br/>
return true;<br/>
}
Вот и все! Теперь добрый дядька Дворник-Сборщик Мусора может сам управлять точками останова. Кто не верит, может написать маленькую тестовую программку:
static int16_t foo = 0;<br/>
static int32_t bar = 0;<br/>
<br/>
int main (int argc, char *argv[]) {<br/>
foo = 1;<br/>
bar = 1;<br/>
SetAccessBreak(pthread_self(), &bar, kDR0, kWhenWritten, kWord);<br/>
foo = 2;<br/>
bar = 2;<br/>
SetAccessBreak(pthread_self(), &foo, kDR0, kWhenWritten, kHalfWord);<br/>
foo = 3;<br/>
bar = 3;<br/>
return 0;<br/>
}<br/>
и запустив её под отладчиком убедится, что исполнение каждый прерывается в правильных местах — т.е. на инструкции следующей за той, которая доступается к области памяти:
(gdb) r
Starting program: /Users/mraleph/test
Reading symbols for shared libraries +++. done
Program received signal SIGTRAP, Trace/breakpoint trap.
main (argc=1, argv=0xbffff9f8) at test.cc:107
106 bar = 2; <= triggered SIGTRAP -- примечание mr.aleph
107 SetAccessBreak(pthread_self(), &foo, kDR0, kWhenWritten, kHalfWord);
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
main (argc=1, argv=0xbffff9f8) at test.cc:109
108 foo = 3; <= triggered SIGTRAP -- примечание mr.aleph
109 bar = 3;
(gdb) c
Continuing.
Program exited normally.