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

Поделиться

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

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите 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 байта (в зависимости от разрядности ядра и особенностей системы команд платформы), что приводит к неожиданным и трудно обнаруживаемым ошибкам.

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 ошибка не проявляется, и т. п.

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

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

[Ссылки]

1. Определения "char const *" и "const char *" - в чем разница?
2. Управление оптимизацией в IAR.

 

Комментарии  

 
-1 #1 ВитГо 20.12.2012 10:19
Хорошие ошибки собраны !

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

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


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

Top of Page