Безопасный и эффективный способ доступа к невыровненным данным в сетевом пакете из C - PullRequest
4 голосов
/ 09 февраля 2009

Я пишу программу на C для Linux на процессоре ARM9. Программа предназначена для доступа к сетевым пакетам, которые включают последовательность помеченных данных, таких как:

<fieldID><length><data><fieldID><length><data> ...

Поля ID поля и длины - оба uint16_t. Данные могут быть 1 или более байтов (до 64 КБ, если использовалась полная длина, но это не так).

Пока <data> имеет четное количество байтов, я не вижу проблемы. Но если у меня есть 1-, 3- или 5-байтовый раздел <data>, то следующий 16-битный fieldID окажется не на 16-битной границе, и я ожидаю проблем с выравниванием. Я давно не делал ничего подобного с нуля, поэтому немного не уверен в деталях. Любые отзывы приветствуются. Спасибо.

Ответы [ 4 ]

6 голосов
/ 09 февраля 2009

Чтобы избежать проблем с выравниванием в этом случае, используйте все данные как unsigned char *. Итак:

unsigned char *p;
//...
uint16_t id = p[0] | (p[1] << 8);
p += 2;

В приведенном выше примере предполагается размещение данных с прямым порядком байтов, где младший байт идет первым в многобайтовом числе.

4 голосов
/ 09 февраля 2009

У вас должны быть функции (встроенные и / или шаблонные, если используемый вами язык поддерживает эти функции), которые будут считывать потенциально не выровненные данные и возвращать интересующий вас тип данных.

uint16_t unaligned_uint16( void* p)
{
    // this assumes big-endian values in data stream
    //  (which is common, but not universal in network
    //  communications) - this may or may not be 
    //  appropriate in your case

    unsigned char* pByte = (unsigned char*) p;

    uint16_t val = (pByte[0] << 8) | pByte[1];

    return val;
}
4 голосов
/ 09 февраля 2009

Самый простой способ - вручную перестроить uint16_t s за счет скорости:

uint8_t *packet = ...;
uint16_t fieldID = (packet[0] << 8) | packet[1];  // assumes big-endian host order
uint16_t length = (packet[2] << 8) | packet[2];
uint8_t *data = packet + 4;
packet += 4 + length;

Если ваш процессор поддерживает его, вы можете набрать каламбур или использовать объединение (но остерегайтесь строгого алиасинга ).

uint16_t fieldID = htons(*(uint16_t *)packet);
uint16_t length = htons(*(uint16_t *)(packet + 2));

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

Если пакет не выровнен, вы всегда можете скопировать его в статический буфер и затем прочитать его:

static char static_buffer[65540];
memcpy(static_buffer, packet, packet_size);  // make sure packet_size <= 65540
uint16_t fieldId = htons(*(uint16_t *)static_buffer);
uint16_t length = htons(*(uint16_t *)(static_buffer + 2));

Лично я бы просто выбрал вариант № 1, поскольку он будет наиболее переносимым.

1 голос
/ 09 февраля 2009

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

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

(это работает, потому что сгенерированный код будет загружать / хранить данные в байтах, что безопасно для выравнивания. Это происходит, когда сгенерированная сборка имеет инструкции, загружающие и хранящие 16/32/64 бита памяти неправильно выровненным образом, что она все разваливается).

...