Написание OpenBSD Loadable Kernel Modules (LKM)

Материал из OpenBSD-Wiki
Перейти к навигации Перейти к поиску

Написание OpenBSD Loadable Kernel Modules (LKM)

Данная статья 100 % копипаст с www.openbsd.ru

Является вольным переводом статьи OpenBSD Loadable Kernel Modules By Peter Werner [mailto: peter_a_werner@yahoo.com peter_a_werner@yahoo.com] оригинал

Введение

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

Недостаток LKM — потенциальная дыра в безопасности. Для загрузки модулей securelevelядра должен быть меньше 0. Если требуется загрузить модуль, то необходимо указать это в rc.securelevel. Если вы разрабатываете модуль, то необходимо установить securelevel = −1, таким образом можно загружать и выгружать модуль в любое время.

Взаимодействие с /dev/lkm выполняется через вызовы ioctl(2). В основном используется modload(8), modunload(8) и modstat(8) для загрузки, выгрузки и получения статистики.

Интерфейс lkm определяет пять типов модулей:

  • System calls
  • Virtual File System
  • Device Driver
  • Execution Interpreter
  • Miscellaneous

System calls — заменяет системные вызовы. Все системные вызовы могут быть заменены, но стоит обратить внимания на ioctl, так как он используется для загрузки и выгрузки модуля. После выгрузки модуля замененный системный вызов возвращается на исходный.
Virtual File System — добавляет виртуальные файловые системы.
Device Driver — добавляет новые символьные или блочные устройства.
Execution Interpreter — добавляет код для загрузки и выполнения двоичных файлов, которые не поддерживаются системой, примером могут быть эмуляции выполнения двоичных файлов различных операционных систем.
Miscellaneous — в этот тип входит все что не описано выше, при этом следует учесть, что при замене системных вызовов, возвращение в исходное состояние должно осуществляться кодом модуля.

Обычно модуль состоит из трех частей:

  1. Обработчик загрузки, выгрузки модуля
  2. Внешняя точка входа, используемая modload
  3. Функциональный код модуля

Модуль System Call

Здесь мы добавим новый системный вызов который будет выводить аргументы вызова. Прототип функции:

int syscall(int, char *)

Внутреннее описание lkm структуры выглядит так:

struct lkm_syscall {
    MODTYPE    lkm_type;            /* тип модуля, в данном случае LM_SYSCALL */  
    int        lkm_ver;             /* версия lkm интерфейса */
    сhar       *lkm_name;           /* имя модуля */
    u_long     lkm_offset;          /* положение системного вызова в таблице */
    struct sysent   *lkm_sysent;    /* указатель на struct sysent системного вызова */
    struct sysent   lkm_oldent;  /* место для копии содержимого таблицы вызовов до загрузки */
};

Инициализация происходит используя макрос MOD_SYSCALL:

MOD_SYSCALL("ourcall", -1, &newcallent);    /* устанавливает имя "ourcall", положение в таблице системных вызовов,
                                             * в данном случае -1 т.е. не имеет значение, положение определяется автоматически,
                                             * newcallent - данные системного вызова
                                             */

Так же должны описать обработчик загрузки/выгрузки модуля. Обработчики устанавливают функции исполняемые при загрузке, выгрузке и получении статистики. Если обработчик не требуется просто указывается функция lkm_nofunc() для данного типа события.

Внешняя точка входа модуля ourcall использует макрос DISPATCH:

int
ourcall(lkmtp, cmd, ver)
    struct lkm_table *lkmtp;
    int              cmd;
    int              ver;
{
    DISPATCH(lkmtp, cmd, ver, ourcall_handler, ourcall_handler, lkm_nofunc);    
}

где ourcall_handler — функция обработчик, а cmd — вид команды, который может принимать значение:

  • LKM_E_LOAD — загрузка
  • LKM_E_UNLOAD — выгрузка
  • LKM_E_STAT — получение статистики

Собственно код модуля (ourcall.c):

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/cdefs.h>
#include <sys/conf.h>
#include <sys/mount.h>
#include <sys/exec.h>
#include <sys/lkm.h>
#include <sys/proc.h>
#include <sys/syscallargs.h>


/* прототип нашего системного вызова */
int newcall (struct proc *, void *, int *);      /* Все системные вызовы имеют три аргумента: Указатель на 
                                                  * struct proc - процесс который вызывает, указатель void на аргументы и 
                                                  * указатель на возвращаемое значение.
                                                  */
 /* аргументы системного вызова */
 struct newcall_args{
      syscallarg(int) value;
      syscallarg(char *) msg;
 };

 /* 
  * Определяем наш системный вызов. Первый аргумент кол-во аргументов, второй - размер аргументов, 
  * третий флаги - SY_NOLOCK или SY_MPSAFE, и четвертый - собственно функция системного вызова.
  */

 static struct sysent newcallent = {2, sizeof(struct newcall_args), 0, newcall};

 /*
  * Инициализируем внутреннюю структуру нашего модуля 
  */    

 MOD_SYSCALL("ourcall", -1, &newcallent);

 /*
  * Обработчик загрузки/выгрузки модуля
  */

 static int
 ourcall_handler(struct lkm_table *lkmtp, int cmd)
 {
      if (cmd == LKM_E_LOAD) 
      printf("hi!\n");
      else if (cmd == LKM_E_UNLOAD)
           printf("bye!\n");

      return(0);
 }

 /* 
  * Внешняя точка входа модуля, используется для загрузки, выгрузки и получения статистики через modload(8),
  * modunload(8) и modstat(8). Устанавливаем функцию обработчик ourcall_handler, вызываемую при загрузке и
  * выгрузке модуля. 
  */

 int
 ourcall(struct lkm_table *lkmtp, int cmd, int ver)
 {
      DISPATCH(lkmtp, cmd, ver, ourcall_handler, ourcall_handler, lkm_nofunc);
 }

 /* 
  * Собственно сам код системного вызова
  */

 int
 newcall(struct proc *p, void *v, int *retval)
 {
      struct newcall_args *uap = v;
      printf("newcall called with msg %s and value %d\n", SCARG(uap, msg), SCARG(uap, value));
      return(0);
 }

Компиляция и установка:

# cc -D_KERNEL -I/sys -c ourcall.c -o syscall.o
# modload -o ourcall.o -eourcall syscall.o
Module loaded as ID 0

# modtstat
Type     Id Off Loadaddr Size Info     Rev Module Name
SYSCALL   0 210 d76cd000 0001 d76cd1a0   2 ourcall

# dmesg | tail -2
hi!
DDB symbols added: 372192 bytes

Протестировать модуль можно при помощи следующей программы (testourcall.c):

#include <stdio.h>
#include <stdlib.h>

static void      usage(void);

int
main(int argc, char *argv[])
{
    int err = 0;
    int syscall_num = 0;

    if (argc != 2)
        usage();

    if ((syscall_num = atoi(argv[1]))>0)
        if (err = syscall(syscall_num, 10, "testourcall"))
            errx(err,"syscall");
}

static void  usage(void){
    extern char *__progname;
    (void)fprintf(stderr, "usage: %s value\n", __progname);
    exit(1);
}

Соберем программу:

# make testcall
cc -O2 -pipe    -o testourcall testourcall.c

Запустим и получим результат(210 — значение offset, полученное через modstat(8)):

# ./testourcall 210
# dmesg | tail -1
newcall called with msg testourcall and value 10

Чтобы выгрузить модуль:

# modunload -n ourcall

Получим результат:

# dmesg | tail -1
bye!

Модуль Virtual File System

Добавление виртуальной файловой системы очень простая задача. Необходимо лишь добавить точку входа.

Структура vfs модуля выглядит так:

struct lkm_vfs {
    MODTYPE lkm_type;               /* тип модуля, в данном случае LM_VFS */
    int     lkm_ver;                /* версия lkm интерфейса */
    char    *lkm_name;              /* имя модуля */
    u_long  lkm_offset;             /* положение vfs */
    struct vfsconf  *lkm_vfsconf;   /* указатель на vfs операции */
};

В этом случае положение (offset) не используется.

Структура инициализируется через макрос MOD_VFS:

MOD_VFS("nullfs", -1, &nullfs_vfsconf)

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

Рассмотрим все вышеописанное на примере nullfs

код модуля (null_lkm.c):

#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/mount.h>
#include <sys/exec.h>
#include <sys/lkm.h>
#include <sys/file.h>
#include <sys/errno.h>

extern struct vfsops null_vfsops;
int nullfs_lkmentry(struct lkm_table *, int, int);

/* Определяем вектор vfs операций */

#define MOUNT_NULLFS    "nullfs"
#define FS_NULLFS   22

struct vfsconf null_vfsconf = {
    &null_vfsops, MOUNT_NULLFS, FS_NULLFS, 0, 0, NULL
};

/*
 * Инициализируем внутреннюю структуру нашего модуля 
 */    
MOD_VFS("nullfs", -1, &null_vfsconf)

/* 
 * Внешняя точка входа модуля, используется для загрузки, выгрузки и получения статистики через modload(8),
 * modunload(8) и modstat(8). Устанавливаем lkm_nofunc, т.к. vfs подсистема сама занимается инициализацией и т.д.
 */

int
nullfs_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
    DISPATCH(lkmtp, cmd, ver, lkm_nofunc, lkm_nofunc, lkm_nofunc);
}

Компиляция и загрузка:

# cd lkm/
# make
# make load
# modstat
Type     Id Off Loadaddr Size Info     Rev Module Name
VFS       0  -1 d80b7000 0002 d80b85e4   2 nullfs

Утилита монтирования nullfs системы находится bin/mount_nullfs Код модуля доступен через cvs, адрес: http://www.openbsd.ru/cgi-bin/cvsweb/src/lkm/nullfs

Модуль Device Driver

Модуль драйвера устройств имеет схожий вид модулем системных вызовов. Описывается как:

struct lkm_dev {
    MODTYPE lkm_type;                 /* тип модуля, в данном случае LM_SYSCALL */
    int     lkm_ver;                  /* версия lkm интерфейса */  
    char    *lkm_name;                /* имя модуля */
    u_long  lkm_offset;               /* положение устройства в таблице cdevsw[] или bdevsw[] */
    DEVTYPE lkm_devtype;              /* тип устройства: блочное или символьное, значения LM_DT_CHAR или LM_DT_BLOCK */
    union {
            void    *anon;
            struct bdevsw   *bdev; 
            struct cdevsw   *cdev;
    } lkm_dev;                        /* функции устройства */
    union
    {
            struct bdevsw bdev;
            struct cdevsw cdev;
    } lkm_olddev;                    /* функции заменяемого устройства, при загрузке */
};

Инициализация происходит через макрос MOD_DEV:

MOD_DEV("ourdev", LM_DT_CHAR, -1, &cdev_ourdev);    /* устанавливает имя символьного устройства с параметрами из cdev_ourdev  */

Наше устройство будет поддерживать четыре вида операций: open, close, read и ioctl. Функционально хранит строку и число которое может быть установлено через ioctl, поддерживает чтение строки через read. Внутренне описывается как:

#define MAXMSGLEN 100

struct ourdev_io {
    int value;
    char msg[MAXMSGLEN];
};

При загрузке модуля мы инициализируем value с значением 13 и msg — «hello world!» как строку. Так же определяем два ioctl получения и установления значений. Оба вызова принимают аргумент ourdev_io.

Код устройства (chardev.c):

#include <sys/param.h>
#include <sys/fcntl.h>
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/exec.h>
#include <sys/conf.h>
#include <sys/lkm.h>


/* 
 * Прототипы поддерживаемых операций - open, close, read and ioctl 
 */

int ourdevopen (dev_t, int, int, struct proc *);
int ourdevclose (dev_t, int, int, struct proc *);
int ourdevread (dev_t, struct uio *, int);
int ourdevioctl (dev_t, u_long, caddr_t, int, struct proc *);
int ourdev_handler (struct lkm_table *, int);

/* 
 * Объявляем внутреннюю структуру устройства
 */

#define MAXMSGLEN 100

struct ourdev_io {
    int value;
    char msg[MAXMSGLEN];
};

static struct ourdev_io dio;

/* 
 * Здесь инициализируем вектор операций устройства. 
 */

cdev_decl(ourdev);
static struct cdevsw cdev_ourdev = cdev_ourdev_init(1, ourdev);


/* 
 * Инициализируем lkm интерфейс
 */

MOD_DEV("ourdev", LM_DT_CHAR, -1, &cdev_ourdev)

/*
 * Действия при открытие
 */

int
ourdevopen(dev_t dev, int oflags, int devtype, struct proc *p)
{
    printf("device opened, hi!\n");
    return(0);
}

/*
 * Действия при закрытии
 */
int
ourdevclose(dev_t dev, int oflags, int devtype, struct proc *p)
{
    printf("device closed! bye!\n");
    return(0);
}

/*
 * Действия при чтении
 */

int
ourdevread(dev_t dev, struct uio *uio, int ioflag)
{
    int resid = MAXMSGLEN;
    int error = 0;
    do {
        if (uio->uio_resid < resid)
            resid = uio->uio_resid;

        error = uiomove(dio.msg, resid, uio);   
    } while (resid > 0 && error == 0);

    return(error);
}

/*
 * Действия на ioctl вызовы 
 */

int
ourdevioctl(dev_t dev, u_long cmd, caddr_t data, int fflag, struct proc *p)
{
    struct ourdev_io *d;
    int error = 0;

    switch(cmd) {
        case ODREAD: /* чтение параметрова */
            d = (struct ourdev_io *)data;
            d->value = dio.value;
            error = copyoutstr(&dio.msg, d->msg, MAXMSGLEN - 1, NULL);
            break;
        case ODWRITE: /* установка параметров */
            if ((fflag & FWRITE) == 0)
                return(EPERM);
            d = (struct ourdev_io *)data;
            dio.value = d->value;
            bzero(&dio.msg, MAXMSGLEN);
            error = copyinstr(d->msg, &dio.msg, MAXMSGLEN - 1, NULL);
    break;
        default: 
            error = ENODEV; /* операция не поддерживается */
            break;
    }

    return(error);
}

/*
 * внешняя точка входя для загрузки/выгрузки и т.д.
 */

int
ourdev(struct lkm_table *lkmtp, int cmd, int ver)
{
    DISPATCH(lkmtp, cmd, ver, ourdev_handler, lkm_nofunc, lkm_nofunc)
}

/*
 * Обработчик загрузки/выгрузки 
 */

int
ourdev_handler(struct lkm_table *lkmtp, int cmd)
{
    struct lkm_dev *args = lkmtp->private.lkm_dev;

    if (cmd == LKM_E_LOAD) {
        dio.value = 13;
        strncpy(dio.msg,"hello world!\n", MAXMSGLEN - 1);
        printf("loading module %s\n", args->lkm_name);
    }

    return 0;
}

Модуль Execution Interpreter

Модуль Miscellaneous

Данный тип модулей используется для категорий которые не прдставлены выше.

Описывается как:

struct lkm_misc {
    MODTYPE         lkm_type;    /* тип модуля, в данном случае LM_MISC */
    int             lkm_ver;     /* версия lkm интерфейса */
    char           *lkm_name;    /* имя модуля */
    u_long          lkm_offset;
};

Структура инициализируется через макрос MOD_MISC:

MOD_MISC("ourmodule");

Следует заметить, что при данном типе модулей используется только имя модуля.

Рассмотрим все вышеописанное на примере rucd

Код модуля (rucd.c):

#include <sys/param.h>
#include <sys/conf.h>
#include <sys/exec.h>
#include <sys/systm.h>
#include <sys/lkm.h>

/* таблица перекодировки utf в koi8r */

static u_int16_t utf2koi8r[256] = {
    0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
    0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
    0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
    0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
    0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
    0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
    0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
    0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
    0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
    0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
    0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
    0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
    0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
    0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
    0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
    0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f,
    0x2500, 0x2502, 0x250c, 0x2510, 0x2514, 0x2518, 0x251c, 0x2524,
    0x252c, 0x2534, 0x253c, 0x2580, 0x2584, 0x2588, 0x258c, 0x2590,
    0x2591, 0x2592, 0x2593, 0x2320, 0x25a0, 0x2219, 0x221a, 0x2248,
    0x2264, 0x2265, 0x00a0, 0x2321, 0x00b0, 0x00b2, 0x00b7, 0x00f7,
    0x2550, 0x2551, 0x2552, 0x0451, 0x2553, 0x2554, 0x2555, 0x2556,
    0x2557, 0x2558, 0x2559, 0x255a, 0x255b, 0x255c, 0x255d, 0x255e,
    0x255f, 0x2560, 0x2561, 0x0401, 0x2562, 0x2563, 0x2564, 0x2565,
    0x2566, 0x2567, 0x2568, 0x2569, 0x256a, 0x256b, 0x256c, 0x00a9,
    0x044e, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433,
    0x0445, 0x0438, 0x0439, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e,
    0x043f, 0x044f, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432,
    0x044c, 0x044b, 0x0437, 0x0448, 0x044d, 0x0449, 0x0447, 0x044a,
    0x042e, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413,
    0x0425, 0x0418, 0x0419, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e,
    0x041f, 0x042f, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412,
    0x042c, 0x042b, 0x0417, 0x0428, 0x042d, 0x0429, 0x0427, 0x042a
};


extern u_char (*cd9660_wchar2char)(u_int32_t wchar);

extern int lkmexists(struct lkm_table *);

int rucd_lkmentry(struct lkm_table *, int, int);

/* 
 * Инициализируем lkm интерфейс
 */

MOD_MISC("rucd")

/* Функция перекодирования с utf в koi8r */

static u_char
conv_utf2koi8r(u_int32_t wchar)
{
    u_char schar;
    int i;

    schar = '?';
    for (i = 0; i < 256; i++) {
        if (utf2koi8r[i] == wchar) {
            schar = i;
            break;
        }
    }
    return (schar);
}

/*
 * внешняя точка входя для загрузки/выгрузки и т.д.
 */

static int
rucd_handle(struct lkm_table *lkmtp, int cmd)
{
    switch (cmd) {
    case LKM_E_LOAD:
        if (lkmexists(lkmtp))
            return (EEXIST);
        cd9660_wchar2char = conv_utf2koi8r;
        break;
    case LKM_E_UNLOAD:
        cd9660_wchar2char = NULL;
        break;
    default:
        return (EINVAL);
    }
    return (0);
}
/*
 * Обработчик загрузки/выгрузки 
 */

int
rucd_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
    DISPATCH(lkmtp, cmd, ver, rucd_handle, rucd_handle, lkm_nofunc);
}