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

Материал из OpenBSD-Wiki
Перейти к навигации Перейти к поиску
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Написание 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);
}