Pull to refresh

Управляемый PageFault в ядре Linux

Reading time 7 min
Views 14K
Обработка исключений занимает важное место в процессе функционирования программных систем. Действительно, обеспечение своевременной и правильной реакции на нештатные события является одной из ключевых задач, выполняемых операционной системой и, в особенности, её ядром. Будучи современным, ядро Linux предоставляет возможность управления процессом обработки исключений, однако ввиду ограниченности его интерфейса, данный механизм не является распространённым среди разработчиков модулей ядра.

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



Исключения в ядре



В качестве примера того, где и как в ядре используются исключения, стоит рассмотреть копирование данных между пространством ядра и пространством пользователя. Обычно, за это отвечают функции copy_from_user и copy_to_user, особенностью работы которых и отличием от memcpy является то, что они корректно обрабатывают исключения, возникающие в процессе пересылки данных между различными адресными пространствами.

Действительно, если рассмотреть ситуацию, когда осуществляется копирование данных из ядра пользователю (функция copy_to_user), то возможно возникновение ситуаций, когда страница процесса пользователя, в которую осуществляется попытка записи, находится в свопе или вообще недоступна процессу. И если в первом случае корректным решением проблемы будет подгрузить данную страницу и продолжить выполнение копирования, то во втором случае необходимо прервать операцию и вернуть пользователю код ошибки (например, -EINVAL).

Очевидно, что выполнение команды, осуществляющей обращение по адресу, соответствующему отсутствующей странице, вызывает исключение, а именно — исключение отказа страницы, или Page Fault (#PF). В этот момент, ядро сохраняет контекст текущей задачи и выполняет код соответствующего обработчика — do_page_fault. Так или иначе, устранив проблему, ядро восстанавливает контекст прерванной задачи. Однако, в зависимости от результата обработки исключения, адрес возврата может отличаться от адреса той инструкции, которая и была причиной исключения. Другими словами, благодаря предусмотренному в ядре механизму, существует возможность задать для потенциально «опасной» инструкции адрес, с которого будет продолжена работа в случае исключения, генерируемого при её выполнении.

Интерфейс обработки исключений



Чтобы понять как реализован обозначенный механизм, стоит рассмотреть реализацию примитива копирования 4 байтов из ядра пользователю — функцию __put_user_4:

62 ENTRY(__put_user_4)
63         ENTER
64         mov TI_addr_limit(%_ASM_BX),%_ASM_BX
65         sub $3,%_ASM_BX
66         cmp %_ASM_BX,%_ASM_CX
67         jae bad_put_user
68         ASM_STAC
69 3:      movl %eax,(%_ASM_CX)    <- здесь может возникнуть исключение
70         xor %eax,%eax
71         EXIT
72 ENDPROC(__put_user_4)
           ...
89 bad_put_user:
90         CFI_STARTPROC
91         movl $-EFAULT,%eax
92         EXIT
          ...
98         _ASM_EXTABLE(3b,bad_put_user)


Как видно, помимо проверки диапазона адресов, данная функция осуществляет непосредственно пересылку данных (инструкция movl в строке 69). Именно здесь можно ожидать исключения, т.к. кроме того, что целевой адрес действительно принадлежит диапазону адресов пространства пользователя, более о нём ничего не известно. Далее, стоит обратить внимание на макрос _ASM_EXTABLE, который представляет собой следующее:

43 # define _ASM_EXTABLE(from,to)                                  \
44         .pushsection "__ex_table","a" ;                         \
45         .balign 8 ;                                             \
46         .long (from) - . ;                                      \
47         .long (to) - . ;                                        \
48         .popsection


Действие этого макроса сводится к тому, чтобы добавить в специальную секцию __ex_table два значения — from и to, которые, как не трудно заметить, соотносятся с адресами «подозрительной» инструкции в строке 69 и инструкции, с которой будет продолжено выполнение, после обработки исключения, а именно — bad_put_user. Добавление записи в таблицу __ex_table делает точку отказа управляемой, т.к. данная таблица используется ядром при обработке исключений.

Таблицы исключений и порядок их обработки



Итак, как было отмечено, таблица исключений представляет собой центральное место, где хранится информация о тех инструкциях, ошибку в ходе выполнения которых нужно обрабатывать отдельно. Забегая вперёд, стоит отметить, что помимо таблицы самого ядра, для каждого модуля также предусмотрена индивидуальная таблица. Однако, сейчас стоит рассмотреть строение её элемента, описываемого структурой exception_table_entry:

97 struct exception_table_entry {
98         int insn, fixup;
99 };


Как видно, формат элемента таблицы соответствует тому, что было выявлено при рассмотрении макроса _ASM_EXTABLE. Первый элемент описывает инструкцию, второй — код, управление которому будет передано в случае возникновения исключения. Каждый раз при возникновении исключения отказа страницы ядро Linux, помимо прочего, проверяет, содержится ли адрес команды, вызвавшей это исключение, в таблице __ex_table ядра, или же в одной из таблиц загруженных модулей. Если такая запись найдена, то выполняется соответствующее действие. В противном случае, ядро выполняет какую-то стандартную логику завершения обработки исключения.

Что касается индивидуальных таблиц исключений модулей ядра, то формат элементов этих таблиц стандартный и соответствует оному для ядра. Ссылка на такую таблицу для каждого модуля доступна по указателю THIS_MODULE->extable, тогда как число элементов таблицы содержится в переменной THIS_MODULE->num_exentries. Сам же макрос THIS_MODULE даёт ссылку на структуру-описатель модуля:

223 struct module
224 {
...
276         /* Exception table */
277         unsigned int num_exentries;
278         struct exception_table_entry *extable;
...
378 };


Далее представлена ключевая функция ядра, которая выполняет поиск обработчика, соответствующего вызвавшей исключение инструкции. Вот её код:

 50 /* Given an address, look for it in the exception tables. */
 51 const struct exception_table_entry *search_exception_tables(unsigned long addr)
 52 {
 53         const struct exception_table_entry *e;
 54 
 55         e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
 56         if (!e)
 57                 e = search_module_extables(addr);
 58         return e;
 59 }


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

Обработка исключений в модуле ядра



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

Итак, пусть генерацией PageFault исключения занимается функция, которая делает обычный NULL pointer dereference:

static void raise_page_fault(void)
{
        debug("    %s enter\n", __func__);

        ((int *)0)[0] = 0xdeadbeef;

        debug("    %s leave\n", __func__);
}


Очевидно, что попытка записи по нулевому указателю приведёт к падению. А это именно то, что нужно. Для того, чтобы правильно отреагировать необходимо:

  • определить адрес инструкции, вызывающей исключение
  • создать валидный элемент exception_table_entry
  • добавить созданный элемент в таблицу extable модуля


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

static int fixup_page_fault(struct exception_table_entry * entry)
{
        ud_t ud;

        ud_initialize(&ud, BITS_PER_LONG, \
                      UD_VENDOR_ANY, (void *)raise_page_fault, 128);

        while (ud_disassemble(&ud) && ud.mnemonic != UD_Iret) {
                if (ud.mnemonic == UD_Imov && \
                    ud.operand[0].type == UD_OP_MEM && ud.operand[1].type == UD_OP_IMM)
                {
                        unsigned long address = \
                                (unsigned long)raise_page_fault + ud_insn_off(&ud);

                        extable_make_insn(entry, address);
                        extable_make_fixup(entry, address + ud_insn_len(&ud));

                        return 0;
                }
        }

        return -EINVAL;
}


Как видно, первым делом настраивается дизассемблер (начало анализа — raise_page_fault). Далее, с заданной глубиной поиска осуществляется перебор команд. Искомая команда (то, во что транслируется операция ((int *)0)[0] = 0xdeadbeef;) представляет собой обычный movl $0xdeadbeef, 0 с первым операндом типа UD_OP_MEM и вторым — типа UD_OP_IMM. Как только адрес команды найден, происходит формирование элемента таблицы. При этом, выполняются функции:

static void extable_make_insn(struct exception_table_entry * entry, unsigned long addr)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,5,0)
        entry->insn = (unsigned int)((addr - (unsigned long)&entry->insn));
#else
        entry->insn = addr;
#endif
}

static void extable_make_fixup(struct exception_table_entry * entry, unsigned long addr)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,5,0)
        entry->fixup = (unsigned int)((addr - (unsigned long)&entry->fixup));
#else
        entry->fixup = addr;
#endif
}


Первая из них, формирует адрес инструкции в структуре. Вторая — адрес фиксапа, т.е. команды, на которую будет передаваться управление. Важно отметить, что начиная с ядра 3.5 в структуре exception_table_entry произошли небольшие изменения, а именно была уменьшена размерность её полей — insn и fixup для 64-битных архитектур. Это позволило сократить требуемый для хранения адресов объём памяти, однако немного поменялась логика расчёта. Так, после ядра 3.5, поля insn и fixup хранят 32-битные значения, соответствующие смещениям адресов относительно данных элементов. Для тех, кому интересно привожу коммит, который всё попортил 706276543b699d80f546e45f8b12574e7b18d952.

Заключение



Приведённый пример демонстрирует возможность управляемой обработки исключений в ядре Linux с использованием модуля ядра. В тестовом примере исключительная ситуация (PageFault) вызывалась в предварительно подготовленном окружении, а именно настроенной таблице exables модуля. Последнее обстоятельство позволило исключить аварийное завершение и продолжить выполнение программы со следующей за аварийной инструкцией команды.

Кроме того, подготовленный тестовый пример, позволяет оценить возможность обработки и некоторых других исключений, таких как division error (#DE) и undefined opcode (#UD):

struct {
        const char * name;
        int (* fixup)(struct exception_table_entry *);
        void (* raise)(void);

} exceptions[] = {
        {
                .name = "0x00 - div0 error (#DE)",
                .fixup = fixup_div0_error,
                .raise = raise_div0_error,
        },
        {
                .name = "0x06 - undefined opcode (#UD)",
                .fixup = fixup_undefined_opcode,
                .raise = raise_undefined_opcode,
        },
        {
                .name = "0x14 - page fault (#PF)",
                .fixup = fixup_page_fault,
                .raise = raise_page_fault,
        },
};
Tags:
Hubs:
+32
Comments 2
Comments Comments 2

Articles