Pull to refresh

Точки останова на ручной тяге (для архитектуры x86)

Reading time9 min
Views5.5K
Любой программист хоть раз заглядывавший в отладчик знаком с понятием точки останова (aka бряк, breakpoint). Казалось бы нет ничего проще, чем поставить точку останова пара кликов мышкой в графическом интерфейсе или команда в консоли отладчика, но не всегда жизнь системного программиста столь проста и иногда возникает необходимость выставлять точки останова автоматически — изнутри самой программы.


Instruction breakpoints



[про этот вид точек останова оказывается достаточно подробно писали два года назад, поэтому я кратко привел лишь общие соображения]

Предположим мы пишем JIT компилятор и хотим выставлять точки останова внутри генерируемого им кода. Оказывается для этого достаточно вставить в поток кода всего лишь одну инструкцию int 3 в тех местах, в которых мы хотели бы остановить исполнение программы. Когда процессор натолкнется на эту инструкцию, то сгенерирует соответсвующее прерывание, которое ядро ОС обработает и превратит, например, под Linux в сигнал SIGTRAP. Запущенная в свободная плаванье программа просто упадет натолкнувшись на int 3, но вот отладчик поймает этот сигнал, остановит программу и позволит исследовать её состояние.

Кстати сами отладчики тоже пользуются этой инструкцией: просто подменяют ей инструкции в памяти, когда мы просим их поставить бряк. Именно поэтому int 3 кодируется одним байтом (0xCC) а не двумя, как остальные инструкции генерации программого прерывания int X (0xCD imm8) — иначе бы int 3 не годилась бы для подмены однобайтовых инструкций.

Как мы видим ничего сложного в ручной расстановке instruction breakpoints нет. Можно даже реализовывать их интерактивное включение-выключение: достаточно запоминать их позиции и заменять ненужные на пустую инструкцию nop (0x90).

Гораздо интереснее дело обстоит с другим типом точек останова — точками останова на доступ к памяти.

Access breakpoints



Допустим мы отлаживаем повреждение памяти в среде исполнения с копирующим сборщиком мусора, который постоянно компактифицирует кучу и всякими другими способами тасует объекты в памяти. Мы смогли выяснить приблизительно какое поле в каком объекте повреждается, но просто загрузить программу в отладчик и поставить им точку останова на доступ к этому полю не получается, потому что GC постоянно путается под ногами своими перемещениями. Поэтому у нас возникает разумное желание, чтобы сам добряк Сборщик Мусора и выставлял/обновлял этот бряк.

Иными словами мы хотим предоставить ему в распоряжение функцию

void SetAccessBreak(void* addr);

Здесь нам на помощь приходят добры молодцы отладочные регистры dr0, dr1, dr2, dr3 и их дядька Черномор dr7, содержащий контрольные флаги.

Пользоваться ими достаточно просто: в один из регистров dr0dr3 загружаем адрес, за которым нужно следить, а в 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.
Tags:
Hubs:
+54
Comments26

Articles

Change theme settings