Путешествие в unmanaged code: туда и обратно

  • Tutorial

Высокоуровневые языки программирования популярны, но существуют области, в которых придется использовать неуправляемые реализации библиотек. Это могут быть вызов специфических функций ОС, низкоуровневый доступ к устройствам, необходимость быстродействия в алгоритмах и другие. Под катом я расскажу, с чем можно столкнуться во время путешествия в unmanaged code и что стоит взять с собой.

Вы стоите на пороге своей уютной IDE, и вас совсем не тянет отправиться в мир исходного кода, где темно и ничего не понятно. Для успеха предприятия прежде всего необходимо разжиться картой – сойдет описание заголовков библиотеки, а лучше иметь полноценную документацию. Обычно она выглядит так:

...
#include <linux/netfilter_ipv4/ip_tables.h>
#include <libiptc/xtcshared.h>

#ifdef __cplusplus
extern "C" {
#endif

#define iptc_handle xtc_handle
#define ipt_chainlabel xt_chainlabel

#define IPTC_LABEL_ACCEPT  "ACCEPT"
#define IPTC_LABEL_DROP    "DROP"
#define IPTC_LABEL_QUEUE   "QUEUE"
#define IPTC_LABEL_RETURN  "RETURN"

/* Does this chain exist? */
int iptc_is_chain(const char *chain, struct xtc_handle *const handle);

/* Take a snapshot of the rules.  Returns NULL on error. */
struct xtc_handle *iptc_init(const char *tablename);

/* Cleanup after iptc_init(). */
void iptc_free(struct xtc_handle *h);
...

Представим, что вам повезло, и документация есть. Здесь описываются сигнатуры функций, используемые структуры, псевдонимы, а также указаны ссылки на другие используемые заголовки. Первый квест – найти библиотеку в ОС. Её название может отличаться от ожидаемого:

~$ find /usr/lib/x86_64-linux-gnu/ -maxdepth 1 -name 'libip*'
/usr/lib/x86_64-linux-gnu/libip6tc.so.0.1.0
/usr/lib/x86_64-linux-gnu/libip4tc.so
/usr/lib/x86_64-linux-gnu/libiptc.so.0
/usr/lib/x86_64-linux-gnu/libip4tc.so.0.1.0
/usr/lib/x86_64-linux-gnu/libip6tc.so.0
/usr/lib/x86_64-linux-gnu/libiptc.so.0.0.0
/usr/lib/x86_64-linux-gnu/libip4tc.so.0
/usr/lib/x86_64-linux-gnu/libiptc.so
/usr/lib/x86_64-linux-gnu/libip6tc.so

Цифровой суффикс означает разные версии библиотек. В общем случае нам требуется оригинал libip4tc.so. Можно заглянуть внутрь одним глазком и убедиться, что дело стоящее:

~$ nm -D /usr/lib/x86_64-linux-gnu/libip4tc.so
...
0000000000206230 D _edata
0000000000206240 B _end
                 U __errno_location
                 U fcntl
000000000000464c T _fini
                 U __fprintf_chk
                 U free
                 U getsockopt
                 w __gmon_start__
0000000000001440 T _init
0000000000003c80 T iptc_append_entry
0000000000003700 T iptc_builtin
0000000000004640 T iptc_check_entry
0000000000003100 T iptc_commit
0000000000002ff0 T iptc_create_chain
00000000000043f0 T iptc_delete_chain
...

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

public static class Libiptc4
{
        /* Prototype: iptc_handle_t iptc_init(const char *tablename) */
        [DllImport("libip4tc.so")]
        public static extern IntPtr iptc_init(string tablename);
} 

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

/* Prototype: iptc_handle_t iptc_init(const char *tablename) */
[DllImport("libip4tc.so")]
public static extern IntPtr iptc_init(IntPtr tblPtr);
...
var tblPtr = Marshal.StringToHGlobalAnsi("filter");
var _handle = Libiptc4.iptc_init_ptr(tblPtr);
Marshal.FreeHGlobal(tblPtr);

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

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

struct ipt_entry {
	struct ipt_ip ip;

	/* Mark with fields that we care about. */
	unsigned int nfcache;

	/* Size of ipt_entry + matches */
	__u16 target_offset;
	/* Size of ipt_entry + matches + target */
	__u16 next_offset;

	/* Back pointer */
	unsigned int comefrom;

	/* Packet and byte counters. */
	struct xt_counters counters;

	/* The matches (if any), then the target. */
	unsigned char elems[0];
};

Обратите внимание на поле unsigned char elems[0] прототипа. Если я не ошибаюсь, это указатель на байтовый массив переменной длины, и его не нужно явно указывать в реализации. В упрощенном виде наш объект устроен следующим образом:

*******************************************
* ip_entry                                *
* 112 bytes                               *
*******************************************
* matches                                 *
* target_offset - 112 bytes               *
*******************************************
* target                                  *
* next_offset - target_offset - 112 bytes *
*******************************************

Динамическая часть объекта (matches и target) пристыковывается к заголовку ip_entry. Создание такого объекта разбивается на два этапа:

  1. Выделение памяти требуемого размера.

  2. Последовательная запись элементов в нужные участки памяти.

Чтобы вычислить размер объекта, необходимо сложить все составные части, имеющие фиксированную структуру. Реализация прототипа заголовка ipt_entry выглядит следующим образом:

[StructLayout(LayoutKind.Sequential)]
public struct IptEntry
{
 	public IptIp ip;
	public uint nfcache;
	public ushort target_offset;
	public ushort next_offset;
	public uint comefrom;
	public IptCounters counters;
};

Размер реализации вычисляется как Marshal.SizeOf<IptEntry>()и равен 112 байт. Затем вычисляются размеры всех составных объектовmatches и target ( которые тоже могут быть динамическими). Нюанс: при работе с библиотекойlibiptc я столкнулся с требованием округлять размеры объектов в большую сторону по модулю 8 ( размер long), так что часть байт в хвосте объектов будет не востребована. Видимо, такой подход ускоряет чтение объектов. Функция выравнивания может выглядеть следующим образом:

static readonly int  _WORDLEN = Marshal.SizeOf<long>();
public static int Align(int size)
{
	return ((size + (_WORDLEN - 1)) & ~(_WORDLEN - 1));
}

После того как размер объекта вычислен, необходимо определить смещения в памяти entry.target_offset и entry.next_offset, выделить память и записать объекты:

IntPtr entryPtr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr<IptEntry>(entryPtr, entry, false);
Marshal.StructureToPtr<Match>(entryPtr + 112, match, false);

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

var entry = Marshal.PtrToStructure<IptEntry>(point);
var match = Marshal.PtrToStructure<Match>(point + 112)

Помимо структур, на вашем пути может встретиться такой зверь как union:

struct xt_entry_match {
	union {
		struct {
			__u16 match_size;

			/* Used by userspace */
			char name[XT_EXTENSION_MAXNAMELEN];
			__u8 revision;
		} user;
		struct {
			__u16 match_size;

			/* Used inside the kernel */
			struct xt_match *match;
		} kernel;

		/* Total length */
		__u16 match_size;
	} u;

	unsigned char data[0];
};

Union - это полиморфизм на уровне памяти: один и тот же участок может быть интерпретирован как разные типы. Для него нет прямого аналога в языке c#. Необходимо отдельно описывать реализацию для каждого прототипа в объединении. Прототипы могут иметь разную длину, однако память будет выделяться по верхней границе (как будто для самого большого размера). Подробнее о реализации объединений читайте в примере.

В описании прототипа можно встретить псевдонимы для дефолтных значений:

#define XT_EXTENSION_MAXNAMELEN   29
...
char name [XT_EXTENSION_MAXNAMELEN]

Как вытащить дефолтное значение в управляемый код для меня осталось загадкой. Поэтому приходится искать значения на просторах header файлов и устанавливать вручную.

Постарайтесь не злить магов, иначе получите проклятье в спину. Ваши ushort, uint и long будут хранить совсем не то, что ожидаете. Все дело в порядке байт. Привычным является прямой порядок: слева старший байт, справа меньший. Тем не менее при работе с сетевыми адресами и номерами портов может понадобиться обратный порядок байт. Для знаковых типов есть готовый метод. Для беззнаковых типов снимать проклятье придется самим:

byte [] convArray = BitConverter.GetBytes(value);
Array.Reverse(convArray);
ushort reverseEndian = BitConverter.ToUInt16(convArray,0);
ushort reverseEndian = (ushort)((value << 8) | (value >> 8));

В конце нашего путешествия пришло время поговорить о перехвате ошибок. При работе с unmanaged code он работает не совсем так как мы привыкли. Функции могут возвращать флаг успех/неудача, а номер ошибки будет содержать переменная errno. В явном виде ее нигде нет. Поэтому берем дополнительный квест, и добавляем к атрибуту настройку:

[DllImport("libip4tc.so", SetLastError = true)]

Теперь, если нас постигнет неудача, можно вызвать:

int errno = Marshal.GetLastWin32Error();
var errPtr = Libiptc4.iptc_strerror(errno);
string errStr = Marshal.PtrToStringAnsi(errPtr);

И это сработает даже в Linux c net.core (видимо, не успели переименовать/забили). Также необходимо обращать внимание на сборку библиотек: могут быть как кросс-платформенные, так и отдельно 32/64 битные версии, для многих библиотек есть готовые порты в Windows . Поэтому ошибки времени запуска чаще всего решаются выбором подходящей версии библиотеки.

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

Похожие публикации

Средняя зарплата в IT

120 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 6 247 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 6

    +8
    Union можно эмулировать через [StructLayout(LayoutKind.Explicit)] и [FieldOffset(0)]. И быстрый конвертер таким образом получить. А сами структуры входящие в этот «union» должны быть [StructLayout(LayoutKind.Sequential), Pack = 1] чтоб пробелов не было. Пока проблем не замечено.
      0
      [DllImport("libip4tc.so")]
      А если не указывать расширение библиотеки, получится кросс-платформенно с минимумом усилий.

      [StructLayout(LayoutKind.Sequential)]
      public struct IptEntry
      В C# структуры по умолчанию LayoutKind.Sequential. Впрочем, можно всё равно указывать для дополнительной ясности.

      Размер реализации вычисляется как Marshal.SizeOf<IptEntry>()
      По возможности используйте sizeof().

      Тем не менее при работе с сетевыми адресами и номерами портов может понадобиться обратный порядок байт. Для знаковых типов есть готовый метод. Для беззнаковых типов снимать проклятье придется самим:
      В принципе, если полей в big endian много, можно скастовать custom marshaler, чтобы он переворачивал байтики вместо вызывающего кода.
        0

        Интересно, в .net можно сделать naked-фунцию? (Функцию, которая не делает предположений о том, как выглядит стек). Такие функции нужны при написании обработчиков прерываний.

          0
          Занимательно. С помощью C++/CLI решать подобные задачи не пробовали? ИМХО, много легче и понятнее…
            0
            Импорт so-шек Вас не смутил? Как запустить C++/CLI на не-windows платформах?
              0
              О как, не знал. Печально: чего полезного не хватишься — того и нету(

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

          Самое читаемое