Русский
Русский
English
Статистика
Реклама

Kernel

Ядерный шелл поверх ICMP

28.08.2020 14:05:29 | Автор: admin


TL;DR: пишу модуль ядра, который будет читать команды из пейлоада ICMP и выполнять их на сервере даже в том случае, если у вас упал SSH. Для самых нетерпеливых весь код на github.

Осторожно! Опытные программисты на C рискуют разрыдаться кровавыми слезами! Я могу ошибаться даже в терминологии, но любая критика приветствуются. Пост рассчитан на тех, кто имеет самое приблизительное представление о программировании на C и хочет заглянуть во внутренности Linux.

В комментариях к моей первой статье упомянули SoftEther VPN, который умеет мимикрировать под некоторые обычные протоколы, в частности, HTTPS, ICMP и даже DNS. Я представляю себе работу только первого из них, так как хорошо знаком с HTTP(S), а туннелирование поверх ICMP и DNS пришлось изучать.

image

Да, я в 2020 году узнал, что в ICMP-пакеты можно вставить произвольный пейлоад. Но лучше поздно, чем никогда! И раз уж с этим можно что-то сделать, значит нужно делать. Так как в своей повседневности чаще всего я пользуюсь командной строкой, в том числе через SSH, идея ICMP-шелла пришла мне в голову первым делом. А чтобы собрать полное буллщит-бинго, решил писать в виде модуля Linux на языке, о котором я имею лишь приблизительное представление. Такой шелл не будет виден в списке процессов, можно загрузить его в ядро и он не будет лежать на файловой системе, вы не увидите ничего подозрительного в списке прослушиваемых портов. По своим возможностям это полноценный руткит, но я надеюсь доработать и использовать его в качестве шелла последней надежды, когда Load Average слишком высокий для того, чтобы зайти по SSH и выполнить хотя бы echo i > /proc/sysrq-trigger, чтобы восстановить доступ без перезагрузки.

Берём текстовый редактор, базовые скиллы программирования на Python и C, гугл и которую не жалко пустить под нож если всё поломается (опционально локальный VirtualBox/KVM/etc) и погнали!

Клиентская часть


Мне казалось, что для клиентской части придётся писать скрипт строк эдак на 80, но нашлись добрые люди, которые сделали за меня . Код оказался неожиданно простым, умещается в 10 значащих строк:

import sysfrom scapy.all import sr1, IP, ICMPif len(sys.argv) < 3:    print('Usage: {} IP "command"'.format(sys.argv[0]))    exit(0)p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))if p:    p.show()

Скрипт принимает два аргумента, адресом и пейлоад. Перед отправкой пейлоад предваряется ключём run:, он нам понадобится чтобы исключить пакеты со случайным пейлоадом.

Ядро требует привилегий для того чтобы крафтить пакеты, поэтому скрипт придётся запускать с правами суперпользователя. Не забудьте дать права на выполнение и установить сам scapy. В Debian есть пакет, называется python3-scapy. Теперь можно проверять, как это всё работает.

Запуск и вывод команды
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Hello, world!"
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
\options \
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hello, world!


Так это выглядит в сниффере
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
Frame 1: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0xd603 [correct]
[Checksum Status: Good]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct]
[Checksum Status: Good]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1]
[Response time: 19.094 ms]
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

^C2 packets captured


Пейлоад в пакете с ответом не меняется.

Модуль ядра


Для сборки в виртуалке с Debian понадобятся как минимум make и linux-headers-amd64, остальное подтянется в виде зависимостей. В статье код целиком приводить не буду, вы его можете склонировать на гитхабе.

Настройка хука


Для начала нам понадобятся две функции для того, чтобы загрузить модуль и чтобы его выгрузить. Функция для выгрузки не обязательна, но тогда и rmmod выполнить не получится, модуль выгрузится только при выключении.

#include <linux/module.h>#include <linux/netfilter_ipv4.h>static struct nf_hook_ops nfho;static int __init startup(void){  nfho.hook = icmp_cmd_executor;  nfho.hooknum = NF_INET_PRE_ROUTING;  nfho.pf = PF_INET;  nfho.priority = NF_IP_PRI_FIRST;  nf_register_net_hook(&init_net, &nfho);  return 0;}static void __exit cleanup(void){  nf_unregister_net_hook(&init_net, &nfho);}MODULE_LICENSE("GPL");module_init(startup);module_exit(cleanup);

Что здесь происходит:

  1. Подтягиваются два заголовочных файла для манипуляций собственно с модулем и с нетфильтром.
  2. Все операции проходят через нетфильтр, в нём можно задавать хуки. Для этого нужно заявить структуру, в которой хук будет настраиваться. Самое важное указать функцию, которая будет выполняться в качестве хука: nfho.hook = icmp_cmd_executor; до самой функции я ещё доберусь.
    Затем я задал момент обработки пакета: NF_INET_PRE_ROUTING указывает обрабатывать пакет, когда он только появился в ядре. Можно использовать NF_INET_POST_ROUTING для обработки пакета на выходе из ядра.
    Вешаю фильтр на IPv4: nfho.pf = PF_INET;.
    Назначаю своему хуку наивысшей приоритет: nfho.priority = NF_IP_PRI_FIRST;
    И регистрирую структуру данных как собсвенно хук: nf_register_net_hook(&init_net, &nfho);
  3. В завершающей функции хук удаляется.
  4. Лицензия обозначена явно чтобы компилятор не ругался.
  5. Функции module_init() и module_exit() задают другие функции в качестве инициализирующей и завершающей работу модуля.

Извлечение пейлоада


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

#include <linux/ip.h>#include <linux/icmp.h>#define MAX_CMD_LEN 1976char cmd_string[MAX_CMD_LEN];struct work_struct my_work;DECLARE_WORK(my_work, work_handler);static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state){  struct iphdr *iph;  struct icmphdr *icmph;  unsigned char *user_data;  unsigned char *tail;  unsigned char *i;  int j = 0;  iph = ip_hdr(skb);  icmph = icmp_hdr(skb);  if (iph->protocol != IPPROTO_ICMP) {    return NF_ACCEPT;  }  if (icmph->type != ICMP_ECHO) {    return NF_ACCEPT;  }  user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));  tail = skb_tail_pointer(skb);  j = 0;  for (i = user_data; i != tail; ++i) {    char c = *(char *)i;    cmd_string[j] = c;    j++;    if (c == '\0')      break;    if (j == MAX_CMD_LEN) {      cmd_string[j] = '\0';      break;    }  }  if (strncmp(cmd_string, "run:", 4) != 0) {    return NF_ACCEPT;  } else {    for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {      cmd_string[j] = cmd_string[j+4];      if (cmd_string[j] == '\0')break;    }  }  schedule_work(&my_work);  return NF_ACCEPT;}

Что происходит:

  1. Пришлось подключить дополнительные заголовочные файлы, на этот раз для манипуляция с IP- и ICMP-хедерами.
  2. Задаю максимальную длину строки: #define MAX_CMD_LEN 1976. Почему именно такую? Потому что на большую компилятор ругается! Мне уже подсказали, что надо разбираться со стеком и кучей, когда-нибудь я обязательно это сделаю и может даже поправлю код. Сходу задаю строку, в которой будет лежать команда: char cmd_string[MAX_CMD_LEN];. Она должна быть видима во всех функциях, об этом подробней расскажу в пункте 9.
  3. Теперь надо инициализировать (struct work_struct my_work;) структуру и связать её с ещё одной функцией (DECLARE_WORK(my_work, work_handler);). О том, зачем это нужно, я также расскажу в девятом пункте.
  4. Теперь объявляю функцию, которая и будет хуком. Тип и принимаемые аргументы диктуются нетфильтром, нас интересует только skb. Это буфер сокета, фундаментальная структура данных, которая содержит все доступные сведения о пакете.
  5. Для работы функции понадобится две структуры, и несколько переменных, в том числе два итератора.

      struct iphdr *iph;  struct icmphdr *icmph;  unsigned char *user_data;  unsigned char *tail;  unsigned char *i;  int j = 0;
    
  6. Можно приступить к логике. Для работы модуля не нужны никакие пакеты кроме ICMP Echo, поэтому парсим буфер встроенными функциями и выкидываем все не ICMP- и не Echo-пакеты. Возврат NF_ACCEPT означает принятие пакета, но можете и дропнуть пакеты, вернув NF_DROP.

      iph = ip_hdr(skb);  icmph = icmp_hdr(skb);  if (iph->protocol != IPPROTO_ICMP) {    return NF_ACCEPT;  }  if (icmph->type != ICMP_ECHO) {    return NF_ACCEPT;  }
    


    Я не проверял, что произойдёт без проверки заголовков IP. Моё минимальное знание C подсказывает: без дополнительных проверок обязательно произойдёт что-нибудь ужасное. Я буду рад, если вы меня в этом разубедите!
  7. Теперь, когда пакет точно нужного типа, можно извлекать данные. Без встроенной функции приходится сначала получать указатель на начало пейлода. Делается это через одно место, нужно взять указатель на начало заголовка ICMP и передвинуть его на размер этого заголовка. Для всего используется структура icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Конец заголовка должен совпадать с концом полезной нагрузки в skb, поэтому получаем его ядерными средствами из соответствующей структуры: tail = skb_tail_pointer(skb);.

    image

    Картинку утащил , можете почитать подробней про буфер сокета.
  8. Получив указатели на начало и конец, можно скопировать данные в строку cmd_string, проверить её на наличие префикса run: и, либо выкинуть пакет в случае его отсутствия, либо снова перезаписать строку, удалив этот префикс.
  9. Ну всё, теперь можно вызвать ещё один хендлер: schedule_work(&my_work);. Так как в такой вызов передать параметр не получится, строка с командой и должна быть глобальной. schedule_work() поместит функцию ассоциированную с переданной структурой в общую очередь планировщика задач и завершится, позволив не ждать завершения команды. Это нужно потому что хук должен быть очень быстрым. Иначе у вас, на выбор, ничего не запустится или вы получите kernel panic. Промедление смерти подобно!
  10. Всё, можно принимать пакет соответствующим возвратом.

Вызов программы в юзерспейсе


Эта функция самая понятная. Название её было задано в DECLARE_WORK(), тип и принимаемые аргументы не интересны. Берём строку с командой и передаём её целиком шеллу. Пусть он сам разбирается с парсингом, поиском бинарей и со всем остальным.

static void work_handler(struct work_struct * work){  static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};  static char *envp[] = {"PATH=/bin:/sbin", NULL};  call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);}

  1. Задаём аргументы в массив строк argv[]. Предположу, что все знают, что программы на самом деле выполняются именно так, а не сплошной строкой с пробелами.
  2. Задаём переменные окружения. Я вставил только PATH с минимальным набором путей, рассчитывая что у всех уже объединены /bin с /usr/bin и /sbin с /usr/sbin. Прочие пути довольно редко имеют значение на практике.
  3. Готово, выполняем! Функция ядра call_usermodehelper() принимает на вход. путь к бинарю, массив аргументов, массив переменных окружения. Здесь я тоже предполагаю, что все понимают смысл передачи пути к исполняемому файлу отдельным аргументом, но можете спросить. Последний аргумент указывает, ждать ли завершения процесса (UMH_WAIT_PROC), запуска процесса (UMH_WAIT_EXEC) или не ждать вообще (UMH_NO_WAIT). Есть ещё UMH_KILLABLE, я не стал разбираться в этом.

Сборка


Сборка ядерных модулей выполняется через ядерный же make-фреймворк. Вызывается make внутри специальной директории привязанной к версии ядра (определяется тут: KERNELDIR:=/lib/modules/$(shell uname -r)/build), а местонахождение модуля передаётся переменной M в аргументах. В таргетах icmpshell.ko и clean целиком используется этот фреймворк. В obj-m указывается объектный файл, который будет переделан в модуль. Синтаксис, которые переделывает main.o в icmpshell.o (icmpshell-objs = main.o) для меня выглядит не очень логичным, но пусть так и будет.

KERNELDIR:=/lib/modules/$(shell uname -r)/build

obj-m = icmpshell.o
icmpshell-objs = main.o

all: icmpshell.ko

icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules

clean:
make -C $(KERNELDIR) M=$(PWD) clean


Собираем: make. Загружаем: insmod icmpshell.ko. Готово, можно проверять: sudo ./send.py 45.11.26.232 "date > /tmp/test". Если у вас на машине появился файл /tmp/test и в нём лежит дата отправки запроса, значит вы сделали всё правильно и я сделал всё правильно.

Заключение


Мой первый опыт ядерной разработки оказался гораздо более простым, чем я ожидал. Даже не имея опыта разработки на C, ориентируясь на подсказки компилятора и выдачу гугла, я смог написать рабочий модуль и почувствовать себя кернел хакером, а заодно и скрипт-кидди. Кроме этого я зашёл на канал Kernel Newbies, где мне подсказали использовать schedule_work() вместо вызова call_usermodehelper() внутри самого хука и пристыдили, справедливо заподозрив скам. Сотня строк кода мне стоила где-то недели разработки в свободное время. Удачный опыт, разрушивший мой личный миф о непосильной сложности системной разработки.

Если кто-то согласится выполнить код-ревью на гитхабе, я буду признателен. Я почти уверен, что допустил много глупых ошибок, особенно, в работе со строками.

Подробнее..

Отлаживаем ядро из командной строки с bpftrace

16.02.2021 00:14:13 | Автор: admin

Это очередная статья из цикла BPF для самых маленьких (0, 1, 2) и первая из серии практических статей про трассировку Linux современными средствами.


Из нее вы узнаете о программе и языке bpftrace самом простом способе погрузиться в мир BPF с практической точки зрения, даже если вы не знаете про BPF ровным счетом ничего. Утилита bpftrace позволяет при помощи простого языка прямо из командной строки создавать программы-обработчики и подсоединять их к огромному количеству событий ядра и пространства пользователя. Посмотрите на КПДВ ниже поздравляю, вы уже умеете трейсить системные вызовы при помощи bpftrace!


В отличие от предыдущих статей серии, эта относительно короткая и ее основная часть написана в формате туториала, так что уже после пяти минут чтения вы сможете создавать обработчики и собирать статистику о любом событии в ядре Linux прямо из командной строки. В конце статьи рассказывается об альтернативах ply и BCC. Минуты во фразе после пяти минут чтения подразумеваются меркурианские. Появление уникальных навыков после пяти минут чтения не гарантируется.



Содержание



Установка bpftrace


Короткая версия. Попробуйте выполнить команду sudo apt install bpftrace (скорректированную под ваш дистрибутив). Если bpftrace установился, то переходите к следующему разделу.


Длинная версия. Хотя bpftrace и доступен в качестве пакета в популярных дистрибутивах, но не во всех, а кроме этого он может быть собран криво, например, без поддержки BTF. Поэтому давайте посмотрим на то, как добыть bpftrace альтернативными средствами.


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


$ docker pull quay.io/iovisor/bpftrace:latest$ cd /tmp$ docker run -v $(pwd):/o quay.io/iovisor/bpftrace:latest /bin/bash -c "cp /usr/bin/bpftrace /o"$ sudo ./bpftrace -Vbpftrace v0.11.4

Если bpftrace ругается на слишком старую версию glibc, то вам нужен другой docker image со старой glibc.


Проверим, что программа работает. Для этого запустим пример из КПДВ, который трейсит системный вызов execve(2) и в реальном времени показывает какие программы запускаются в системе, и кем:


$ sudo ./bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(uptr(args->filename))); }'Attaching 1 probe...bash -> /bin/echobash -> /usr/bin/lsgnome-shell -> /bin/shsh -> /home/aspsk/bin/geeqiesh -> /usr/local/sbin/geeqie...

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


$ sudo mv /tmp/bpftrace /usr/local/bin

Если вам этого мало, то можете скачать исходники, запустить чуть больше проверок, собрать свое ядро с поддержкой BTF, а также свою версию bpftrace, при помощи docker или локально. Инструкции по самостоятельной сборке bpftrace см. в официальной документации.


Важная деталь. Если bpftrace и/или ядро собрано без поддержки BTF, то для полноценной работы нужно установить kernel headers. Если вы не знаете как это сделать, то в документации bpftrace есть универсальный дистрибутивонезависимый рецепт.


Мы не будем в этой статье подробно рассказывать про BTF, скажем только, что BTF убирает зависимость от kernel headers, позволяет писать более простые программы, а также расширяет набор событий, к которым мы можем подключиться. Если внешние обстоятельства требуют от вас изучения BTF прямо сейчас, то начните с этой статьи и продолжите этой и этой.


Какие события мы можем трейсить?


Вы сидите в лесу с фотоаппаратом на удобном раскладном стуле. Идет мелкий дождь, но он вам не страшен, ведь шалаш из еловых веток надежно укрывает вас как от дождя, так и от случайных взглядов. Рядом стоит термос с пуэром, йогурт и поднос с копчеными селедками. Но тут вы вдруг задумываетесь: а зачем я сюда пришел и что я буду снимать?! Когда вы просыпаетесь, на лице краснеет слепок клавиатуры, а в терминале написано


$ sudo apt install bpftraceThe following NEW packages will be installed:  bpftracePreparing to unpack .../bpftrace_0.11.0-1_amd64.deb ...Unpacking bpftrace (0.11.0-1) ...Setting up bpftrace (0.11.0-1) ...Processing triggers for man-db (2.9.3-2) ...$

Итак, bpftrace у нас уже установлен. Какие события мы можем инструментировать? Давайте посмотрим на них по очереди, а заодно познакомимся с синтаксисом языка bpftrace. Вот спойлер-оглавление данной секции:



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


Bpftrace: hello world


Язык bpftrace создавался по аналогии с языком awk и поэтому в нем есть два специальных события, BEGIN и END, которые случаются в момент запуска и выхода из bpftrace, соответственно. Вот первая простая программа:


# bpftrace -e 'BEGIN { printf("Hello world!\n"); }'Attaching 1 probe...Hello world!^C

Программа сразу после старта напечатала "Hello world!". Заметьте, что нам пришлось нажимать Ctrl-C, чтобы завершить работу bpftrace это его поведение по умолчанию. Мы можем завершить работу bpftrace из любого события при помощи функции exit. Продемонстрируем это, а заодно добавим и обработку END:


# bpftrace -e ' BEGIN { printf("Hello world!\n"); exit(); } END { printf("So long\n"); } 'Attaching 2 probes...Hello world!So long

Kprobes динамическая инструментация ядра


Ядро это большая программа, функции этой программы, как водится, состоят из инструкций, а механизм ядра под названием kprobes (Kernel Probe ядерный зонд) позволяет нам поставить точку останова практически на любой инструкции, а точнее, по началу конкретной функции или коду внутри нее. В контексте данной статьи нам, вообще говоря, не важно как именно создаются обработчики kprobes, но вы можете узнать об этом из предыдущих статей этой серии, ссылки на которые есть в конце, а также из будущих статей, в которых мы разберем все технические подробности трассировки Linux с BPF.


В качестве примера давайте посмотрим на то, как часто и кем вызывается функция schedule:


$ sudo bpftrace -e '    k:schedule { @[comm] = count(); }    i:s:2 { exit();}    END { print(@, 10); clear(@); }'Attaching 3 probes...@[Timer]: 147@[kworker/u65:0]: 147@[kworker/7:1]: 153@[kworker/13:1]: 158@[IPC I/O Child]: 170@[IPC I/O Parent]: 172@[kworker/12:1]: 185@[Web Content]: 229@[Xorg]: 269@[SCTP timer]: 1566

Мы также сказали программе выйти через две секунды и в конце напечатать только десять верхних элементов словаря @.


Много ли функций можно потрейсить при помощи кей-проб? Это легко проверить:


$ sudo bpftrace -l 'k:*' | wc -l61106

Это почти все функции загруженного в данный момент ядра. Исключения составляют функции, которые компилятор решил встроить в код и немногочисленные функции, которые запрещено трейсить при помощи kprobe, например, функции, которые реализуют сам механизм kprobes.


kretprobes


Для каждой kprobe мы можем создать обработчик kretprobe. Если kprobe запускается в момент входа в функцию, то kretporobe запускается в момент выхода из функции. При этом код возврата функции содержится в специальной встроенной переменной retval.


Например, вот что на отрезке в две секунды возвращала функция vfs_write на моей системе (в виде логарифмической гистограммы):


$ sudo bpftrace -e 'kr:vfs_write { @ = hist(retval); } i:s:2 { exit(); }'Attaching 2 probes...@:[1]                  606 |@@@@@@@@@@@@@@@@@@@@@@@@@                           |[2, 4)                 0 |                                                    |[4, 8)                 0 |                                                    |[8, 16)             1223 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[16, 32)               0 |                                                    |[32, 64)              25 |@                                                   |

uprobes и uretprobes


Кроме инструментации функций ядра, мы можем инструментировать каждую программу (и библиотеку), работающую в пространстве пользователя. Реализуется это при помощи тех же kprobes. Для этого в bpftrace определены события uprobes и uretprobes вызов и возврат из функции.


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


$ sudo bpftrace -e 'uretprobe:/bin/bash:readline { printf("readline: [%d]: \"%s\"\n", uid, str(uptr(retval))); }'Attaching 1 probe...readline: [1000]: "echo "hello habr""readline: [0]: "echo "hello from root""^C

Динамическая инструментация ядра, версия 2


Для счастливых обладателей CONFIG_DEBUG_INFO_BTF=y в конфиге ядра существует более дешевый, по сравнению с kprobes, способ динамической инструментации ядра, основанный на bpf trampolines. Так как BTF в дистрибутивных ядрах обычно выключен, я про эти события дальше не рассказываю. Если интересно, то смотрите сюда и/или задавайте вопросы в комментариях.


Tracepoints статическая инструментация ядра


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


$ sudo bpftrace -l 't:*'

Их сильно меньше, чем kprobes:


$ sudo bpftrace -l 't:*' | wc -l1782

но самой важной особенностью tracepoints является то, что они предоставляют стабильный API: вы можете быть уверены, что tracepoint, на основе которой вы написали свой код для отладки или сбора информации, не пропадет или не поменяет семантику в самый неудобный момент. Еще одним удобным отличием является то, что bpftrace может нам рассказать о том, какие аргументы передаются в конкретный tracepoint, например:


$ sudo bpftrace -lv tracepoint:thermal:thermal_temperaturetracepoint:thermal:thermal_temperature    __data_loc char[] thermal_zone;    int id;    int temp_prev;    int temp;

В случае kprobe, если у вас не включен BTF, вам придется читать исходники ядра, причем той версии, которую вы используете. А с BTF вы можете смотреть и на строение kprobes и kfuncs.


usdt статическая инструментация в пространстве пользователя


Статическая инструментация программ пользователя позволяет напихать в программу статических точек останова в момент компиляции. Давайте сразу посмотрим на пример (который я стянул почти без изменений отсюда):


#include <sys/sdt.h>#include <sys/time.h>#include <unistd.h>int main(int argc, char **argv){    struct timeval tv;    for (;;) {        gettimeofday(&tv, NULL);        DTRACE_PROBE1(test, probe, tv.tv_sec);        sleep(1);    }}

Мы добавили одну статическую точку останова под названием test:probe, в которую передаем один аргумент tv.tv_sec текущее время в секундах. Чтобы скомпилировать эту программу, нужно поставить пакет systemtap-sdt-dev (или аналогичный для вашего дистрибутива). Дальше мы можем посмотреть на то, что получилось:


$ cc /tmp/test.c -o /tmp/test$ sudo bpftrace -l 'usdt:/tmp/test'usdt:/tmp/test:test:probe

Запустим /tmp/test в одном терминале, а в другом скажем


$ sudo bpftrace -e 'usdt:/tmp/test:test:probe { printf("московское время %u\n", arg0); }'Attaching 1 probe...московское время 1612903991московское время 1612903992московское время 1612903993московское время 1612903994московское время 1612903995...

Здесь arg0 это значение tv.tv_sec, которое мы передаем в breakpoint.


События Perf


Программа bpftrace поддерживает множество событий, предоставляемых подсистемой ядра Perf. Мы сейчас коротко посмотрим на следующие типы событий, поддерживаемые bpftrace:


  • software: статически-сгенерированные софтверные события
  • hardware: железные PMCs
  • interval: интервальное событие
  • profile: интервальное событие для профилирования

События типа `software`


В ядре определяется несколько статических событий perf, посмотреть их список можно так:


# bpftrace -l 's:*'software:alignment-faults:software:bpf-output:software:context-switches:software:cpu-clock:software:cpu-migrations:software:dummy:software:emulation-faults:software:major-faults:software:minor-faults:software:page-faults:software:task-clock:

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


# bpftrace -e 'software:cpu-migrations:10 { @[comm] = count(); }'Attaching 2 probes...^C@[kworker/u65:1]: 1@[bpftrace]: 1@[SCTP timer]: 2@[Xorg]: 2

Подсчитает каждое десятое событие миграции процесса с одного CPU а другой. Значение событий из списка выше объясняется в perf_event_open(2), например, cpu-migrations, которую мы использовали выше можно найти в этой man-странице под именем PERF_COUNT_SW_CPU_MIGRATIONS.


События типа `hardware`


Ядро дает нам доступ к некоторым hardware counters, а bpftrace может вешать на них программы BPF. Точный список событий зависит от архитектуры и ядра, например:


bpftrace -l 'h*'hardware:backend-stalls:hardware:branch-instructions:hardware:branch-misses:hardware:bus-cycles:hardware:cache-misses:hardware:cache-references:hardware:cpu-cycles:hardware:frontend-stalls:hardware:instructions:hardware:ref-cycles:

Посмотрим на то как мой процессор предсказывает инструкции перехода (считаем каждое стотысячное событие, см. PERF_COUNT_HW_BRANCH_MISSES):


bpftrace -e 'hardware:branch-misses:100000 { @[tid] = count(); }'Attaching 3 probes...@[1055]: 4@[3078]: 4@[1947]: 5@[1066]: 6@[2551]: 6@[0]: 29

События типа `interval` и `profile`


События типов interval и profile позволяют пользователю запускать обработчики через заданные интервалы времени. Событие первого типа запустится один раз на одном CPU, а событие второго на каждом из CPU.


Мы уже использовали интервал раньше, чтобы выйти из программы. Давайте посмотрим на этот пример еще раз, но чуть пропатчим его:


$ sudo bpftrace -e '    kr:vfs_write { @ = hist(retval); }    interval:s:2 { print(@); clear(@); }'

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


Аналогично можно использовать и profile:


# bpftrace -e '  profile:hz:99 { @[kstack] = count(); }  i:s:10 { exit(); }  END { print(@,1); clear(@); }'Attaching 3 probes...@[    cpuidle_enter_state+202    cpuidle_enter+46    call_cpuidle+35    do_idle+487    cpu_startup_entry+32    start_secondary+345    secondary_startup_64+182]: 14455

Здесь мы запускаем profile на каждом ядре 99 раз в секунду, через десять секунд выстрелит интервал и вызовет exit(), а секция END напечатает только верхний элемент словаря @ самый часто встречающийся ядерный стек (по которому мы видим, что моя система, в основном, бездействует).


Bpftrace: tutorial


Базовые навыки


Начнем с простого, запустим bpftrace без аргументов:


# bpftrace

Программа напечатает короткую справку о том, как ее использовать. В частности, посмотрите какие переменные окружения использует bpftrace. Заметьте, что мы запускаем bpftrace от рута. Для вывода справки это не нужно, но для любых других действий да.


Посмотрим внимательнее на аргумент -l. Он позволяет найти события по шаблону. (Если что-то дальше не ясно, то читайте про события в предыдущем разделе, который вы пропустили.) Вот как можно искать события среди всех возможных:


# bpftrace -l '*kill_all*'kprobe:rfkill_allockprobe:kill_allkprobe:btrfs_kill_all_delayed_nodes

А здесь мы ищем события только среди tracepoints:


# bpftrace -l 't:*kill*'tracepoint:cfg80211:rdev_rfkill_polltracepoint:syscalls:sys_enter_killtracepoint:syscalls:sys_exit_killtracepoint:syscalls:sys_enter_tgkilltracepoint:syscalls:sys_exit_tgkilltracepoint:syscalls:sys_enter_tkilltracepoint:syscalls:sys_exit_tkill

Подмножество tracepoint:syscalls, на которое мы только что наткнулись, можно использовать для самостоятельных экспериментов по изучению bpftrace. Для каждого системного вызова X определены две точки останова:


tracepoint:syscalls:sys_enter_Xtracepoint:syscalls:sys_exit_X

Поиграемся с каким-нибудь системным вызовом, например, execve(2). Для того, чтобы посмотреть на то, как использовать какой-либо tracepoint можно использовать дополнительный аргумент -v, например:


# bpftrace -lv 't:s*:sys_*_execve'tracepoint:syscalls:sys_enter_execve    int __syscall_nr;    const char * filename;    const char *const * argv;    const char *const * envp;tracepoint:syscalls:sys_exit_execve    int __syscall_nr;    long ret;

(заметьте как ловко мы дважды использовали *, чтобы не писать syscalls полностью и чтобы не перечислять sys_enter_execve и sys_exit_execve по отдельности). Параметры, перечисленные в выводе -lv доступны через встроенную переменную args:


# bpftrace -e '    t:s*:sys_enter_execve { printf("ENTER: %s\n", str(uptr(args->filename))); }    t:s*:sys_exit_execve { printf("EXIT: %s: %d\n", comm, args->ret); }'Attaching 2 probes...ENTER: /bin/lsEXIT: ls: 0ENTER: /bin/lssEXIT: bash: -2

Этот короткий листинг позволяет разглядеть несколько интересных вещей.


В первом обработчике мы печатаем аргумент args->filename. Так как передается он нам как указатель, нам нужно вычитать строку при помощи встроенной функции str, но просто так ее использовать нельзя: указатель этот указывает в пространство пользователя, а значит мы должны об этом специально сказать при помощи функции uptr. Сам bpftrace постарается угадать принадлежность указателя, но он не гарантирует результат. Также, к сожалению, вызов bpftrace -lv не расскажет нам о семантике указателя, для этого придется изучать исходники, в данном случае, мы посмотрим на определение системного вызова execve (обратите внимание на квалификатор типа __user).


Во втором обработчике мы используем встроенную переменную comm, которая возвращает текущее имя потока. Код возврата системного вызова доступен через переменную args->ret. Как известно, этот системный вызов "не возвращается" в текущую программу, так как его работа заключается собственно в замене кода программы новым. Однако, в случае ошибки он-таки вернется, как мы и можем видеть в выводе выше: в первом случае я запустил /bin/ls из баша и exec запустился нормально и вернул 0 (внутри процесса ls прямо перед запуском кода /bin/ls), а во втором случае я запустил несуществующую программу /bin/lss и exec вернул -2 внутри процесса bash.


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


Структура программ `bpftrace`


Язык bpftrace создавался по аналогии с языком awk (см. также главу 6 в книжке The AWK Programming Language) и имеет очень похожую структуру. Программы состоят из списка блоков вида


<probe> <filter> { <action> }

Например,


# bpftrace -e 'BEGIN { printf("Hello\n") } END { printf("World\n") }'

Здесь <probe> это BEGIN и END, а <action> это printf. Поле
<filter> является опциональным и используется для фильтрации событий,
например, программа


# bpftrace -e 'p:s:1 /cpu == 0/ { printf("Привет с CPU%d\n", cpu); }'Attaching 1 probe...Привет с CPU0Привет с CPU0^C

будет передавать привет, только если запускается на CPU 0.


Упражнение. Что выведет эта команда на вашей машине, если убрать фильтр /cpu == 0/?


На практике <filter> удобно использовать для синхронизации между двумя событиями. Например, вы хотите подсчитать время выполнения системного вызова write на вашей системе. Для этого мы можем использовать пару трейспоинтов sys_enter_write и sys_exit_write и считать время выполнения по тредам:


# cat /tmp/write-times.btt:syscalls:sys_enter_write {  @[tid] = nsecs}t:syscalls:sys_exit_write /@[tid]/ {  @write[comm] = sum((nsecs - @[tid]) / 1000);  delete(@[tid]);}END {  print(@write, 10);  clear(@write);  clear(@);}

Эта программа уже довольно длинная, поэтому мы записали ее в отдельный файл. Запустить ее можно так:


# bpftrace /tmp/write-times.bt

В первом событии, sys_enter_write, мы запоминаем время запуска системного вызова write в наносекундах в словаре @, ключом к которому служит tid.


Во втором событии, sys_exit_write, мы при помощи фильтра /@[tid]/ проверяем, запускался ли обработчик первого события для данного потока. Нам нужно это делать, ведь мы могли запустить программу в тот момент, когда какой-то поток был внутри системного вызова write. Дальше мы записываем потраченное время (в микросекундах) в отдельный словарь @write и удаляем элемент @[tid].


Наконец, после того как мы нажимаем ^C, запускается секция END, в которой мы печатаем десять самых прожорливых процессов и чистим словари @write и @, чтобы bpftrace не выводил их содержимое.


Упражнение. Так что же именно может пойти не так, если убрать фильтр /@[tid]/?


Храним состояние: переменные и мапы


Внутри программ bpftrace вы можете использовать обычные для языка C конструкции, например, :?, ++, --. Вы можете использовать блоки if {} else {}. Можно составлять циклы при помощи while и экзотического unroll (который появился в то время, когда в архитектуре BPF циклы были запрещены). Содержание же во все эти конструкции добавляют переменные и структуры ядра, доступные из контекста.


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


# bpftrace -e 'BEGIN { $x = 1; printf("%d\n", ++$x); exit(); }'# bpftrace -e 'BEGIN { if (1) { $x = 1; } printf("%d\n", ++$x); exit(); }'

а следующее нет:


# bpftrace -e 'BEGIN { $x = 1; exit(); } END { printf("%d\n", $x); }'

Глобальные переменные, с которыми мы уже встречались выше, начинаются со знака @ и доступны между событиями. Вы можете использовать "безымянную" глобальную переменную @, как мы делали выше для хранения начала вызова write (@[tid]). (Глобальные переменные в bpftrace хранятся в мапах специальных размеченных областях памяти. Они, между прочим, глобальные в более общем смысле: любая программа с рутовым доступом на системе может их читать и писать. Но для данной статьи это не так важно, смотрите предыдущие серии, если вам интересны подробности.)


И теперь, мы переходим к самому главному: зачем нам нужны переменные и что мы в них будем записывать? Каждое событие bpftrace запускается с определенным контекстом. Для kprobes нам доступны аргументы вызываемой функции, для tracepoints аргументы, передаваемые в tracepoint, а для событий Perf, как и для других программ, глобальные переменные. Мы уже видели как мы можем работать с tracepoints, в этой и следующих секциях мы посмотрим на kprobes, а в секции Веселые Картинки мы посмотрим на события Perf.


Аргументы kprobes доступны внутри программы как arg0, arg1, и т.д. Аргументы передаются без типа, так что вам придется к нужному типу их приводить вручную. Пример:


#include <linux/skbuff.h>#include <linux/ip.h>k:icmp_echo {  $skb = (struct sk_buff *) arg0;  $iphdr = (struct iphdr *) ($skb->head + $skb->network_header);  @pingstats[ntop($iphdr->saddr), ntop($iphdr->daddr)]++;}

Эта программа строит статистику о том, кто пингует данный хост. Мы цепляемся к kprobe на входе в функцию icmp_echo, которая вызывается на приход ICMPv4 пакета типа echo request. Ее первый аргумент, arg0 в нашей программе, это указатель на структуру типа sk_buff, описывающую пакет. Из этой структуры мы достаем IP адреса и увеличиваем соответствующий счетчик в глобальной переменной @pingstats. Все, теперь у нас есть полная статистика о том, кто и как часто пинговал наши IP адреса! Раньше для написания такой программы вам пришлось бы писать модуль ядра, регистрировать в нем обработчик kprobe, а также придумывать механизм взаимодействия с user space, чтобы хранить и читать статистику.


Посмотрим на нее еще раз. Вначале мы включили два хедера ядра, для этого нужно установить пакет с kernel headers. Эти хедеры нужны нам для определения структур sk_buff и iphdr, которые мы собираемся разыменовывать. (Если бы у нас был собран BTF, то нам не нужно было бы это делать ни устанавливать пакет, ни включать хедеры.) В первой строчке программы мы приводим единственный аргумент функции icmp_echo к указателю на sk_buff и сохраняем его в локальной переменной $skb. На второй строчке мы разыменовываем $skb и находим место в памяти, где хранится сетевой заголовок, который мы, в свою очередь, приводим к указателю на iphdr. На третьей строчке мы используем сетевой заголовок и встроенную функцию ntop языка bpftrace, которая преобразует бинарный IP адрес в строку.


Упражнение. Возьмите любую интересующую вас функцию ядра и попробуйте разыменовать ее аргументы. Не забывайте про uptr и kptr. Например: возьмите функцию vfs_write ядра, ее первый аргумент это указатель на структуру struct file, определенную в заголовке <linux/fs.h>. Попробуйте напечатать интересующие вас флаги файла до и после вызова vfs_write. (Hint: как вы можете передать указатель на struct file внутрь kretprobe?)


Упражнение. Напишите программу bpftrace, которая будет печатать имя и пароль пользователя, всякий раз, как он запускает sudo.


Считаем и агрегируем события


В предыдущей программе про ping мы сделали ошибку не защитились от того, что программа может быть запущена на разных CPU. Для более точного подсчета мы можем использовать функцию count. Следующий пример иллюстрирует проблему:


# bpftrace -e 'p:hz:5000 { @x++; @y = count(); } i:s:10 { exit(); }'Attaching 2 probes...@x: 760528@y: 799002

В течение 10 секунд по 5000 раз в секунду на каждом из 16 ядер моей системы мы увеличиваем значения двух счетчиков @x и @y. Операция ++ выполняется безо всяких блокировок и поэтому значение счетчика не совсем точное. Операция count() на самом деле выполняется тоже безо всяких блокировок, но использует CPU-локальные переменные: для каждого из CPU хранится свой счетчик, значения которых при печати суммируются.


Кроме подсчета событий в bpftrace есть несколько полезных функций, которые могут быстро показать какие-то баги в работающей системе. Главный инструмент тут это гистограммы. Посмотрим на простой пример.


# bpftrace -e 'kr:vfs_write { @ = hist(retval); } i:s:10 { exit() }'Attaching 2 probes...@:[1]             14966407 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[2, 4)                 0 |                                                    |[4, 8)                 0 |                                                    |[8, 16)             1670 |                                                    |[16, 32)               0 |                                                    |[32, 64)             123 |                                                    |[64, 128)              0 |                                                    |[128, 256)             0 |                                                    |[256, 512)             0 |                                                    |[512, 1K)              0 |                                                    |[1K, 2K)               0 |                                                    |[2K, 4K)               0 |                                                    |[4K, 8K)               0 |                                                    |[8K, 16K)        7531982 |@@@@@@@@@@@@@@@@@@@@@@@@@@                          |

В течении десяти секунд мы строим гистограмму возвращаемых значений функции vfs_write, и мы можем заметить, что кто-то уверенно пытается писать по одному байту! Давайте чуть усовершенствуем программу (то заняло у меня около 20 секунд):


# bpftrace -e '    kr:vfs_write /retval == 1/ { @[pid, comm] = hist(retval); }    i:s:10 { exit() }    END { print(@, 1); clear(@); }'Attaching 3 probes...@[133835, dd]:[1]             14254511 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|

Для начала, мы смотрим только на записи, в которых retval равен единице. Для того, чтобы различить процессы мы добавляем ключи идентификатор и имя процесса. Наконец, в END мы печатаем только процесс, который сделал больше всего записей. Что же он делает? Думаю вы уже догадались:


# tr '\000' ' ' < /proc/133835/cmdlinedd if=/dev/zero of=/dev/null bs=1

Упражнение. Найдите ссылку на официальный туториал по bpftrace в конце статьи и выполните все шаги, в которых встречаются гистограммы.


Веселые Картинки: flame graphs


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


profile:hz:rateprofile:s:rateprofile:ms:rateprofile:us:rate

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


Одной из интересных глобальных переменных, которые мы можем сэмплировать, является стек ядра, выглядит он так:


bpftrace -e 'profile:hz:99 { print(kstack); exit() }'Attaching 1 probe...        cpuidle_enter_state+202        cpuidle_enter+46        call_cpuidle+35        do_idle+487        cpu_startup_entry+32        rest_init+174        arch_call_rest_init+14        start_kernel+1724        x86_64_start_reservations+36        x86_64_start_kernel+139        secondary_startup_64+182

здесь в момент сэмплирования мы попали на ядро-бездельника.


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


Для начала, нам нужно запустить bpftrace следующим образом:


# bpftrace -e 'profile:hz:333 { @[kstack] = count(); } i:s:10 { exit() }' > /tmp/raw# wc -l /tmp/raw3374 /tmp/raw

Здесь мы по 333 раза в секунду сэмплируем стек ядра и считаем сколько раз мы увидели разные стеки (мы используем kstack как ключ в словаре @, ведь kstack это просто строка).


Далее нам нужно склонировать репозиторий FlameGraph и запустить пару скриптов:


$ git clone https://github.com/brendangregg/FlameGraph.git$ cd FlameGraph$ ./stackcollapse-bpftrace.pl /tmp/raw > /tmp/ready$ ./flamegraph.pl /tmp/ready > /tmp/kstack.svg

Первый скрипт приводит вывод bpftrace к каноническому виду, а второй строит по нему картинку (кликните на нее, чтобы открылся gist с SVG):



Здесь моя система наполовину бездействует, а на половине CPU крутится все тот же dd, копирующий /dev/zero в /dev/null по одному байту. Кликайте на картинку, чтобы посмотреть подробности.


Упражнение. Снимки стека можно делать не только при помощи bpftrace. Загляните в в репозиторий FlameGraph и сделайте снимок своей системы другим способом.


Пора закругляться


Если вы дочитали до этого момента и запустили хотя бы половину примеров, то вы уже можете считать себя профессионалом в нелегком деле отладки при помощи bpftrace. Смотрите на ссылки в конце статьи для того, чтобы узнать куда двигаться дальше.


BCC: утилиты и фреймворк


Благодаря проекту BCC люди, роботы и не определившиеся могут использовать возможности BPF без необходимости утруждать себя программированием проект содержит почти 100 готовых утилит. Эти утилиты не случайные примеры, а рабочие инструменты, используемые повседневно в недрах Netflix, Facebook и других компаний добра. См. ссылки на книжки БГ в конце статьи, в которых подробно описано большинство утилит и подробно обсуждается почему и зачем они нужны.


Утилиты написаны на основе libbcc и libbpf и представляют из себя код на питоне, в который встроены куски псевдо-C кода, так что их легко редактировать и расширять даже на боевой машине. Также вы можете писать новые утилиты по аналогии с существующими, см. следующий подраздел.


Утилиты BCC должны быть более-менее опакечены в популярных дистрибутивах. Например, на убунте достаточно поставить пакет bpfcc-tools. После этого мы можем сразу ими пользоваться. Например, команда


# funccount-bpfcc 'vfs_*' -d 5Tracing 67 functions for "b'vfs_*'"... Hit Ctrl-C to end.FUNC                                    COUNTb'vfs_statfs'                               1b'vfs_unlink'                               1b'vfs_lock_file'                            2b'vfs_fallocate'                           31b'vfs_statx_fd'                            32b'vfs_getattr'                             80b'vfs_getattr_nosec'                       88b'vfs_open'                               108b'vfs_statx'                              174b'vfs_writev'                            2789b'vfs_write'                             6554b'vfs_read'                              7363Detaching...

посчитает сколько раз были вызваны функции ядра с префиксом vfs_ на интервале в пять секунд. Чуть интереснее подсунуть программе параметр -p, в котором передается PID процесса. Например, вот что делает мой mplayer, пока я это пишу:


# funccount-bpfcc 'vfs_*' -d 5 -p 29380Tracing 67 functions for "b'vfs_*'"... Hit Ctrl-C to end.FUNC                                    COUNTb'vfs_write'                              208b'vfs_read'                               629Detaching...

Пишем новую утилиту BCC


Давайте напишем простую утилиту BCC. Эта утилита будет считать сколько раз в секунду были вызваны функции ядра mutex_lock и mutex_unlock. Ее полный код приведен далее, также вы можете прочитать его здесь.


#! /usr/bin/python3from bcc import BPFfrom ctypes import c_intfrom time import sleep, strftimefrom sys import argvb = BPF(text="""BPF_PERCPU_ARRAY(mutex_stats, u64, 2);static inline void inc(int key){    u64 *x = mutex_stats.lookup(&key);    if (x)        *x += 1;}void do_lock(struct pt_regs *ctx) { inc(0); }void do_unlock(struct pt_regs *ctx) { inc(1); }""")b.attach_kprobe(event="mutex_lock", fn_name="do_lock")b.attach_kprobe(event="mutex_unlock", fn_name="do_unlock")print("%-8s%10s%10s" % ("TIME", "LOCKED", "UNLOCKED"))while 2 * 2 == 4:    try:        sleep(1)    except KeyboardInterrupt:        exit()    print("%-8s%10d%10d" % (          strftime("%H:%M:%S"),          b["mutex_stats"].sum(0).value,          b["mutex_stats"].sum(1).value))    b["mutex_stats"].clear()

Вначале мы подключаем нужные библиотеки. Понятно, что самая интересная часть тут это импорт класса BPF:


from bcc import BPF

Этот класс позволяет нам определить программы BPF, которые мы будем подключать к событиям. В качестве параметра класс BPF принимает текст программы на псевдо-C. В нашем случае это


BPF_PERCPU_ARRAY(mutex_stats, u64, 2);static inline void inc(int key){    u64 *x = mutex_stats.lookup(&key);    if (x)        *x += 1;}void do_lock(struct pt_regs *ctx)   { inc(0); }void do_unlock(struct pt_regs *ctx) { inc(1); }

Этот код написан на магическом C, вы не сможете скомпилировать его в таком виде, но при инициализации класса BPF некоторые части будут заменены реальным кодом на C.
Так или иначе, вначале мы определяем массив mutex_stats из двух элементов типа u64, наших счетчиков. Заметьте, что мы использовали PERCPU массив, это означает, что для каждого логического CPU будет создан свой массив. Далее мы определяем функцию inc, принимающую в качестве аргумента индекс в массиве mutex_stats. Эта функция увеличивает значение соответствующего счетчика.
Наконец, тривиальные функции do_lock и do_unlock увеличивают каждая свой счетчик.


На этом с ядерной частью почти покончено во время инициализации класс BPF обратится к библиотеке libllvm, чтобы скомпилировать код, и потом зальет его в ядро. Осталось только подключить программы к интересующим нас kprobes:


b.attach_kprobe(event="mutex_lock", fn_name="do_lock")b.attach_kprobe(event="mutex_unlock", fn_name="do_unlock")

Пользовательская часть кода занимается исключительно сбором информации:


print("%-8s%10s%10s" % ("TIME", "LOCKED", "UNLOCKED"))while 2 * 2 == 4:    try:        sleep(1)    except KeyboardInterrupt:        exit()    print("%-8s%10d%10d" % (          strftime("%H:%M:%S"),          b["mutex_stats"].sum(0).value,          b["mutex_stats"].sum(1).value))    b["mutex_stats"].clear()

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


sum(index) {    result = 0    для каждого CPU {        result += cpu->mutex_stats[index]    }    return result}

Вот и все. Программа должна работать примерно так:


$ sudo ./bcc-tool-exampleTIME        LOCKED  UNLOCKED18:06:33     11382     1299318:06:34     11200     1278318:06:35     18597     2255318:06:36     20776     2551618:06:37     59453     6820118:06:38     49282     5806418:06:39     25541     2742818:06:40     22094     2528018:06:41      5539      725018:06:42      5662      735118:06:43      5205      690518:06:44      6733      8438

Где-то в 18:06:35 я переключился из терминала на вкладку с youtube в Firefox, поставил youtube на паузу и затем в 18:06:40 переключился назад в терминал. Итого, программа показала, что при просмотре youtube вы заставляете ядро локать примерно сорок тысяч мьютексов в секунду.


Напоследок хочется сказать, что если вы предпочитаете писать на C, то смотрите в сторону libbpf и CO-RE. Использование libbpf напрямую позволяет избавиться от тяжелых зависимостей времени запуска, типа libllvm, ускоряет время работы, а также экономит дисковое пространство. В частности, некоторые утилиты BCC уже переписаны на libbpf + CO-RE прямо внутри проекта BCC, см. libbpf-tools. За подробностями обращайтесь к статье BCC to libbpf conversion guide (или ждите следующую статью из этой серии).


Ply: bpftrace для бедных


Утилита ply, написанная шведом Tobias Waldekranz в доисторическом 2015 году, является в определенном смысле прямым предком bpftrace. Она поддерживает awk-подобный язык для создания и инструментации ядра программами BPF, например,


ply 'tracepoint:tcp/tcp_receive_reset {    printf("saddr:%v port:%v->%v\n", data->saddr, data->sport, data->dport);}'

Отличительной особенностью ply является минимизация зависимостей: ей нужна только libc (любая). Это удобно, если вы хотите с минимальными усилиями поиграться в BPF на встроенных системах. Для того, чтобы отрезать все зависимости, в ply встроен компилятор ply script language -> BPF.


Однако, не умаляя достоинств ply, стоит отметить, что разработка проекта к настоящему времени заглохла ply работает, поддерживается, но новые фичи не появляются. Вы все еще можете использовать ply, например, для того, чтобы потестировать сборку ядра на встроенной системе или для тестирования прототипов, но я бы советовал сразу писать программы на C с использованием libbpf для эмбедщиков это не составит труда, см., например, статью Building BPF applications with libbpf-bootstrap.


Ссылки


Предыдущие серии:



Ссылки на ресурсы по bpftrace, BCC и вообще отладке Linux:


  • The bpftrace One-Liner Tutorial. Это туториал по bpftrace, в котором перечисляются основные возможности. Представляет из себя список из двенадцати однострочных, или около того, программ.


  • bpftrace Reference Guide. Все, что вы хотели знать про использование bpftrace, но боялись спросить. Если вам этого документа мало, то идите читать про внутренности bpftrace.


  • BCC Tutorial. Если вы освоились с bpftrace и хотите копнуть глубже (но еще не готовы к освоению libbpf и настоящим приключениям), то смотрите на этот туториал по BCC, на BCC Reference Guide, а также на книжки, перечисленные ниже.


  • Brendan Gregg, BPF Performance Tools. БГ распознал потенциал BPF в деле трассировки Linux сразу после его появления и в данной книжке описывает результаты работы последних пяти лет сотню или больше отладочных утилит из проекта BCC. Книжка является отличным справочным дополнением по BPF к следующей.


  • Brendan Gregg, Systems Performance: Enterprise and the Cloud, 2nd Edition (2020). Это второе издание знаменитой Systems Performance. Главные изменения: добавлен материал по BPF, выкинут материал по Solaris, а сам БГ стал на пять лет опытнее. Если книжка BPF Performance Tools отвечает на вопрос как?, то эта книжка отвечает на вопрос почему?, а также рассказывает о других техниках (не BPF единым жив человек).


Подробнее..

NAPI в сетевых драйверах Linux

10.05.2021 20:18:52 | Автор: admin

Привет, Хабр!
Поговорим о драйверах сетевых устройств Linux, механизме NAPI и его изменениях в ядре 5.12.

Сетевая подсистема Linux (рисунок) построена по примеру стека BSD, в ней прием и передача данных на транспортном и сетевом уровнях происходит с помощью интерфейса сокетов. В отличие от unix-сокетов для межпроцессного взаимодействия, TCP/IP сокеты используют для работы сетевой протокол и при создании (sys_socket) принимают параметры домен, тип, локальные и удаленные IP-адрес и порт. Буфер сокета (sk_buff) - фактически, пакет. Связный список экземпляров таких структур составляет очередь сетевого интерфейса (tx_queue, rx_queue).

Упрощенно некоторые важные поля sk_buff:

struct sk_buff {union {struct {    /* Двусвязный список */struct sk_buff*next;struct sk_buff*prev;struct net_device*dev;};struct list_headlist;};struct sock*sk;unsigned intlen,data_len;__u16mac_len,hdr_len;/* Часть NAPI-интерфейса */#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)union {unsigned intnapi_id;unsigned intsender_cpu;};#endif__u8inner_ipproto;__u16inner_transport_header;__u16inner_network_header;__u16inner_mac_header;__be16protocol;__u16transport_header;__u16network_header;__u16mac_header;sk_buff_data_ttail;sk_buff_data_tend;unsigned char*head,*data;unsigned inttruesize;};

Драйвера отвечают за реализацию канального уровня (разрешение MAC-адресов) и предоставление интерфейса между системными вызовами ядра и сетевой картой. Обработка входящих и исходящих пакетов происходят с помощью функций xmit и rx, от одновременного доступа они защищены спин блокировками, как и обновление статистики stats и изменение параметров передачи. Сам интерфейс определяется структурой net_device, для создания и регистрации вызываются функции alloc_netdev и register_netdev.

Важные поля net_device:

struct net_device {charname[IFNAMSIZ];    // Строка в стиле printfunsigned longmem_end;unsigned longmem_start;unsigned longbase_addr;unsigned longstate;struct list_headdev_list;struct list_headnapi_list;unsigned intflags;unsigned intpriv_flags;const struct net_device_ops *netdev_ops;unsigned shorthard_header_len;unsigned intmtu;struct net_device_statsstats; atomic_long_trx_dropped;atomic_long_ttx_dropped;atomic_long_trx_nohandler;const struct ethtool_ops *ethtool_ops;const struct header_ops *header_ops;unsigned charif_port;unsigned chardma;/* Interface address info. */unsigned charperm_addr[MAX_ADDR_LEN];unsigned short          dev_id;unsigned short          dev_port;spinlock_taddr_list_lock;intirq;unsigned char*dev_addr;struct netdev_rx_queue*_rx;unsigned intnum_rx_queues;struct netdev_queue*_tx ____cacheline_aligned_in_smp;unsigned intnum_tx_queues;struct timer_listwatchdog_timer;intwatchdog_timeo;};

Сетевой драйвер похож на блочный: передает и получает данные по запросу, но блочные драйверы отвечают только на запросы ядра, а сетевые получают пакеты асинхронно извне. Долгое время в Linux, когда сетевое устройство просило поместить входящие пакеты в ядро, действовал механизм обработки аппаратных прерываний.

Схематичные действия в обработчике прерываний для очистки очереди входящих пакетов: (драйвер intel Ethernet e1000):

static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,  // Сетевое устройство       struct e1000_rx_ring *rx_ring, // Очередь входящих пакетов       int *work_done, int work_to_do){while (rx_desc->status & E1000_RXD_STAT_DD) {struct sk_buff *skb;u8 *data;u8 status;        if (netdev->features & NETIF_F_RXALL) {    total_rx_bytes += (length - 4);     total_rx_packets++;    e1000_receive_skb(adapter, status, rx_desc->special, skb);}     }if (cleaned_count)    // Создание нового буфераadapter->alloc_rx_buf(adapter, rx_ring, cleaned_count);    // Обновление статистикиadapter->total_rx_packets += total_rx_packets;adapter->total_rx_bytes += total_rx_bytes;netdev->stats.rx_bytes += total_rx_bytes;netdev->stats.rx_packets += total_rx_packets;return cleaned;}

До ядер версии 2.3 после самого обработчика прерывания (top half) для выполнения основных задач использовались нижние половины (bottom half) и очереди задач (task queue). Начиная с версии 2.3 на замену интерфейсу BH пришли отложенные прерывания (softirq), тасклеты (tasklet) и очереди отложенных действий (work queue). Преимущество softirq в том, что они могут одновременно выполняться на разных процессорах. Они напрямую используются в сетевой подсистеме.

Немного о NAPI

Пока сетевой трафик был умеренным, механизм прерываний при получении пакета эффективно справлялся со своей задачей. С ростом трафика и появлением высоконагруженных систем постоянная обработка прерываний стала приводить к нехватке процессорного времени для пользовательских программ и потере пакетов. Решение проблемы было предложено в 2001 году и появилось в виде интерфейса New API в ядрах серии 2.4. (В оригинальной статье результаты тестирования для SMP-системы, генератор трафика наподобие pktgen).

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

В NAPI-совместимых драйверах прерывания отключаются, когда на интерфейс приходит пакет. Обработчик в этом случае только вызывает rx_schedule, гарантирующий, что обработка пакетов произойдет в дальнейшем. Когда приходящие пакеты заполняют буфер (предельное количество budget), для обработки вызывается метод dev->poll. Метод poll будет вызываться одновременно не более, чем на одном процессоре, что упрощает синхронизацию. Если нагрузка падает, снова разрешаются прерывания. Это позволяет динамически регулировать производительность в зависимости от нагрузки интерфейса. Метод poll может использоваться также и для передачи пакетов.

Пример poll из драйвера e1000:

static void e1000_netpoll(struct net_device *netdev){struct e1000_adapter *adapter = netdev_priv(netdev);if (disable_hardirq(adapter->pdev->irq))e1000_intr(adapter->pdev->irq, netdev);enable_irq(adapter->pdev->irq);}

При реализации NAPI-совместимого драйвера должны быть выполнены некоторые требования:

  • Возможность хранения входящих пакетов в кольце DMA или буфере в самой карте

  • Возможность отключить прерывания

  • В методе poll должна быть реализована возможность забрать несколько пакетов за раз

  • Так как метод poll работает в контексте softirq и управляется демоном ksoftirqd, в системах с высокой загрузкой нужно менять приоритет поллинга для обеспечения баланса ресурсов между обработчиком прерываний и пользовательскими программами.

Недостатки NAPI:

  • В некоторых случаях в системе могуть быть задержки, если весь обработчик прерываний помещен в dev->poll

  • Маскировка прерываний может быть медленной

  • Возможно состояние IRQ-гонки, если пакет приходит во время проверки бита наличия новых пакетов и включения прерываний.

Что нового у NAPI в 5.12?

В серии патчей в ядре 5.12 метод poll из softirq контекста перенесен в поток ядра.

Wei Wang в комментарии к патчу рассказывает, что причина такого решения отсутствие возможности отследить программные прерывания в системе. Планировщик не может измерить время, затрачиваемое на обработку softirq. Поток ядра же видим для планировщика задач CPU, это позволит избежать перегрузки процессора, на котором он работает, и сделать планирование userspace-процессов более детерминированным. Его проще контролировать системному администратору. Kthread можно связать с определенной группой CPU, чтобы явно отделить пользовательские потоки от процессоров, опрашивающих сетевые интерфейсы.

Изменения затронули в основном net/core/dev.c. Обновлен метод __napi_poll, вызываемый из контекста napi_poll. Появился новый sysfs атрибут в net_device для включения/выключения поточного режима опроса для всех экземпляров napi данного сетевого устройства без необходимости вызова up/down.

В napi_struct добавлено поле threaded для реализации опроса внутри потока, причем для включения поддержки потоков после создания kthread нужно вызвать napi_set_threaded (флаг NAPI_STATE_THREADED).

Обновленная структура napi_struct:

struct napi_struct {        struct list_head        dev_list;        struct hlist_node       napi_hash_node;        unsigned int            napi_id;        struct task_struct      *thread; };

Создание потока ядра:

static int napi_kthread_create(struct napi_struct *n){       int err = 0;       /* Create and wake up the kthread once to put it in        * TASK_INTERRUPTIBLE mode to avoid the blocked task        * warning and work with loadavg.        */       n->thread = kthread_run(napi_threaded_poll, n, "napi/%s-%d",                               n->dev->name, n->napi_id);       if (IS_ERR(n->thread)) {               err = PTR_ERR(n->thread);               pr_err("kthread_run failed with err %d\n", err);               n->thread = NULL;       }       return err;}

В связи с добавлением поточности появился новый метод napi_thread_wait.

Wei Wang получил следующие результаты сравнения эффективности softirq, kthread и очередей отложенных действий:

Основные источники - LDD3 и статьи:

NAPI polling in kernel threads
Threadable NAPI polling, softirqs, and proper fixes
Reworking NAPI
Driver porting: Network drivers

Заранее спасибо за уточнения и указания на ошибки!

Подробнее..

Перехват и обработка событий в файловой системе Linux

20.01.2021 10:13:24 | Автор: admin

Введение

В предыдущей статье мы рассмотрели сборку и установку пакета на Linux системах, в которой упомянули про Linux Kernel Module (LKM) и обещали раскрыть позднее подробности о пути к нему и его создании. Ну что ж, настало его время. LKM мы выбираем тебя.

Необходимость реализации

"Windows драйвер мы заменили на Linux Kernel Module LKM" итак, вернёмся мысленно к самому началу пути. Мы имеем Windows драйвер, который обеспечивает отслеживание и перехват событий обращения к файлу. Как его перенести или чем заменить в Linux системах? Покопавшись в архитектуре, почитав про перехват и реализацию подобных технологий в Linux мы поняли, что задача абсолютно нетривиальная, содержащая кучу подводных камней.

Inotify

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

Virtual File System

Следующим этапом изучения стала возможность реализации перехвата обращений при помощи VFS.

После анализа VFS на основе Dtrace, eBPF и bcc, стало понятно, что при использовании данной технологии возможно выполнять мониторинг событий, происходящих в системе. В данном случае, перехват осуществляется через LKM. В рамках изучения реализации различных модулей под разные ядра выявлено следующее: перехват не всегда позволяет отследить полный путь к файлу; при перехвате обращения к файлу через открытое приложение, а не из проводника, отсутствует путь к файлу в аргументах; для каждого ядра необходима своя реализация.

Janus, SElinux и AppArmor

В ходе исследования, была найдена статья по расширению функциональности системы безопасности ядра Linux. Отсюда следует, что на рынке существует достаточное количество решений. Самым легко реализуемым является Janus. Минусом решения выступает отсутствие поддержки свежих ядер и все вышеописанные проблемы LKM хука. Реализация SELinux и AppArmor представляет квинтэссенцию всего описанного и изученного ранее. Модуль SELinux включает в себя основные компоненты: сервер безопасности; кэш вектора доступа (англ. Access Vector Cache, AVC); таблицы сетевых интерфейсов; код сигнала сетевого уведомления; свою виртуальную файловую систему (selinuxfs) и реализацию функций-перехватчиков.

Долгожданное решение

После всех этих бесконечных но, на помощь нам пришёл Хабр! Наткнувшись на статью, стало ясно, что это наш случай.

Обработка перехвата

Изучив предложенные данные по ftrace и реализации из самой статьи, сделали аналогичный LKM модуль на базе ftrace. Данная утилита, в свою очередь, работает на базе файловой системы debugfs, которая в большинстве современных дистрибутивов Linux смонтирована по умолчанию. Hook'и добавили на события к уже имеющимся clone и open: openat, rename, unlink, unlinkat. Таким образом, удалось обработать открытие, переименование, перемещение, копирование, удаление файла.

Взаимодействие

Теперь нам нужно реализовать связь между модулем ядра и приложением userspace. Для решения данной задачи существуют разные подходы, но в основном выделяют два: socket между kernel и userspace; запись/чтение в системной директории в файл.

В итоге, мы выбрали netlink socket, так как в Windows мы используем аналогичный интерфейс - FltSendMessage. Можно было использовать inet socket, но это наименее защищённое решение. Также столкнулись с такой проблемой, что на .Net Core, на которой реализовано userspace приложение, отсутствует реализация netlink.

Поэтому пришлось реализовывать динамическую библиотеку с реализацией netlink и уже её подключать в проект.

int open_netlink_connection(void){    //initialize our variables    int sock;    struct sockaddr_nl addr;    int group = NETLINK_GROUP;    //open a new socket connection    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK);    //if the socket failed to open,    if (sock < 0)     {        //inform the user        printf("Socket failed to initialize.\n");        //return the error value        return sock;    }    //initialize our addr structure by filling it with zeros    memset((void *) &addr, 0, sizeof(addr));    //specify the protocol family    addr.nl_family = AF_NETLINK;    //set the process id to the current process id    addr.nl_pid = getpid();    //bind the address to the socket created, and if it failed,    if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0)     {        //inform the user        printf("bind < 0.\n");        //return the function with a symbolic error code        return -1;    }    //set the option so that we can receive packets whose destination    //is the group address specified (so that we can receive the message broadcasted by the kernel)    if (setsockopt(sock, 270, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)) < 0)     {        //if it failed, inform the user        printf("setsockopt < 0\n");        //return the function with a symbolic error code        return -1;    }    //if we got thus far, then everything    //went fine. Return our socket.    return sock;}char* read_kernel_message(int sock){    //initialize the variables    //that we are going to need    struct sockaddr_nl nladdr;    struct msghdr msg;    struct iovec iov;    char* buffer[CHUNK_SIZE];    char* kernelMessage;    int ret;    memset(&msg, 0, CMSG_SPACE(MAX_PAYLOAD));    memset(&nladdr, 0, sizeof(nladdr));    memset(&iov, 0, sizeof(iov));    //specify the buffer to save the message    iov.iov_base = (void *) &buffer;    //specify the length of our buffer    iov.iov_len = sizeof(buffer);    //pass the pointer of our sockaddr structure    //that will save the source IP and port of the connection    msg.msg_name = (void *) &(dest_addr);    //give the size of our structure    msg.msg_namelen = sizeof(dest_addr);    //pass our scatter/gather I/O structure pointer    msg.msg_iov = &iov;    //we will pass only one buffer array,    //therefore we will specify that here    msg.msg_iovlen = 1;    //listen/wait for new data    ret = recvmsg(sock, &msg, 0);    //if message was received successfully,    if(ret >= 0)    {        //get the string data and save them to a local variable        char* buf = NLMSG_DATA((struct nlmsghdr *) &buffer);        //allocate memory for our kernel message        kernelMessage = (char*)malloc(CHUNK_SIZE);        //copy the kernel data to our allocated space        strcpy(kernelMessage, buf);        //return the pointer that points to the kernel data        return kernelMessage;    }        //if we got that far, reading the message failed,    //so we inform the user and return a NULL pointer    printf("Message could not received.\n");    return NULL;}int send_kernel_message(int sock, char* kernelMessage){    //initialize the variables    //that we are going to need    struct msghdr msg;    struct iovec iov;    char* buffer[CHUNK_SIZE];        int ret;    memset(&msg, 0, CMSG_SPACE(MAX_PAYLOAD));    memset(&iov, 0, sizeof(iov));    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);    nlh->nlmsg_pid = getpid();    nlh->nlmsg_flags = 0;    char buff[160];    snprintf(buff, sizeof(buff), "From:DSSAgent;Action:return;Message:%s;", kernelMessage);    strcpy(NLMSG_DATA(nlh), buff);    iov.iov_base = (void *)nlh;    iov.iov_len = nlh->nlmsg_len;    //pass the pointer of our sockaddr structure    //that will save the source IP and port of the connection    msg.msg_name = (void *) &(dest_addr);    //give the size of our structure    msg.msg_namelen = sizeof(dest_addr);    msg.msg_iov = &iov;    msg.msg_iovlen = 1;    printf("Sending message to kernel (%s)\n",(char *)NLMSG_DATA(nlh));    ret = sendmsg(sock, &msg, 0);    return ret;}int sock_netlink_connection(){sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);    if (sock_fd < 0)        return -1;    memset(&src_addr, 0, sizeof(src_addr));    src_addr.nl_family = AF_NETLINK;    src_addr.nl_pid = getpid(); /* self pid */    bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr));    memset(&dest_addr, 0, sizeof(dest_addr));    dest_addr.nl_family = AF_NETLINK;    dest_addr.nl_pid = 0; /* For Linux Kernel */    dest_addr.nl_groups = 0; /* unicast */    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);    nlh->nlmsg_pid = getpid();    nlh->nlmsg_flags = 0;    strcpy(NLMSG_DATA(nlh), "From:DSSAgent;Action:hello;");    iov.iov_base = (void *)nlh;    iov.iov_len = nlh->nlmsg_len;    msg.msg_name = (void *)&dest_addr;    msg.msg_namelen = sizeof(dest_addr);    msg.msg_iov = &iov;    msg.msg_iovlen = 1;    printf("Sending message to kernel\n");    sendmsg(sock_fd, &msg, 0);    printf("Waiting for message from kernel\n");    /* Read message from kernel */    recvmsg(sock_fd, &msg, 0);    printf("Received message payload: %s\n", (char *)NLMSG_DATA(nlh));return sock_fd;}void sock_netlink_disconnection(int sock){close(sock);    free(nlh);}

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

char* get_username_by_pid(int pid){   register struct passwd *pw;  register uid_t uid;  int c;  FILE *fp;  char filename[255];  sprintf(filename, "/proc/%d/loginuid", pid);  char cc[8];    // чтение из файла  if((fp= fopen(filename, "r"))==NULL)    {        perror("Error occured while opening file");        return "";    }  // считываем, пока не дойдем до конца  while((fgets(cc, 8, fp))!=NULL) {}       fclose(fp);    uid = atoi(cc);  pw = getpwuid (uid);  if (pw)  {      return pw->pw_name;  }  else  {      return "";  }}

Доработка модуля

По итогу добавили соединение по netlink в инициализацию LKM.

static int fh_init(void){    int err;struct netlink_kernel_cfg cfg ={#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 6, 0).groups = 1,#endif.input = nl_recv_msg,};#if LINUX_VERSION_CODE > KERNEL_VERSION(2, 6, 36)nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);#elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 32)nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, 0, nl_recv_msg, NULL, THIS_MODULE);#elsenl_sk = netlink_kernel_create(NETLINK_USER, 0, nl_recv_msg, THIS_MODULE);#endifif (!nl_sk){printk(KERN_ERR "%s Could not create netlink socket\n", __func__);return 1;}err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));if (err)return err;p_list_hook_files = (tNode *)kmalloc(sizeof(tNode), GFP_KERNEL);p_list_hook_files->next = NULL;p_list_hook_files->value = 0;pr_info("module loaded\n");return 0;}module_init(fh_init);static void fh_exit(void){delete_list(p_list_hook_files);fh_remove_hooks(hooks, ARRAY_SIZE(hooks));netlink_kernel_release(nl_sk);pr_info("module unloaded\n");}module_exit(fh_exit);

Socket ожидает перехвата события обращения к файлу. Модуль, перехватывая событие, передаёт имя файла, pid и имя процесса. Userspace приложение, получая данную информацию, обрабатывает её и отвечает, что делать с файлом (блокировать или разрешать доступ). Впоследствии модуль возвращает соответствующий системный вызов.

static void send_msg_to_user(const char *msgText){int msgLen = strlen(msgText);struct sk_buff *skb = nlmsg_new(NLMSG_ALIGN(msgLen), GFP_KERNEL);if (!skb){printk(KERN_ERR "%s Allocation skb failure.\n", __func__);return;}struct nlmsghdr *nlh = nlmsg_put(skb, 0, 1, NLMSG_DONE, msgLen, 0);if (!nlh){printk(KERN_ERR "%s Create nlh failure.\n", __func__);nlmsg_free(skb);return;}NETLINK_CB(skb).dst_group = 0;strncpy(nlmsg_data(nlh), msgText, msgLen);int errorVal = nlmsg_unicast(nl_sk, skb, pid);if (errorVal < 0)printk(KERN_ERR "%s nlmsg_unicast() error: %d\n", __func__, errorVal);}static void return_msg_to_user(struct nlmsghdr *nlh){pid = nlh->nlmsg_pid;const char *msg = "Init socket from kernel";const int msg_size = strlen(msg);struct sk_buff *skb = nlmsg_new(msg_size, 0);if (!skb){printk(KERN_ERR "%s Failed to allocate new skb\n", __func__);return;}nlh = nlmsg_put(skb, 0, 0, NLMSG_DONE, msg_size, 0);NETLINK_CB(skb).dst_group = 0;strncpy(nlmsg_data(nlh), msg, msg_size);int res = nlmsg_unicast(nl_sk, skb, pid);if (res < 0)printk(KERN_ERR "%s Error while sending back to user (%i)\n", __func__, res);}

Позднее, для работы другого приложения в данный модуль, добавили возможность блокирования доступа к определённому файлу (по его полному пути) для всех процессов, кроме определённого (определяем по pid процесса).

static void parse_return_from_user(char *return_msg){char *msg = np_extract_value(return_msg, "Message", ';');const char *file_name = strsep(&msg, "|");printk(KERN_INFO "%s Name:(%s) Permiss:(%s)\n", __func__, file_name, msg);if (strstr(msg, "Deny"))reload_name_list(p_list_hook_files, file_name, Deny);elsereload_name_list(p_list_hook_files, file_name, Allow);}static void free_guards(void){// Possibly unpredictable behavior during cleaningmemset(&guards, 0, sizeof(struct process_guards));}static void change_guards(char *msg){char *path = np_extract_value(msg, "Path", ';');char *count_str = np_extract_value(msg, "Count", ';');if (path && strlen(path) && count_str && strlen(count_str)){int i, found = -1;for (i = 0; i < guards.count; ++i)if (guards.process[i].file_path && !strcmp(path, guards.process[i].file_path))found = i;guards.is_busy = 1;int count;kstrtoint(count_str, 10, &count);if (count > 0){if (found == -1){strcpy(guards.process[guards.count].file_path, path);found = guards.count;guards.count++;}for (i = 0; i < count; ++i){char buff[8];snprintf(buff, sizeof(buff), "Pid%d", i + 1);char *pid = np_extract_value(msg, buff, ';');if (pid && strlen(pid))kstrtoint(pid, 10, &guards.process[found].allow_pids[i]);elseguards.process[found].allow_pids[i] = 0;}guards.process[found].allow_pids[count] = 0;}else{if (found >= 0){for (i = found; i < guards.count - 1; ++i)guards.process[i] = guards.process[i + 1];guards.count--;}}guards.is_busy = 0;}}// Example message is "From:CryptoCli;Action:clear;" or "From:DSSAgent;Action:init;"static void nl_recv_msg(struct sk_buff *skb){printk(KERN_INFO "%s <--\n", __func__);struct nlmsghdr *nlh = (struct nlmsghdr *)skb->data;printk(KERN_INFO "%s Netlink received msg payload:%s\n", __func__, (char *)nlmsg_data(nlh));char *msg = (char *)nlmsg_data(nlh);if (msg && strlen(msg)){char *from = np_extract_value(msg, "From", ';');char *action = np_extract_value(msg, "Action", ';');if (from && strlen(from) && action && strlen(action)){if (!strcmp(from, "DSSAgent")){if (!strcmp(action, "init")){return_msg_to_user(nlh);}else if (!strcmp(action, "return")){parse_return_from_user(msg);}else{printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);}}else if (!strcmp(from, "CryptoCli")){if (!strcmp(action, "clear")){free_guards();}else if (!strcmp(action, "change")){change_guards(msg);}else{printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);}}else{printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);}}else{printk(KERN_ERR "%s Failed parse msg, don`t found \"From\" and \"Action\" (%s)\n", __func__, msg);}}else{printk(KERN_ERR "%s Failed parse struct nlmsg_data, msg is empty\n", __func__);}printk(KERN_INFO "%s -->\n", __func__);}static bool check_file_access(char *fname, int processPid){if (fname && strlen(fname)){int i;for (i = 0; i < guards.count; ++i){if (!strcmp(fname, guards.process[i].file_path) && guards.process[i].allow_pids[0] != 0){int j;for (j = 0; guards.process[i].allow_pids[j] != 0; ++j)if (processPid == guards.process[i].allow_pids[j])return true;return false;}}// Not found filename in guardsif (strstr(fname, filetype)){char *processName = current->comm;printk(KERN_INFO "%s service pid = %d\n", __func__, pid);printk(KERN_INFO "%s file name = %s, process pid: %d, , process name = %s\n", __func__, fname, processPid, processName);if (processPid == pid){return true;}else{add_list(p_list_hook_files, processPid, fname, None);char *buffer = kmalloc(4096, GFP_KERNEL);sprintf(buffer, "%s|%s|%d", fname, processName, processPid);send_msg_to_user(buffer);kfree(buffer);ssleep(5);bool ret = true;if (find_list(p_list_hook_files, fname) == Deny)ret = false;delete_node(p_list_hook_files, fname);return ret;}}}return true;}

Интеграция в процесс установки

Так как первые два минуса LKM удалось преодолеть через реализацию ftrace, третий никто не отменял. Мало того, что под каждое ядро нужна сборка модуля, уже в процессе использования он может протухнуть. Было принято решение добавить его пересборку перед каждым запуском userspace приложения. В статье по сборке Linux пакетов было описано, что службу, для которой мы реализовываем обработку перехвата обращения к файлу, мы демонизировали путём добавления в system. Поэтому для демона.service добавляем два дополнительных пункта, помимо ExecStart и ExecStop будут:

ExecStartPre=/bin/sh /путь_до_расположения/prestart.shExecStopPost=/sbin/rmmod имя_модуля.ko

а в сам prestart.sh:

#!/bin/shMOD_VAL=$(lsmod | grep имя_модуля | wc -l)cd /путь_до_расположения_модуляmake cleanmake allif [ $MOD_VAL = 1 ]then    for proc in $(ps aux | grep DSS.Agent | awk '{print $2}'); do kill -9 $proc; doneelse    /sbin/insmod / путь_до_расположения_модуля/имя_модуля.kofi

Заключение

В завершение, хочется отметить: возможно, путь, по которому мы пошли, не самый красивый и элегантный, но, он содержит отработанную и проверенную логику работы на ОС Windows. Было бы полезно услышать в комментариях мнение читателей статьи. Возможно, есть более разумное решение задачи. Например, наш DevOps, в тот момент, когда мы автоматизировали сборку пакета Linux и обрабатывали/добавляли LKM, предложил реализовать логику с использованием Access Control List (ACL). Скорее всего, в дальнейшем мы займёмся переработкой нашего продукта под Linux. И, да, скоро будет новая статья, о том, как мы переносили MS Forms на Avalonia и его интеграции в Linux.

Ссылки которые нам помогли

Подробнее..

Эволюция системы обновления Android

17.12.2020 06:12:58 | Автор: admin
Индиана устанавливает новый активный раздел: меняет золотого идола на мешок с песком в легендарной сцене из фильма Индиана Джонс: В поисках утраченного ковчегаИндиана устанавливает новый активный раздел: меняет золотого идола на мешок с песком в легендарной сцене из фильма Индиана Джонс: В поисках утраченного ковчега

В этой статье мы рассмотрим все возможные варианты обновления прошивки на устройствах под управлением Fuchsia Android. Особое внимание уделим самому популярному способу обновлению по воздуху или OTA (over-the-air) и расскажем об этапах его развития.

Итак, как можно обновить Android на мобильных устройствах? Занимаясь разработкой ТВ-приставок под управлением этой ОС, мы определили для себя 4 способа, отбросив совсем уж экзотические варианты:

  1. перепрошивка flash-памяти через аппаратный интерфейс JTAG (если есть);

  2. перепрошивка flash-памяти с использованием загрузчика (bootloader);

  3. обновление через Recovery Mode;

  4. OTA (over-the-air).

Рассмотрим подробнее каждый из вариантов.

1. Обновление Android через JTAG-интерфейс

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

Однако обычно JTAG-интерфейс присутствует только на отладочных платах, что сильно сужает область применения этого варианта обновится.

2. Обновление Android через Recovery Mode

Обычно загрузчик является проприетарным, его разрабатывает производитель чипа. Именно bootloader инициализирует доверенную среду выполнения (TEE, trusted execution environment) и проверяет целостность разделов boot и recovery перед переносом выполнения в ядро Linux. Сам загрузчик часто является составным, часть его уровней может быть открытой (например, на базе U-boot), а часть проприетарной.

Bootloader Android позволяет перепрошивать flash-память устройства подготовленными образами разделов. Для этого используется протокол fastboot либо его аналог (в случае Amlogic это будет протокол WorldCup Device). Fastboot,как и его аналог WorldCup Device, это протокол взаимодействия с bootloader через USB-интерфейс или локальную сеть Ethernet.

Для перепрошивки необходимо подключить устройство через USB к хосту (есть вариант использовать LAN Ethernet), перевести загрузчик (bootloader) в специальный update-режим и в этом режиме перепрошить flash-память устройства.

Плюсы и минусы данного метода всё те же, что и для JTAG: так как обновление проходит без участия самой системы Android, при перепрошивке нет ограничений, связанных с версией системы/сборки или ключами безопасности.

Но, как всегда, есть одно НО. :-) Bootloader должен быть разблокирован, а это значит, что мы можем перепрошить сам загрузчик или разделы устройства. Блокировка/разблокировка производится командой fastboot flashing lock/unlock, но для выполнения этой команды может понадобится пароль, установленный тем, кто добрался до этого устройства раньше вас (обычно это производитель).

3. Обновление Android через Recovery Mode и OTA

Если первые два варианта обновления оставались неизменными на протяжении всего времени развития Android, то следующие два варианта обновление через Recovery Mode и OTA реализуются средствами самой Android и эволюционировали вместе со всей ОС.

Стоит упомянуть, что Recovery Mode и OTA это два различных варианта вызова движка обновления Android.

Recovery или non-A/B System Updates

Recovery и движок обновления updater (bootable/recovery/updater) это как раз то, с чего началась система обновления Android (располагается в bootable/recovery в дереве исходников AOSP).

Схема обновления Recovery (или non-A/B System Updates) задействует специальный раздел восстановления (Recovery), где содержится специальная ОС на основе ядра Linux. Эта ОС на базе Linux содержит программное обеспечение для распаковки загруженного образа обновления и его применения к другим разделам. Так и проходит обновление Android.

Пример разметки flash-памяти на устройстве с Android 6.0:

Карта разделов Android 6.0.1

[mmcblk0p01] bootloader offset 0x000000000000, size 0x000000400000

[mmcblk0p02] reserved offset 0x000002400000, size 0x000004000000

[mmcblk0p03] cache offset 0x000006c00000, size 0x000020000000

[mmcblk0p04] env offset 0x000027400000, size 0x000000800000

[mmcblk0p05] logo offset 0x000028400000, size 0x000002000000

[mmcblk0p06] recovery offset 0x00002ac00000, size 0x000002000000

[mmcblk0p07] rsv offset 0x00002d400000, size 0x000000800000

[mmcblk0p08] tee offset 0x00002e400000, size 0x000000800000

[mmcblk0p09] crypt offset 0x00002f400000, size 0x000002000000

[mmcblk0p10] misc offset 0x000031c00000, size 0x000002000000

[mmcblk0p11] instaboot offset 0x000034400000, size 0x000020000000

[mmcblk0p12] boot offset 0x000054c00000, size 0x000002000000

[mmcblk0p13] system offset 0x000057400000, size 0x000060000000

[mmcblk0p14] data offset 0x0000b7c00000, size 0x0002ec200000

Сам процесс обновления происходит в два этапа:

  1. после загрузки с раздела Recovery происходит обновления всех остальных разделов Android;

  2. и уже после перезагрузки и запуска новой версии Android происходит обновление раздела Recovery.

При обновлении с использованием движка updater на первом этапе проверяется версия и цифровая подпись образа, поэтому откатить на старую версию ОС уже не получится.

Обновиться по схеме Recovery можно как локально, выбрав в bootloader режим Recovery Mode и запустив движок обновления updater через меню Recovery Mode, либо удаленно, через OTA, когда приложение, работающее в Android, вызывает тот же updater из Java. И как раз при таком удаленном запуске можно организовать массовое обновление целой серии устройств. Этот вариант используют операторы цифрового ТВ при обновлении своих абонентских ТВ-приставок.

Сам раздел Recovery при non-A/B-схеме обновления является физическим разделом во flash-памяти. С появлением A/B-схемы раздел Recovery переместился на RAM-диск в оперативной памяти устройства, но возможность сделать его отдельным физическим разделом так же осталась.

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

Одним из важных недостатков схемы Recovery или non-A/B System Updates является то, что при любом сбое во время обновления или битой прошивке мы получаем пусть и не кирпич (с раздела Recovery всё еще можно запустить устройство в Recovery Mode), но всё же не полнофункциональное и требующее восстановления устройство.

С этим, видимо, решено было что-то делать, потому что следующим этапом эволюции системы обновления стало бесшовное обновление (seamless updates) или A/B-схема обновления.

Бесшовное обновление или A/B-схема

Эта возможность появилась в Android 7.0, она реализована в новом движке update_engine, который располагается в system/update_engine в дереве исходников AOSP.

Главной особенностью A/B-схемы стало то, что в случае сбоев при обновлении можно загрузится с предыдущей рабочей версии системы Android. Flash-память устройства содержит дублирующиеся системные разделы или слоты (slot A и B), отсюда и название A/B system updates (вечная проблема с выбором названий). За выбор слота для загрузки (A или B) отвечает bootloader, анализируя состояние слотов.

Принцип бесшовного обновления Android по A/B-схеме (активный раздел отмечен птичкой)Принцип бесшовного обновления Android по A/B-схеме (активный раздел отмечен птичкой)

Итак, как же происходит обновление:

1) Загружая систему, например, со слотов A, мы скачиваем и прошиваем обновления на слоты B.

2) После перезагрузки со слотов B мы проверяем работоспособность системы, и, если все ОК, сообщаем bootloader, что обновление прошло успешно.

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

На официальном сайте для разработчиков Android Source этот процесс расписан более детально в 9 шагах, также там объясняется, как все работает после перезагрузки.

Особенность бесшовной A/B-схемы обновление это съедение большего объема flash- памяти. Насколько большего? Это можно оценить по приведенным ниже схемам разделов для Android 9.0. Как уже упоминалось ранее, разработчик может выбирать, какую из схем A/B или non-A/B применять в конфигурации системы.

Карта разделов Android P (recovery)

[mmcblk0p01] bootloader offset 0x000000000000, size 0x000000400000

[mmcblk0p02] reserved offset 0x000002400000, size 0x000004000000

[mmcblk0p03] cache offset 0x000006c00000, size 0x000046000000

[mmcblk0p04] env offset 0x00004d400000, size 0x000000800000

[mmcblk0p05] logo offset 0x00004e400000, size 0x000000800000

[mmcblk0p06] recovery offset 0x00004f400000, size 0x000001800000

[mmcblk0p07] misc offset 0x000051400000, size 0x000000800000

[mmcblk0p08] dtbo offset 0x000052400000, size 0x000000800000

[mmcblk0p09] cri_data offset 0x000053400000, size 0x000000800000

[mmcblk0p10] param offset 0x000054400000, size 0x000001000000

[mmcblk0p11] boot offset 0x000055c00000, size 0x000001000000

[mmcblk0p12] rsv offset 0x000057400000, size 0x000001000000

[mmcblk0p13] metadata offset 0x000058c00000, size 0x000001000000

[mmcblk0p14] vbmeta offset 0x00005a400000, size 0x000000200000

[mmcblk0p15] tee offset 0x00005ae00000, size 0x000002000000

[mmcblk0p16] vendor offset 0x00005d600000, size 0x000040000000

[mmcblk0p17] odm offset 0x00009de00000, size 0x000008000000

[mmcblk0p18] system offset 0x0000a6600000, size 0x000050000000

[mmcblk0p19] product offset 0x0000f6e00000, size 0x00000800000

Карта разделов Android P (A/B-схема)

[mmcblk0p01] bootloader offset 0x000000000000, size 0x000000400000

[mmcblk0p02] reserved offset 0x000002400000, size 0x000004000000

[mmcblk0p03] cache offset 0x000006c00000, size 0x000000000000

[mmcblk0p04] env offset 0x000007400000, size 0x000000800000

[mmcblk0p05] logo offset 0x000008400000, size 0x000000800000

[mmcblk0p06] boot_a offset 0x000009400000, size 0x000001000000

[mmcblk0p07] misc offset 0x00000ac00000, size 0x000000800000

[mmcblk0p08] dtbo_a offset 0x00000bc00000, size 0x000000800000

[mmcblk0p09] dtbo_b offset 0x00000cc00000, size 0x000000800000

[mmcblk0p10] cri_data offset 0x00000dc00000, size 0x000000800000

[mmcblk0p11] param offset 0x00000ec00000, size 0x000001000000

[mmcblk0p12] boot_b offset 0x000010400000, size 0x000001000000

[mmcblk0p13] rsv offset 0x000011c00000, size 0x000001000000

[mmcblk0p14] metadata_a offset 0x000013400000, size 0x000001000000

[mmcblk0p15] metadata_b offset 0x000014c00000, size 0x000001000000

[mmcblk0p16] vbmeta_a offset 0x000016400000, size 0x000000200000

[mmcblk0p17] vbmeta_b offset 0x000016e00000, size 0x000000200000

[mmcblk0p18] tee offset 0x000017800000, size 0x000002000000

[mmcblk0p19] vendor_a offset 0x00001a000000, size 0x000040000000

[mmcblk0p20] vendor_b offset 0x00005a800000, size 0x000040000000

[mmcblk0p21] odm_a offset 0x00009b000000, size 0x000008000000

[mmcblk0p22] odm_b offset 0x0000a3800000, size 0x000008000000

[mmcblk0p23] system_a offset 0x0000ac000000, size 0x000050000000

[mmcblk0p24] system_b offset 0x0000fc800000, size 0x000050000000

[mmcblk0p25] product_a offset 0x00014d000000, size 0x000008000000

[mmcblk0p26] product_b offset 0x000155800000, size 0x000008000000

[mmcblk0p27] data offset 0x00015e000000, size 0x000245e00000

Если сравнить эти две конфигурации, то можно заметить, что раздел data при A/B-схеме меньше на 1,6 ГБ, и это цена дублирующихся системных разделов. Много это или мало каждый решает сам, ориентируясь на характеристики своего устройства/проекта.

Проект Treble

Следующие изменения в системе обновления произошли в Android 8.0. Начиная с Android O (8.0) и продолжая в Android P (9.0), Google реализует свой проект Treble. Идея проекта состоит в том, чтобы упростить технологический процесс создания обновления для андроид-устройства. Google предложил разделить с помощью неизменных интерфейсов части прошивки, созданием которых занимаются разные компании. Процесс разработки прошивки для конкретного девайса можно упрощенно разделить на следующие шаги:

  1. команда Android создает новую версию своей OC;

  2. разработчик чипа или системы на кристалле (Silicon Manufacturer) создает аппаратно-зависимые патчи для запуска этой версии Android на своих платах;

  3. и уже разработчики конечного устройства (Vendors) делают свою часть для реализации всех функций конкретного продукта для рынка электроники.

Проект Treble разделяет ОС Android с дополнениями от производителей чипов/СнК и код разработчика конечного устройства, так что теперь операционная система может получать обновления без реализации изменений от производителя устройства.

Разделение происходит как с помощью программного интерфейса (переход с Hardware Abstraction Layer 1.0 на HAL2.0), так и за счет выделения отдельных разделов на flash-памяти для Silicon Manufacturer и Vendor (выше в карте разделов Android 9.0 можно увидеть разделы odm, vendor, product).

Переход с HAL1.0 на HAL2.0 заключается в отказе от прямого связывания с системными библиотеками. Вместо этого, используя IPC Binder, можно подключаться к системным сервисам.

И еще одно небольшое, но полезное изменение: начиная с Android 8.0, в update_engine добавлена поддержка потоковых обновлений по A/B-схеме, в ходе которых идет прямая запись в слот B без необходимости промежуточного хранения данных в /data. Для таких потоковых обновлений практически не требуется временное хранилище, достаточно всего лишь 100 килобайт для сохранения метаданных.

При этом необходимо, чтобы http-сервер, используемый для скачивания обновления, поддерживал HTTP range requests или другими словами докачку.

Проект Mainline

Следующим серьезным этапом в развитии системы обновления Android стал проект Mainline. Реализация этого проекта началась с Android 10.0 и продолжилась в текущем Android 11.0.

Проект Mainline позволяет обновлять отдельные системные компоненты без обновления ОС целиком. Нужные данные загружаются через Google Play отдельно от OTA-обновления прошивки от производителя. Предполагается, что прямая доставка обновлений, не привязанных к оборудованию частей Android, позволит существенно сократить время получения обновлений, увеличить оперативность исправления уязвимостей и снизить зависимость от производителей устройств в поддержке безопасности ОС.

Для реализации проекта Mainline выбранные компонентов системы Android преобразуется в модули. Часть этих модулей имеет старый формат APK, а часть конвертируется в новый APEX-формат, который отличается от APK возможностью применения на раннем этапе загрузки системы. На случай возможных сбоев предусмотрен режим отката изменений.

С APEX-пакетами работает системный сервис APEX manager (apexd). Это нативный сервис, который после проверки распаковывает APEX-пакет в пользовательское пространство на диске и добавляет запись о нем в свою базу данных. При следующей загрузке системы APEX manager проверяет все пакеты из базы данных, создает loop-устройство для ext4-образа каждого APEX-пакета и монтирует его по пути /apex/name@ver.

Модули с обновлениями изначально будут поставляться с открытым кодом, они будут сразу доступны в репозиториях AOSP (Android Open Source Project) и смогут включать улучшения и исправления, подготовленные сторонними участниками.

В рамках проекта Mainline в Android 10 было добавлено 13 обновляемых модулей, а в Android 11 в дополнение к уже существующим прибавилось еще 11 модулей.

Схема Virtual A/B

Также в Android 11 к схемам non-A/B и A/B была добавлена схема Virtual A/B. Этот новый механизм обновления сочетает преимущества обоих предшественников, он обеспечивает устойчивое к сбоям обновление устройства, задействуя при этом минимальный объем flash-памяти. Это стало возможным благодаря созданию снимков файловой системы (snapshot) с использованием технологии Device-mapper (подсистема ядра Linux, позволяющая создавать виртуальные блочные устройства) и Dynamic Partitions.

Dynamic Partitions это система организации динамических разделов для Android. С ее помощью можно создавать, изменять размер или уничтожать разделы прямо в процессе обновления по воздуху (OTA). При использовании динамических разделов разработчикам больше не нужно беспокоиться о размере отдельных разделов, таких как system, vendor и product. Вместо них на устройстве выделяется суперраздел, внутри которого можно динамически изменять размер подразделов. Больше нет необходимости оставлять свободное пространство для будущих OTA-обновлений внутри отдельных образов разделов. Оставшееся свободное место в суперразделе теперь доступно для всех динамических подразделов.

И в заключении последние слухи конца 2020 года вишенка на торте. Google конвертирует Android Runtime в модуль Mainline. Android Runtime или ART это среда выполнения Android-приложений, включающая компиляцию байт-кода приложения в машинные инструкции. Так что есть вероятность, что уже в Android 12 можно будет обновить ART через GooglePlay, установив APEX-пакет.

Также, вероятно, система обновления Android мигрирует в Fuchsia, новую ОС Google, которая сейчас находится в процессе разработки. Они традиционно копируют удачные решения в своих программных продуктах. Так, например, update_engine для A/B-схемы, который применяется сейчас в Android, используется в еще одной ОC Google Chrome OS. Или еще один пример: в Fuchsia предлагается библиотека Machina, которая позволяет запускать Linux-программы в специальной изолированной виртуальной машине по аналогии с тем, как организован запуск Linux-приложений в той же Chrome OS.

Желаем всем успешных обновлений!

P.S. Как там было в Индиане Джонсе?
Как вы меня узнали?
У вас глаза вашего отца.
И уши моей матери. Но все остальное принадлежит вам.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru