Pull to refresh

Comments 37

Зачем использовать труднопереносимый между платформами сплайсинг, если в Kallsyms есть адрес таблицы сисколов?
В статье описывается метод перехвата функций ядра с использованием патчинга пролога. Под сплайсингом обычно понимают немного другой метод встраивания. А что именно вы хотели сказать, говоря о том, что в Kallsyms есть адрес «таблицы сисколов»?..
Эм. Сплайсинг — это замена инструкций пролога на переход (обычно jmp ADDR или push ADDR; ret) и создание «трамплина», обратно, содержащего в т. ч. код этого самого пролога и переход на оставшуюся часть функции (если вы не на каком-нибудь арме с фиксированной длиной инсструкций, настоятельно рекомендуется прикрутить дизассемблер длин, а то может случиться конфуз). Судя по картинке со схемой и листинге ассемблерного кода используется именно он, правда без каких-либо попыток понять размер затираемых инструкций, что ещё сильнее снижает переносимость (соберут ядро каким-нибудь clang с флагами оптимизации, а там в прологе будут другие инструкции).
Далее. Функции ядра из юзерспейса вызываются посредством механизма системных вызовов (syscall), адреса которых хранятся в sys_call_table. Поскольку при необходимости перехватить что-то в ядре, обычно нужно перехватить именно вызовы из юзерспейса, бывает достаточно заменить в этой таблице адрес искомой функции. Версии ядра начиная с 2.6 не экспортируют напрямую символ sys_call_table, поэтому искать его следует в Kallsyms.
На моей памяти, сплайсинг — это когда CALL xx.xx.xx.xx меняют на CALL yy.yy.yy.yy осуществляя при этом перехват без необходимости порчи структуры кода. Ну да не в этом суть.

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


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

Функции ядра из юзерспейса вызываются посредством механизма системных вызовов (syscall), адреса которых хранятся в sys_call_table. Поскольку при необходимости перехватить что-то в ядре, обычно нужно перехватить именно вызовы из юзерспейса, бывает достаточно заменить в этой таблице адрес искомой функции. Версии ядра начиная с 2.6 не экспортируют напрямую символ sys_call_table, поэтому искать его следует в Kallsyms.


Перехват системных вызовов — отдельная тема и я о ней ещё напишу. Но, поверьте, перехвата только лишь сисколов не достаточно для решения большого класса задач. Кроме того, перехват единичного системного вызова вполне может быть реализован посредством перехвата реализующей его функции (см., например sys_open). Да и искать sys_call_table через Kallsyms не лучший тон :)
Ссылка на вики — это убедительно :) Для себя я буду называть сплайсингом именно «бесшовный» патчинг.
Предварительно я почитал топ выдачи гугла по запросу «перехват сплайсинг». Убедился, что определение из wiki общепринятое, не противоречит найденным статьям.
В конце-концов, какая разница — патчинг он и есть патчинг!
Я конечно извиняюсь! (с)

>на SMP-системах, поток, выполняющийся на одном из процессоров
> и там же снимающий бит WP, может быть прерван и перемещён на другой процессор!
Но почему бы не менять значение регистра cr0 в той самой stop_machine?
Почему бы просто не обрамить критическую секцию preempt_disable();… preempt_enable()?
для SMP эти функции бесполезны, они также бесполезны при PREEMPT_ENABLE=n
для SMP эти функции бесполезны

Почему вдруг? Вызов preempt_disable гарантирует, что скедулер не будет вызван до тех пор, пока preempt_enable не уменьшит счётчик запрета скедулирования до 0. А единственный способ обычному ядерному коду быть переключенным на другой процессор — это через скедулер.

они также бесполезны при PREEMPT_ENABLE=n

При CONFIG_PREEMPT=n ядерный код не может рескедулиться в произвольных точках, только в тех где он сам засыпает/вызывает скедулер.
все очень просто — прерывание и ваш код останавливается в произвольной точке, потом возвращается но не обязательно тому самом процессору, его может продолжить выполнять уже другой процессор, в обсуждаемом вопросе это приведет к проблемам.

в случае однопроцессорной системы вызов preempt_disable блокирует прерывания, в случае smp — нет
прерывание и ваш код останавливается в произвольной точке, потом возвращается но не обязательно тому самом процессору

Вы неправы. Посмотрите на кода возврата из обработчика прерываний. Например, для x86 мы попадаем в ret_from_intr, а оттуда в resume_kernel, который в случае CONFIG_PREEMPT тут. Посмотрите на проверку значения __preempt_count и обход вызова скедулера (preempt_schedule_irq) если этот счётчик ненулевой. Дальше мы попадаем в restore_all, который восстанавливает регистры и возвращается из прерывания.

в случае однопроцессорной системы вызов preempt_disable блокирует прерывания

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

В чем я действительно не прав:
preempt_disable вообще не блокирует прерывания, ни в smp ни в up

что бы не тонуть дальше в деталях, я напомню что preempt_disable не запрещает ядру прекинуть исполнение кода на другой процессор в smp системе, а stop_machine гаранитирует исполнение кода только одним процессором
В чем я действительно не прав: «в случае однопроцессорной системы вызов preempt_disable блокирует прерывания, в случае smp — нет»
на самом деле preempt_disable вообще не блокирует прерывания, ни в smp ни в up
так только увеличение счетчиков, блокировки прервыний нет и не было

В том и смысл: preempt_disable не запрещает прерывания, только скедулирование.

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

Разумеется. Тут вы правы.

что бы не тонуть дальше в деталях

Детали важны. Простой вопрос: если после прерывания ядерного кода он может продолжить своё выполнение на другом процессоре, то куда вернётся процессор, на котором этот код прервали?
Я в этом коде довольно хорошо ориентируюсь, и могу достаточно подробно рассказать, что там происходит и зачем. Дорисовать вашу картину мира, если вам интересно.

напомню что preempt_disable не запрещает ядру прекинуть исполнение кода на другой процессор в smp системе

Вы опять повторяете своё ошибочное утверждение. Запрещает. Именно в этом его смысл. Зачем по-вашему нужен preempt_disable?

stop_machine гаранитирует исполнение кода только одним процессором

Вообще-то stop_machine принимает маску процессоров, на которых нужно выполненить заданную функцию.
>Простой вопрос: если после прерывания ядерного кода он может продолжить своё выполнение на другом процессоре, то куда вернётся процессор, на котором этот код прервали?
это зависит от желания левой пятки ядра, никакой гарантии что код будет выполнятся тем же процессором нет и для 99% задач вообще неважно какой процессор будет их продолжать.
это важно ТОЛЬКО для нашей очень специфической задачи, потому что мы изменяем регистр конкретного процессора, для того что бы обойти защиту страницы памяти, если код будет исполнятся на другом процессоре, у которого этот флаг не сброшен, произойдет Oops

>Вы опять повторяете своё ошибочное утверждение. Запрещает. Именно в этом его смысл. Зачем по-вашему нужен preempt_disable?
>Дорисовать вашу картину мира, если вам интересно
в таком случае, объясните мне, пожалуйста, почему preempt_disable этому будет препятствовать, особенно когда при PREEMPT_ENABLE=n этот макрос развернется в обычный барьер?
это зависит от желания левой пятки ядра

Я не буду вас просить сказать поконкретнее, потому что очевидно, что вы не знаете, как происходит этот выбор.

почему preempt_disable этому будет препятствовать, особенно когда при PREEMPT_ENABLE=n этот макрос развернется в обычный барьер

Потому что ядро собранное с CONFIG_PREEMPT=n переключает контекст внутри ядерного кода только при явном вызове schedule (или какого-нибудь блокирующего ожидания, которое вызывает schedule внутри себя); при возврате из прерывания переключения контекста нет вообще, прерывания всегда возвращаются в то место, которое было прервано.
В случае CONFIG_PREEMPT=y возврат из прерывания проверяет счётчик __preempt_count как я вам показывал выше. Если счётчик нулевой будет вызван скедулер. Если во время обработки прерывания произошло что-то, что активизировало задачу с более высоким приоритетом, чем та, что выполнялась, скедулер среди прочего заменит указатель стека со стека прерванной задачи на стек новой задачи. Так что при возврате из прерывания будет загружен новый указатель стека и регистры восстановлены с него (это как раз левая пятка и есть).
То есть, вы хотите сказать, что при CONFIG_PREEMPT=n, в случае возникновения прерывания и после его обработки, код продолжит выполнятся тем же ядром всегда и везде?

а в случае включенного примита, возможны миграции, но их-то мы и отключаем с помощью preemt_disable?
продолжит выполнятся тем же ядром (под ядром я подразумеваю core — виртуальный cpu) всегда и везде?
при CONFIG_PREEMPT=n, в случае возникновения прерывания и после его обработки, код продолжит выполнятся тем же ядром

Если прерывание было в коде ядра — да, именно так. При возврате в юзерспейс скедулинг вызывается вне зависимости от CONFIG_PREEMPT.

а в случае включенного примита, возможны миграции, но их-то мы и отключаем с помощью preemt_disable?

Да.
А зачем так сложно? ftrace/dtrace уже не?
с каких пор из ринг3 давали модифицировать области памяти находящиеся в ринг0 ??
Через /dev/mem и /dev/kmem — очень давно, чуть ли не с самого начала.
ftrace/dtrace предназначен исключительно для манипуляций в юзерспейсе,
Видимо у хабра опять туго с тегом сарказм
А зачем их модифицировать? Топик, вроде, называется «перехват»?
Интересно: изменение кода через альтернативный мэппинг, и ни слова о когерентности кешей.
На x86 PIPT кеш? Общий для кода и данных? Если нет, то кто обеспечивает то, что по оригинальному
адресу ядерного кода который вы поменяли будет виден ваш jmp?
Хороший вопрос. Я экспериментировал с stop_machine и оказалось, что вроде бы её достаточно. По крайней мере, убрав из кода патчинга вызов CPUID я ничего плохого не заметил :)
вроде бы её достаточно

Prepare for unforeseen consequences. Реально, проблемы с кешем — это не то, что легко отладить на железе. Поэтому может быть лучше потратить дополнительное время на теоретическое исследование, чем потом на анализ странных багрепортов.
Посмотрел, как осуществляется синхронизация в ftrace. Действительно, там вызывается CPUID для каждого ядра (sync_core). Наверное, стоит дополнить пример.
Разве для вашей задачи не подошла бы функциональность sourceware.org/systemtap/kprobes/? Можно было бы не заниматься error-prone архитектурно-специфичными вещами.
Метод с kprobes зависит от наличия опции CONFIG_KPROBES, указанной при сборке ядра целевой системы.
А мне кажется, что совершенно приличная, интересная статья.
Но комментарии — это какой-то кураж… долбоёпов!
Одна из интереснейших серий статей… на общем фоне довольно посредственных.
И наехали, затюкали автора — о чём попало, только не о том, о чём написана статья.
Sign up to leave a comment.