Всем привет! Наша группа занимается RISC-V Linux и загрузчиками в компании «Синтакор». Однажды перед нами возникла задача — реализовать поддержку аппаратных триггеров в ядре Linux и OpenSBI. Она стала началом исследования, в ходе которого я изучил смысл аппаратных триггеров с точки зрения отладчика, их устройство и использование для вотчпойнтов (watchpoint) и брейкпойнтов (breakpoint), а также принял участие в совершенствовании поддержки аппаратных триггеров в RISC-V Linux и OpenSBI. 

Этими знаниями я хотел бы поделиться в статье. Покажу на примерах, как устроены брейкпойнты и вотчпойнты в отладчиках, сравню их программную и аппаратную реализации, покопаюсь в деталях их работы в ядре Linux. Начну с легкого способа сломать GDB, а к каким выводам он приведет, вы узнаете далее под катом.

С места в карьер: напишем функцию, которая определяет, является ли число простым.

int result;

__attribute__((noinline))
int is_prime(uint64_t val) {
  result = 1;
  for(uint64_t i = 2; i * i < val; ++i) {
    if(val % i == 0) {
      result = 0;
      break;
    }
  }
  return result;
}

Глобальная переменная result — это фу, но в будущем она упростит нам работу. Далее будем вызывать функцию со значением первого аргумента командной строки и выводить результат ее выполнения в более приятном виде:

int main(int argc, char* argv[]) {
  uint64_t val;
  sscanf(argv[1], "%"PRIu64, &val);

  int prime = is_prime(val);

  if (prime) {
    printf("%"PRIu64 " is prime\n", val);
  } else {
    printf("%"PRIu64 " is not prime\n", val);
  }
}

Теперь рассмотрим стандартный отладочный цикл функции is_prime: установим брейкпойнт на функцию is_prime, сделаем continue и будем ожидать, что:

  • отладчик поймает брейкпойнт;

  • приложение остановится ровно на том месте, куда мы этот брейкпойнт поставили;

  • отладчик передаст нам управление;

  • мы произведем всю необходимую отладочную рутину;

  • мы еще раз вызовем continue

  • программа завершится. 

$ gdb --args is_prime.elf 479001599
…
(gdb) break is_prime
…
(gdb) continue
Breakpoint 1, is_prime (val=val@entry=479001599) at is_prime.c:11
11 for(uint64_t i= 2; i * i< val; ++i) {
… // здесь мы долго и усердно дебажим
(gdb) continue
Continuing.

Child exited with status 0

Ровно так и получается.

А теперь пора ломать. Сделаем objdump исследуемой функции и запомним первую ее инструкцию — а точнее, опкод, 0x4791:

000000000000085e <is_prime>:
  85e: 4791      li a5,4
  860: 02a7f263  bgeu a5,a0,884
  864: 4789      li a5,2
  866: a029      j 870
…

В рамках этого примера нам неважно, что именно делает та или иная инструкция, RISC-V это, ARM или х86. Нам важен опкод.

Провернем на первый взгляд странную вещь: на рантайме перед выполнением функции is_prime перепишем опкод ее первой инструкции на тот же самый. Для этого напишем функцию, которая:

  • выдаст нам права на запись в страницу с интересующей нас инструкцией;

  • перепишет первую инструкцию на ту же самую;

  • уделит внимание кешам, чтобы самомодифицирующийся код работал корректно;

  • отберет права на запись в модифицированную страницу — в рамках профилактики паранойи.

Получится примерно такая конструкция:

void smc_magic() {
  // находим страницу, на которой расположена функция is_prime
  size_t page_size = getpagesize();
  size_t is_prime_addr = (size_t)is_prime;
  size_t is_prime_page = is_prime_addr & ~(page_size - 1); 

  // выдаем себе права на запись
  if (mprotect((void*)is_prime_page, page_size, 
PROT_WRITE | PROT_EXEC | PROT_READ) < 0)
  	exit(errno);

  // делаем магию: меняем инструкцию на такую же
  *(uint16_t*)is_prime = 0x4791;

  // уделяем капельку внимания кешам
  asm volatile(“fence rw, rw” ::: “memory”);

  // забираем у себя права на запись
  if (mprotect((void*)is_prime_page, page_size, 
PROT_EXEC | PROT_READ) < 0)
  	exit(errno);
}

Вызов функции smc_magic никак не влияет на последовательность исполняемых инструкций, то есть результат исполнения программы останется такой же.

Темные закоулки C

Сейчас мы заглядываем в очень темный и пыльный угол стандарта C — пытаемся преобразовать указатель на функцию к указателю на void. Это implementation defined поведение, с которым нужна аккуратность, потому что адрес функции не всегда будет совпадать с адресом ее первой инструкции или первой исполняемой инструкции этой функции. Компилятор умеет в inline, у нас есть position-dependent код и архитектурно-зависимые фичи. Но у нас RISC-V, мы не под MS-DOS, мы были аккуратны с параметрами компиляции, и поэтому все получится!

А сейчас случится магия. Перед тестируемой функцией is_prime вызовем функцию smc_magic:

int main(int argc, char* argv[]) {
  smc_magic();

  uint64_val;
  sscanf(argv[1], "%"PRIu64, &val);

  int prime = is_prime(val);

  if(prime) {
    printf("%"PRIu64 " is prime\n", val);
  } else{
    printf("%"PRIu64 " is not prime\n", val);
  }
}

Теперь запустим отладчик и в точности повторим описанный ранее сценарий отладки:

$ gdb--args is_prime.elf 479001599
…
0x0000003ff7fec022 in _start () from target:/lib/ld-linux-riscv64-lp64d.so.1
(gdb) break is_prime
Breakpoint 1 at 0x2aaaaaa862: file is_prime.c, line 12.
(gdb) continue
Continuing.

Child exited with status 0

Возможно, этот пример покажется вам несколько высосанным из пальца. Однако о�� подсвечивает потенциальную проблему, которую можно встретить при отладке приложений, модифицирующих свой код на ходу. Например, JIT-компиляторами — будь то большой и страшный JVM, V8 или маленький обработчик скриптов на luajit.

Но в чем заключается эта особенность? Почему брейкпойнт не сработал? Чтобы это понять, нужно разобраться, как отладчик совместно с операционной системой реализует программные брейкпойнты.

Как отладчик ставит брейкпойнты

Рассмотрим, что происходит, когда GDB исполняет команды break и continue. Это выведет нас к разгадке. 

В первом примере мы вызвали команду break is_prime

$ gdb --args is_prime.elf 479001599
…
(gdb) break is_prime
…
(gdb)

Тогда отладчик на самом деле заменил первую инструкцию функции is_prime на инструкцию ebreak:

000000000000085e <is_prime>:
  85e: 4791      ebreak // а было `li a5,4`
  860: 02a7f263  bgeu a5,a0,884
  864: 4789      li a5,2
  866: a029      j 870
…

Во многих архитектурах есть подобная инструкция: int 3 в x86, bkpt в ARM. Такого рода инструкции делают только одно — генерируют аппаратное исключение при исполнении.

После вызова continue отладчик продолжил исполнение отлаживаемого приложения, ожидая, пока ОС уведомит его о событиях в приложении. В нашем случае — пока отлаживаемый процесс поймает SIGTRAP, который ОС генерирует в ответ на аппаратное исключение при исполнении ebreak. Когда GDB узнал, что отлаженное приложение получило SIGTRAP, он заменил ebreak обратно на исходную инструкцию и передал управление пользователю.

Когда мы сделали continue во второй раз, после остановки на брейкпойнте, перед отладчиком встали две очень важные задачи: исполнить замененную инструкцию и заново восстановить ebreak по адресу брейкпойнта — чтобы у последнего была возможность сработать еще раз.

Для этого отладчик ставил ebreak на следующую инструкцию и продолжал исполнение приложения. Когда он в очередной раз получил уведомление о SIGTRAP, то восстановил брейкпойнт на исходном адресе, заменил второй ebreak на инструкцию, которая была ранее, и продолжил исполнение, то есть фактически выполнил команду step.

Здесь становится очевидно, что же пошло не так во втором примере. Впервые мы получили управление процессом перед исполнением функции _start, которая находится где-то в crt0.s — недрах рантайма C. Поставили брейкпойнт на функцию is_prime. Соответственно, отладчик заменил первую инструкцию на ebreak. После чего поток исполнения дошел до функции smc_magic, которая переписала этот ebreak на исходный опкод. Когда же мы сделали continue, наша функция отработала нормально, ebreak не случился, и отладчик «не понял», что должен был сработать брейкпойнт.

Проблемы программной реализации и способы их разрешения

Все это время GDB по умолчанию использовал программную реализацию брейкпойнтов. Ее описание говорит, что их нельзя установить на адрес, по которому запрещена запись. Например, если секция кода оказалась в shared-памяти.

Любое приложение, которое модифицирует свой исходный код, — рискованное место для отладки программными методами. Брейкпойнты могут совершенно неожиданно не срабатывать, если приложение успело изменить код между установкой брейкпойнта по некоторому адресу и исполнением инструкции по этому же адресу. А приложение может осознанно так и сделать — усложняя тем самым свой реверс-инжиниринг!

Можно ли побороть эту проблему? Да, с помощью аппаратных брейкпойнтов. Но прежде чем перейти к ним, я рассмотрю еще один минус программной реализации — работу вотчпойнтов.

Софтверные вотчпойнты

Снова откроем GDB и запустим исследуемое приложение, скажем, с числом 479001599. Оно простое и большое. Вспомним нашу «юродивую» переменную result, поставим на нее программный вотчпойнт и сделаем continue:

$ gdb --args is_prime.elf 479001599
…
0x0000003ff7fec022 in _start () from target:/lib/ld-linux-riscv64-lp64d.so.1
(gdb) set can-use-hw-watchpoints 0
(gdb) watch result
Watchpoint 1: result
(gdb) continue
Continuing.

Приложение остановится, когда функция is_prime изменит значение выражения result. Вот только до этого мы успеем заварить, выпить и еще раз заварить чай — придется подождать минут 15. Софтверные вотчпойнты работают невыносимо медленно, хоть и делают отладчик гораздо удобнее. Среди иных способов узнать, что значение выражения изменилось, мне в голову приходит либо step до достижения нужного результата, либо обмазывание кода принтами.

При установке вотчпойнтов на некоторое выражение отладчик запоминает его последнее значение, после чего начинает исполнять отлаживаемое приложение по одной инструкции и проверять, не изменилось ли оно. То есть получается как с брейкпойнтом:

  • отладчик заменяет первую инструкцию на ebreak,

  • останавливает приложение,

  • смотрит, изменилось ли значение выражения,

  • переставляет ebreak на следующую инструкцию,

  • повторяет цикл снова и снова.

При этом каждый раз при генерации аппаратного исключения поток исполнения проваливается в ядро, идет из него вместе с сигналом SIGTRAP обратно в user space и уже там возвращается в отладчик. Думаю, миссия «Аполлон» долетала до Луны за меньшее число инструкций…

С очевидными минусами программной реализации разобрались. Предлагаю перейти к решению, которое устраняет эти недостатки.

Аппаратные триггеры

В аппаратуре ядер процессоров предусмотрен модуль с некоторым числом сущностей, называемых триггерами, — обычно их 4, 8 или 16. Каждому триггеру задают множество адресов, при доступе к которым на чтение/запись/исполнение триггер будет генерировать аппаратное исключение — так же, как и ebreak. Благодаря такому функционалу «железа» GDB может реализовать отладку без модификации кода приложения. 

Аппаратная реализация вотчпойнтов используется в GDB по умолчанию. А чтобы задействовать аппаратные брейкпойнты необходимо вместо break использовать hbreak. Так оба вышеупомянутых примера будут работать быстро и без ошибок. Казалось бы, панацея найдена, но почему тогда ее не используют повсеместно?

Дело в том, что в отличие от программной реализации, аппаратных триггеров предусмотрено мало. В x86 их всего четыре. В ARM — порядка 2–6 (Cortex-M), иногда 16 (AArch64-системы), хотя архитектура позволяет иметь до 64 триггеров. В RISC-V число триггеров тоже порядка 16 (SiFive).

Преимущество двух реализаций в том, что они компенсируют недостатки друг друга. Давайте сведем плюсы и минусы обоих методов.

Аппаратная реализация:

  • Позволяет отлаживать приложение без модификации его кода. Соответственно, не нужны и права на такие изменения. Можно отлаживать ROM, можно страницы, к которым не получить доступ на запись, или приложения, использующие SMC.

  • Позволяет реализовать вотчпойнты быстрее.

  • Число триггеров ограничено и довольно мало́.

Программная реализация:

  • Подходит для большинства сценариев отладки.

  • Число триггеров ничем не ограничено.

Осознанное использование обеих реализаций повышает эффективность и надежность отладки. Но не это самое интересное в аппаратных триггерах. Для меня как для разработчика операционных систем это была лишь затянувшаяся прелюдия, ведь настоящий катарсис ждал под капотом ядра — в механизмах, которые конфигурируют триггеры в железе по запросам из userspace. Им необходимо:

  • обрабатывать исключения, генерируемые аппаратными триггерами, и отправлять сигналы процессам;

  • определять, какому процессу принадлежит триггер, и избегать ситуаций, когда SIGTRAP был отправлен процессу, который не устанавливал триггер;

  • различать триггеры, используемые userspace, процессами и самим ядром;

  • активировать триггер, только когда процесс исполняется.

Чтобы разобраться, как это работает, предлагаю отступить от темы и поговорить про широко известную утилиту для профилирования — perf. Опыты с ним выведут нас к тому, как реализовать поддержку аппаратных триггеров в ядре. Но для этого придется сделать page fault, ведь то, как обрабатывается он, очень похоже на то, что мы хотим иметь для обработки аппаратных триггеров!

Perf

Продолжим издеваться над нашим примером — будем профилировать его perf’ом! Например, попросим его узнать, в каких компонентах примера случаются minor page fault’ы, ведь я ожидаю как минимум один в функции smc_magic. Почему так?

При попытке считать или записать данные по виртуальному адресу, с которым не ассоциирован никакой физический адрес, процессор генерирует аппаратное исключение для обработки ядра. Такая ситуация может возникать в двух случаях:

  • Userspace-приложение разыменовывает указатель на адрес, не принадлежащий пространству виртуальных адресов процесса. В ответ на это ядро отправляет процессу сигнал SIGSEGV. Это называется major page fault.

  • Ядро намеренно допустило ситуацию, когда невозможно ассоциировать виртуальный адрес с физическим, и знает, как это прозрачно исправить для исполняемых процессов. Это называется minor page fault.

Изначально я ожидаю, что секция .text, которая имеет доступ только на чтение и исполнение, расположена на shared-страницах памяти, так как ядро отображает их из файла в память. Это экономит память в ситуациях, когда запущено несколько процессов из одного исполняемого файла.

В момент, когда mprotect выдает странице кода права на запись, ядро аллоцирует новую страницу в таблице страниц процесса, но не ассоциирует с ней физическую страницу. Так оно делает почти всегда, поскольку:

  • доступ к аллоцированной странице памяти может и не произойти,

  • mmap и mprotect работают быстрее.

И только при первом доступе на запись к вновь аллоцированной виртуальной странице ядро ловит аппаратное исключение, ассоциирует с ней физическую страницу и копирует туда данные. Это Copy-on-Write оптимизация, в результате которой все оказываются в плюсе. Процесс, изменивший свой .text, продолжает исполнение, а другие процессы могут быть запущены из исходного, уже загруженного исполняемого файла. Именно здесь и происходит minor page fault.

Давайте проверим это предположение:

$ perf record -e faults ./is_prime.elf
$ perf report
# Samples: 29  of event 'minor-faults'
# Event count (approx.): 48
#
# Overhead  Command          Shared Object                Symbol                     
# ........  ...............  ...........................  ...........................
#
…
     4.17%  is_prime.elf  is_prime.elf         [.] smc_magic
…

Работает! Но как perf узнал о событиях в ядре, если это userspace-приложение? И как ядро подсчитало эти события? Ответ на этот вопрос кроется в системе ядра с логичным названием Perf Events и системном вызове perf_event_open.

Perf Events

Perf Events — это фреймворк для профилирования и трассировки в ядре Linux. Его основная задача — собирать информацию о том, как программное обеспечение в userspace и в самом ядре взаимодействует с аппаратным обеспечением. Живет фреймворк в файле /kernel/events/core.c и является поистине огромной системой, способной:

  • собирать профили как аппаратных, так и программных событий;

  • определять, какому процессу, потоку и уровню доступа принадлежит событие;

  • взаимодействовать с eBPF, другими системами ядра и многое другое.

Все события делятся на группы, наиболее примечательными из которых являются:

  • Hardware events — архитектурные и микроархитектурные события, например, число потраченных циклов процессора, выполненных инструкций, доступов и промахов в кеши, циклов простоя конвейера и пр.

  • Software events — программные события, например, вышеупомянутые minor и major page faults, переключения контекста.

Каждая группа событий объединяется в свой Performance Monitoring Unit (PMU), о которых и рапортует perf:

$ perf list
List of pre-defined events (to be used in -e or -M):

  branch-instructions OR branches                    [Hardware event]
  branch-misses                                      [Hardware event]
  cache-misses                                       [Hardware event]
  cache-references                                   [Hardware event]
  …
  major-faults                                       [Software event]
  minor-faults                                       [Software event]
  page-faults OR faults                              [Software event]
  task-clock                                         [Software event]

Мы даже можем сами семплировать интересующее нас событие вручную без помощи perf — например, тот самый minor page fault, — если воспользуемся системным вызовом perf_event_open и еще чуть-чуть модифицируем пример.

Добавим обертку для функции, так как glibc ее не определяет:

#include <linux/perf_event.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <unistd.h>

int perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu,
		int group_fd, unsigned long flags) {
	return syscall(SYS_perf_event_open, attr, pid, cpu, group_fd, flags);
}

Здесь pid определяет процесс, которому принадлежит трассируемое событие, cpu — логическое ядро, на котором это событие необходимо трассировать. Их можно выбирать! То есть мы можем заставить Perf Events считать события для заданного процесса и заданного логического ядра!

Создадим счетчик интересующего нас события и запомним его файловый деск��иптор. Запретим при этом трассировать события, произошедшие в самом ядре и гипервизоре.

int event_fd;

void prepare_event() {
	struct perf_event_attr attr = {};
	attr.type = PERF_TYPE_SOFTWARE;
	attr.size = sizeof(struct perf_event_attr);
	attr.config = PERF_COUNT_SW_PAGE_FAULTS_MIN; // трассируем minor page faults
	attr.disabled = 1;
	attr.exclude_kernel = 1; // не считаем, те, что породило ядро
	attr.exclude_hv = 1; // не считаем, те, что породил гипервизор

	// регистрируем событие для нашего процесса и всех логических ядер строго по man’у
	event_fd = perf_event_open(&attr, 0, -1, -1, 0);
	if (event_fd == -1)
		exit(EXIT_FAILURE);
}

И, собственно, обмажем интересующую нас строку функции smc_magic кодом, запускающим и останавливающим семплирование:

void start_sampling() {
	// сбрасываем счетчик событий
	ioctl(event_fd, PERF_EVENT_IOC_RESET);
	// запускаем их подсчет
	ioctl(event_fd, PERF_EVENT_IOC_ENABLE);
}

void finish_sampling() {
	long long count;

	// останавливаем подсчет событий
	ioctl(event_fd, PERF_EVENT_IOC_DISABLE);
	// считываем число случившихся
	read(event_fd, &count, sizeof(count));

	printf("%lld minor page faults happen\n", count);
}

void smc_magic() {
  size_t page_size = getpagesize();
  size_t is_prime_addr = (size_t)is_prime;
  size_t is_prime_page = is_prime_addr & ~(page_size - 1); 

  if (mprotect((void*)is_prime_page, page_size, PROT_WRITE | PROT_EXEC | PROT_READ) < 0)
  	exit(errno);

  // добавляем семплирование
  start_sampling();
	*(uint16_t*)is_prime = 0x4791;
  finish_sampling();

  asm volatile(“fence rw, rw” ::: “memory”);

  if (mprotect((void*)is_prime_page, page_size, PROT_EXEC | PROT_READ) < 0)
  	exit(errno);

}

Запустим и получим заветное…

$ ./is_prime.elf 13
1 minor page faults happen
13 is prime

Давайте разберемся, что сделало ядро, чтобы детектировать minor page fault и уведомить о нем userspace. После этого я обещаю: мы вернемся к аппаратным триггерам. 

При перезаписи первой инструкции функции is_prime, как мы выяснили ранее, случилось аппаратное исключение, в котором была аллоцирована новая физическая страница для функции is_prime, а так же невзначай вызвана функция:

perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs, address);

Она уведомила Perf Events о том, что произошло одно событие PERF_COUNT_SW_PAGE_FAULTS_MIN — которое мы и трассируем.

Кажется, возможности Perf Events очень схожи с требованиями, которые мы выдвигали к обработчику аппаратных триггеров в ядре. Напомню, он должен уметь:

  • обрабатывать исключения, генерируемые аппаратными триггерами, и отправлять сигналы процессам;

  • определять, какому процессу принадлежит триггер, и избегать ситуаций, когда SIGTRAP был отправлен процессу, который не устанавливал триггер;

  • различать триггеры, используемые userspace-процессами и самим ядром;

  • активировать триггер, только когда процесс исполняется.

Очень похожая функциональность была использована для детектирования minor page faults. Все совпадения были неслучайны, и я выделял их жирным шрифтом. Эх, если бы Perf Events только мог вызывать callback каждый раз, когда случается событие. Мы бы из этого callback’а смогли отправлять SIGTRAP в userspace. Перед каждым буллитом можно было бы поставить зеленую галочку и радоваться! А стоп, он же это может.

Аппаратный брейкпойнт — это Perf Event

На самом деле каждый аппаратный триггер — это Perf Event с лимитом на переполнение счетчика, установленным на единицу. Callback на это переполнение выглядит так:

static void ptrace_hbptriggered(struct perf_event *bp,
				struct perf_sample_data *data,
				struct pt_regs *regs)
{
	force_sig_fault(SIGTRAP, TRAP_HWBKPT, (void __user *)regs->badaddr);
}

То есть он отправляет сигнал SIGTRAP процессу, которому заданный триггер принадлежит. 

Ядро точно так же, как и с minor page fault, ловит аппаратное исключение, которое сгенерировал триггер. Затем чуть-чуть разбирается, кто это исключение породил:

void handle_break(struct pt_regs *regs)
{
	if (probe_single_step_handler(regs))
		return;

	if (probe_breakpoint_handler(regs))
		return;
…

И отправляет его обработчику:

	if (notify_die(DIE_DEBUG, "EBREAK", regs, 0, regs->cause, SIGTRAP)
	    == NOTIFY_STOP)
		return;
…
}

Он уведомит Perf Event, что сработал аппаратный триггер — точно так же, как его уведомляли о minor page fault:

static int hw_breakpoint_handler(struct die_args *args) {
…
  perf_bp_event(any_triggered_event, args->regs);
…
}

Чтобы всего этого достичь, ядро регистрирует отдельный PMU для аппаратных брейкпойнтов:

static struct pmu perf_breakpoint = {
	.task_ctx_nr	= perf_sw_context, /* could eventually get its own */

	.event_init	 = hw_breakpoint_event_init,
	.add		      = hw_breakpoint_add,
	.del		      = hw_breakpoint_del,
	.start		= hw_breakpoint_start,
	.stop		      = hw_breakpoint_stop,
	.read		      = hw_breakpoint_pmu_read,
};

…

perf_pmu_register(&perf_breakpoint, "breakpoint", PERF_TYPE_BREAKPOINT);

И говорит, что каждый раз, когда процесс начинает исполнение, необходимо вызвать функцию hw_breakpoint_add, чтобы установить все аппаратные триггеры:

static int hw_breakpoint_add(struct perf_event *bp, int flags)
{
	…
	return arch_install_hw_breakpoint(bp);
}

Когда же процесс приостанавливает исполнение, их необходимо убрать посредством функции hw_breakpoint_del, иначе они выстрелят в чужом процессе:

static void hw_breakpoint_del(struct perf_event *bp, int flags)
{
	arch_uninstall_hw_breakpoint(bp);
}

Функции arch_* определяются для каждой архитектуры и тем или иным способом конфигурируют регистры, управляющие аппаратными триггерами. А все остальное берет на себя Perf Events! Думаю, события, которые генерируют аппаратные триггеры, можно попытаться даже поймать посредством perf_event_open. Но это не точно.

На самом деле, Perf Events — это очень мощный и многофункциональный фреймворк в ядре, на базе которого реализованы не только трассировка событий, но и другие средства отладки и профилирования — например, kprobes и tracepoints. Также он умеет взаимодействовать с eBPF, что делает его неограниченно гибким в отладке и профилировании. Его активно используют и некоторые драйверы. Как это обычно бывает в Linux, названия обманчивы: лично я разочаровался в них после первого же опыта — когда мне рассказали про eBPF.

Итоги

Совместное и осознанное использование программной и аппаратной реализаций брейкпойнтов и вотчпойнтов, а также знание плюсов и минусов каждой из реализаций помогают повысить скорость и качество отладки. И все это возможно благодаря большому и могучему фреймворку Perf Events, живущему в ядре, простому с точки зрения интерфейса и открытому к вашему использованию.

Тема этой статьи — лишь одна из многочисленных тем System Level Meetup, прошедшего в мае. Уже послезавтра, 22 ноября, состоится следующий, не менее насыщенный двухпоточный System Level Meetup о C++ и Linux Kernel. Регистрируйтесь для участия в онлайн-трансляции, чтобы задать вопросы спикерам в прямом эфире.