eBPF ( расширенный фильтр пакетов Berkeley ) возник в ядре Linux и представляет собой изолированную программу, которая может работать в ядре операционной системы. Его технология безопасно и эффективно расширяет функциональность ядра. Без изменения исходного кода ядра или загрузки модулей ядра.
eBPF широко используется для:
-
Трассировка производительности ядра
-
Кибербезопасность и наблюдаемость
-
Безопасность приложений и контейнеров во время выполнения
...
1. Общий процесс выполнения программы eBPF
Программа eBPF сначала использует C или Rust для написания программы eBPF, LLVM компилирует ее в байт-код, а программа пользовательского режима использует библиотеку eBPF для загрузки байт-кода eBPF в ядро Linux с помощью системного вызова bpf.
Валидатор ядра ebpf, проверяющий байт-код BPF:
-
Независимо от того, имеет ли процесс, который инициирует системный вызов bpf, соответствующие разрешения, процесс должен иметь соответствующие возможности Linux (CAP_BPF) или права root;
-
Проверьте, не приведет ли программа к сбою ядра, например, есть ли неинициализированные переменные, есть ли операторы, которые могут привести к выходу за границы массива, доступу по нулевому указателю;
-
Проверьте, выполняется ли программа за ограниченное время, eBPF допускает только ограниченное количество циклов и переходов и позволяет выполнять только ограниченное количество инструкций.
После того, как программа eBPF построена, она монтируется в соответствующем событии ядра, таком как системный вызов.Когда генерируется системный вызов, ядро запускается для вызова соответствующей программы eBPF. Программа ядра ebpf взаимодействует с программой пользовательского режима через структуру данных карты для выполнения соответствующих функций.
2. Отслеживание eBPF
2.1 Типы датчиков
Зонды ядра: обеспечивают динамический доступ к внутренним компонентам ядра.
Точки трассировки: обеспечивает динамический доступ к программам, работающим в пользовательском пространстве.
Зонды пользовательского пространства: обеспечивает динамический доступ к программам, работающим в пользовательском пространстве.
Статически определяемые пользователем точки трассировки: обеспечивает статический доступ к программам, работающим в пользовательском пространстве.
2.2 Зонд ядра
Зонды ядра могут устанавливать динамические флаги или прерывания на любую инструкцию ядра, и когда ядро достигает этих флагов, код, прикрепленный к зонду, будет выполнен, после чего ядро вернется в нормальный режим.
Зонды ядра делятся на две категории: kprobes и kretprobes.
2.2.1 kпробы
kprobes позволяют вставлять программы BPF перед выполнением любой инструкции ядра, необходимо знать сигнатуру функции точки вставки, зонды ядра не являются стабильными ABI (программный двоичный интерфейс), поэтому необходимо соблюдать осторожность при запуске программ, которые устанавливают зонды в другом ядре. версии . Когда ядро выполнит инструкцию для установки зонда, оно начнет выполнение программы BPF с точки выполнения кода и вернется к месту, где программа BPF была вставлена, чтобы продолжить выполнение после завершения выполнения программы BPF.
2.2.2 кретзонды
kretprobes вставляются в программу BPF, когда инструкция ядра имеет возвращаемое значение. Часто мы используем и kprobes, и kretprobes в программе BPF, чтобы получить полное представление об инструкциях ядра.
2.3 Точки отслеживания
Точки трассировки — это статические маркеры кода ядра, которые можно использовать для присоединения кода к работающему ядру. Основное различие между точками трассировки и kprobes заключается в том, что точки трассировки записываются и изменяются в ядре разработчиками ядра. Поскольку трекпойнт существует статически, ABI трекера является наиболее стабильным.
Точки трассировки добавляются разработчиками ядра, поэтому точки трассировки могут не охватывать все подсистемы ядра.
Содержимое каталога /sys/kernel/debug/tracing/events позволяет просмотреть все доступные точки трассировки в системе.
Каждый подкаталог в приведенном выше выводе соответствует точке трассировки, к которой может подключаться программа BPF. Есть два дополнительных файла: первый файл enable, который позволяет включать и отключать все точки трассировки подсистемы BPF. Если содержимое этого файла равно 0, это означает, что точка трассировки отключена. Если содержимое файла равно 1, точка трассировки включена.
Зонды ядра и точки трассировки обеспечивают полный доступ к ядру. По возможности используйте точки трассировки, поскольку они более безопасны.
2.4 Зонды пользовательского пространства
Зонды пользовательского пространства позволяют устанавливать динамические флаги в программах, работающих в пользовательском пространстве. Они эквивалентны зондам ядра, а зонды пользовательского пространства — это мониторинг систем, работающих в пользовательском пространстве. Когда мы определяем зонд, ядро создаст ловушку для прикрепленной инструкции, и когда программа выполнит инструкцию, ядро инициирует событие для вызова функции зонда, вызвав функцию обратного вызова. Upprobes также может получить доступ к любой библиотеке, с которой связана программа, и, если известно имя инструкции, можно отследить соответствующий вызов.
Как и зонды ядра, зонды пользовательского пространства также делятся на две категории: uprobes и uretporbes, в зависимости от того, на какой фазе командного цикла находится вставленная программа BPF.
2.4.1 U-зонды
Upprobes — это ловушки, которые ядро вставляет в набор инструкций для конкретной программы перед ее выполнением. Сигнатуры функций могут различаться для разных версий ядра. В Linux вы можете использовать команду nm, чтобы вывести список всех символов, включенных в объектный файл ELF, и проверить, существует ли в программе инструкция трассировки.
2.4.2 уретральные зонды
uretprobes — это параллельные зонды kretprobes, подходящие для использования пользовательскими программами. Он добавляет программу BPF к возвращаемому значению инструкции, позволяя получить доступ к возвращаемому значению из регистров через код BPF.
Комбинация upprobes и uretprobes позволяет писать более сложные программы BPF.
● eBPF позволяет создавать точки трассировки в ядре
○ Системные вызовы
○ Сетевой интерфейс (сокет/xdp)
○ Вход/выход из функции
○ Точки трассировки ядра
○ Контейнеры (cgroups)
○ Функция пользовательского режима
...
● eBPF позволяет создавать зонды:
○ Зонд ядра (kprobe)
○ Зонд пользователя (upprobe)
3. Компоненты программы eBPF
4. Отображение eBPF
Карты BPF хранятся в ядре как ключ/значение и могут быть доступны любой программе BPF. Программы пользовательского пространства также могут получать доступ к картам BPF через файловые дескрипторы. Любой тип данных, который реализует указанный размер, может быть сохранен в карте BPF. Ядро обрабатывает ключи и значения как двоичные блоки, а это значит, что ядру все равно, что именно содержит карта BPF.
Валидаторы BPF используют несколько мер безопасности для обеспечения безопасного создания и доступа к картам BPF.
Самый простой способ создать карту BPF — использовать системный вызов bpf. Если первый параметр этого системного вызова установлен в BPF_MAP_CREATE, это означает создание новой карты. Этот вызов вернет дескриптор файла, связанный с созданием карты. Второй параметр системного вызова bpf — это настройка карты BPF.
union bpf_attr(){
struct {
__u32 map_type; /*bpf_map_type*/
__u32 key_size;
__u32 value_size;
__u32 max_entries;
__u32 map_flags;
};
}
Третий параметр системного вызова bpf — установка размера свойства, создание сопоставления хеш-таблицы ключей и целых чисел без знака:
union bpf_attr_my_map {
.map_type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 100,
.map_flags = BPF_F_NO_PREALLOC,
};
int fd = bpf(BPF_MAP_CREATE, &my_map, sizeof(my_map));
Если системный вызов завершается ошибкой, ядро возвращает -1.Есть три причины сбоя, которые различаются по errno.
-
Если атрибут недействителен, ядро возвращает EINVAL.
-
Если привилегий недостаточно для выполнения операции, ядро возвращает EPERM.
-
Если для хранения отображения недостаточно памяти, ядро вернет ENOMEM.
4.1 Создание карт BPF с использованием соглашений ELF
В ядре существуют некоторые соглашения и вспомогательные функции для создания и использования карт BPF. Несмотря на то, что эти соглашения выполняются в ядре, нижний уровень по-прежнему использует системный вызов bpf для создания отображения.
Вспомогательная функция bpf_map_create инкапсулирует код, который мы использовали выше, и может легко инициализировать карту по требованию.
int fd;
fd = bpf_map_create(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, BPF_F_NO_PREALOC);
4.2 Использование отображения BPF
Связь между ядром и пространством пользователя является основой для написания программ BPF. Код программы ядра и пользовательского пространства может получить доступ к карте, но они используют разные сигнатуры API.
4.2.1 Обновление элементов карты BPF
Создайте содержимое обновления карты, для чего ядро предоставляет вспомогательную функцию bpf_map_update_elem.
Программе ядра необходимо загрузить функцию bpf_map_update_elem из файла bpf/bpf_helpers.h, а пользовательскую программу необходимо загрузить из файла tools/lib/bpf/bpf.h, поэтому сигнатура функции, к которой обращается программа ядра, отличается. из сигнатуры функции, к которой обращается пользовательское пространство.
Программа ядра может напрямую обращаться к карте, в то время как программа пользователя должна использовать файловый дескриптор для обращения к карте.
int key, value, result;
key = 1, value = 1234;
result = bpf_map_update_elem(map_data[0].fd, &key, &value, BPF_ANY);
if(result == 0)
printf("Map updated with new element\n");
else
printf("Failed to update map with new value: %d (%s)\n", result, strerror(errno));
4.2.2 Чтение элементов карты BPF
BPF предоставляет две разные вспомогательные функции для чтения элементов карты в зависимости от того, где выполняется программа. Обе функции называются bpf_map_lookup_elem.
Прочитайте карту из пространства ядра:
int key, value, result;
key = 1;
result = bpf_map_lookup_elem(&my_map, &key, &value);
if(result == 0)
printf("Value to read from the map: '%d'\n", value);
else
printf("Failed to read value from the map: %d (%s)\n", result, strerror(errno));
Чтение карты из пользовательского пространства:
int key, value, result;
key = 1;
result = bpf_map_lookup_elem(map_data[0].fd, &key, &value);
if(result == 0)
printf("Value to read from the map: '%d'\n", value);
else
printf("Failed to read value from the map: %d (%s)\n", result, strerror(errno));
Первый параметр в bpf_map_lookup_elem будет заменен дескриптором отображаемого файла. Вспомогательная функция ведет себя так же, как в примере выше.
4.2.3 Удаление элементов карты BPF
BPF предоставляет две разные вспомогательные функции для удаления элементов карты в зависимости от того, где выполняется программа. Обе функции называются bpf_map_delete_element.
Удалите значение, вставленное в карту, из пространства ядра:
int key, value, result;
key = 1;
result = bpf_map_delete_element(&my_map, &key);
if(result == 0)
printf("Element deleted from the map\n");
else
printf("Failed to delete element from the map: %d (%s)\n", result, strerror(errno));
Чтение карты из пользовательского пространства:
int key, value, result;
key = 1;
result = bpf_map_delete_element(map_data[0].fd, &key);
if(result == 0)
printf("Element deleted from the map\n");
else
printf("Failed to delete element from the map: %d (%s)\n", result, strerror(errno));
4.2.4 Перебор элементов карты BPF
Найдите любой элемент в BPF. BPF предоставляет инструкцию bpf_map_get_next_key, которая применима только к программам, работающим в пользовательском пространстве.
int next_key, lookup_key;
lookup_key = -1;
while(bpf_map_get_next_key(map_data[0].fd, &lookup_key, &next_key) == 0){
printf("The next key in the map is: '%d'\n", next_key);
lookup_key = next_key;
}
4.2.5 Поиск и удаление элементов карты
bpf_map_lookup_and_delete_elem. Эта функция заключается в том, чтобы найти указанный ключ в карте и удалить элемент. При этом программа присваивает значение элемента переменной.
int key, value, result, it;
key = 1;
for (it =0; it < 2; it++){
result = bpf_map_lookup_and_delete_element(map_data[0].fd, &key, &value);
if(result == 0)
printf("Value read from the map: '%d'\n", value);
else
printf("Failed to read value from the map: %d (%s)\n", result, strerror(errno));
}
4.2.6 Одновременный доступ к элементам карты
Одновременный доступ к одним и тем же элементам карты может создать условия гонки в программах BPF. BPF добавляет концепцию спин-блокировки BPF, которая может блокировать доступ к элементам карты при работе с элементами карты. Спин-блокировки доступны только для массивов, хэшей и карт хранилища cgroup.
В ядре есть две вспомогательные функции для использования со спин-блокировками: bpf_spin_lock lock, bpf_spin_unlock unlock. Пользовательские программы могут использовать флаг BPF_F_LOCK.
Использование спин-блокировки сначала требует создания элемента, к которому вы хотите заблокировать доступ, а затем добавления сигнала к этому элементу.
struct concurrent_element{
struct bpf_spin_lock semaphore;
int count;
}
Мы можем объявить карту, которая содержит эти элементы. Отображение должно быть аннотировано с использованием формата типа BPF (BTF), чтобы валидатор знал, как интерпретировать BTF. BTF может предоставлять более подробную информацию ядру и другим инструментам, добавляя отладочную информацию к двоичным объектам. В ядре мы можем использовать макросы ядра libbpf, чтобы аннотировать эту параллельную карту.
struct bpf_map_def SEC("maps") concurrent_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(struct concurrent_element),
.max_entries = 100,
};
BPF_ANNOTATE_KV_PAIR(concurrent_map, int, struct concurrent_element);
Используйте эти две вспомогательные функции, чтобы защитить эти элементы от условий гонки.
5. Типы отображения BPF
5.1 Отображение хэш-таблицы
Сопоставления хеш-таблиц — это первые сопоставления общего назначения, добавленные в BPF. Тип карты определяется как BPF_MAP_TYPE_HASH.
5.2 Отображение массива
Карта массива — это вторая карта BPF, добавленная в ядро. Тип карты определяется как BPF_MAP_TYPE_ARRAY. При инициализации карты массива все элементы предварительно размещаются в памяти и обнуляются. Ключ является индексом в массиве и должен иметь размер ровно четыре байта. Элементы в карте массива не могут быть удалены.
5.3 Отображение массива программ
Карта массива программ добавляется к первой выделенной карте ядра. Тип карты определяется как BPF_MAP_TYPE_PROC_ARRAY. Этот тип содержит ссылку на программу BPF, файловый дескриптор программы BPF. Тип отображения массива программ можно использовать в сочетании со вспомогательной функцией bpf_tail_call для перехода между программами, преодоления предела максимального количества инструкций одной программы BPF и уменьшения сложности реализации. Ключ и значение должны иметь размер четыре байта. При переходе к новой программе новая программа будет использовать тот же стек памяти, поэтому программа не будет использовать всю доступную память. Если вы перейдете к несуществующей программе, хвостовой вызов потерпит неудачу и вернется, чтобы продолжить выполнение текущей программы.
5.4 Отображение массива событий Perf
Эта карта типов хранит данные perf_events в кольцевом буфере для связи в реальном времени между программами BPF и программами пользовательского пространства.
Тип карты определяется как BPF_MAP_TYPE_PERF_EVENT_ARRAY. Он может перенаправлять события, созданные инструментами трассировки ядра, в программы пользовательского пространства для дальнейшей обработки.
Объявите структуру события:
struct data_t{
u32 pid;
char program_name[16];
}
Создайте сопоставление для отправки событий в пространство пользователя:
struct bpf_map_def SEC("maps") events = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
.max_entries = 2,
}
После объявления типа данных и сопоставления мы можем создать программу BPF для захвата данных и отправки их в пространство пользователя:
SEC("kprobe/sys_exec")
int bpf_capture_exec(struct pt_regs *ctx){
data_t data;
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.program_name, sizeof(data.program_name));
bpf_perf_event_output(ctx, &events, 0, &data, sizeof(data));
return 0;
}
ссылка
«Технология наблюдения ядра Linux BPF»
https://ebpf.io/zh-cn/