Безопасно разыменовывать указатели на объединения структур в C?

Я пытаюсь написать декодер пакетов для протокола SCTP на C и сталкиваюсь с некоторыми проблемами при разыменовывании указателя на объединение структур (которые представляют фрагменты SCTP).

Не все могут быть знакомы с SCTP (протокол передачи управления потоком), поэтому вот несколько кратких вводных материалов:

Протокол передачи управления потоком
Структура пакета SCTP
RFC 4960

Короче говоря, после общего заголовка SCTP идет серия из одного или нескольких «фрагментов». В большинстве случаев каждый пакет SCTP имеет только один тип фрагмента, но вы можете использовать объединение фрагментов, при котором определенные типы фрагментов могут быть объединены вместе в один пакет. Из-за этого связывания я не могу просто определить объединение внутри моей структуры заголовка SCTP и положить этому конец.

Вот что у меня есть для struct sctp_header:

struct sctp_header {
    uint16_t srcport;
    uint16_t dstport;
    uint32_t vtag;
    uint32_t chksum;
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    void *chunks;
};


И для union sctp_chunks (усечено до двух типов фрагментов для этого вопроса):

struct sctp_data {
    uint32_t tsn;
    uint16_t stream_id;
    uint16_t stream_seq;
    uint32_t pp_id;
    void *data;
};

struct sctp_init {
    uint32_t initate_tag;
    uint32_t a_rwnd;
    uint16_t num_out_streams;
    uint16_t num_in_streams;
    uint32_t tsn;
    void *params;
};

union sctp_chunks {
    struct sctp_data *data;
    struct sctp_init *init;
};



Прямо сейчас я накладываю sctp_chunks на sctp_header->chunks (после того, как я сделал все остальные необходимые проверки, чтобы убедиться, что я сижу на пакете SCTP). Затем я читаю sctp_header->ctype в операторе switch и на основе этого знаю, могу ли я получить доступ к sctp_header->chunks->data->tsn или sctp_header->chunks->init->initate_tag (после приведения к (sctp_chunks *)) и так далее для других типов фрагментов. Позже я займусь математикой и проверю оставшиеся фрагменты и повторно наложу объединение sctp_chunks на оставшиеся данные, пока не обработаю все фрагменты. На данный момент я работаю только с первым фрагментом.

Проблема в том, что попытка доступа к data->tsn или init->initiate_tag (или любому другому члену объединения) вызывает SIGSEGV. У меня нет под рукой GDB (я всего лишь пользователь машины, на которой это кодируется), поэтому мне трудно понять, почему моя программа дает сбой. Я считаю, что мое использование структур/объединений правильное, но такова природа C, вероятно, это что-то ДЕЙСТВИТЕЛЬНО тонкое, что меня здесь зацепило.

Печать адресов указателей для chunks->data или chunks->init показывает мне адреса, которые не являются указателями NULL, и я не получаю никаких серьезных ошибок или предупреждений от gcc, поэтому я немного в тупике.

Что-то необычное, или есть лучший способ справиться с этим?


person Kumba    schedule 16.08.2011    source источник
comment
Вы можете распечатать короткий дамп памяти вместо просто значений указателя и проверить, имеет ли смысл содержимое. Правильно ли выровнены структуры (пакет #pragma и т. д.)?   -  person hamstergene    schedule 16.08.2011
comment
Если нет gdb, то делайте это грязным способом, то есть много printf.   -  person hari    schedule 16.08.2011
comment
@Eugene: не пробовал #pragma pack, но я внес некоторые изменения (переместил тип блока, длину, флаги в sctp_chunks) и могу разыменовать эти элементы, как только перестану делать их указателями. Но теперь я смещен на четыре байта, что странно.   -  person Kumba    schedule 16.08.2011


Ответы (2)


Когда вы говорите, что «накладываете» свою структуру данных на пакет (по крайней мере, это то, что вы говорите), значение, которое появляется в четырех или восьми байтах вашего члена данных указателя void* (в зависимости от вашей платформы ), скорее всего, является значением внутри фрагмента asctp_init или scpt_data, а не фактическим указателем на этот фрагмент данных. Скорее всего, поэтому ваш указатель не равен NULL, но при разыменовывании значения в указателе происходит сбой сегмента.

Во-вторых, наложение структуры на сериализованные данные без прагм и/или директив компилятора при упаковке структуры может быть опасным... иногда то, как вы думаете, что компилятор может выделить/заполнить структуру, не совпадает с тем, как он на самом деле это делает, и тогда структура не соответствует фактическому формату пакета данных, и вы получите недопустимые значения.

Итак, я предполагаю, что прямо сейчас вы пытаетесь сделать что-то подобное, напрямую накладывая первые N байтов вашего пакета на структуру scpt_header:

|scpt_header .........|void*|
|packet information ..|scpt_chunk.................|

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

Один из способов может выглядеть следующим образом:

#include <arpa/inet.h>
unsigned char* packet_buffer = malloc(sizeof(PACKET_SIZE));

//... proceed to copy into the buffer however you are reading your packet

//now fill in your structure
unsigned char* temp = packet_buffer;
struct sctp_header header_data;

header_data.src_port = ntohs(*((uint16_t*)temp));
temp += sizeof(uint16_t);
header_data.dstport = ntohs(*((uint16_t*)temp));
temp += sizeof(uint16_t);
header_data.vtag = ntohl(*((uint32_t*)temp));
temp += sizeof(uint32_t);
//... keep going for all data members

//allocate memory for the first chunk (we'll assume you checked and it's a data chunk)
header_data.chunks = malloc(sizeof(sctp_data));
scpt_data* temp_chunk_ptr = header_data.chunks;

//copy the rest of the packet chunks into your linked-list data-structure
while (temp < (packet_buffer + PACKET_SIZE))
{
    temp_chunk_ptr->tsn = ntohl(*((uint32_t*)temp));
    temp += sizeof(uint32_t);
    //... keep going for the rest of this data chunk

    //allocate memory in your linked list for the next data-chunk
    temp_chunk_ptr->data = malloc(sizeof(scpt_data));
    temp_chunk_ptr = temp_chunk_ptr->data;
}

//free the packet buffer when you're done since you now have copied the data into a linked
//list data-structure in memory
free(packet_buffer);

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

Также не позволяйте вашему scpt_header выходить за рамки, не уничтожив связанный список, которым он «владеет», иначе вы столкнетесь с утечкой памяти.

Обновление. Если вы все еще хотите пойти по пути наложения, сначала убедитесь, что вы используете директиву компилятора для упаковки своей структуры, чтобы не было добавлено дополнение. В gcc это будет

typdef struct sctp_header {
    uint16_t srcport;
    uint16_t dstport;
    uint32_t vtag;
    uint32_t chksum;
} __attribute__((packed)) sctp_header;

typedef struct sctp_data 
{
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    uint32_t tsn;
    uint16_t stream_id;
    uint16_t stream_seq;
    uint32_t pp_id;
} __attribute__((packed)) sctp_data;

typedef struct sctp_init 
{
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    uint32_t initate_tag;
    uint32_t a_rwnd;
    uint16_t num_out_streams;
    uint16_t num_in_streams;
    uint32_t tsn;
} __attribute__((packed)) sctp_init;

но было бы что-то еще в других компиляторах. Также обратите внимание, что я немного изменил ваши структуры, чтобы лучше отразить, как они на самом деле представлены пакетами SCTP в памяти. Поскольку размер двух разных типов пакетов различен, мы не можем на самом деле сделать объединение и наложить его в памяти... объединение технически будет иметь размер самого большого члена типа фрагмента, и это создаст проблемы, когда мы пытаемся создать массивы и т.д. Я также избавляюсь от указателей... Я вижу, что вы пытаетесь с ними сделать, но опять же, поскольку вы хотите наложить эти структуры данных на данные пакета, это снова вызовет проблемы, поскольку вы на самом деле пытаетесь «переместить» данные. Были внесены изменения в ваши исходные структуры данных, чтобы фактически отразить то, как данные размещаются в памяти, без какого-либо причудливого смещения или приведения указателя. Поскольку тип каждого пакета представлен unsigned char, теперь мы можем сделать следующее:

enum chunk_type { DATA = 0, INIT = 1 };

unsigned char* packet_buffer = malloc(sizeof(PACKET_SIZE));
//... copy the packet into your buffer

unsigned char* temp = packet_buffer;

sctp_header* header_ptr = temp;
temp += sizeof(sctp_header);

//... do something with your header

//now read the rest of the packets
while (temp < (packet_buffer + PACKET_SIZE))
{
    switch(*temp)
    {
        case DATA:
            sctp_data* data_ptr = temp;
            //... do something with the data
            temp += data_ptr->clength;
            break;

        case INIT:
            sctp_init* init_ptr = temp;
            // ... do something with your init type
            temp += init_ptr->clength;
            break;

        default:
            //do some error correction here
    }
}

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

person Jason    schedule 16.08.2011
comment
Я думаю, ты на что-то наткнулся. Я последовал идее Юджина, чтобы заглянуть в пакет #pragma, и теперь читаю биты с правильными смещениями. Но я снова сталкиваюсь с кровавыми проблемами с порядком байтов (я становлюсь сторонником прямого порядка байтов....). Тем не менее, это что-то новое для меня. У меня не было проблем с этим, когда я писал драйвер ядра. Но я думаю, что там я накладывал структуру на буфер, который не был сериализован (т. е. какая-то функция ядра уже изолировала данные), и все работало, потому что я начал с первого фрагмента данных. - person Kumba; 16.08.2011
comment
Можете ли вы предоставить небольшой пример кода для демонстрации копирования в буфер и последующего наложения на него? Это может лучше соответствовать тому, с чем я имел дело в драйвере ядра. - person Kumba; 16.08.2011
comment
Это выглядит громоздко. Я знаю, что видел другие случаи, когда данные были скопированы из void * в новый буфер (и распределены по распределению), а затем наложена структура и доступ к ее членам (хотя через -> или ., я забыл). Я делал подобные трюки на других языках, но у меня был хороший удобный отладчик, чтобы я мог видеть, что я делаю. Порядок байтов на самом деле не должен вызывать беспокойства, если я не перейду к битовым полям, тогда я просто использую #ifdef и определяю конкретные версии LE и BE. ntohs и ntohl могут позаботиться о конкретных переменных, как только моя структура будет правильно выровнена. - person Kumba; 16.08.2011
comment
PS, я читаю только пакеты. Не выписывая измененные или новые обратно. - person Kumba; 16.08.2011
comment
Хорошо, я дал вам обновление в ответе, в котором адрес просто накладывает ваши структуры на данные пакета. - person Jason; 16.08.2011
comment
Незначительные примечания: есть общий заголовок SCTP, который занимает 12 байтов в начале каждого пакета SCTP. После общего заголовка может быть 1 или более типов блоков. Чаще всего 1, но на самом деле у меня есть тестовые данные PCAP, где у меня есть два фрагмента, объединенных вместе. Я думаю, что подход к этому заключается в чтении общего заголовка SCTP путем наложения этой структуры на данные пакета (которые уже должны быть указателем после заголовка IP и других битов). Затем увеличьте временный указатель на 12 байт, и я должен оказаться на первом фрагменте. - person Kumba; 17.08.2011
comment
Вот где союзы вступили в игру. Все типы чанков имеют три общих поля: тип чанка, флаги и длину (byte, byte, int16). Я полагал, что использование структуры с этими тремя членами, тогда объединение для представления различных данных фрагмента было бы лучшим подходом, поскольку я могу «переключиться» на тип фрагмента и убедиться, что выполняется только код, соответствующий правильному фрагменту. - person Kumba; 17.08.2011
comment
Я думаю, если я правильно читаю ваш блок кода, новая идея будет состоять в том, чтобы наложить sctp_chunk_header, прочитать его, переместить указатель temp, а затем, в зависимости от типа фрагмента, наложить правильную структуру. Это избавило бы от необходимости дублировать три общих поля для каждого типа фрагмента. Возможно, я хотел бы использовать союзы. Я смотрю на некоторые другие программы, которые выполняют декодирование пакетов (например, Wireshark и Snort), и Snort использует объединение для декодирования ICMP, накладывая различные структуры и считывая соответствующие данные после того, как он имеет дело с общим заголовком ICMP. - person Kumba; 17.08.2011
comment
Ваш последний комментарий правильно интерпретирует то, что я предлагаю. Если вы хотите, вы можете наложить тип объединения, а не явный тип структуры, как это сделал я, но объединение будет объединением двух структур данных, как я их объявил. , а не ваши исходные с указателями в конце и без информации о заголовке фрагмента в начале. Что касается союзов, то я не совсем уверен, как будет работать директива компилятора для упаковки структур. Надеюсь, они по-прежнему будут правильно упакованы, но я никогда не делал этого таким образом, так что вам придется поэкспериментировать с этим. - person Jason; 17.08.2011
comment
Указатели void * в конце этих двух являются заполнителями. Именно здесь можно найти данные/параметры переменной длины, характерные для чанка. Для этого потребуются дополнительные структуры (или объединения). SCTP удобен тем, что использует формат TLV, так что он просто правильно проходит фрагменты. Я, вероятно, повторю тот же подход (как только он будет скомпилирован) для чтения параметров, как я делал для фрагментов. - person Kumba; 17.08.2011
comment
Я понимаю, что вы пытаетесь сделать с помощью void*, но вы понимаете, что любой, кто читает ваш код, будет думать, что это настоящий указатель. Необходимость выполнять кучу искажений данных просто заставляет любого, кому, возможно, придется использовать/обслуживать ваш код, проходить множество запутанных шагов, а void* не моделирует фактическое базовое представление данных фрагмента. Когда я смотрю на представление чанка из статьи Википедии, я вижу заголовок чанка, в котором указана его длина и т. д., и я вижу полезную нагрузку данных. Я не вижу никакого void*, поэтому это просто создает путаницу. - person Jason; 17.08.2011
comment
Да, я уже избавился от них. Использование временного указателя из вашего примера и присвоение ему структуры, а затем увеличение длины заголовка (или размера фрагмента) работает до сих пор. Даже союзы работают корректно до сих пор. За последние несколько дней не добился дальнейшего прогресса из-за других приоритетов. - person Kumba; 19.08.2011
comment
Работает до сих пор. Спасибо! Я думаю, что могу работать с тем, что у меня есть, и ссылаться на остальную часть вашего ответа, если у меня возникнут какие-либо проблемы. - person Kumba; 23.08.2011

Проблема в том, что вы вообще не должны использовать указатели. sctp_chunks должно быть:

union sctp_chunks {
    struct sctp_data data;
    struct sctp_init init;
};

И chunks в sctp_header должно быть union sctp_chunks chunks[1] (вы можете безопасно индексировать фрагменты за пределами 1, если знаете, что данные будут действительными.

Я только что видел ответ Джейсона, и он прав насчет упаковки структуры. Если вы используете gcc, определите структуру следующим образом:

struct sctp_header __attribute__ ((packed)) {
  ...
};

Вам придется сделать это для каждой структуры.

person Klox    schedule 16.08.2011
comment
Является ли __attribute__ расширением GCC? У меня нет доступа к MSVC++, чтобы проверить, понимает ли он эту директиву. - person Kumba; 16.08.2011
comment
Кроме того, почему chunks[1]? Я думал, что массивы в C были нулевыми индексами? - person Kumba; 16.08.2011
comment
Сотрите первый комментарий. __attribute__ зависит от GCC, согласно этому вопросу. Я думаю, что безопаснее использовать #pragma pack, так как он выглядит более переносимым. - person Kumba; 16.08.2011
comment
Я хотел сказать, что gcc не использует #pragma (потому что в нашем коде мы по-разному относимся к gcc и MSVC++), но быстрый Google показывает, что gcc учитывает MSVC++ #pragma. - person Klox; 17.08.2011
comment
Массивы имеют нулевой индекс, но в этом случае [1] является счетчиком. Я почти уверен, что gcc разрешит [0] для размера (только для такой ситуации, когда у вас есть неизвестное количество фрагментов), но я использовал [1] для совместимости с другими компиляторами. Число не имеет значения, если только вы не используете sizeof() или не выделяете память для структуры. Я предположил, что у вас есть указатель байта, который вы приводили в качестве структуры, поэтому вы не знали, сколько фрагментов у вас будет. - person Klox; 17.08.2011