Программирование ARM: решение проблем, FAQ Какие я часто делаю ошибки в языке C Fri, April 19 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Какие я часто делаю ошибки в языке C Печать
Добавил(а) microsin   

Сколько можно наступать на одни и те же грабли?.. Пора их положить на полку, на самое видное место.

1. Неправильно меняю типизированный указатель. При изменении (например, инкрементировании) указателя забываю, что у него есть тип, и от размера этого типа зависит результат - какой адрес получится. Например, при вычислении абсолютного адреса почтового ящика CAN я написал:

AT91PS_CAN_MB CAN_Mailbox = AT91C_BASE_CAN0_MB0 + 0x20 * numMailbox;

В результате если, например, numMailbox==2, то получаю ошибочный адрес 0xFFFD0A00 вместо правильного 0xFFFD0240, так к AT91C_BASE_CAN0_MB0 прибавится не 0x20*2, как я хотел, а 0x20 * (sizeof(AT91PS_CAN_MB)*2).

Правильно надо было написать:

AT91PS_CAN_MB CAN_Mailbox = AT91C_BASE_CAN0_MB0 + numMailbox;

После такого вычисления получится правильный адрес 0xFFFD0240, так как к AT91C_BASE_CAN0_MB0 прибавится numMailbox размеров AT91PS_CAN_MB, т. е. в нашем примере sizeof(AT91PS_CAN_MB)*2 или 0x20*2.

2. Warning[Pa082]: undefined behavior: the order of volatile accesses is ...\at91lib\peripherals\can\can.c 117

Такое предупреждение возникает, если в выражении используется две переменные с атрибутом volatile, например (тут в правой стороне выражения регистры интерфейса CAN, которые имеют атрибут volatile, а слева обычная переменная u32):

canstatus = (AT91C_BASE_CAN0->CAN_SR) & (AT91C_BASE_CAN0->CAN_IMR);

исправить эту ошибку можно так:

canstatus  = AT91C_BASE_CAN0->CAN_SR;
canstatus &= AT91C_BASE_CAN0->CAN_IMR;

Еще пример такого же предупреждения (и в левой, и в правой части выражения переменные имеют атрибут volatile):

mailbox_in_use |= 1 << (pTransfer -> mailbox_number);

исправить эту ошибку можно так:

mailbox_in_use = pTransfer -> mailbox_number;
mailbox_in_use <<= 1;

3. ../graphics.c:100: warning: assignment discards qualifiers from pointer target type

Предупреждение возникло в AVR Studio (GCC) на следующий код:

const u8 ZXFONT [] = {...};
u8* adr;
adr = ZXFONT + (symcode-0x20)*8;

Проблема была в том, что я неправильно объявил указатель adr. Надо было объявить его, как и массив ZXFONT, с атрибутом const:

const u8* adr;

4. Забываю брать адрес (оператор &) от имени переменной, когда передаю его в функцию.

5. При портировании кода с ARM на AVR и обратно бывают ошибки с типом int: на AVR тип int двухбайтовый, а на ARM четырехбайтовый. Такие ошибки бывает трудно отловить. У меня одна такая встретилась в подпрограмме вычисления контрольной суммы. Пришлось одновременно запустить 2 отладчика, один для AVR, другой для ARM, и только тогда понял, в чем проблема.

6. Иногда вместо оператора ИЛИ (палка |) ставлю оператор отрицания (восклицательный знак !). Визуально различие малозаметно, и компилятор часто не ругается (например, ему все равно, какой оператор стоит, != или |=).

7. Иногда забываю правильно по времени установить семафор (флаг) какого-то действия. Такую ошибку бывает очень трудно найти и отловить. Семафор нужно всегда устанавливать ПЕРЕД действием, которое обозначает этот семафор, и ни в коем случае после действия. Это потому, что последовательность двух операций (действие и установка его семафора) не атомарна, и между ними может произойти прерывание, которое меняет семафор.

Поясню на примере. Есть некая аппаратура, например конечная точка интерфейса USB, которая требует, чтобы в момент процесса передачи в неё не осуществлялась запись. Для сигнализации о том, что эта конечная точка занята передачей, существует семафор, который показывает, что конечная точка занята, и писать данные в неё нельзя. Семафор сбрасывается прерыванием окончания передачи, а стартует передача и устанавливается семафор занятости в обычном коде. Основная программа по циклу постоянно мониторит семафор, и если семафор показывает, что "свободно", и если есть какие-то данные для передачи, то взводит семафор в состояние "занято", и пишет в конечную точку данные. Семафор перекидывается в состояние "свободно" в обработчике прерывания окончания передачи. Если по ошибке семафор взводить в состояние "занято" после записи в буфер, то прерывание окончания передачи, которое сбрасывает семафор в состояние "свободно", может сработать раньше выставления семафора в состояние "занято" в основной программе. В результате передача прекратится, так как семафор навсегда останется в состоянии "занято".

UPD140124

8. Забываю про скобки в макросах с параметрами. К примеру, можно задать макрос для вычисления таймаута:

#define TIMEOUT(sec) (sec*10)

Задумывался этот макрос для проверки счетчика времени, который тикает каждые 100 мс. Значение секунд (параметр sec) нужно умножить на 10, чтобы получить значение в единицах счетчика.

На первый взгляд макрос задан правильно. Но если ему передать в параметре sec значение 6+4, и ожидать в результате развертки макроса число 100 (ведь (6+4)*10 = 100), то на самом деле получим значение 46. Это происходило потому, что при раскрытии макроса получалось выражение 6+4*10, и это совсем не то, что ожидалось. Чтобы макрос отрабатывал правильно, нужно параметр sec заключить в дополнительные скобки: 

#define TIMEOUT(sec) ((sec)*10)

UPD140205

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

void DummyWait (u32 iterations)
{
   while(iterations--);
}

компилятор на уровнях оптимизации High и Medium может просто выкинуть, и никакой реальной задержки не получится. Кроме того, такой способ генерации задержки не гарантирует стабильную задержку, и впустую расходует ресурс процессора. Никогда не делайте задержек подобным образом, используйте глобальный таймер и прерывания. А если же все-таки решились на задержку в цикле, то используйте управление оптимизацией [2] на уровне модулей или функций (на функции с циклом задержки оптимизацию следует выключить).

UPD141219

10. Выравнивание - очень коварная штука. Забываю про выравнивание по умолчанию полей структур, из-за чего в них могут неожиданно появляться дырки, или поля могут оказаться не в том месте, где ожидалось (что вдруг обнаруживается при переносе кода с одной платформы на другую). Вот, к примеру, как нужно задавать упакованную структуру заголовка файла BMP для компилятора GCC:

typedef struct __attribute__ ((__packed__))
{ u8 magic[2]; /* "магическая" сигнатура, обозначающая формат файла BMP:
                    0x42 0x4D (HEX-код, который указывает на буквы B и M).
                    Возможны следующие варианты:
                       BM - Windows 3.1x, 95, NT, ... и т. д.
                       BA - OS/2 Bitmap Array
                       CI - OS/2 Color Icon
                       CP - OS/2 Color Pointer
                       IC - OS/2 Icon
                       PT - OS/2 Pointer. */
  u32 filesz;    /* размер файла BMP в байтах */
  u16 creator1;  /* зарезервировано */
  u16 creator2;  /* зарезервировано */
  u32 offset;    /* смещение, т. е. начальный адрес, с которого можно найти данные растра. */
} bmp_header_t;

А так надо задать ту же самую структуру для IAR:

typedef __packed struct
{ u8 magic[2]; /* "магическая" сигнатура, обозначающая формат файла BMP:
                    0x42 0x4D (HEX-код, который указывает на буквы B и M).
                    Возможны следующие варианты:
                       BM - Windows 3.1x, 95, NT, ... и т. д.
                       BA - OS/2 Bitmap Array
                       CI - OS/2 Color Icon
                       CP - OS/2 Color Pointer
                       IC - OS/2 Icon
                       PT - OS/2 Pointer. */
  u32 filesz;    /* размер файла BMP в байтах */
  u16 creator1;  /* зарезервировано */
  u16 creator2;  /* зарезервировано */
  u32 offset;    /* смещение, т. е. начальный адрес, с которого можно найти данные растра. */
} bmp_header_t;

И это еще не самое неприятное, что может произойти. Некоторые платформы не допускают доступ к данным по адресу, не выровненному на 2 или на 4 байта (в зависимости от разрядности ядра и особенностей системы команд платформы), что приводит к неожиданным и трудно обнаруживаемым ошибкам.

Пример типичной ошибки, связанной с выравниванием, которая может неожиданно всплыть в любой момент: через байтовый массив uint8_t передаются данные 4-байтной переменной uint32_t с помощью указателя на uint32_t*:

int enter_GX8002_ROM_bootloader (uint8_t *data, uint32_t datalen)
{ uint32_t ROM_bootloader_baud = *((uint32_t*)data); return UartInit(UART_GX8002, ROM_bootloader_baud);
}

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

Проблему выравнивания можно решить двумя способами. Первый способ: надо применить соответствующие атрибуты компилятора (например для gcc это атрибут aligned [8]) при определении массивов и переменных (в этом примере надо было правильно определить хранилище, чтобы адрес указателя данных *data нацело делился на 4). Например, если определить буфер следующим образом, то его можно смело передавать в параметре *data:

uint8_t buffer [32] __attribute__ ((aligned (4)));

Второй способ: использовать функцию memcpy для копирования не выровненных данных в нужное место:

   //uint32_t ROM_bootloader_baud = *((uint32_t*)data);
   memcpy(&ROM_bootloader_baud, data, sizeof(ROM_bootloader_baud));

UPD151227

11. Переполнение из-за ограниченной разрядности используемых целых чисел. Вот типичный пример: вычисление угла поворота часовой стрелки.

#define TOPANGLE ((u16)LED_STRIP_PIXELS*256)
 
u16 anglehh;
u8 ss, mm, hh;
 
ss = BCDtoBIN(rtc.reg.ss);
mm = BCDtoBIN(rtc.reg.mm);
hh = BCDtoBIN(rtc.reg.hh & 0x1F);
 
//Вычисление текущего угла часовой стрелки
// с учетом минут и секунд:
anglehh = (u32)TOPANGLE*((hh*60+mm)*60+ss)/((u32)12*60*60);
...

И вроде бы все тут хорошо - данные из регистров микросхемы DS1307 преобразуются в двоичный формат, и подставляются в выражение для вычисления угла поворота часовой стрелки anglehh. Однако здесь вкралась ошибка - первым вычисляется значение в скобках hh*60+mm, но число hh байтовое, и в нем возможно переполнение, когда происходит умножение на количество часов, большее 4. Так что в этом месте требуется повышение разрядности, чтобы не происходило переполнения, например вот так:

anglehh = (u32)TOPANGLE*(((u32)hh*60+mm)*60+ss)/((u32)12*60*60);

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

[Многопоточные приложения]

В среде многопоточного выполнения встраиваемых приложений (RTOS, VDK, операционная система реального времени) встречаются ошибки особого рода, связанные с проблемой синхронизации, нехваткой памяти, неправильным распределением ресурсов и ошибочной расстановкой приоритетов.

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

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

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

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

Утечка памяти. При использовании динамического выделения памяти некорректно написанный код, который выделяет и освобождает память, но освобождает её не полностью, может привести к исчерпанию кучи, и в результате через некоторое время произойдет сбой. Ошибку утечки памяти бывает обнаружить довольно трудно, если код сложный, или написан не Вами - например, какая-нибудь библиотека. Скорее всего, Вы пользуетесь готовыми библиотеками, поэтому будьте готовы ко всему - даже самые известные производители иногда предоставляют библиотеки с ошибками.

UPD171018

13. Забываю, что в операторе printf (sprintf, vsprintf) нужно строго следить за соответствием строки форматирования и типов заполняемых переменных. К сожалению, некоторые компиляторы (наподобие GCC) не предупреждают о такой ситуации, хотя это серьезная ошибка, которая может привести к порче переменных, переполнению буфера и нарушению нормального хода выполнения программы.

UPD180920

14. Добавляю новый флаг для битового поля [3], и при этом забываю увеличить его длину. Потом долго ищу ошибку в отладчике, даже с просмотром ассемблерных инструкций - почему только что добавленный бит флага не устанавливается?..

UPD180926

15. Забываю увеличить разрядность переменной индекса, особенно часто при увеличении размера кольцевого буфера [4], когда размер увеличивается с 256 до 512. Индекс остается u8, хотя его надо сделать u16, после этого удивляюсь, почему буфер используется только наполовину, и почему индекс не достигает максимального своего значения.

UPD180928

16. По ошибке забываю удалить пробел перед открывающей круглой скобкой в макросе с параметрами. Особенно часто это происходит, когда переделываю функцию в макрос. Например, вот этот код выдаст ошибку Pe969 на строку "sprintf ...", которая совсем не относится к причине ошибки. Попробуйте догадаться, что причина в том, что после msg_at забыли удалить пробел:

IAR Pe969 VA ARGS

UPD181004

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

Известно, что разные виды процессора могут иметь различную разрядность. Поэтому существует такая вещь, как разрядность числа по умолчанию для конкретного процессора (компилятора), если разрядность не указана явно. Вот пример операции создания маски:

u32 maskvalue;
maskvalue |= (1 << 16);
printf("%08X", maskvalue);

На 32-разрядном процессоре ARM маска будет создана правильно, и printf выведет значение "00010000". Однако код для AVR выведет "FFFFFFFF". Причина в том, что константа 1 по-разному интерпретируется компилятором на разных платформах. Компилятор IAR ARM будет считать единицу числом 32-разрядным числом типа int, а компилятор AVR-GCC будет считать единицу 16-разрядным числом, или даже 8-разрядным (в зависимости от опций компиляции).

Поэтому во избежание ошибки добавляйте явное приведение числа-константы к нужному типу, вот так:

u32 maskvalue;
maskvalue |= ((u32)1 << 16);
printf("%08X", maskvalue);

Другой способ определения разрядности констант, часто встрачающийся в заголовочных файлах, это добавление суффикса к константе [6]. Мне этот способ не очень нравится, потому что надо помнить о назначении суффиксов, они тоже могут различаться для разных платформ и компиляторов:

u32 maskvalue;
maskvalue |= (1UL << 16);
printf("%08X", maskvalue);

UPD181018

18. Лишняя точка с запятой. Оставляю точку с запятой после скобок условия. Код при этом компилируется даже без предупреждения и работает, однако работает совсем не так, как хотелось. Сразу заметить подобную ошибку довольно трудно, пример:

   if (-1 != brdidx);   // Здесь не должно быть точки с запятой!
   {
      x = 4 + (brdidx << 2);
      if (NORMAL_MODE == adcinfo[brdidx].statenew)
      {
         clearAnimation(x,   3);
         clearAnimation(x+2, 3);
         animarr[0] = TABL_str[2][x];
         place_sym_bitmap(animarr[0], x,   3);
         animarr[0] = TABL_str[2][x+2];
         place_sym_bitmap(animarr[0], x+2, 3);
      }
   }

В этом примере блок кода, который следует за первым оператором if, выполнится всегда, независимо от того, какой будет результат от вычисления условия if (-1 != brdx).

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

for(uint32_t idx=0; idx < notusedidx; idx++);   // ошибка!
{
   bk_printf("[%u]\t%s\n", idx, memlogstore[idx].funcname);
   memset(tmptxt, 0, sizeof(tmptxt));
   memcpy(tmptxt, memlogstore[idx].txt, 4);
   bk_printf("\t%s\t%08X %08X %08X\n", tmptxt,
                                       memlogstore[idx].data32[0],
                                       memlogstore[idx].data32[1],
                                       memlogstore[idx].data32[2]);
}

UPD190226

19. Неправильно применяю формат для sscanf. Компилятор этого часто не замечает, даже не генерирует предупреждение. Неправильный формат легко вызовет непредсказуемое поведение программы, вплоть до зависания. Например, применяю строку формата, подразумевающую 32-разрядное число (к примеру, "%u"), подоставляя при этом указатель на 16-битную переменную (для которой нужно представить другой формат "%hu"). С функцией printf в формате тоже можно легко ошибиться, но особенно опасно неправильное использование sscanf, что может привести к порче памяти.

UPD190319

20. Ошибочно открываю двоичный файл как текстовый. Это может привести к неправильной интерпретации записываемых данных. Пример открытия файла картинки на запись:

int openOutputFile(char *fileName)
{
    outFile = fopen( fileName, "wb" );
    if( ! outFile )
    {
        fprintf(stderr, "Error opening output file.\n");
        return 0;
    }
    return 1;
}

Если в функции fopen указать "w" вместо "wb", то по умолчанию файл будет открыт как текстовый. Это приведет к тому, что попадающиеся одиночные байты 0x0A, которые соответствуют окончанию строки в стиле Unix, будут автоматически заменены на двухбайтовую последовательность окончания строки 0x0D 0x0A в стиле Windows. Найти эту ошибку бывает очень трудно, если не помнить о такой особенности библиотек файлового ввода/вывода.

UPD201106

21. Некоторые процессоры требуют, чтобы его слово в памяти всегда начиналось с байтового адреса, который нацело делится на 2 или на 4. Тогда если передать слово по нечетному адресу, то возникнет исключение. На этапе компиляции эта ошибка обычно не обнаруживается, и происходит во время выполнения программы. Пример для процессора Blackfin:

typedef struct
{
   u16 id;
   u8 dlc;
   // Внимание, массив data может начинаться 
   // с любого адреса, даже нечетного:
   u8 data[8];
}TCanPacket;
 
TCanPacket *pkt;
TCanPacket CANdata[8];
...
 
pkt = &CANdata[idx];
// На этом вызове произойдет сбой, если адрес начала
// массива data не делится нацело на 4:
DecodeErrorCode(*(u32*)(&pkt->data[0]));

UPD201219

22. Забываю объявлять переменную как volatile, когда она может асинхронно изменяться в другом потоке или в обработчике прерывания. См. разбор такой ситуации в разделе "Простой пример, где нужен volatile" статьи [7].

UPD240305

23. Пропустил оператор return в функции, которая должна возвращать значение, в результате получил трудно обнаруживаемую ошибку. Как это ни странно, компилятор GCC не всегда сообщает о такой ошибке, или вы можете пропустить предупреждение, если в логе компиляции предупреждений много (вот почему я терпеть не могу оставлять не исправленные предупреждения компиляции).

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

[Ссылки]

1. Определения "char const *" и "const char *" - в чем разница?
2Управление оптимизацией в IAR.
3. IAR, битовые поля (bitfields).
4. Работа с кольцевым буфером.
5. IAR EW ARM: устранение ошибок компиляции.
6. Integer literal in C/C++ (Prefixes and Suffixes) geeksforgeeks.org.
7Как использовать ключевое слово volatile на языке C.
8. Specifying Attributes of Variables site:gcc.gnu.org.

 

Комментарии  

 
0 #2 Hoksmur 11.06.2019 08:04
№16: пишем функции без пробела после имени.
№17: в случае малейших сомнений использовать суффиксы - 1UL; в вашем примере единица ещё и знаковая! (но разрядность платформы надо помнить).
Цитировать
 
 
0 #1 ВитГо 20.12.2012 10:19
Хорошие ошибки собраны! Странно, что никто не отписался по ним, ведь в реале каждый на них напарывается и теряет уйму времени на отладку и понимание.
Цитировать
 

Добавить комментарий


Защитный код
Обновить

Top of Page