'Тайны ПОРТИ'+(is_shit(CODE) ? 'М':'Н')+'ГА'

Заинтересовала статья про портирование простого стека TCP/IP для AVR на STM32, хотел написать там, но как-то постепенно получилось очень много для простого вопроса, и скорее всего — возникнут еще новые вопросы.

Тема портирования, правильного портирования и написания портируемых программ для MCU интересна многим, например я, — не знаю как это ПРАВИЛЬНО делать. Поэтому буду периодически «разжигать» здесь вопросами по Си: что и зачем добавлено, и как правильнее. Исходники стека берем из этой статьи.

I. 32-бит CPU и структуры, директивы препроцессора для «упаковки» структур.

Сравнивая в Total Commander(Меню/Файлы/Сравнить по содержим.) файлы lan.h из оригинала и порта, заметил, что добавилось окаймление всех структур директивами препроцессора #pragma pack, типа такого:

#pragma pack(push, 1)
typedef struct 
{
...
} 
any_struct_type;
#pragma pack(pop)

CPU aligned & unaligned access to memory.

Изначально либа делалась для 8-бит MCU AVR, у него 8-бит шина данных, т.е. чтение/запись данных в память (RAM) производится посредством минимальной единицы адресации к памяти — байта. Но, либа портирована на 32-бит MCU ARM Cortex c 32-бит шиной данных, и для него возможен доступ к памяти 32-бит или 16-бит словами или байтами. С байтами, собственно, проблем нет, но чтение/запись 32-бит слов может производится только по адресам памяти кратным 4-м (0x1000, 0x1004, 0x1008 и т.д.), а 16-бит слов — кратным 2-м (0x1000, 0x1002, 0x1004 и т.д.). Это логично, если процессор записывает-читает 32,16-бит слова в-из памяти за 1 цикл – такой доступ к памяти называется выровненный доступ(aligned access).

К чести ARM и на радость публике, надо добавить, что ARM Cortex (в отличии от более старых версий ARM) имеет, и так называемый, не выровненный доступ к памяти (unaligned access). Т.е. может читать-записывать 32,16-бит слова по не выровненному адресу, но это естественно требует дополнительного цикла чтения/записи из памяти, т.е. дополнительных тактов процессора: команда, выполняющая не выровненный доступ к памяти выполняется минимум в 2 раза медленнее, чем та же команда, выполняющая выровненный доступ. Например, рассмотрим команду чтения 32-бит слова с нечетного адреса памяти 0x1003 в регистр: при этом по шине вначале считывается байт по адресу 0x1003, а затем считывается 32-бит слово по адресу 0x1004, из него берутся только 3 первых байта и добавляются к уже считанному байту, только тогда полученное 32-бит слово поступает в регистр процессора. В этом примере использовалась команда ldr (load register) – чтение 32-бит слова из памяти в регистр, ее аналог для 16-бит слова – ldrh, а ldrb – для байта. Для записи из регистра в память, соответственно используются команды: str, strh и strb (store register) – пресловутая регистровая load-store концепция для доступа к памяти в RISC CPU.

Компиляторы 32-бит процессора по умолчанию (и для ARM Cortex с его возможностями unaligned access to memory, тоже) выравнивают (align) начальные адреса всех переменных, массивов и структур по адресам кратным 4, т.е. как для 32-бит слов (32-bit_word-aligned) – так удобнее, универсальнее, совместимее со старыми ARM, и как говорилось выше – так быстрее читать-записывать данные из-в памяти.

Проблема со структурами.

Со структурами вопрос отдельный – структура может иметь нечетную длину в байтах, и самое главное: начальные адреса ее полей (байтов, 64, 32, 16-бит слов, массивов и вложенных структур) могут иметь любое значение, т.е. не обязательно кратное 4-м или 2-м. Т.е. имеем эти самые не выровненные начальные адреса объектов данных в памяти. Не выровненные для 32-бит процессора, но для 8-бит процессора они выровнены, т.к. минимальное выравнивание для него итак равно 1 байту.

Компиляторы для 32-бит процессоров по умолчанию выравнивают начальные адреса полей в структурах по границе 4-х байт(по адресу кратному 4-м), а 16-бит слова по границе 2-х байт(по адресу кратному 2-м), добавляя в образовавшиеся промежутки ”пустые” байты (padding bytes), кроме того, если длина структуры не кратна 4-м байтам – она также дополняется в конце padding bytes до длины кратной 4-м байтам. Поэтому оператор sizeof(any_struct_type) будет возвращать не запроектированный разработчиком размер структуры в байтах, а сколько отвел под нее байтов компилятор с учетом ”пустых” padding bytes :D

Директивы препроцессора для «упаковки» структур.

Чтобы избежать этого, применяют специальные директивы препроцессора для ”сжатия” (packing) структур, т.е. указать компилятору, что в структуры не надо вставлять gaps в виде padding bytes, т.е. не надо выравнивать данные для удобства работы 32-бит процессора. В данном случае просто указываем ему величину выравнивания данных в 1 байт (byte-alignment) в этом месте, т.е. не 4 байта(или 2 для 16-бит слов в структурах), как по умолчанию. Директивы #pragma pack действуют только на объявления struct и union, т.е. ”пакуют”(выравнивают) данные только внутри них, сами же начальные адреса структур будут выровнены по умолчанию(4 байт).

// Компилятору запомнить текущее значение выравнивания
// и с этого места включить выравнивание в 1 байт для структур
#pragma pack(push, 1)  
typedef struct 
{
...
} 
any_struct_type; 	
// С этого места компилятору восстановить 
// предыдущее значение выравнивания
#pragma pack(pop)

Такой вид директив #pragma pack (#pragma pack(push/pop)) заимствован в GCC из компилятора Си Microsoft (для совместимости), в компиляторе IAR (IAR EWARM — IAR Embedded Workbench for ARM) они тоже есть. Но директивы #pragma pack(push/pop) не поддерживаются компилятором ARM Keil (MDK-ARM — ARM Microcontroller Development Kit), что not good for the true portable library.

Также удобна для этих целей директива препроцессора __attribute__((packed)) имеющаяся в GCC и в Keil (в IAR ее нет), ее применяют к идентификаторам структур или полей структур, которые надо упаковать с выравниванием 1 байт:

typedef struct 
{
...
} 
// Эта структура будет упакована
__attribute__((packed))  any_struct_type; 	

В IAR имеется следующая разновидность директивы #pragma pack (интересно, что она есть и в GCC и в Keil):

// С этого места включить выравнивание в 1 байт внутри структур
#pragma pack(1)
typedef struct 
{
...
} 
any_struct_type; 	
// Восстановить значение выравнивания по умолчанию в структурах
#pragma pack()

Т.е. вариантов масса, но когда не понятно как делать правильно – надо посмотреть, как это делают в индустрии: в кучах кода библиотек TI StellarisWare встретил следующее решение задачи упаковки структур, с учетом портирования кода на все 3 основные компилятора для ARM: GCC, Keil и IAR, что-то типа:

// Здесь начинается блок определения всех packed структур
#ifdef __ICCARM__        // IAR  
#define PACKEDSTRUCT
#pragma pack(1)            
#else			// GCC, Keil 
#define PACKEDSTRUCT __attribute__((packed))
#endif

typedef struct 
{
...
} 
PACKEDSTRUCT any_struct1_type; 		
...
typedef struct 
{
...
} 
PACKEDSTRUCT any_structN_type; 		

#ifdef __ICCARM__ 	// IAR
#pragma pack()             
#endif
// Блок определения packed структур закончился

Здесь __ICCARM__ — это предопределенный макрос IAR, он используется для идентификации компилятора. (Справка: для GCC, соответственно: __GNUC__, а для Keil: __arm__ или __ARMCC_VERSION).

Возникает вопрос:
Почему они не использовали просто #pragma pack(1) перед началом блока структур и #pragma pack() в конце его – эти директивы же поддерживаются во всех 3-х компиляторах? Или же #pragma pack(1) в GCC и Keil работает неправильно, когда определений структур более одного и они идут друг за другом? Какие есть еще ПРАВИЛЬНЫЕ варианты решения этой задачи?

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

RSS свернуть / развернуть
ИМХО, проблемы связанные с нестандартизированными расширениями языка (#pragma pack, #pragma once, #pragma comment и т. д.) – это меньшая из проблем, т. к. она решается директивами условной компиляции без особых дополнительных затрат.

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

Вот, например, фокус упакованными структурами не будет работать для ARM7TDMA т. к. есть большой шанс споймать DATA_ABORT. И просто от этого не отделаешься – придётся менять код чтобы сделать его универсальным, либо писать 2 версии кода для доступа к полям пакета.

Правда, на практике редко когда стоит задача написания абсолютно универсального кода, обычно, все же, четко очерчивают границы его переносимости.
+1
упакованные структуры будут работать на arm7 потому что компилятор правильно раскидает доступ побайтно если надо обратиться к невыровненному полю в упакованной структуре.

но вот сами по себе упакованные структуры — зло. использование которого не особо оправдано даже в случае с передачей данных по сети (tcp в частности), а других случаев где применение упакованных структур может быть хоть как-то оправдано — нет, да и там вполне можно обойтись и без них.
используя макросы и смещения. один хрен нужны макросы для переворота Little/Big endian.
и что-нибудь вроде

MAC_PUTL(IP_DESTINATION_OFS, ipAddr)
MAC_PUTS(UDP_LEN_OFS, len)

выглядит конечно страшненько, но не сильно хуже чем

udp_packet_t *udp = (void*)(ip->data);
udp->len = htons(len);

для примера tcp стэк от того же TI: slaa137a
+1
  • avatar
  • _pv
  • 07 мая 2013, 20:24
упакованные структуры будут работать на arm7 потому что компилятор правильно раскидает доступ побайтно если надо обратиться к невыровненному полю в упакованной структуре.

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

Не знаю как другие компиляторы, но GCC при подобных манипуляциях с указателями просто выдает ворнинг, типа возможно обращение на не выровненные данные.
+1
именно про GCC для arm7 не скажу, но там где с подобным сталкивался, компилятор разбирал побайтно.
+1
Хм, а Вы правы.

В GCC это выглядит так

typedef struct {
	int some_int;
} some_struct;


int foo(char * buffer) {
	some_struct * bar = (some_struct * ) buffer;
	return bar->some_int; 	
}

arm-none-eabi-gcc.exe -mcpu=arm7tdmi -Os -Wall -c test.c

На выходе имеем
00000000 <foo>:
   0:   e5900000        ldr     r0, [r0]
   4:   e12fff1e        bx      lr


Тобиш никакого дополнительного кода, если buffer не будет выровнен – получим DATA_ABORT

А если написать
#pragma pack(push, 1)
typedef struct {
	int some_int;
} some_struct;
#pragma pack(pop)

То результат
00000000 <foo>:
   0:   e5d02001        ldrb    r2, [r0, #1]
   4:   e5d03000        ldrb    r3, [r0]
   8:   e1833402        orr     r3, r3, r2, lsl #8
   c:   e5d02002        ldrb    r2, [r0, #2]
  10:   e5d00003        ldrb    r0, [r0, #3]
  14:   e1833802        orr     r3, r3, r2, lsl #16
  18:   e1830c00        orr     r0, r3, r0, lsl #24
  1c:   e12fff1e        bx      lr
0
правильное решение — это y = (y & ~X_MASK) | ((X>>X_SHIFT) & X_MASK)
или макросы для этого.
такого барахла в программе будет не много, только в местах, где что-то грузится/пишется из файла или отправляется/принимается по какому-либо интерфейсу. Внутри проги просто структуры, без всякой прагмы. В кваче первой например так сделани и в иксах. Да и много где ещё, видимо.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.