Русский
Русский
English
Статистика
Реклама

Си

FizzBuzz по-сениорски

31.01.2021 16:07:40 | Автор: admin

- Добрый день, я на интервью на позицию старшего разработчика.

- Здравствуйте, давайте начнем с небольшого теста, пока я ваше CV смотрю. Напишите программу, которая выводила бы числа от 1 до, скажем, миллиарда, притом если число кратно трем, то вместо числа выводится Fizz, если кратно пяти, то Buzz, а если и трем, и пяти, то FizzBuzz.

Серьезно, FizzBuzz? Задачка для начальной школы, на сениорскую позицию? Ну ладно.


Я достаю свой верный лаптоп, и пишу такой код:

#include <stdio.h>#define LIMIT 1000000000int main(void) {    for (int i = 1; i <= LIMIT; i++) {        if (0 == i % 3) {            if (0 == i % 5) {                printf("FizzBuzz\n");            } else {                printf("Fizz\n");            }        } else if (0 == i % 5) {            printf("Buzz\n");        } else {            printf("%d\n", i);        }    }    return 0;}

Запускаю программу, она себе бежит, но не так чтобы сильно быстро, через 3 с чем-то минуты, после первого миллиона, я ее прерываю и быстренько высчитываю, что весь процесс займет больше двух суток. Да, наверно надо было включить буферизацию, это бы несколько ускорило, но это не спасет, лучше просто перенаправить вывод в файл, что я и делаю, и через 41.5 секунду у меня есть красивенький файл на 7.5 гигов.

- Вам не кажется, что можно побыстрее? - спрашивает интервьюер.

- Да ладно, основное время занимает I/O, 7.5 гигов записать не шутка, даже на SSD.

- А давайте перенаправим вывод в /dev/null.

- Без проблем.

Через минуту:

- Как это 39.5 секунд? То есть весь I/O занимает 2 секунды, а все остальное время мой код?

- Да, так получается. Это не самая медленная реализация, на каждой итерации два сравнения и один printf, я часто вижу вариант с тремя сравнениями и двумя printfами. Для джуниора, я бы сказал, это даже хорошо. А вот для сениора ...

Это было больно, но, пожалуй, заслужено. Ладно, я тебе покажу, кто тут джуниор.

- Сейчас сделаю побыстрее.

- Попробуйте. Только объясняйте, что вы делаете.

- Видите, что у нас тут есть паттерн каждые 3*5, то есть 15 итераций цикла логика полностью повторяется. Тогда можно переделать цикл:

    for (i = 1; i < LIMIT - 15; i += 15) {        printf( "%d\n"          // 1                "%d\n"          // 2                "Fizz\n"        // 3                "%d\n"          // 4                "Buzz\n"        // 5                "Fizz\n"        // 6                "%d\n"          // 7                "%d\n"          // 8                "Fizz\n"        // 9                "Buzz\n"        // 10                "%d\n"          // 11                "Fizz\n"        // 12                "%d\n"          // 13                "%d\n"          // 14                "FizzBuzz\n",   // 15                i, i+1, i+3, i+6, i+7, i+10, i+12, i+13);    }

- Если раньше на каждые 15 чисел у нас приходилось 15 сравнений переменной цикла и два ifа в теле цикла, то есть в общей сложности 45 сравнений, а каждое сравнение это потенциальная проблема с branch predictionом, то теперь одно. Да и вызовов printfа стало в 15 раз меньше. Одна только проблема цикл не дойдет ровно до миллиарда, а только до 999999990 (макс число, кратное 15 и меньшее миллиарда), так что оставим старый цикл, но только для обработки хвоста, то есть последних 10 значений (это практически не влияет на производительность).

После всех изменений получился такой код.

- И что у нас со временем получается?

- Если вывод в файл, то 22.5 секунды, если в /dev/null 20.2

Интервьюер доволен, похоже, чего-то такого он от меня и ожидал. Но зря он про джуниора сказал.

- Я думаю, что это не предел.

- В самом деле? А что тут можно еще оптимизировать?

- Я уменьшил количество вызовов printfа в 15 раз, но при этом сами эти printfы стали тяжелее. Да и вообще printf сам по себе тяжелый, из-за своей мощности это ведь фактически виртуальная машина со своим языком, полным по Тьюрингу, на нем даже крестики-нолики писали. В данной ситуации используется лишь небольшая часть возможностей printf, так что можно его заменить на что-то свое, более легкое:

#define NUM cur += myitoa(num++, cur)#define FIZZ do { memcpy(cur, "Fizz\n", 5); cur += 5; num++; } while (0)#define BUZZ do { memcpy(cur, "Buzz\n", 5); cur += 5; num++; } while (0)#define FIZZBUZZ do { memcpy(cur, "FizzBuzz\n", 9); cur += 9; } while (0)void print(int num) {    static char wrkbuf[CHUNK_SIZE];    char *cur = wrkbuf;    NUM;    NUM;    FIZZ;    NUM;    BUZZ;    FIZZ;    NUM;    NUM;    FIZZ;    BUZZ;    NUM;    FIZZ;    NUM;    NUM;    FIZZBUZZ;    fwrite(wrkbuf, cur - wrkbuf, 1, stdout);}

- Можно, конечно, использовать уже готовую itoa, но это нестандартная функция, не везде есть, да и она слишком универсальная, поскольку поддерживает разные системы счисления, а у нас только десятичная система - упрощаем все, что можно. Ну и, конечно, в главном цикле просто вызываем print(i) вместо длинного printfа.

Получается такой код.

Я подхожу к доске и рисую табличку с результатами запусков:

Вариант

Вывод в файл

Вывод в /dev/null

Время (сек)

Относ наивной

Относ предыдущей

Время (сек)

Относ наивной

Относ предыдущей

наивная

41.429

1x

-

39.650

1x

-

оптимизация цикла

22.546

1.83x

1.83x

20.151

1.97x

1.97x

отказ от printf

12.563

3.30x

1.80x

8.771

4.52x

2.30x

- В принципе на вывод в файл можно особо не смотреть там какое-то время съедается на I/O, и оно плавает, так что лучше ориентироваться на время без I/O.

Я стираю ту часть, где про вывод в файл.

- Итого ускорение в 4 с половиной раза.

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

- Я знаю, как можно еще ускорить.

- Серьезно?

- Абсолютно. Я до этого использовал чисто технические способы ускорения, а ведь можно еще и алгоритмически улучшить. Смотрите, что будет напечатано, когда мы вызываем, например, print(150000001) и следующий за ним print(150000016):

150000001\n150000002\nFizz\n150000004\nBuzz\nFizz\n150000007\n150000008\nFizz\nBuzz\n150000011\nFizz\n150000013\n150000014\nFizzBuzz\n150000016\n150000017\nFizz\n150000019\nBuzz\nFizz\n150000022\n150000023\nFizz\nBuzz\n150000026\nFizz\n150000028\n150000029\nFizzBuzz\n       ^^         ^^               ^^                     ^^         ^^                     ^^               ^^         ^^ 

- Отличия всего в 16 байтах, а программа всю строку пересобирает с нуля. Можно просто менять байты на месте. Правда, заранее неизвестно, сколько десятичных разрядов надо поменять, так что это потребуется вычислить сравнить два буфера, и определить, где они отличаются. Это, пожалуй, тяжеловатая задача, но у нас есть, я делаю театральную паузу векторные инструкции и интринсики для них!

Я не озвучиваю, но подразумеваю, что джуниор такого бы не придумал. В этот момент понимаю, что интервьюер тоже.

Открываю Intelовскую страницу с интринсиками и нахожу там нужные векторные функции для работы с 16-байтными векторами. У меня тут максимум 10-байтные, но их можно добить нулями до 16, не проблема. И да, 16-байтные вектора это SSE инструкции, никакой AVX-512 тут не нужен, мой 4-летний мобильный проц это точно потянет.

Получаю такой кусок с жирными и вкусными интринсиками:

unsigned int diff = 0xFFFF & ~_mm_movemask_epi8(_mm_cmpeq_epi8(                                  _mm_load_si128((__m128i const *)prev_first_number),                                  _mm_load_si128((__m128i const *)last_number)));unsigned int diff_pos = 16 - _tzcnt_u32(diff);   // number of changed digits

Быстрая проверка flags в /proc/cpuinfo нужные для выбранных мной интринсиков SSE2 (еще со времен Pentium 4) и BMI1 (появился в Haswellах) в CPU есть, все должно работать.

Запускаю тот код, что получился, смотрю уже только вывод в /dev/null и обновляю табличку:

Вариант

Время (сек)

Относительно наивной

Относительно предыдущей

наивная

39.650

1x

-

оптимизация цикла

20.151

1.97x

1.97x

отказ от printf

8.771

4.52x

2.30x

переиспользование буфера

4.490

8.83x

1.95x

Еще почти в 2 раза ускорились! А по сравнению с начальным вариантов так вообще почти в 9. Жаль, до 10 раз не дотянул.

- Ну все, наверно теперь уже хватит. Это уже вполне по-сениорски.

Во взгляде интервьюера читается облегчение.

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

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

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

for (int j = 0; j < THREAD_COUNT; j++) {        thread_pool[j].start_num = i;        thread_pool[j].count = NUMS_PER_THREAD;        thread_pool[j].buf = malloc(BUFFER_SIZE);        pthread_create(&thread_pool[j].id, NULL, worker, (void *)&thread_pool[j]);        i += NUMS_PER_THREAD;    }    int active_threads = THREAD_COUNT;    int max = LIMIT / 15 * 15;    for (int j = 0; active_threads; j = (j+1) % THREAD_COUNT) {        pthread_join(thread_pool[j].id, NULL);        fwrite(thread_pool[j].buf, thread_pool[j].buflen, 1, stdout);        if (max - i > NUMS_PER_THREAD) {            thread_pool[j].start_num = i;            pthread_create(&thread_pool[j].id, NULL, worker, (void *)&thread_pool[j]);            i += NUMS_PER_THREAD;        } else if (max > i) {            thread_pool[j].start_num = i;            thread_pool[j].count = max - i + 1;            pthread_create(&thread_pool[j].id, NULL, worker, (void *)&thread_pool[j]);            i += max - i + 1;        } else {            free(thread_pool[j].buf);            active_threads--;        }    } 

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

Проведя несколько замеров, я остановился на кусках по 3 миллиона чисел удобное число, кратное 15, и результат хороший.

Получился такой код.

Запускаю, и обновляю данный в табличке:

Вариант

Время (сек)

Относительно наивной

Относительно предыдущей

наивная

39.650

1x

-

оптимизация цикла

20.151

1.97x

1.97x

отказ от printf

8.771

4.52x

2.30x

переиспользование буфера

4.490

8.83x

1.95x

многопоточность

1.748

22.68x

2.57x

- Ну вот, я уменьшил время обработки в 22 с лишним раза. И код получился очень даже сениорский умный алгоритм, многопоточность, интринсики опять же. Как считаете?

Я отвернулся от доски и увидел, что в переговорке я один. Через стеклянную стенку переговорки я разглядел интервьюера, который что-то быстро говорил секретарю, показывая пальцем в мою сторону. Он что, вызывает охрану? Похоже, что мне тут больше не рады.

Я быстро закрыл лаптоп и покинул офис. Почему-то мне так и не перезвонили.


Все тесты делались на Dell Latitude 7480 с i7-7600U 2.8 Ghz, 16 Gb памяти, SSD и OpenSUSE Leap 15.1 с kernelом 4.12.14, каждый тест не менее 10 раз, выбиралось наименьшее значение. При компиляции использовались флаги -O3 -march=native -pthread

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

Подробнее..

Перевод bdshemu эмулятор шелл-кода в Bitdefender

16.11.2020 16:04:47 | Автор: admin
Совсем скоро, 19 ноября, у нас стартует курс Этичный хакер, а специально к этому событию мы подготовили этот перевод о bdshemu написанном на языке C эмуляторе с открытым исходным кодом в Bitdefender для обнаружения эксплойтов на 32- и 64-битной архитектуре. Эмулятор очень прост, а благодаря нацеленности на уровень инструкций он работает с любой операционной системой. Кроме того, этот эмулятор зачастую сохраняет расшифрованный эксплойт в бинарный файл. Подробности и пример обнаружения Metasploit под катом, ссылка на репозиторий проекта на Github в конце статьи.





Введение


Обнаружение эксплойтов одна из основных сильных сторон Hypervisor Memory Introspection (HVMI). Возможность мониторинга гостевых страниц физической памяти на предмет различных видов доступа, таких как запись или выполнение, позволяет HVMI накладывать ограничения на критические области памяти: например, страницы стека или кучи могут быть помечены как невыполняемыми на уровне EPT, поэтому, когда эксплойту удается получить доступ к произвольному выполнению кода, вмешивается логика интроспекции и блокирует выполнение шелл-кода.

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

В этом посте поговорим об эмуляторе Bitdefender Shellcode Emulator, или, для краткости, bdshemu. Это библиотека, способная эмулировать базовые инструкции x86, наблюдая при этом похожее на шелл-код поведение. Легальный код например JIT-код будет выглядеть иначе, чем традиционный шелл-код, bdshemu пытается определить, ведёт ли себя код в эмуляции, как шелл-код.

Обзор bdshemu


bdshemu библиотека на C, частью проекта bddisasm (и, конечно же, она использует bddisasm для расшифровки инструкций). Библиотека bdshemu создана только для эмуляции кода x86, поэтому не имеет поддержки вызовов API. На самом деле, среда эмуляции сильно ограничена и урезана, доступны только две области памяти:

  1. Содержащие эмулируемый код страницы.
  2. Стек.

Обе эти области виртуализированы, то есть на самом деле являются копиями эмулируемой фактической памяти, поэтому внесенные в них изменения не влияют на фактическое состояние системы. Любой доступ эмулируемого кода за пределы этих областей (которые мы будем называть шелл-кодом и стеком соответственно), спровоцирует немедленное завершение эмуляции. Например, вызов API автоматически вызовет ветку вне области шелл-кода, тем самым прекратив эмуляцию. Однако в bdshemu всё, что нас волнует это поведение кода на уровне инструкций, которого достаточно, чтобы понять, вредоносен код или нет.

Хотя bdshemu предоставляет основную инфраструктуру для обнаружения шелл-кодов внутри гостевой операционной системы, стоит отметить, что это не единственный способ, которым HVMI определяет, что выполнение определённой страницы является вредоносным. Используются ещё два важных индикатора:

  1. Выполняемая страница находится в стеке это обычное дело для уязвимостей на основе стека.
  2. Подделка стека когда страница выполняется впервые, а регистр RSP указывает за пределы выделенного для потока обычного стека.

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

Архитектура bdshemu


bdshemu создаётся как отдельная библиотека C и зависит только от bddisasm. Работать с bdshemu довольно просто, поскольку у этих двух библиотек имеется общий API:

SHEMU_STATUSShemuEmulate(    SHEMU_CONTEXT *Context    );

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

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

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

  1. Входные регистры, такие как сегменты, регистры общего назначения, регистры MMX и SSE; их можно оставить в значении 0, если они неизвестны или не актуальны.
  2. Входной код, то есть код для эмуляции.
  3. Входной стек, который может содержать фактическое содержимое стека, или может быть оставлен со значением 0.
  4. Информация о среде, например режим (32 или 64 бита) или кольцо (0, 1, 2 или 3).
  5. Параметры управления: минимальная длина строки стека, минимальная длина цепочки NOP или максимальное количество инструкций, которые должны быть эмулированы.

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

bdshemu построен как быстрый и простой эмулятор инструкций x86. Поскольку он работает только с самим шелл-кодом и небольшим виртуальным стеком, ему не нужно имитировать какие-то архитектурные особенности прерывания или исключения, таблицы дескрипторов, таблицы страниц. Кроме того, поскольку мы имеем дело только с шелл-кодом и стековой памятью, bdshemu не выполняет проверку доступа к памяти потому, что не разрешает доступ даже к другим адресам. Единственное состояние, кроме регистров, к которому можно получить доступ, это сам шелл-код и стек, и они оба копии фактического содержимого памяти. То есть состояние системы никогда не изменяется во время эмуляции, изменяется только предоставленный SHEMU_CONTEXT. Это делает bdshemu чрезвычайно быстрым, простым и позволяет сосредоточиться на его основной цели: обнаружении шелл-кода.

Что касается поддержки инструкций, bdshemu поддерживает все основные инструкции x86, такие как ветвления, арифметика, логика, сдвиг, манипуляции с битами, умножение и деление, доступ к стеку и инструкции передачи данных. Кроме того, он также поддерживает другие инструкции, например, некоторые базовые инструкции MMX или AVX. Два хороших примера PUNPCKLBW и VPBROADCAST.

Методы обнаружения bdshemu


Есть несколько индикаторов, которые использует bdshemu. Чтобы определить, ведёт ли себя эмулируемый фрагмент кода как шелл-код.

NOP Sled


Это классическое представление шелл-кода; поскольку точка его входа при выполнении может быть неизвестна точно, злоумышленники обычно добавляют длинную последовательность инструкций NOP, закодированную 0x90. Параметры длины последовательностей NOP можно контролировать при вызове эмулятора через контекстное поле NopThreshold. Значение SHEMU_DEFAULT_NOP_THRESHOLD по умолчанию равно 75. Это означает, что минимум 75 % всех эмулируемых инструкций должны быть инструкциями NOP.

RIP Load


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

  1. CALL $ + 5/POP ebp выполнение этих двух инструкций приведёт к тому, что значение указателя инструкции сохранится в регистре ebp; затем можно получить доступ к данным внутри шелл-кода, используя смещения относительно значения ebp
  2. FNOP/FNSTENV [esp-0xc]/POP edi первая инструкция это любая инструкция FPU (не обязательно FNOP), а вторая инструкция FNSTENV сохраняет среду FPU в стеке; третья инструкция получит указатель инструкции FPU из esp-0xc, который является частью среды FPU и содержит адрес последнего выполненного FPU в нашем случае FNOP. С этого момента для доступа к данным шелл-кода можно использовать адресацию относительно edi
  3. Внутренне bdshemu отслеживает все экземпляры указателя инструкции, сохранённые в стеке. Последующая загрузка указателя инструкции из стека каким-либо образом приведёт к срабатыванию этого обнаружения. Благодаря тому, что bdshemu отслеживает сохранённые указатели инструкций, не имеет значения, когда, где и как шелл-код пытается загрузить регистр RIP и использовать его: bdshemu всегда будет запускать обнаружение.

В 64-битном режиме относительная адресация RIP может использоваться напрямую: это позволяет кодировка инструкций. Однако, как ни странно, большое количество шелл-кода по-прежнему использует классический метод получения указателя инструкций (обычно технику CALL/POP), но, вероятно, указывает на то, что 32-битные шелл-коды были перенесены на 64-битные с минимальными изменениями.

Запись шелл-кода самим шелл-кодом


Чаще всего шелл-код закодирован или зашифрован, чтобы избежать некоторых плохих символов (например 0x00, который должен напоминать строку, может сломать эксплойт), или чтобы избежать обнаружения технологиями безопасности например, AV-сканерами. Это означает, что во время выполнения шелл-код должен декодировать себя обычно на месте изменяя свое собственное содержимое, а затем выполняя текстовый код. Типичные методы декодирования включают алгоритмы дешифрования на основе XOR или ADD.

Конечно, bdshemu знает о таком поведении и отслеживает каждый изменённый внутри шелл-кода байт. Каждый раз, когда предполагаемый шелл-код записывает какую-то часть себя, а затем выполняет её, срабатывает обнаружение самостоятельной записи шелл-кода.

Доступ к TIB


После того как шелл-код получил возможность выполнения, ему необходимо найти несколько функций внутри разных модулей, чтобы позаботиться о полезной нагрузке (например, загрузке файла или создании процесса). В Windows наиболее распространенный способ сделать это разобрать структуры загрузчика пользовательского режима, чтобы найти адреса, по которым загружены требуемые модули, а затем найти необходимые функции внутри этих модулей. Ниже дана последовательность структур, к которым будет обращаться шелл-код:

  1. Блок среды потока (TEB), который расположен в fs: [0] (32-битный поток) или gs: [0] (64-битный поток).
  2. Блок среды процесса (PEB), который расположен по адресу TEB + 0x30 (32 бит) или TEB + 0x60 (64 бит).
  3. Информация о загрузчике (PEB_LDR_DATA), расположенная внутри PEB.


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

При каждом обращении к памяти bdshemu увидит, пытается ли предполагаемый шелл-код получить доступ к полю PEB внутри TEB. bdshemu отслеживает обращения к памяти, даже если они выполняются без классических префиксов сегментов fs/gs до тех пор, пока идентифицирован доступ к полю PEB внутри TEB, будет срабатывать обнаружение доступа к TIB.

Направленный вызов SYSCALL


Легитимный код будет полагаться на несколько библиотек для вызова служб операционной системы например для создания процесса в Windows обычный код будет вызывать одну из функций CreateProcess. Легитимный код редко вызывает SYSCALL напрямую, поскольку интерфейс SYSCALL со временем может измениться. По этой причине bdshemu запускает обнаружение SYSCALL всякий раз, когда обнаруживает, что предполагаемый шелл-код напрямую вызывает системную службу с помощью инструкций SYSCALL, SYSENTER или INT.

Строки стека


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

push 0x6578652Epush 0x636C6163

В приведённом выше коде в стеке сохраняется строка calc.exe, которую затем можно использовать как обычную строку во всем шелл-коде.

Для каждого сохранённого в стеке значения, напоминающего строку, bdshemu отслеживает общую длину созданной в стеке строки. Как только порог, указанный полем StrLength внутри контекста, будет превышен, будет запущено обнаружение строки стека. Значение по умолчанию для этого поля SHEMU_DEFAULT_STR_THRESHOLD равно 8. Это означает, что динамическое построение строки длиной не менее 8 символов в стеке вызовет это обнаружение.

Методы обнаружения для шелл-кода режима ядра


Хотя вышеупомянутые методы общие и могут применяться к любому шелл-коду, в любой операционной системе, как в 32-, так и в 64-битной версии (за исключением обнаружения доступа к TIB, которое специфично для Windows) bdshemu может определять специфичное для ядра поведение шелл-кода.

Доступ KPCR


Область управления процессором ядра (KPCR) это структура для каждого процессора в системах Windows, которая содержит много критически важной для ядра информации, но также может быть полезной для злоумышленника. Обычно шелл-код может ссылаться на текущий выполняющийся поток, который можно получить, обратившись к структуре KPCR, со смещением 0x124 в 32-битных системах и 0x188 в 64-битных системах. Так же, как и в методе обнаружения доступа к TIB, bdshemu отслеживает обращения к памяти, и когда эмулируемый код считывает текущий поток из KPCR, он запускает обнаружение доступа к KPCR.

Выполнение SWAPGS


SWAPGS это системная инструкция, которая выполняется только при переходе из пользовательского режима в режим ядра, и наоборот. Иногда, из-за специфики определенных эксплойтов ядра злоумышленнику в конечном счёте нужно выполнить SWAPGS например, полезная нагрузка ядра EternalBlues, как известно, перехватывает обработчик SYSCALL, поэтому нужно выполнить SWAPGS, когда состоялся SYSCALL, подобно тому, как это сделал бы обычный системный вызов.

bdshemu запускает обнаружение SWAPGS всякий раз, когда инструкция SWAPGS выполняется подозрительным шелл-кодом.

Чтение и запись MSR


Иногда шелл-код (например вышеупомянутая полезная нагрузка ядра EternalBlue) должен изменить обработчик SYSCALL, чтобы перейти в стабильную среду выполнения (например, потому что исходный шелл-код выполняется в высоких значениях диапазона IRQL, которые необходимо снизить перед вызовом полезных подпрограмм). Это делается путём изменения MSR SYSCALL с помощью инструкции WRMSR, а затем ожидания выполнения системного вызова (который находится на более низком уровне IRQL) для продолжения выполнения (здесь также пригодится метод SWAPGS потому, что на 64-битной версии SWAPGS должна выполняться после каждого SYSCALL).

Кроме того, чтобы найти образ ядра в памяти и, следовательно, полезные процедуры ядра, быстрый и простой способ запросить SYSCALL MSR (которая обычно указывает на обработчик SYSCALL внутри образа ядра), а затем пройти по страницам назад, пока не будет найдено начало образа ядра.

bdshemu будет запускает обнаружение доступа MSR всякий раз, когда подозрительный шелл-код обращается к MSR SYSCALL, (как в 32-, так и в 64-битном режиме).

Пример


Проект bdshemu содержит несколько синтетических тестовых примеров, но лучший способ продемонстрировать его функциональность использовать реальный шелл-код. В этом отношении Metasploit замечателен в создании разных видов полезной нагрузки с использованием всех видов кодировщиков. В качестве чисто дидактического [прим. перев. назидательного] примера возьмём такой код:

DA C8 D9 74 24 F4 5F 8D 7F 4A 89 FD 81 ED FE FFFF FF B9 61 00 00 00 8B 75 00 C1 E6 10 C1 EE 10 83 C5 02 FF 37 5A C1 E2 10 C1 EA 10 89 D3 09 F3 21 F2 F7 D2 21 DA 66 52 66 8F 07 6A 02 03 3C 24 5B 49 85 C9 0F 85 CD FF FF FF 1C B3 E0 5B 62 5B 62 5B 02 D2 E7 E3 27 87 AC D7 9C 5C CE 50 45 02 51 89 23 A1 2C 16 66 30 57 CF FB F3 9A 8F 98 A3 B8 62 77 6F 76 A8 94 5A C6 0D 4D 5F 5D D4 17 E8 9C A4 8D DC 6E 94 6F 45 3E CE 67 EE 66 3D ED 74 F5 97 CF DE 44 EA CF EB 19 DA E6 76 27 B9 2A B8 ED 80 0D F5 FB F6 86 0E BD 73 99 06 7D 5E F6 06 D2 07 01 61 8A 6D C1 E6 99 FA 98 29 13 2D 98 2C 48 A5 0C 81 28 DA 73 BB 2A E1 7B 1E 9B 41 C4 1B 4F 09 A4 84 F9 EE F8 63 7D D1 7D D1 7D 81 15 B0 9E DF 19 20 CC 9B 3C 2E 9E 78 F6 DE 63 63 FE 9C 2B A0 2D DC 27 5C DC BC A9 B9 12 FE 01 8C 6E E6 6E B5 91 60 F2 01 9E 62 B0 07 C8 62 C8 8C

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



В disasmtool инструменте проекта bddisasm, для запуска эмулятора шелл-кода на входе можно воспользоваться параметром -shemu.

disasmtool -b32 -shemu -f shellcode.bin

Выполнение отобразит пошаговую информацию о каждой эмулируемой команде, но так как эта трассировка длинная, давайте перейдем непосредственно к концу if:

Emulating: 0x0000000000200053 XOR       eax, eax        RAX = 0x0000000000000000 RCX = 0x0000000000000000 RDX = 0x000000000000ee00 RBX = 0x0000000000000002        RSP = 0x0000000000100fd4 RBP = 0x0000000000100fd4 RSI = 0x0000000000008cc8 RDI = 0x000000000020010c        R8  = 0x0000000000000000 R9  = 0x0000000000000000 R10 = 0x0000000000000000 R11 = 0x0000000000000000        R12 = 0x0000000000000000 R13 = 0x0000000000000000 R14 = 0x0000000000000000 R15 = 0x0000000000000000        RIP = 0x0000000000200055 RFLAGS = 0x0000000000000246Emulating: 0x0000000000200055 MOV       edx, dword ptr fs:[eax+0x30]Emulation terminated with status 0x00000001, flags: 0xe, 0 NOPs        SHEMU_FLAG_LOAD_RIP        SHEMU_FLAG_WRITE_SELF        SHEMU_FLAG_TIB_ACCESS

Мы видим, что последняя эмулированная инструкция MOV edx, dword ptr fs: [eax + 0x30] это инструкция доступа к TEB, но она также запускает эмуляцию, которая должна быть остановлена, поскольку это доступ за пределы памяти шелл-кода (вспомним, что bdshemu остановится при первом обращении к памяти вне шелл-кода или стека). Более того, этот небольшой шелл-код (сгенерированный с помощью Metasploit) вызвал 3 обнаружения в bdshemu:

  1. SHEMU_FLAG_LOAD_RIP шелл-код загружает RIP в регистр общего назначения, чтобы определить его позицию в памяти.
  2. SHEMU_FLAG_WRITE_SELF расшифровывает сам себя, а затем выполняет расшифрованные фрагменты.
  3. SHEMU_FLAG_TIB_ACCESS обращается к PEB, чтобы найти важные библиотеки и функции.


Этих срабатываний более чем достаточно, чтобы сделать вывод, что эмулируемый код, без сомнения, является шелл-кодом. Что еще более удивительно в bdshemu, так это то, что обычно в конце эмуляции память содержит расшифрованную форму шелл-кода. disasmtool достаточно хорош, чтобы сохранить память шелл-кода после завершения эмуляции: создаётся новый файл с именем shellcode.bin_decoded.bin, содержащий декодированный шелл-код. Давайте посмотрим на него:



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

Заключение


В этом посте мы представили эмулятор шелл-кода Bitdefender, который является важной частью технологии обнаружения эксплойтов HVMI. bdshemu построен для обнаружения индикаторов шелл-кода на уровне двоичного кода, без необходимости эмулировать сложные вызовы API, сложное расположение памяти или архитектурные объекты, такие как таблицы страниц, таблицы дескрипторов и т. д. bdshemu фокусируется на том, что имеет наибольшее значение, эмулируя инструкции и определяя шелл-код по поведению.

Благодаря своей простоте bdshemu работает с шелл-кодом, нацеленным на любую операционную систему: большинство методов обнаружения определены на уровне поведения инструкций, а не на высокоуровневом поведении (например на уровне вызовов API). Кроме того, он работает как с 32-битным, так и с 64-битным кодом, а также с кодом, специфичным для режимов пользователя или ядра.

Ссылка на Github

На тот случай если вы задумали сменить сферу или повысить свою квалификацию промокод HABR даст вам дополнительные 10 % к скидке указанной на баннере.

image



Рекомендуемые статьи


Подробнее..

Почему PVS-Studio не предлагает автоматические правки кода

19.11.2020 10:11:44 | Автор: admin
Почему PVS-Studio не предлагает автоматические правки кода

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

Иногда программисты, которые начинают пробовать PVS-Studio спрашивают: почему инструмент не предлагает автоматически исправить ошибку? Что интересно, пользователи такой вопрос уже не задают. После некоторого времени использования анализатора, им становится понятно, что для подавляющего большинства обнаруживаемых ошибок никакая автоматическая замена невозможна. По крайней мере, пока не изобретут искусственный интеллект :).

Причина в том, что PVS-Studio не является анализатором стиля кода. Он не предлагает изменения, связанные с форматированием или именованием. Не предлагает он (по крайней мере на момент написания статьи :) заменить в C++ коде все NULL на nullptr. Это хоть и хорошее предложение, но оно не имеет практически ничего общего с поиском и устранением ошибок.

PVS-Studio выявляет ошибки и потенциальные уязвимости. Многие ошибки заставляют задуматься и требуют изменения поведения программы. И только программист может решить, как исправить ту или иную ошибку.

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

Рассмотрим ошибку, которую я разбирал в статье "31 февраля".

static const int kDaysInMonth[13] = {  0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};bool ValidateDateTime(const DateTime& time) {  if (time.year < 1 || time.year > 9999 ||      time.month < 1 || time.month > 12 ||      time.day < 1 || time.day > 31 ||      time.hour < 0 || time.hour > 23 ||      time.minute < 0 || time.minute > 59 ||      time.second < 0 || time.second > 59) {    return false;  }  if (time.month == 2 && IsLeapYear(time.year)) {    return time.month <= kDaysInMonth[time.month] + 1;  } else {    return time.month <= kDaysInMonth[time.month];  }}

Анализатор понимает, что обе проверки являются истинными. Но почему, анализатору непонятно. Он ничего не знает про дни, месяцы и прочие сущности. И научить понимать такое ой как непросто. Единственное, что реально сделать это чтобы анализатор предлагал упростить функцию:

bool ValidateDateTime(const DateTime& time) {  if (time.year < 1 || time.year > 9999 ||      time.month < 1 || time.month > 12 ||      time.day < 1 || time.day > 31 ||      time.hour < 0 || time.hour > 23 ||      time.minute < 0 || time.minute > 59 ||      time.second < 0 || time.second > 59) {    return false;  }  if (time.month == 2 && IsLeapYear(time.year)) {    return true;  } else {    return true;  }}

Или, чего уж мелочиться, пусть он предложит такую автоматическую замену:

bool ValidateDateTime(const DateTime& time) {  if (time.year < 1 || time.year > 9999 ||      time.month < 1 || time.month > 12 ||      time.day < 1 || time.day > 31 ||      time.hour < 0 || time.hour > 23 ||      time.minute < 0 || time.minute > 59 ||      time.second < 0 || time.second > 59) {    return false;  }  return true;}

Прикольно, но бессмысленно ;). Анализатор убрал код, который с точки зрения языка C++ является лишним. И только человек может понять, является код действительно избыточным (а такое тоже часто бывает), или в коде допущена опечатка и надо заменить month на day.

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

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

static DWORDget_rel_wait(const struct timespec *abstime){  struct __timeb64 t;  _ftime64_s(&t);  time_t now_ms = t.time * 1000 + t.millitm;  time_t ms = (time_t)(abstime->tv_sec * 1000 +    abstime->tv_nsec / 1000000);  DWORD rel_wait = (DWORD)(ms - now_ms);  return rel_wait < 0 ? 0 : rel_wait;}

Раз переменная rel_wait имеет беззнаковый тип, то последующая проверка rel_wait < 0 не имеет смысла. Предупреждение PVS-Studio: V547 [CWE-570] Expression 'rel_wait < 0' is always false. Unsigned type value is never < 0. os_thread_windows.c 359

Кто-то воодушевился статьёй и начал массово исправлять описанные в ней ошибки: Fix various issues reported by PVS-Studio analysis.

И как же было предложено исправить код? Весьма бесхитростно: core: simplify windows timer implementation.

Неудачное исправление кода

Но код был упрощен, а не исправлен! Это заметили и началась соответствующая дискуссия: ISSUE: os_thread_windows.c get_rel_wait() will block if abstime is in the past.

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

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


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why PVS-Studio Doesn't Offer Automatic Fixes.
Подробнее..

Espressif IoT Development Framework 71 выстрел в ногу

20.01.2021 16:09:28 | Автор: admin

0790_Espressif_IoT_Development_Framework_ru/image1.png
Один из наших читателей обратил наше внимание на Espressif IoT Development Framework. Он нашёл ошибку в коде проекта и поинтересовался, смог бы её найти статический анализатор PVS-Studio. Именно эту ошибку анализатор пока найти не может, зато нашёл множество других. По мотивам этой истории и найденных ошибок, мы решили написать классическую статью про проверку открытого проекта. Приятного изучения того, из-за чего IoT устройства могут "выстрелить вам в ногу".


Программно-аппаратные системы


Отец языка C++ Бьярне Страуструп как-то сказал:


"Си" позволяет очень просто выстрелить себе в ногу. На "Си++" сделать это сложнее, но, когда вы это делаете, отрывает всю ногу.

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


Такие проекты, как Espressif IoT Development Framework, служат для реализации программно-аппаратных систем, взаимодействующих с человеком и управляющие объектами в реальном мире. Всё это накладывает дополнительные требования к качеству и надёжности программного кода. Именно отсюда берут основы такие стандарты как MISRA или AUTOSAR. Впрочем, это уже другая тема.


Вернёмся к Espressif IoT Development Framework (исходный код на сайте GitHub: esp-idf). Вот его краткое описание:


ESP-IDF is Espressif's official IoT Development Framework for the ESP32 and ESP32-S series of SoCs. It provides a self-sufficient SDK for any generic application development on these platforms, using programming languages such as C and C++. ESP-IDF currently powers millions of devices in the field, and enables building a variety of network-connected products, ranging from simple light bulbs and toys to big appliances and industrial devices.

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


Предыстория


Ещё хочется рассказать, как появилась эта статья. Мне написал Юрий Попов (Hardcore IoT fullstack dev & CTO), который с интересом следит за нашими публикациями. Незадолго до этого он самостоятельно вручную нашёл ошибку в Espressif IoT Development Framework и поинтересовался, может ли выявить этот дефект PVS-Studio. Ошибка связана с опечаткой коде, а PVS-Studio всегда славился тем, что хорошо выявляет подобные ошибки.


Некорректный код находился в файле mdns.c:


mdns_txt_linked_item_t * txt = service->txt;while (txt) {  data_len += 2 + strlen(service->txt->key) + strlen(service->txt->value);  txt = txt->next;}

Происходит обход списка и определённым образом должны суммироваться длины строк, на которые ссылаются различные объекты, хранящиеся в списке. Вот только всё время считается длина строк, хранящаяся в первом объекте.


Правильный код:


data_len += 2 + strlen(txt->key) + strlen(txt->value);

К обоюдному разочарованию меня и читателя Юры, PVS-Studio не смог заметить эту ошибку. Он просто не знает про такой паттерн ошибки. Собственно, и наша команда не знала про такой паттерн. PVS-Studio, как и любой другой анализатор, умеет замечать только то, на что его запрограммировали :).


Что ж, жаль, но не страшно. Это один из источников, где можно черпать идеи по развитию PVS-Studio. Пользователи и клиенты присылают различные паттерны ошибок, которые они выявили в коде своих проектов, но про которые не знает PVS-Studio. И мы постепенно создаём новые диагностические правила. Так произойдёт и с рассмотренным выше паттерном. Мы уже выписали этот пример в TODO и реализуем новое диагностическое правило для выявления схожих ситуаций в одной из следующих версий анализатора.


По итогам всего этого, Юра сам написал небольшую заметку про эту ошибку, как он её искал и про PVS-Studio: "Баг в ESP-IDF: MDNS, Wireshark и при чём тут единороги". Плюс он уведомил авторов проекта о найденной ошибке: Spurious MDNS collision detection (IDFGH-4263).


На этом история не закончилось. Юра предложил нашей команде проверить проект и написать заметку о результатах. Мы не стали отказываться, так как весьма часто делаем подобные публикации для популяризации методологии статического анализа кода и заодно инструмента PVS-Studio :).


Правда проверку мы повели достаточно неуклюже. К сожалению, нет примера "собрать всё". Ну или мы не разобрались. Мы начали с getting_started\hello_world. Вроде бы он использует часть фреймворка, но не полностью. Так что можно найти и другие ошибки, добившись компиляции большего количества файлов фреймворка. Другими словами, то, что в статье будет описана только 71 ошибка, это наша недоработка :).


Надо понимать, что у меня не было задачи найти как можно больше ошибок. Поэтому, когда я бегло пробежался по неполному отчёту, я сразу понял, что материала и так более чем достаточно для статьи. Поэтому я поленился углубляться дальнейшее изучение проекта.


К счастью, у Юрия Попова, который заварил всю эту кашу, гораздо больше энтузиазма, чем у меня. Он сообщил, что смог добиться более полной компиляции фреймворка и проверил гораздо больше файлов. И, очень вероятно, вслед за моей статьей, выйдет его статья, где он рассмотрит дополнительную порцию ошибок.


Примеры, откуда берутся ложные/бессмысленные срабатывания


Всех исследователей, которые захотят проверить Espressif IoT Development Framework, я хочу предупредить, что понадобится предварительная настройка анализатора. Без неё вы утоните в большом количестве ложных/бесполезных срабатываний. Но анализатор не виноват.


В коде проекта очень активно используются директивы условной компиляции (#ifdef) и макросы. Такой стиль кодирования запутывает анализатор и порождает множество однотипных бесполезных предупреждений. Чтобы было понятнее, как и почему это происходит, рассмотрим пару примеров.


Предупреждение PVS-Studio: V547 Expression 'ret != 0' is always true. esp_hidd.c 45


esp_err_t esp_hidd_dev_init(....){  esp_err_t ret = ESP_OK;  ....  switch (transport) {#if CONFIG_GATTS_ENABLE  case ESP_HID_TRANSPORT_BLE:    ret = esp_ble_hidd_dev_init(dev, config, callback);    break;#endif /* CONFIG_GATTS_ENABLE */  default:    ret = ESP_FAIL;    break;  }  if (ret != ESP_OK) {    free(dev);    return ret;  }  ....}

Выбран такой режим компиляции, при котором макрос CONFIG_GATTS_ENABLE не объявлен. Поэтому, для анализатора этот код выглядит так:


esp_err_t ret = ESP_OK;....switch (transport) {default:  ret = ESP_FAIL;  break;}if (ret != ESP_OK) {

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


Рассмотрим другой пример. В коде активно используется своя разновидность assert-макросов. К сожалению, они тоже сбивают анализатор с толку. Предупреждение PVS-Studio: V547 Expression 'sntp_pcb != NULL' is always true. sntp.c 664


#define LWIP_PLATFORM_ASSERT(x) do \  {printf("Assertion \"%s\" failed at line %d in %s\n", \    x, __LINE__, __FILE__); fflush(NULL); abort();} while(0)#ifndef LWIP_NOASSERT#define LWIP_ASSERT(message, assertion) do { if (!(assertion)) { \  LWIP_PLATFORM_ASSERT(message); }} while(0)#else  /* LWIP_NOASSERT */#define LWIP_ASSERT(message, assertion)#endif /* LWIP_NOASSERT */sntp_pcb = udp_new_ip_type(IPADDR_TYPE_ANY);LWIP_ASSERT("Failed to allocate udp pcb for sntp client", sntp_pcb != NULL);if (sntp_pcb != NULL) {

Анализатор видит, что код в которой раскрывается LWIP_ASSERT остановит выполнение программы (см. вызов функции abort), если указатель sntp_pcb будет нулевой. Поэтому PVS-Studio предупреждает, что следующая проверка (sntp_pcb != NULL) не имеет смысла.


С одной стороны, анализатор прав. Но всё поменяется, если макрос в другом режиме компиляции будет раскрыт в "ничто". В этом случае проверка уже будет иметь смысл. Да, при втором сценарии анализатор ругаться не будет, но сути это уже не меняет. В первом то случае у нас лишнее предупреждение.


Впрочем, всё это не страшно. Если взяться за проект всерьез, то можно настроить анализатор так, что большинство бессмысленных сообщений исчезнет. Ещё в ряде мест можно улучшить ситуацию, изменив стиль написания кода и макросов. Но это уже выходит за рамки данной статьи. Дополнительно можно использовать механизм подавления предупреждений в конкретных местах, в макросах и т.д. Есть ещё и механизм массовой разметки. Про всё это подробнее можно почитать в статье "Как внедрить статический анализатор кода в legacy проект и не демотивировать команду".


Security


Начнём с предупреждений, которые, на мой взгляд, связаны с темой security. Разработчики операционных систем, фреймворков и других подобных проектов, должны с особенным вниманием относиться к поиску слабостей кода, которые потенциально могут приводить к возникновению уязвимостей.


Для удобства классификации слабостей кода можно использовать CWE (Common Weakness Enumeration). В PVS-Studio можно включить отображение CWE ID для предупреждений. Для предупреждений этой главы я дополнительно приведу соответствующий CWE ID.


Подробнее тема поиска потенциальных уязвимостей раскрыта в статье "Статический анализатор кода PVS-Studio как защита от уязвимостей нулевого дня".


Ошибка N1; Порядок аргументов


Предупреждение PVS-Studio: V764 Possible incorrect order of arguments passed to 'crypto_generichash_blake2b__init_salt_personal' function: 'salt' and 'personal'. blake2b-ref.c 457


int blake2b_init_salt_personal(blake2b_state *S, const uint8_t outlen,                               const void *personal, const void *salt);intblake2b_salt_personal(uint8_t *out, const void *in, const void *key,                      const uint8_t outlen, const uint64_t inlen,                      uint8_t keylen, const void *salt, const void *personal){  ....  if (blake2b_init_salt_personal(S, outlen, salt, personal) < 0)    abort();  ....}

При вызове функции blake2b_init_salt_personal перепутаны местами аргументы personal и salt. Мне кажется, вряд ли это задумано специально и, скорее всего, это ошибка, возникшая по невнимательности. Я не ориентируюсь в коде проекта и в криптографии, но что-то мне подсказывает, что такая путаница может иметь нехорошие последствия.


Согласно CWE эта ошибка классифицируется как CWE-683: Function Call With Incorrect Order of Arguments.


Ошибка N2; Отбрасывание значащих бит


Предупреждение PVS-Studio: V642 Saving the 'memcmp' function result inside the 'unsigned char' type variable is inappropriate. The significant bits could be lost breaking the program's logic. mbc_tcp_master.c 387


static esp_err_t mbc_tcp_master_set_request(  char* name, mb_param_mode_t mode, mb_param_request_t* request,  mb_parameter_descriptor_t* reg_data){  ....  // Compare the name of parameter with parameter key from table  uint8_t comp_result = memcmp((const char*)name,                               (const char*)reg_ptr->param_key,                               (size_t)param_key_len);  if (comp_result == 0) {  ....}

Сохранять результат работы функции memcmp в однобайтовую переменную это очень плохо. Это дефект, который вполне может превратиться в реальную уязвимость, подобную этой: CVE-2012-2122. Подробнее, почему так писать нельзя, описано в документации к диагностике V642.


Если совсем кратко, то некоторые реализации функция memset могут возвращать в случае несовпадения блоков памяти не только значения 1 или -1. Функция, например, может вернуть значение 1024. А это число, записанное в переменную типа uint8_t превратится в 0.


Согласно CWE эта ошибка классифицируется как CWE-197: Numeric Truncation Error.


Ошибка N3 N20; Приватные данные остаются в памяти


Предупреждение PVS-Studio: V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 854


#ifndef os_memset#define os_memset(s, c, n) memset(s, c, n)#endifstatic int dpp_derive_k1(const u8 *Mx, size_t Mx_len, u8 *k1,       unsigned int hash_len){  u8 salt[DPP_MAX_HASH_LEN], prk[DPP_MAX_HASH_LEN];  const char *info = "first intermediate key";  int res;  /* k1 = HKDF(<>, "first intermediate key", M.x) */  /* HKDF-Extract(<>, M.x) */  os_memset(salt, 0, hash_len);  if (dpp_hmac(hash_len, salt, hash_len, Mx, Mx_len, prk) < 0)    return -1;  wpa_hexdump_key(MSG_DEBUG, "DPP: PRK = HKDF-Extract(<>, IKM=M.x)",      prk, hash_len);  /* HKDF-Expand(PRK, info, L) */  res = dpp_hkdf_expand(hash_len, prk, hash_len, info, k1, hash_len);  os_memset(prk, 0, hash_len);             // <=  if (res < 0)    return -1;  wpa_hexdump_key(MSG_DEBUG, "DPP: k1 = HKDF-Expand(PRK, info, L)",                  k1, hash_len);  return 0;}

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


Согласно CWE эта ошибка классифицируется как CWE-14: Compiler Removal of Code to Clear Buffers.


Другие ошибки этого типа:


  • V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 883
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 942
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'psk' buffer. The memset_s() function should be used to erase the private data. dpp.c 3939
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 5729
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'Nx' buffer. The memset_s() function should be used to erase the private data. dpp.c 5934
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'val' buffer. The memset_s() function should be used to erase the private data. sae.c 155
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'keyseed' buffer. The memset_s() function should be used to erase the private data. sae.c 834
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'keys' buffer. The memset_s() function should be used to erase the private data. sae.c 838
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'pkey' buffer. The memset_s() function should be used to erase the private data. des-internal.c 422
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'ek' buffer. The memset_s() function should be used to erase the private data. des-internal.c 423
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'finalcount' buffer. The memset_s() function should be used to erase the private data. sha1-internal.c 358
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'A_MD5' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 95
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'P_MD5' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 96
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'A_SHA1' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 97
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'P_SHA1' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 98
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'T' buffer. The memset_s() function should be used to erase the private data. sha256-kdf.c 85
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'hash' buffer. The memset_s() function should be used to erase the private data. sha256-prf.c 105

Ошибка N21; Не удаляется буфер с приватными данными


Предупреждение PVS-Studio: V575 The null pointer is passed into 'free' function. Inspect the first argument. sae.c 1185


static int sae_parse_password_identifier(struct sae_data *sae,           const u8 *pos, const u8 *end){  wpa_hexdump(MSG_DEBUG, "SAE: Possible elements at the end of the frame",        pos, end - pos);  if (!sae_is_password_id_elem(pos, end)) {    if (sae->tmp->pw_id) {      wpa_printf(MSG_DEBUG,           "SAE: No Password Identifier included, but expected one (%s)",           sae->tmp->pw_id);      return WLAN_STATUS_UNKNOWN_PASSWORD_IDENTIFIER;    }    os_free(sae->tmp->pw_id);    sae->tmp->pw_id = NULL;    return WLAN_STATUS_SUCCESS; /* No Password Identifier */  }  ....}

Если с паролем что-то не так и указатель pw_id не нулевой, то выводится отладочное предупреждение и функция завершает свою работу. Что интересно, далее происходит попытка освободить буфер, используя нулевой указатель. Более того, в нулевой указатель вновь записывается NULL. Всё это не имеет смысла. Скорее всего, строчки освобождения памяти находятся не на своём месте. И мне кажется, код должен быть таким:


if (!sae_is_password_id_elem(pos, end)) {  if (sae->tmp->pw_id) {    wpa_printf(MSG_DEBUG,         "SAE: No Password Identifier included, but expected one (%s)",         sae->tmp->pw_id);    os_free(sae->tmp->pw_id);    sae->tmp->pw_id = NULL;    return WLAN_STATUS_UNKNOWN_PASSWORD_IDENTIFIER;  }  return WLAN_STATUS_SUCCESS; /* No Password Identifier */}

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


Согласно CWE, эта формально ошибка классифицируется как CWE-628: Function Call with Incorrectly Specified Arguments. Так её классифицирует PVS-Studio, но, по сути и последствиям, это какая-то другая слабость кода.


Ошибка N22, N23; Использование неинициализированного буфера в качестве ключа


Предупреждение PVS-Studio: V614 Uninitialized buffer 'hex' used. Consider checking the second actual argument of the 'memcpy' function. wps_registrar.c 1657


int wps_build_cred(struct wps_data *wps, struct wpabuf *msg){  ....  } else if (wps->use_psk_key && wps->wps->psk_set) {    char hex[65];    wpa_printf(MSG_DEBUG,  "WPS: Use PSK format for Network Key");    os_memcpy(wps->cred.key, hex, 32 * 2);    wps->cred.key_len = 32 * 2;  } else if (wps->wps->network_key) {  ....}

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


В любом случае этот код нуждается во внимательной проверке.


Согласно CWE, эта ошибка классифицируется как CWE-457: Use of Uninitialized Variable.


Аналогичная ошибка: V614 Uninitialized buffer 'hex' used. Consider checking the second actual argument of the 'memcpy' function. wps_registrar.c 1678


Опечатки и Copy-Paste


Ошибка N24; Copy-Paste классический


Предупреждение PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. timer.c 292


esp_err_t timer_isr_register(....){  ....  if ((intr_alloc_flags & ESP_INTR_FLAG_EDGE) == 0) {    intr_source = ETS_TG1_T0_LEVEL_INTR_SOURCE + timer_num;  } else {    intr_source = ETS_TG1_T0_LEVEL_INTR_SOURCE + timer_num;  }  ....}

Есть подозрение, что строчку скопировали, но забыли что-то в ней изменить. В результате, независимо от условия, в переменную intr_source записывается одно и тоже значение.


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


Ошибка N25; Не там поставлена скобка


Предупреждение PVS-Studio: V593 Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. esp_tls_mbedtls.c 446


esp_err_t set_client_config(....){ .... if ((ret = mbedtls_ssl_conf_alpn_protocols(&tls->conf, cfg->alpn_protos) != 0)) {   ESP_LOGE(TAG, "mbedtls_ssl_conf_alpn_protocols returned -0x%x", -ret);   ESP_INT_EVENT_TRACKER_CAPTURE(tls->error_handle, ERR_TYPE_MBEDTLS, -ret);   return ESP_ERR_MBEDTLS_SSL_CONF_ALPN_PROTOCOLS_FAILED; } ....}

Приоритет оператора сравнения выше, чем приоритет оператора присваивания. Поэтому условие вычисляется следующим образом:


TEMP = mbedtls_ssl_conf_alpn_protocols(....) != 0;if ((ret = TEMP))  PRINT(...., -ret);

В принципе, ошибочная ситуация поймается и обработается в коде, но не так, как задумано. Предполагалось распечатывать статус ошибки, который хранится в переменной ret. Но значение ret всегда будет равно 0 или 1. Поэтому, если что-то пойдёт не так, всегда будет распечатываться только одно значение (-1).


Ошибка возникла из-за того, что не там поставлена скобочка. Правильный код:


if ((ret = mbedtls_ssl_conf_alpn_protocols(&tls->conf, cfg->alpn_protos)) != 0)

Теперь всё будет вычисляться как нужно:


ret = mbedtls_ssl_conf_alpn_protocols(....);if (ret != 0)  PRINT(...., -ret);

Рассмотрим ещё один очень похожий случай.


Ошибка N26; MP_MEM превращается в MP_YES


V593 Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. libtommath.h 1660


В начале рассмотрим некоторые константы. Они пригодятся нам чуть ниже.


#define MP_OKAY       0   /* ok result */#define MP_MEM        -2  /* out of mem */#define MP_VAL        -3  /* invalid input */#define MP_YES        1   /* yes response */

Далее следует сказать, что существует функция mp_init_multi, которая может возвращать значения MP_OKAY и MP_MEM:


static int mp_init_multi(mp_int *mp, ...);

И теперь собственно код с ошибкой:


static intmp_div(mp_int * a, mp_int * b, mp_int * c, mp_int * d){  ....  /* init our temps */  if ((res = mp_init_multi(&ta, &tb, &tq, &q, NULL) != MP_OKAY)) {     return res;  }  ....}

Рассмотрим проверку более тщательно:


if ((res = mp_init_multi(....) != MP_OKAY))

Вновь не там поставлена скобка. Поэтому в начале вычисляется:


TEMP = (mp_init_multi(....) != MP_OKAY);

Значение TEMP может быть только 0 или 1. Этим числам соответствуют константы MB_OKAY и MP_YES.


Далее выполняется присваивание и одновременно проверка:


if ((res = TEMP))   return res;

Видите подвох? Статус ошибки MP_MEM (-2) вдруг превратился в статус MB_YES (1). Последствия предсказать не могу, но ничего хорошего в этом нет.


Ошибка N27; Забыли разыменовать указатель


Предупреждение PVS-Studio: V595 The 'outbuf' pointer was utilized before it was verified against nullptr. Check lines: 374, 381. protocomm.c 374


static int protocomm_version_handler(uint32_t session_id,                                     const uint8_t *inbuf, ssize_t inlen,                                     uint8_t **outbuf, ssize_t *outlen,                                     void *priv_data){    protocomm_t *pc = (protocomm_t *) priv_data;    if (!pc->ver) {        *outlen = 0;        *outbuf = NULL;                                  // <=        return ESP_OK;    }    /* Output is a non null terminated string with length specified */    *outlen = strlen(pc->ver);    *outbuf = malloc(*outlen);                           // <=    if (outbuf == NULL) {                                // <=        ESP_LOGE(TAG, "Failed to allocate memory for version response");        return ESP_ERR_NO_MEM;    }    memcpy(*outbuf, pc->ver, *outlen);    return ESP_OK;}

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


Если указатель pc->ver является нулевым, то функция досрочно завершает свою работу и при этом записывает значение по адресу, хранящегося в указателе outbuf:


*outbuf = NULL;

Запись по этому адресу происходит и далее:


*outbuf = malloc(*outlen);

А не нравится анализатору то, что затем этот указатель проверяется:


if (outbuf == NULL)

Действительно, это неправильно, в начале разыменовывать указатель, а только потом его проверять. Ляп в том, что на самом то деле должны были проверять не сам указатель, а то, что в него записали. Здесь просто в проверке опечатались и пропустили оператор разыменования (*).


Правильный код:


*outbuf = malloc(*outlen);if (*outbuf == NULL) {  ESP_LOGE(TAG, "Failed to allocate memory for version response");  return ESP_ERR_NO_MEM;}

Ошибка N28; Повторное присваивание


Предупреждение PVS-Studio: V519 The 'usRegCount' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 186, 187. mbfuncholding.c 187


eMBExceptioneMBFuncReadHoldingRegister( UCHAR * pucFrame, USHORT * usLen ){  ....  USHORT          usRegCount;  ....  usRegCount = ( USHORT )( pucFrame[MB_PDU_FUNC_READ_REGCNT_OFF] << 8 );  usRegCount = ( USHORT )( pucFrame[MB_PDU_FUNC_READ_REGCNT_OFF + 1] );  ....}

Код явно писался методом Copy-Paste. Строчку скопировали, но изменили только частично. По соседству есть вот такой осмысленный код:


usRegCount = ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF] << 8 );usRegCount |= ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF + 1] );

Видимо, и в рассмотренном коде с ошибкой, следовало в первой строке использовать оператор =, а во второй оператор |=.


Логические ошибки


Ошибка N29 N31; Неправильная работа с кодами возврата (Rare)


Предупреждение PVS-Studio: V547 Expression is always false. linenoise.c 256


static int getColumns(void) {  ....  /* Restore position. */  if (cols > start) {    char seq[32];    snprintf(seq,32,"\x1b[%dD",cols-start);    if (fwrite(seq, 1, strlen(seq), stdout) == -1) {      /* Can't recover... */    }    flushWrite();  }  ....}

Это безобидный вариант неправильной обработки статуса, возвращаемого функцией. Ошибка безобидна в том смысле, что никакой обработки и не предусмотрено. Не получилось записать строчку, так не получилось :). Хотя, как я уже сказал, этот безобидный вариант, такой стиль написания программ явно не является образцом для подражания.


Суть же самой ошибки в том, что функция fwrite не возвращает статус -1. Это физически невозможно, так как функция fwrite возвращает значение целочисленного типа size_t:


size_t fwrite( const void *restrict buffer, size_t size, size_t count,               FILE *restrict stream );

А вот что возвращает эта функция:


The number of objects written successfully, which may be less than count if an error occurs.

If size or count is zero, fwrite returns zero and performs no other action.

Таким образом, проверка статуса является неверной.


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


  • V547 Expression is always false. linenoise.c 481
  • V547 Expression is always false. linenoise.c 569

Ошибка N32, N33; Неправильная работа с кодами возврата (Medium)


Предупреждение PVS-Studio: V547 Expression is always false. linenoise.c 596


int linenoiseEditInsert(struct linenoiseState *l, char c) {  ....  if (fwrite(&c,1,1,stdout) == -1) return -1;  ....}

Хотя перед нами та же ошибка, что и в предыдущем случае, она более серьезна. Если не удаётся записать символ в файл, то функция linenoiseEditInsert должна прекратить свою работу и вернуть статус -1. Но этого не произойдёт, так как fwrite никогда не вернёт значение -1. Перед нами логическая ошибка обработки ситуации, когда не удаётся что-то записать в файл.


Аналогичную ошибку можно найти здесь: V547 Expression is always false. linenoise.c 742.


Ошибка N34; Неправильная работа с кодами возврата (Well Done)


Предупреждение PVS-Studio: V547 Expression is always false. linenoise.c 828


static int linenoiseEdit(char *buf, size_t buflen, const char *prompt)  ....  while(1) {    ....    if (fread(seq+2, 1, 1, stdin) == -1) break;    ....  }  ....}

Ошибка в том, что, как и в случае с fwrite, функция fread не возвращает в качестве статуса значение -1.


size_t fread( void *restrict buffer, size_t size, size_t count,              FILE *restrict stream );

Return value

Number of objects read successfully, which may be less than count if an error or end-of-file condition occurs.

If size or count is zero, fread returns zero and performs no other action.

fread does not distinguish between end-of-file and error, and callers must use feof and ferror to determine which occurred.

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


Ошибка N35; Использование оператора || там, где нужен оператор &&


Предупреждение PVS-Studio: V547 Expression is always true. essl_sdio.c 209


esp_err_t essl_sdio_init(void *arg, uint32_t wait_ms){  ....  // Set block sizes for functions 1 to given value (default value = 512).  if (ctx->block_size > 0 || ctx->block_size <= 2048) {    bs = ctx->block_size;  } else {    bs = 512;  }  ....}

Эту ошибку можно, конечно, отнести просто к опечаткам, но, мне кажется, по духу она ближе к логическим ошибкам. Думаю, читатель понимает, что деление ошибок по категориям часто является достаточным условным.


Итак, перед нами всегда истинное условие. Ведь некая переменная всегда или больше 0 или меньше 2048. Из-за этого размер какого-то блока не будет ограничен значением 512.


Правильный вариант кода:


if (ctx->block_size > 0 && ctx->block_size <= 2048) {  bs = ctx->block_size;} else {  bs = 512;}

Ошибка N35 N38; Переменная не изменяется


Предупреждение PVS-Studio: V547 Expression 'depth <= 0' is always false. panic_handler.c 169


static void print_backtrace(const void *f, int core){  XtExcFrame *frame = (XtExcFrame *) f;  int depth = 100;                                          // <=  //Initialize stk_frame with first frame of stack  esp_backtrace_frame_t stk_frame =    {.pc = frame->pc, .sp = frame->a1, .next_pc = frame->a0};  panic_print_str("\r\nBacktrace:");  print_backtrace_entry(esp_cpu_process_stack_pc(stk_frame.pc),                        stk_frame.sp);  //Check if first frame is valid  bool corrupted =    !(esp_stack_ptr_is_sane(stk_frame.sp) &&      (esp_ptr_executable((void *)esp_cpu_process_stack_pc(stk_frame.pc)) ||       /* Ignore the first corrupted PC in case of InstrFetchProhibited */       frame->exccause == EXCCAUSE_INSTR_PROHIBITED));  //Account for stack frame that's already printed  uint32_t i = ((depth <= 0) ? INT32_MAX : depth) - 1;      // <=  ....}

Переменной depth присваивается значение 100, и до момента проверки этой переменной её значение нигде не изменяется. Это весьма подозрительно. Где-то что-то забыли сделать?


Аналогичные случаи:


  • V547 Expression 'xAlreadyYielded == ((BaseType_t) 0)' is always true. event_groups.c 260
  • V547 Expression 'xAlreadyYielded == ((BaseType_t) 0)' is always true. tasks.c 1475
  • V547 Expression 'xAlreadyYielded == ((BaseType_t) 0)' is always true. tasks.c 1520

Ошибка N39; Использование неинициализированного буфера


Предупреждение PVS-Studio: V614 Potentially uninitialized buffer 'k' used. Consider checking the second actual argument of the 'sae_derive_keys' function. sae.c 854


int sae_process_commit(struct sae_data *sae){  u8 k[SAE_MAX_PRIME_LEN];  if (sae->tmp == NULL ||      (sae->tmp->ec && sae_derive_k_ecc(sae, k) < 0) ||      (sae->tmp->dh && sae_derive_k_ffc(sae, k) < 0) ||      sae_derive_keys(sae, k) < 0)    return ESP_FAIL;  return ESP_OK;}

Ошибка в логике. Предположим, что указатели ec и dh являются нулевыми. В этом случае массив k не инициализируется, но функция sae_derive_keys всё равно начнёт его обрабатывать.


Ошибка N40; Всегда ложное условие


Предупреждение PVS-Studio: V547 Expression 'bit_len == 32' is always false. spi_flash_ll.h 371


static inline void spi_flash_ll_set_usr_address(spi_dev_t *dev, uint32_t addr,                                                int bit_len){  // The blank region should be all ones  if (bit_len >= 32) {    dev->addr = addr;    dev->slv_wr_status = UINT32_MAX;  } else {    uint32_t padding_ones = (bit_len == 32? 0 : UINT32_MAX >> bit_len);    dev->addr = (addr << (32 - bit_len)) | padding_ones;  }}

Как легко увидеть, условие bit_len == 32 всегда даст ложный результат. Возможно, выше следовало написать не больше-или-равно (>=), а просто больше (>).


Ошибка N41; Повторное присваивание


Предупреждение PVS-Studio: V519 The '* pad_num' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 46, 48. touch_sensor_hal.c 48


void touch_hal_get_wakeup_status(touch_pad_t *pad_num){  uint32_t touch_mask = 0;  touch_ll_read_trigger_status_mask(&touch_mask);  if (touch_mask == 0) {    *pad_num = -1;  }  *pad_num = (touch_pad_t)(__builtin_ffs(touch_mask) - 1);}

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


void touch_hal_get_wakeup_status(touch_pad_t *pad_num){  uint32_t touch_mask = 0;  touch_ll_read_trigger_status_mask(&touch_mask);  if (touch_mask == 0) {    *pad_num = -1;  } else {    *pad_num = (touch_pad_t)(__builtin_ffs(touch_mask) - 1);  }}

Выход за границу массива


Ошибка N42; Неправильная граничная проверка


Предупреждение PVS-Studio: V557 Array overrun is possible. The value of 'frame->exccause' index could reach 16. gdbstub_xtensa.c 132


int esp_gdbstub_get_signal(const esp_gdbstub_frame_t *frame){  const char exccause_to_signal[] =    {4, 31, 11, 11, 2, 6, 8, 0, 6, 7, 0, 0, 7, 7, 7, 7};  if (frame->exccause > sizeof(exccause_to_signal)) {    return 11;  }  return (int) exccause_to_signal[frame->exccause];}

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


if (frame->exccause >= sizeof(exccause_to_signal)) {

Ошибка N43; Длинный пример ошибки :)


В рассматриваемой функции выход за границу массива может произойти в двух местах, поэтому и предупреждений анализатора сразу два:


  • V557 Array overrun is possible. The value of 'other_if' index could reach 3. mdns.c 2206
  • V557 Array overrun is possible. The '_mdns_announce_pcb' function processes value '[0..3]'. Inspect the first argument. Check lines: 1674, 2213. mdns.c 1674

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


typedef enum mdns_if_internal {    MDNS_IF_STA = 0,    MDNS_IF_AP = 1,    MDNS_IF_ETH = 2,    MDNS_IF_MAX} mdns_if_t;

Обратите внимание, что значение константы MDNS_IF_MAX равно 3.


Теперь взглянем на определение структуры mdns_server_s. Здесь нам важно, что массив interfaces состоит из 3 элементов:


typedef struct mdns_server_s {    struct {        mdns_pcb_t pcbs[MDNS_IP_PROTOCOL_MAX];    } interfaces[MDNS_IF_MAX];    const char * hostname;    const char * instance;    mdns_srv_item_t * services;    SemaphoreHandle_t lock;    QueueHandle_t action_queue;    mdns_tx_packet_t * tx_queue_head;    mdns_search_once_t * search_once;    esp_timer_handle_t timer_handle;} mdns_server_t;mdns_server_t * _mdns_server = NULL;

Это ещё не всё. Нам понадобится заглянуть внутрь функции _mdns_get_other_if. Обратите внимание, что она может вернуть константу MDNS_IF_MAX. Т.е. она может вернуть значение 3.


static mdns_if_t _mdns_get_other_if (mdns_if_t tcpip_if){  if (tcpip_if == MDNS_IF_STA) {    return MDNS_IF_ETH;  } else if (tcpip_if == MDNS_IF_ETH) {     return MDNS_IF_STA;  }  return MDNS_IF_MAX;}

И вот, наконец, мы добрались до ошибок:


static void _mdns_dup_interface(mdns_if_t tcpip_if){    uint8_t i;    mdns_if_t other_if = _mdns_get_other_if (tcpip_if);    for (i=0; i<MDNS_IP_PROTOCOL_MAX; i++) {        if (_mdns_server->interfaces[other_if].pcbs[i].pcb) {        // <=            //stop this interface and mark as dup            if (_mdns_server->interfaces[tcpip_if].pcbs[i].pcb) {                _mdns_clear_pcb_tx_queue_head(tcpip_if, i);                _mdns_pcb_deinit(tcpip_if, i);            }            _mdns_server->interfaces[tcpip_if].pcbs[i].state = PCB_DUP;            _mdns_announce_pcb(other_if, i, NULL, 0, true);          // <=        }    }}

Итак, мы знаем, что функция _mdns_get_other_if может вернуть тройку. Значит переменная other_if может быть равна трём. И вот первый потенциальный выход за границу массива:


if (_mdns_server->interfaces[other_if].pcbs[i].pcb)

Второе место, где опасно используется переменная other_if, - это вызов функции _mdns_announce_pcb:


_mdns_announce_pcb(other_if, i, NULL, 0, true);

Заглянем в эту функцию:


static void _mdns_announce_pcb(mdns_if_t tcpip_if,                               mdns_ip_protocol_t ip_protocol,                               mdns_srv_item_t ** services,                               size_t len, bool include_ip){  mdns_pcb_t * _pcb = &_mdns_server->interfaces[tcpip_if].pcbs[ip_protocol];  ....}

Опять может использоваться индекс 3 для доступа к массиву, состоящего из 3 элементов. А максимальный доступный индекс это двойка.


Нулевые указатели


Ошибка N44 N47; Ошибка очерёдности проверки указателей


Предупреждение PVS-Studio: V595 The 'hapd->wpa_auth' pointer was utilized before it was verified against nullptr. Check lines: 106, 113. esp_hostap.c 106


bool hostap_deinit(void *data){  struct hostapd_data *hapd = (struct hostapd_data *)data;  if (hapd == NULL) {    return true;  }  if (hapd->wpa_auth->wpa_ie != NULL) {    os_free(hapd->wpa_auth->wpa_ie);  }  if (hapd->wpa_auth->group != NULL) {    os_free(hapd->wpa_auth->group);  }  if (hapd->wpa_auth != NULL) {    os_free(hapd->wpa_auth);  }  ....}

Неправильная последовательность проверки указателей:


if (hapd->wpa_auth->group != NULL)....if (hapd->wpa_auth != NULL)

Если указатель hapd->wpa_auth окажется нулевым, то всё плохо. Последовательность действий нужно поменять местами и сделать вложенной:


if (hapd->wpa_auth != NULL){  ....  if (hapd->wpa_auth->group != NULL)  ....}

Аналогичные ошибки:


  • V595 The 'hapd->conf' pointer was utilized before it was verified against nullptr. Check lines: 118, 125. esp_hostap.c 118
  • V595 The 'sm' pointer was utilized before it was verified against nullptr. Check lines: 1637, 1647. esp_wps.c 1637
  • V595 The 'sm' pointer was utilized before it was verified against nullptr. Check lines: 1693, 1703. esp_wps.c 1693

Ошибка N48 N64; Нет проверки указателя после выделения памяти


В целом, в проекте принято проверять, удалось выделить память или нет. Т.е. много кода с подобными проверками:


dhcp_data = (struct dhcp *)malloc(sizeof(struct dhcp));if (dhcp_data == NULL) {  return ESP_ERR_NO_MEM;}

Но местами про проверки забыли.


Предупреждение PVS-Studio: V522 There might be dereferencing of a potential null pointer 'exp'. Check lines: 3470, 3469. argtable3.c 3470


TRex *trex_compile(const TRexChar *pattern,const TRexChar **error,int flags){  TRex *exp = (TRex *)malloc(sizeof(TRex));  exp->_eol = exp->_bol = NULL;  exp->_p = pattern;  ....}

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


Другие места, где отсутствуют проверки:


  • V522 There might be dereferencing of a potential null pointer 's_ledc_fade_rec[speed_mode][channel]'. Check lines: 668, 667. ledc.c 668
  • V522 There might be dereferencing of a potential null pointer 'environ'. Check lines: 108, 107. syscall_table.c 108
  • V522 There might be dereferencing of a potential null pointer 'it'. Check lines: 150, 149. partition.c 150
  • V522 There might be dereferencing of a potential null pointer 'eth'. Check lines: 167, 159. wpa_auth.c 167
  • V522 There might be dereferencing of a potential null pointer 'pt'. Check lines: 222, 219. crypto_mbedtls-ec.c 222
  • V522 There might be dereferencing of a potential null pointer 'attr'. Check lines: 88, 73. wps.c 88
  • V575 The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 725, 724. coap_mbedtls.c 725
  • V575 The potential null pointer is passed into 'memset' function. Inspect the first argument. Check lines: 3504, 3503. argtable3.c 3504
  • V575 The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 496, 495. mqtt_client.c 496
  • V575 The potential null pointer is passed into 'strcpy' function. Inspect the first argument. Check lines: 451, 450. transport_ws.c 451
  • V769 The 'buffer' pointer in the 'buffer + n' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 186, 181. cbortojson.c 186
  • V769 The 'buffer' pointer in the 'buffer + len' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 212, 207. cbortojson.c 212
  • V769 The 'out' pointer in the 'out ++' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 233, 207. cbortojson.c 233
  • V769 The 'parser->m_bufferPtr' pointer in the expression equals nullptr. The resulting value of arithmetic operations on this pointer is senseless and it should not be used. xmlparse.c 2090
  • V769 The 'signature' pointer in the 'signature + curve->prime_len' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 4112, 4110. dpp.c 4112
  • V769 The 'key' pointer in the 'key + 16' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 634, 628. eap_mschapv2.c 634

Ошибка N65, N66; Нет проверки указателя после выделения памяти (показательный случай)


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


Предупреждение PVS-Studio: V701 realloc() possible leak: when realloc() fails in allocating memory, original pointer 'exp->_nodes' is lost. Consider assigning realloc() to a temporary pointer. argtable3.c 3008


static int trex_newnode(TRex *exp, TRexNodeType type){  TRexNode n;  int newid;  n.type = type;  n.next = n.right = n.left = -1;  if(type == OP_EXPR)    n.right = exp->_nsubexpr++;  if(exp->_nallocated < (exp->_nsize + 1)) {    exp->_nallocated *= 2;    exp->_nodes = (TRexNode *)realloc(exp->_nodes,                                      exp->_nallocated * sizeof(TRexNode));  }  exp->_nodes[exp->_nsize++] = n; // NOLINT(clang-analyzer-unix.Malloc)  newid = exp->_nsize - 1;  return (int)newid;}

Во-первых, если функция realloc вернёт NULL, то будет потеряно предыдущее значение указателя exp->_nodes. Возникнет утечка памяти.


Во-вторых, если функция realloc вернёт NULL, то запись значения произойдёт вовсе не по нулевому указателю. Имеется в виду эта строка:


exp->_nodes[exp->_nsize++] = n;

Значение exp->_nsize++ может быть любым, и, если запись произойдёт в какую-то случайную область памяти, доступную для записи, то программа продолжит своё выполнение, как ни в чём не бывало. При этом будут разрушены структуры данных, что приведёт к непредсказуемым последствиям.


Ещё одна такая ошибка: V701 realloc() possible leak: when realloc() fails in allocating memory, original pointer 'm_context->pki_sni_entry_list' is lost. Consider assigning realloc() to a temporary pointer. coap_mbedtls.c 737


Прочие ошибки


Ошибка N67; Лишний или неверный код


Предупреждение PVS-Studio: V547 Expression 'ret != 0' is always false. sdio_slave.c 394


esp_err_t sdio_slave_start(void){  ....  critical_exit_recv();  ret = ESP_OK;  if (ret != ESP_OK) return ret;  sdio_slave_hal_set_ioready(context.hal, true);  return ESP_OK;}

Это странный код, который можно сократить до:


esp_err_t sdio_slave_start(void){  ....  critical_exit_recv();  sdio_slave_hal_set_ioready(context.hal, true);  return ESP_OK;}

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


Ошибка N68; Лишний или неверный код


Предупреждение PVS-Studio: V547 Expression 'err != 0' is always false. sdio_slave_hal.c 96


static esp_err_t sdio_ringbuf_send(....){  uint8_t* get_ptr = ....;  esp_err_t err = ESP_OK;  if (copy_callback) {    (*copy_callback)(get_ptr, arg);  }  if (err != ESP_OK) return err;  buf->write_ptr = get_ptr;  return ESP_OK;}

В общем-то, всё то же самое, что и в предыдущем случае. Переменная err является лишней или её забыли изменить.


Ошибка N69; Использование потенциально неинициализированного буфера


Предупреждение PVS-Studio: V614 Potentially uninitialized buffer 'seq' used. Consider checking the first actual argument of the 'strlen' function. linenoise.c 435


void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int plen) {    char seq[64];    if (hintsCallback && plen+l->len < l->cols) {        int color = -1, bold = 0;        char *hint = hintsCallback(l->buf,&color,&bold);        if (hint) {            int hintlen = strlen(hint);            int hintmaxlen = l->cols-(plen+l->len);            if (hintlen > hintmaxlen) hintlen = hintmaxlen;            if (bold == 1 && color == -1) color = 37;            if (color != -1 || bold != 0)                snprintf(seq,64,"\033[%d;%d;49m",bold,color);            abAppend(ab,seq,strlen(seq));                       // <=            abAppend(ab,hint,hintlen);            if (color != -1 || bold != 0)                abAppend(ab,"\033[0m",4);            /* Call the function to free the hint returned. */            if (freeHintsCallback) freeHintsCallback(hint);        }    }}

Буфер seq может быть заполнен, а может быть и не заполнен! Он заполняется только при выполнении условия:


if (color != -1 || bold != 0)  snprintf(seq,64,"\033[%d;%d;49m",bold,color);

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


Чтобы исправить ситуацию, стоит изменить код следующим образом:


if (color != -1 || bold != 0){  snprintf(seq,64,"\033[%d;%d;49m",bold,color);  abAppend(ab,seq,strlen(seq));}

Ошибка N70; Странная маска


Предупреждение PVS-Studio: V547 Expression is always false. tasks.c 896


#ifndef portPRIVILEGE_BIT  #define portPRIVILEGE_BIT ( ( UBaseType_t ) 0x00 )#endifstatic void prvInitialiseNewTask(...., UBaseType_t uxPriority, ....){  StackType_t *pxTopOfStack;  UBaseType_t x;  #if (portNUM_PROCESSORS < 2)  xCoreID = 0;  #endif  #if( portUSING_MPU_WRAPPERS == 1 )    /* Should the task be created in privileged mode? */    BaseType_t xRunPrivileged;    if( ( uxPriority & portPRIVILEGE_BIT ) != 0U )    {      xRunPrivileged = pdTRUE;    }    else    {      xRunPrivileged = pdFALSE;    }  ....}

Константа portPRIVILEGE_BIT имеет значение 0. Поэтому странно использовать его как маску:


if( ( uxPriority & portPRIVILEGE_BIT ) != 0U )

Ошибка N71, Утечка памяти


Предупреждение PVS-Studio: V773 The function was exited without releasing the 'sm' pointer. A memory leak is possible. esp_wpa2.c 753


static int eap_peer_sm_init(void){  int ret = 0;  struct eap_sm *sm;  ....  sm = (struct eap_sm *)os_zalloc(sizeof(*sm));  if (sm == NULL) {    return ESP_ERR_NO_MEM;  }  s_wpa2_data_lock = xSemaphoreCreateRecursiveMutex();  if (!s_wpa2_data_lock) {    wpa_printf(MSG_ERROR, ".......");  // NOLINT(clang-analyzer-unix.Malloc)    return ESP_ERR_NO_MEM;             // <=  }  ....}

Если функция xSemaphoreCreateRecursiveMutex не сможет создать мьютекс, то функция eap_peer_sm_init завершит свою работу и при этом произойдёт утечка памяти. Как я понимаю, следует добавить вызов функции os_free для очистки памяти:


  s_wpa2_data_lock = xSemaphoreCreateRecursiveMutex();  if (!s_wpa2_data_lock) {    wpa_printf(MSG_ERROR, ".......");    os_free(sm);    return ESP_ERR_NO_MEM;  }

Что интересно, компилятор Clang тоже предупреждает об этой ошибке. Однако автор кода почему-то проигнорировал и даже специально подавил соответствующее предупреждение:


// NOLINT(clang-analyzer-unix.Malloc)

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


Заключение


Спасибо за внимание. Как видите, ошибок весьма много. А это ведь был только беглый просмотр неполного отчёта. Надеюсь, Юрий Попов примет эстафету и опишет ещё больше ошибок в своей последующей статье :).


Используйте статический анализатор PVS-Studio регулярно. Это позволит:


  1. Находить многие ошибки на раннем этапе, что существенно сократит расходы на их обнаружение и исправление;
  2. Находя и исправляя глупые опечатки и прочие ляпы с помощью статического анализа, вы высвободите время, которое можно потратить на более высокоуровневый обзор кода и алгоритмов;
  3. Лучше контролировать качество кода новичков и быстрее обучать их писать красивый надежный код;
  4. Если речь идёт о программном обеспечении для встраиваемых устройств, то очень важно устранить как можно больше ошибок до выпуска устройств в эксплуатацию. Поэтому любая дополнительно найденная ошибка с помощью анализатора кода, это здорово. Каждая незамеченная ошибка в программно-аппаратном устройстве потенциально несёт репутационные риски и затраты на обновление прошивок.

Приглашаю скачать и попробовать демонстрационную версию анализатора PVS-Studio. Также напоминаю, что если вы разрабатываете открытый проект или используете анализатор в академических целях, то для таких случаев мы предлагаем несколько вариантов бесплатных лицензий. Не ждите, когда коварный баг съест вашу ногу, начните использовать PVS-Studio прямо сейчас.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Espressif IoT Development Framework: 71 Shots in the Foot.

Подробнее..

PVS-Studio, Blender цикл заметок о пользе регулярного использования статического анализа

03.03.2021 20:06:54 | Автор: admin

PVS-Studio мониторит код Blender
В статьях мы регулярно повторяем важную мысль: статический анализатор должен использоваться регулярно. В этом случае многие ошибки выявляются на самом раннем этапе, а их исправление максимально дёшево. Однако теория это одно, но намного лучше подкреплять слова практическими примерами. Рассмотрим несколько свежих ошибок, появившихся в новом коде проекта Blender.


Недавно мы настроили регулярную проверку проекта Blender, о чём мой коллега рассказал в статье "Just for fun: команда PVS-Studio придумала мониторить качество некоторых открытых проектов". В дальнейшем планируем начать мониторить ещё некоторые интересные проекты.


Сразу скажу, что мы не ставим перед собой задачу найти как можно больше ошибок. Целью является периодическое написание небольших заметок (таких как эта), в которых мы будем на практике показывать достоинства регулярного анализа кода. Другими словами, мы иногда будем описывать некоторые интересные ошибки в новом коде, найденные при очередном ночном запуске PVS-Studio, и тем самым популяризировать правильное использование методологии статического анализа кода.


Итак, давайте посмотрим, что найдено в свежем коде проекта Blender.


Фрагмент первый: double-checked locking


typedef struct bNodeTree {  ....  struct NodeTreeUIStorage *ui_storage;} bNodeTree;static void ui_storage_ensure(bNodeTree &ntree){  /* As an optimization, only acquire a lock if the UI storage doesn't exist,   * because it only needs to be allocated once for every node tree. */  if (ntree.ui_storage == nullptr) {    std::lock_guard<std::mutex> lock(global_ui_storage_mutex);    /* Check again-- another thread may have allocated the storage       while this one waited. */    if (ntree.ui_storage == nullptr) {      ntree.ui_storage = new NodeTreeUIStorage();    }  }}

Предупреждение PVS-Studio. V1036: Potentially unsafe double-checked locking. node_ui_storage.cc 46


Перед нами неправильная реализация блокировки с двойной проверкой. Для пояснения проблемы процитирую фрагмент статьи "C++ and the Perils of Double-Checked Locking", написанной Scott Meyers и Andrei Alexandrescu ещё в 2004 году. Как видите, проблема давно известна, но это не защищает разработчиков от того, чтобы наступать на одни и те же грабли. Хорошо, что анализатор PVS-Studio помогает выявлять подобные проблемы :). Итак, фрагмент из статьи:


Consider again the line that initializes pInstance: pInstance = newSingleton;

This statement causes three things to happen:

Step 1: Allocate memory to hold a Singleton object.

Step 2: Construct a Singleton object in the allocated memory.

Step 3: Make pInstance point to the allocated memory.

Of critical importance is the observation that compilers are not constrainedto perform these steps in this order! In particular, compilers are sometimes allowed to swap steps 2 and 3. Why they might want to do that is a question we'll address in a moment. For now, let's focus on what happens if they do.

Consider the following code, where we've expanded pInstance's initialization line into the three constituent tasks we mentioned above and where we've merged steps 1 (memory allocation) and 3 (pInstance assignment) into a single statement that precedes step 2 (Singleton construction). The idea is not that a human would write this code. Rather, it's that a compiler might generate code equivalent to this in response to the conventional DCLP source code (shown earlier) that a human would write.

Если вы хотите подробнее разобраться с написанием блокировки с двойной проверкой, то отсылаю вас к описанию диагностики и статьи, ссылки на которые были даны выше. Для нас важно другое.


Подобные ошибки очень коварны! Они могут очень редко проявлять себя. Программа вроде как работает, проходит все тесты и так далее. Но время от времени она неожиданно падает у пользователей, и понять причину может быть крайне проблематично. Воспроизвести такую ошибку может быть очень сложно. Другим словами, исправление этой ошибки по жалобе от пользователей может обойтись в 1000 раз дороже, чем правка кода, сделанная по результату анализа кода с помощью PVS-Studio или аналогичного инструмента.


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


Фрагмент второй: realloc


static void icon_merge_context_register_icon(struct IconMergeContext *context,                                             const char *file_name,                                             struct IconHead *icon_head){  context->read_icons = realloc(context->read_icons,    sizeof(struct IconInfo) * (context->num_read_icons + 1));  struct IconInfo *icon_info = &context->read_icons[context->num_read_icons];  icon_info->head = *icon_head;  icon_info->file_name = strdup(path_basename(file_name));  context->num_read_icons++;}

Анализатор PVS-Studio выдаёт здесь два предупреждения, и это правильно. Здесь действительно допущено сразу две ошибки различного плана.


Первая: V701: realloc() possible leak: when realloc() fails in allocating memory, original pointer 'context->read_icons' is lost. Consider assigning realloc() to a temporary pointer. datatoc_icon.c 252


Если память не удастся выделить, функция realloc вернёт значение NULL. Нулевой указатель будет записан в переменную context->read_icons, а её предыдущее значение будет потеряно. Раз предыдущее значение указателя потеряно, то и невозможно освободить ранее выделенный блок памяти, на который ссылался этот указатель. Произойдёт утечка памяти.


Вторая: V522: There might be dereferencing of a potential null pointer 'context->read_icons'. Check lines: 255, 252. datatoc_icon.c


Описанная выше ошибка в каком-то смысле и не ошибка с точки зрения автора кода. Он и не собирался писать код, который будет продолжать работу в случае невозможности увеличения блока выделенной памяти. Такой случай просто не рассматривается. Автор предполагает, что, если память выделить не удалось, программа просто аварийно завершится при разыменовании нулевого указателя. И поэтому смело работает с указателем, не выполняя его предварительную проверку. Оставим в стороне вопрос, насколько красивым является такое поведение программы. На мой взгляд, такое поведение библиотек недопустимо.


Интереснее другое. На самом-то деле падение может и не произойти. Запись производится вовсе не по нулевому указателю, а куда-то дальше. Теоретически возможна ситуация, когда этот адрес уже не находится в странице памяти, защищенной от записи, и никакого падения не будет. Будут испорчены какие-то случайные данные в памяти, и программа продолжит своё выполнение. Последствия работы с испорченными данными непредсказуемы. Подробнее всё это разобрано в статье "Почему важно проверять, что вернула функция malloc".


Фрагмент третий: разыменование указателя для проверки


static int node_link_invoke(bContext *C, wmOperator *op, const wmEvent *event){  ....  bNodeLinkDrag *nldrag = node_link_init(bmain, snode, cursor, detach);  nldrag->last_picked_multi_input_socket_link = NULL;  if (nldrag) {    op->customdata = nldrag;  ....}

Предупреждение PVS-Studio: V595: The 'nldrag' pointer was utilized before it was verified against nullptr. Check lines: 1037, 1039. node_relationships.c


Один из самых частых паттернов ошибок (proof). В начале указатель nldrag разыменовывается. Но из следующего условного оператора становится видно, что на самом деле этот указатель может быть нулевым.


Всё просто и понятно. Но, согласитесь, намного лучше поправить такую ошибку сразу, ещё на этапе написания кода, а не после того, когда на неё наткнётся тестировщик или пользователь.


Кстати, нашлась ещё одна такая-же ошибка, но описывать её неинтересно. Приведу только сообщение: V595: The 'seq' pointer was utilized before it was verified against nullptr. Check lines: 373, 385. strip_add.c


Заключение


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


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio, Blender: Series of Notes on Advantages of Regular Static Analysis of Code.

Подробнее..

Как PVS-Studio защищает от поспешных правок кода

24.03.2021 12:04:07 | Автор: admin

Недостижимый код
Хотя только недавно была заметка про проект CovidSim, есть хороший повод вновь про него вспомнить и продемонстрировать пользу регулярного использования PVS-Studio. Бывает, что все мы спешим и вносим правки в код, потеряв сосредоточенность. Статический анализатор может оказаться здесь хорошим помощником.


Всё началось с написания вот этих двух небольших заметок про открытый проект COVID-19 CovidSim Model:



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


Только мы начали проверять этот проект, как результаты не заставили себя ждать :). Сейчас я покажу свежую ошибку, вызванную недавними неаккуратными правками кода. Конечно, мы не станем описывать в дальнейшем каждый баг и недочёт, обнаруженный анализатором. Будем писать только про что-то интересное.


Вот что получилось, после недавних модификаций файла CovidSim.cpp:


Правки


Кто-то решил создавать массивы не на стеке, а в куче. Но был невнимателен, внося изменения. Обратите внимание, что освобождение памяти находится после оператора return:


int GetXMLNode(....){  char* buf = new char[65536];  char* CloseNode = new char[2048];  char* CloseParent = new char[2048];  ....  if (ResetFilePos) fseek(dat, CurPos, 0);  return ret;  delete[] buf;  delete[] CloseNode;  delete[] CloseParent;}

В результате перед нами фрагмент недостижимого кода (unreachable code). И заодно утечка памяти.


Хорошо, что PVS-Studio тут же сообщает про эту ошибку: V779 Unreachable code detected. It is possible that an error is present. CovidSim.cpp 675


Используйте статический анализ регулярно и сможете устранить множество ошибок на самом раннем этапе! Согласитесь, лучше сразу исправить этот мелкий баг, чем гадать, почему вдруг программа начала потреблять слишком много оперативной памяти.


И последнее, что хочется отметить. Эта ошибка была бы невозможна, если не вручную управлять выделением и освобождением памяти, а применить RAII подход и использовать умные указатели.


Правильный и надёжный вариант кода:


std::unique_ptr<char[]> buf(new char[65536]);std::unique_ptr<char[]> CloseNode(new char[2048]);std::unique_ptr<char[]> CloseParent(new char[2048]);

Спасибо за внимание. Следуйте за мной в мир С++ и багов :). Twitter. Facebook.

Подробнее..

Теперь PVS-Studio ещё лучше знает, что за зверь такой strlen

27.04.2021 16:10:26 | Автор: admin

0824_DataFlow_And_Strlen_ru/image1.png
Как-то так несправедливо сложилось, что мы почти не уделяем в наших заметках внимание усовершенствованию внутренних механизмов анализатора, в отличие от новых диагностик. Поэтому давайте для разнообразия познакомимся с новым полезным усовершенствованием, коснувшимся анализа потока данных.


Всё началось с твита от JetBrains CLion IDE


На днях я увидел в Twitter пост от JetBrains про новые возможности статического анализатора, встроенного в CLion.


0824_DataFlow_And_Strlen_ru/image2.png


Поскольку мы скоро планируем выпустить плагин PVS-Studio для CLion, то я не мог пройти мимо и не написать, что мы тоже не лыком шиты. И что есть смысл попробовать PVS-Studio как плагин для CLion, чтобы находить ещё больше ошибок.


0824_DataFlow_And_Strlen_ru/image3.png


Ну и ещё немного с ними мило попереписывался:



После всего этого я подумал. А ведь они молодцы! Улучшили анализ потока данных и рассказывают миру. А мы чем хуже? Мы ведь тоже постоянно что-то улучшаем внутри анализатора, в том числе и тот же механизм анализа потока данных. И вот я уже пишу эту заметку.


А что у нас интересного с Data Flow


Пару дней назад была сделана доработка для клиента, описавшего ошибку, которую, к сожалению, анализатор PVS-Studio не смог заблаговременно выявить в его коде. Анализатор в некоторых случаях путался со значениями беззнаковых переменных, если возникало переполнение. Проблема была с кодом приблизительно такого вида:


bool foo(){  unsigned N = 2;  for (unsigned i = 0; i < N; ++i)  {    bool stop = (i - 1 == N);    if (stop)      return true;  }  return false;}

Анализатор не мог понять, что переменной stop всегда присваивается значение false.


Почему false? Давайте быстро посчитаем:


  • диапазон значения переменной i = [0; 1];
  • возможные значения выражения i-1 = [0; 0] U [UINT_MAX; UINT_MAX];
  • значение переменной N, равное двойке, не входит в множество { 0, UINT_MAX };
  • условие всегда ложно.

Примечание. Неопределённого поведения здесь нет, так как происходит переполнение (wrap) при работе с беззнаковым типом.


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


Например, возникли ложные срабатывания, связанные с обработкой длины строк. Борьба с ними привела к новым улучшениям и обучению анализатора лучше понимать, как и зачем используют такие функции, как strlen. Сейчас мы на практике покажем, о каких улучшениях идёт речь.


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


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

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


Предупреждение PVS-Studio: V512 A call of the 'strcpy' function will lead to overflow of the buffer 'astr'. asm.cpp 21


Всё равно не видите ошибку? Давайте внимательно разберём код. Для начала уберём всё не относящееся к делу:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

Есть локальный массив из 128 байт, в который планируется скопировать строчку, переданную в качестве аргумента. Копирование не должно выполняться, если строка пустая или содержит более 127 символов (не считая терминальный ноль).


Пока всё логично и правильно? На первый взгляд, да. Но что это?! Что это за константа 0x127?!


Это вовсе не 127. Совсем не 127 :)


Константа задана в шестнадцатеричной системе. Если перевести в десятичную, то получается 295.


Итак, написанный код эквивалентен следующему:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 295)) return 1;  strcpy(astr,str);  ....}

Как видите, проверка никак не защищает от переполнения буфера, и анализатор совершенно правильно предупреждает о проблеме.


Раньше анализатор не мог найти ошибку, будучи не в состоянии понять, что две функции strlen работают с одной строкой. И эта строка не меняется между двумя вызовами strlen. С точки зрения программиста, всё это очевидно, а вот анализатор нужно учить всё это понимать :).


Теперь PVS-Studio выводит из выражения, что длина строки str лежит в диапазоне [1..295], а значит, может возникнуть выход за границу массива, если попытаться его скопировать в буфер astr.


0824_DataFlow_And_Strlen_ru/image4.png


Новые вызовы


Описанная ошибка присутствует и в текущей версии кодовой базы проекта FCEUX. Но мы её не найдём, так как код изменился и теперь длина строки сохраняется в переменной. Это разрывает взаимосвязь между строкой и её длиной. Анализатор пока, к сожалению, молчит на новый вариант кода:


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  int len = strlen(str);  if ((!len) || (len > 0x127)) return 1;  strcpy(astr,str);  ....}

Человеку такой код может показаться даже проще, но, с точки зрения статического анализа, он труден для отслеживания значений. Нужно учитывать, что значение переменной len является длиной строки str. Дополнительно требуется аккуратно отслеживать, когда разоврётся эта взаимосвязь при модификации содержимого строки или переменной len.


Пока это анализатор PVS-Studio делать не умеет. Зато видно, куда можно и нужно развиваться! Со временем научимся находить ошибку и в этом новом коде.


Кстати, читатель может задаться вопросом, а почему мы анализируем старый код проектов и не обновляем их регулярно? Всё просто. Если обновлять проекты, мы не сможем проводить регрессионное тестирование. Будет непонятно, появились изменения в работе анализатора из-за правки кода самого анализатора или из-за правки в коде проверяемого проекта. Поэтому мы не обновляем открытые проекты, используемые в качестве базы для тестирования.


А чтобы тестировать анализатор на современном коде, написанном на C++14, C++17 и т.д., мы постепенно пополняем базу новыми проектами. Например, относительно недавно мы добавили коллекцию header-only C++ библиотек (awesome-hpp).


Заключение


Развивать механизмы анализа потока данных интересно и полезно. А если вам интересней в целом больше узнать о работе статических анализаторов кода, то предлагаем вашему вниманию следующие наши публикации:


  1. Анализатор кода не прав, да здравствует анализатор
  2. Ложные срабатывания в PVS-Studio: как глубока кроличья нора
  3. Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей
  4. Использование машинного обучения в статическом анализе исходного кода программ

Приглашаю скачать анализатор PVS-Studio и проверить свои проекты.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio Learns What strlen is All About.

Подробнее..

Маленькое удобство, способное прекратить вселенские споры

22.06.2020 00:17:16 | Автор: admin
Все те, кто пишет на Си-подобных языках, знакомы с двумя немного отличающимися стилями кода. Выглядят они вот так:

for (int i=0;i<10;i++) {    printf("Hello world!");    printf("Hello world again!");}


for (int i=0;i<10;i++){    printf("Hello world!");    printf("Hello world again!");}


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

Компромиссное решение могло бы выглядеть вот так:

image

Что, собственно, здесь произошло? Всего лишь небольшая магия или фокус. Потому что настоящие фигурные скобки никуда не делись. Они были заменены редактором на символы верхнего и нижнего подчёркивания. В исходном файле все скобки на месте, но при чтении исходного файла ваша любимая IDE заменяет скобки на подчёркивания по предлагаемому алгоритму.

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

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

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

И с другой стороны красота остаётся нетронутой! Визуально блок кода выделяется самой симметричной из всех симметричных конструкцией. Глаза всё так же легко найдут начало и конец. Ну и возражать против такого стиля будет просто некому, ведь оппоненты наверняка все перейдут в лагерь сторонников подхода.

Таким образом нам остаётся лишь рассмотреть алгоритм замены фигурных скобок на знаки подчёркивания.

Алгоритм замены скобок.



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

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

Для вставки подчёркивания алгоритм должен обнаружить сдвиг текста после начала блока. Если сдвига нет, значит автор кода не предполагал использовать ни один из рассмотренных стилей, ну и поэтому мы не будем вставлять ему никаких подчёркиваний (и убирать скобки тоже не будем).

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

Но остаётся небольшая сложность в виде бесшовного совмещения процесса редактирования с отображением замен в виде подчёркиваний.

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

Теперь изменения помельче. Если что-то правится внутри блока, то опять делать ничего не нужно. Но если к блоку добавляется новая строка, то самым логичным было бы ожидать, что разработчик установит курсор в конец последней строки и нажмёт enter. После этого мы ждём ввода первого символа и удаляем нижнее подчёркивание из начала последней строки и вставляем его в начало следующей (пока ещё пустой). Далее разработчик пишет текст после подчёркивания и некоторого набора пробелов (или табуляции), обозначающего отступ блока.

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

Выведем общие правила, соблюдение которых позволит достаточно просто производить замены.

Пока мы в блоке текста, включая крайние строки, делать ничего не нужно. Когда мы нажимаем enter в любом месте последней строки кроме конца нужно перенести подчёркивание. Если enter нажимается в конце строки ждём ввода символа. Когда мы нажимаем backspace в последней строке в позиции подчёркивания нужно переместить текущее подчёркивание на одну линию выше. И разумеется, при всех операциях нужно не забывать вставлять и удалять скобки в тот текст, который будет сохраняться на диск.

Вот и весь алгоритм. Остаётся дождаться, когда знатоки плагинов сделают нам всем такой приятный подарок. Ну а за ними подтянется и спецификация языка, ведь как ей не соответствовать тому, что удобно, да ещё и используется очень многими?

И напоследок о символе overscore, который я здесь условно назвал верхним подчёркиванием. Его, к сожалению, нет на стандартной клавиатуре, а потому нет возможности вводить его напрямую без помощи редактора. Именно поэтому важна помощь IDE. Хотя теоретически можно вводить последовательность юникода (\u00af = ), которую некоторые редакторы автоматически преобразуют в символ overscore, но всё же делать так каждый раз, когда нам нужна скобка, было бы просто издевательством над разработчиками. Поэтому и нужны плагины, ну и изменения в спецификациях языков.

Всё, ждём срочных обновлений спецификаций языков и массу удобных плагинов для всех возможных IDE :)
Подробнее..
Категории: Javascript , C++ , C , Java , Код , Си , Стиль кода

ИК датчик движения на STM32

19.09.2020 00:21:00 | Автор: admin

Приветствую, в этой статье вы узнаете: как сделать датчик движения с использованием ИК диода и ИК приемника на STM32 с минимальным использованием ядра (т.е. с максимальной загрузкой периферии) на регистрах, используя таймеры.

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

Оглавление:

Общая картина

Схемы устройств

Работа ИК диода и ИК приемника

Код для передатчика ИК сигнала

Код для приемника ИК сигнала

Полный код

Дополнительно

Заключение

Общая картина

Используются два устройства: передатчик ИК сигнала и приемник ИК сигнала.

Общий принцип работы следующий: передатчик ИК сигнала излучает сигнал инфракрасного диапазона длин волн, а приемник ИК сигнала их принимает. Между этими устройствами образуется луч, пересечение которого каким-либо объектом фиксируется приемным устройством.

В качестве ИК диода используется TSAL6200, в качестве ИК приемника TSOP4856.

TSAL6200TSAL6200 TSOP4856TSOP4856

Схемы устройств

Опустим цепи питания и другие элементы реальных устройств и рассмотрим только необходимые.

Передатчик ИК сигнала состоит из микроконтроллера STM32L151C8T6, полевого транзистора 2N7002, резистора 1 Ом и ИК диода TSAL6200. Ниже представлена электрическая схема передатчика.

Транзистор необходим для усиления тока, протекающего через ИК диод, так как выходной ток с пина МК ограничен (выходной ток с пина МК STM32L151C8T6 не более 25 мА, максимальный постоянный ток через ИК диод TSAL6200 100 мА).

Выбор транзистора на ваше усмотрение. Здесь выбран транзистор 2N7002, потому что он дешевый и его характеристик достаточно для моего использования. Стоит выбирать транзистор с меньшим пороговым напряжением затвора (Gate Threshold Voltage), так как на затвор вы сможете подать напряжение не более напряжения питания без дополнительных цепей, в нашем случае 3.3 В.

Точный расчет величины сопротивления резистора является трудным, конечно, существуют специальные формулы, но я предлагаю подобрать резистор опытным путем. Так в моем случае используется резистор 1 Ом, амплитуда тока цепи составляет 20 мА, дальность связи при таком токе достигает 14 м в зависимости от конфигурации передаваемого сообщения (количество импульсов в пачке, скважность, об этом ниже). Если вам требуется большая дальность, следует или подобрать резистор поменьше, или увеличить напряжение питания для диода, или выбрать транзистор с меньшим пороговым напряжением и меньшим падением напряжения на сток-истоке.

Приемник ИК сигнала состоит из микроконтроллера STM32L151C8T6, ИК приемника TSOP4856, резистора 100 Ом и конденсатора 0.1 мкФ. Ниже представлена электрическая схема приемника.

Работа ИК диода и ИК приемника

При протекании тока через диод TSAL6200 излучается ИК сигнал с длиной волны 940 нм. Это излучение необходимо промодулировать меандром с частотой 56 кГц (приемник настроен на данную частоту, см. даташит). Передавать следует пачки таких импульсов с определенной скважностью.

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

Остается только запрограммировать работу передатчика и приемника.

Код передатчика ИК сигнала

Будем использовать таймеры для формирования пачек импульсов с выводом сигнала на конкретный пин. Таким образом, ядро МК только инициализирует эти таймеры, а дальше может заниматься какой-нибудь другой работой.

Выбор пина должен быть обоснован. Необходимо выбирать такой пин, к которому может быть подключен какой-либо канал (кроме четвертого) любого General-purpose таймера. В нашем случае выбран пин PB6, к нему подключается первый канал TIM4. Приемник же будет подключен к пину PB7, второй канал TIM4.

Будем использовать два таймера: TIM4 будет генерировать меандр с частотой 56 кГц, TIM2 будет управлять таймером TIM4, т.е. создавать пачки из меандра. TIM2 будет работать в режиме Master, TIM4 Slave. Почему именно таймер TIM2? Он выбран исходя из таблицы даташита подключений таймеров друг к другу.

Приступим к написанию функции инициализации таймеров Tim_Init_Transmitter(). Код написан для пустого проекта. Сначала объявляем функцию, в теле main её инициализируем, а после мэйна прописываем уже саму функцию.

#include "main.h"void Timer_Init_Transmitter(void);int main(void){   RCC->ICSCR |= RCC_ICSCR_MSIRANGE_6;   // MSI 4.194 MHz enable   Timer_Init_Transmitter();   while(1)   {   }}void Timer_Init_Transmitter(void){}

RCC->ICSCR |= RCCICSCRMSIRANGE_6 - эта строчка просто устанавливает частоту 4.194 МГц МК. У вас может быть любая другая частота.

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

Подробнее про включение тактирования

В даташите находим регистр RCC_AHBENR. Нужно прописать 1 в поле GPIOBEN.

Для этого выбираем в библиотеке CMSIS строчку RCC_AHBENR_GPIOBEN, которая ставит данный бит в единицу.

И изменяем регистр AHBENR следующим образом:

RCC->AHBENR |= RCC_AHBENR_GPIOBEN; //GPIO port B clock enable

Подробнее про включение альтернативной функции

В даташите находим регистр GPIOx_MODER. Нужно прописать 10 в поле MODER6 (для пина PB6).

Для этого выбираем в библиотеке CMSIS строчку GPIO_MODER_MODER6_1, которая ставит второй бит в единицу.

В дальнейшем все такие обозначения будут браться аналогично.

И изменяем регистр MODER следующим образом:

GPIOB->MODER |= GPIO_MODER_MODER6_1;//Alternative function mode enable

Включение высокой скорости для пина делается аналогично включению альтернативной функции.

Подробнее про выбор альтернативной функции

Обозначение альтернативных функций представлено на картинке ниже.

Нам нужна альтернативная функция для TIM4 это AF2.

Стоит отметить, что существует два регистра для выбора альтернативных функций: нижний GPIOx_AFRL для пинов с номерами от 0 до 7 и верхний GPIOx_AFRH для пинов с номерами от 8 до 15. Однако, в библиотеке CMSIS определен сдвоенный регистр AFR[2], прописав в скобки 0, мы выбираем нижний регистр, прописав 1, выбираем верхний регистр.

В даташите находим регистр GPIOx_AFRL.

Нам нужно прописать 0010 в поле AFRL6, для этого нам нужно число в шестнадцатеричной форме 0x2000000, 2 седьмая справа для пина PB6, потому что первые шесть цифр для пинов с номерами от 0 до 5.

И изменяем регистр AFRL следующим образом:

GPIOB->AFR[0] |= 0x2000000; //Pin PB6 TIM4 alternative function AF2 enable

Получили следующий код для настройки пина внутри нашей функции:

void Timer_Init_Transmitter (void){   //Settings for GPIO PB6   RCC->AHBENR |= RCC_AHBENR_GPIOBEN;             //GPIO port B clock enable   GPIOB->MODER |= GPIO_MODER_MODER6_1;           //Alternative function mode enable   GPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR6_1;  //High speed   GPIOB->AFR[0] |= 0x2000000;                    //Pin PB6 TIM4 alternative function AF2 enable}

Теперь перейдем к настройке TIM4.

Нам нужно выбрать прескейлер PSC (делитель частоты), рассчитать значения для регистров CCR1 (длительность импульса) и ARR (период импульсов), выбрать режим работы таймера ШИМ, выбрать входной управляющий сигнал от таймера TIM2, выбрать режим для Slave и подать выходной сигнал таймера на пин PB6.

Включение тактирования осуществляется аналогично включению тактирования GPIO.

Подробнее про выбор прескейлера PSC и расчет CCR1 и ARR

Выберем значение преселлектора входной частоты равное 0, в этом случае частота тактирования таймера будет равна частоте МК.

И изменяем регистр PSC следующим образом:

TIM4->PSC = 0; //Prescaler value

Перейдем к расчету ARR.

Для расчета ARR нам нужно поделить тактовую частоту таймера 4.194 МГц (у вас своя) на 56 кГц. Получаем 74,89, округляем до целого. Я округлил до 75. И вписываем в регистр ARR:

TIM4->ARR = 75 //Auto-reload value

Осталось рассчитать CCR1.

Так как у нас простой меандр, то в регистр CCR1 при режиме ШИМ нужно указать половину от значения ARR:

TIM4->CCR1 = 37;//Capture/Compare 1 value

Подробнее про выбор режима работы ШИМ таймера

Так как у нас первый канал, то используем регистр CCMR1. Нас интересует в этом регистре поле OC1M. Режим ШИМ позволяет настраивать длительность импульса при фиксированном периоде.

Выберем PMW mode 1, вписав две единицы во второй и третий биты, т.е. 110:

TIM4->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; //Output compare PMW mode 1

Подробнее про настройку подачи выходного сигнала на пин PB6

В регистр CCER в поле CC1E нужно вписать 1, включив подачу выходного сигнала на пин.

Вписываем следующую строчку:

TIM4->CCER |= TIM_CCER_CC1E; //OC3 signal is output on the corresponding pin

Подробнее про выбор управляющего сигнала и режима Slave

Для управления TIM4 таймером TIM2 нужно выбрать входной сигнал ITR1. Для этого нужно вписать в поле TS регистра TIMx_SMCR 001. Также выберем режим для Slave, вписав 101 в поле SMS. В этом режиме пока входной сигнал ITR1 будет высоким, то TIM4 будет работать и выдавать меандр на пин, как только ITR1 станет низким, TIM4 выключается.

Для этого вписываем:

TIM4->SMCR |= TIM_SMCR_TS_0;//choosing ITR1

TIM4->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_2; //Gated Mode

Добавим к уже существующему коду, и получим:

void Timer_Init_Transmitter (void){   //Settings for GPIO PB6   RCC->AHBENR |= RCC_AHBENR_GPIOBEN;             //GPIO port B clock enable   GPIOB->MODER |= GPIO_MODER_MODER6_1;           //Alternative function mode enable   GPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR6_1;  //High speed   GPIOB->AFR[0] |= 0x2000000;                    //Pin PB6 TIM4 alternative function AF2 enable   //Settings for TIM4 - Slave   RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;            //TIM4 clock enable   TIM4->PSC = 0;                                 //Prescaler value   TIM4->ARR = 75;                                //Auto-reload value   TIM4->CCR1 = 37;                               //Capture/Compare 1 value   TIM4->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; //Output compare PMW mode 1    enable   TIM4->CCER |= TIM_CCER_CC1E;                   //OC3 signal is output on the corresponding output pin   TIM4->SMCR |= TIM_SMCR_TS_0;                   //choosing ITR1   TIM4->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_2; //Gated Mode   TIM4->CR1 |= TIM_CR1_CEN;                      //TIM4 enable}

Включение TIM4 можно производить, а можно и нет, так как управляющий сигнал с TIM2, всё равно включит его.

Теперь перейдем к настройке TIM2.

Необходимо подать тактирование на таймер, рассчитать PSC, CCR1 и ARR, выбрать режим работы таймера ШИМ, выбрать какой сигнал будет выходным (входным управляющим для TIM4) и включить таймер.

Включение тактирования аналогично, как и для таймера TIM4.

RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;//TIM2 clock enable

Подробнее про расчет PSC, CCR1 и ARR для TIM2

Так как по даташиту на диод в пачке следует использовать не менее 10 импульсов (на самом деле можно и меньше, но качество работы может быть не очень хорошим), то выберем тактирование таймера TIM2 в десять раз меньше, чем TIM4.

Как видим в формуле для расчета частоты тактирования таймера в знаменателе уже есть +1, т.е. нам нужно записать в регистр PSC 9, чтобы получить частоту в 10 раз меньшую, чем частоту МК.

TIM2->PSC = 9;//Prescaler value

Теперь рассчитаем значение для CCR1: отправлять будем 10 импульсов, следовательно, нам нужно взять значение ARR для TIM4 (период одного импульса, это 75) и умножить на 10, т.е. получаем 750, однако, у нас есть прескейлер, который делит частоту на 10, т.е. нам нужно поделить 750 на 10, в итоге получаем снова 75 (как и в TIM4, но уже с другим прескейлером). Запишем это значение в регистр CCR1 таймера TIM2.

TIM2->CCR1 = 75;//Capture/Compare 1 value

Перейдём к расчету ARR: тут всё просто, допустим, хотим стрелять пачками со скважностью 11.2, при этом период излучения пачек будет около 2 мс (из расчета, что 1 мс это 4194000/1000 = 4194 тика, и умножаем на 2, получаем округлённо 8400, а с прескейлером 10, получаем 840 тиков), умножаем длительность пачки 75 на 11.2 и получаем 840, как видите, значения совпадают. Запишем это в регистр ARR.

TIM2->ARR = 840;//Auto-reload value

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

Режим работы таймера TIM2 точно такой же, как и у TIM4 - ШИМ.

TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; //Output compare PMW mode 1 enable

Подробнее про выбор выходного сигнала TIM2 (управляющего для TIM4)

Найдем регистр TIMx_CR2.

Нам нужно подать сигнал с первого канала (мы же используем CCR1), на выход с таймера TIM2. Этим сигналом является OC1REF. Ниже на картинке можно найти комбинацию битов для этого сигнала 100.

В поле MMS необходимо вписать 1 в третий бит.

TIM2->CR2 |= TIM_CR2_MMS_2;//OC1REF signal is used as trigger output (TRGO)

Теперь осталось включить TIM2, записав следующую строчку:

TIM2->CR1 |= TIM_CR1_CEN;//TIM2 enable

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

Добавим эти строчки к предыдущим и получим:

void Timer_Init_Transmitter (void){   //Settings for GPIO PB6   RCC->AHBENR |= RCC_AHBENR_GPIOBEN;             //GPIO port B clock enable   GPIOB->MODER |= GPIO_MODER_MODER6_1;           //Alternative function mode enable   GPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR6_1;  //High speed   GPIOB->AFR[0] |= 0x2000000;                    //Pin PB6 TIM4 alternative function AF2 enable   //Settings for TIM4 - Slave   RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;            //TIM4 clock enable   TIM4->PSC = 0;                                 //Prescaler value   TIM4->ARR = 75;                                //Auto-reload value   TIM4->CCR1 = 37;                               //Capture/Compare 1 value   TIM4->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; //Output compare PMW mode 1 enable   TIM4->CCER |= TIM_CCER_CC1E;                   //OC3 signal is output on the corresponding output pin   TIM4->SMCR &= ~TIM_SMCR_TS;                    //clear bits   TIM4->SMCR |= TIM_SMCR_TS_0;                   //choosing ITR1   TIM4->SMCR &= ~TIM_SMCR_SMS;                   //clear bits   TIM4->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_2; //Gated Mode   TIM4->CR1 |= TIM_CR1_CEN;                      //TIM4 enable   //Settings for TIM2 - Master   RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;            //TIM2 clock enable   TIM2->PSC = 9;                                 //Prescaler value   TIM2->ARR = 840;                               //Auto-reload value   TIM2->CCR1 = 75;                               //Capture/Compare 1 value   TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; //Output compare PMW mode 1 enable   TIM2->CR2 |= TIM_CR2_MMS_2;                    //OC1REF signal is used as trigger output (TRGO)   TIM2->CR1 |= TIM_CR1_CEN;                      //TIM2 enable}

Теперь у нас ИК диод будет излучать пачки по 10 импульсов, у которых частота 56 кГц, со скважностью 11.2, т.е. с периодом следования пачек в 2 мс. Заметим, что момент пересечения каким-либо объектом внутри периода пачек не определен, т.е. мы можем судить о пересечении ИК луча только по отсутствию следующей пачки. Таким образом, погрешность измерения момента времени пересечения луча составляем 2 мс.

Код приемника ИК сигнала

Для приема ИК сигнала снова будем использовать таймеры, а точнее всего один. В микроконтроллерах STM32 таймеры могут быть не только Master или Slave, а могут быть Master/Slave, т.е. они могут управлять сами собой.

В разделе статьи Код передатчика ИК сигнала мы уже выбрали пин PB7 для приемника ИК сигнала. Схему подключения см. выше. К этому пину подключается второй канал TIM4.

Ниже на картинке представлена схема устройства таймера.

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

Принцип работы для нашего проекта заключается в следующем: пусть наш таймер будет просто считать время, допустим, равное 5 периодам излучаемых пачек ИК сигнала, следовательно, в ARR будет число, равное 5 периодам пачек, т.е. 840 * 5 = 4200. Настроим таймер так, чтобы при приеме каждой пачки таймер перезагружался и начинал считать заново. При достижении таймером числа в регистре ARR таймер выдает прерывание, которое означает, что на вход уже как 5 периодов не приходит пачка, следовательно, луч пересекается каким-то объектом. Вот и вся работа таймера. Осталось настроить TIM4.

Создадим новый проект и функцию инициализации таймера:

#iclude "main.h"void Timer_Init_Receiver(void);int main(void){   RCC->ICSCR |= RCC_ICSCR_MSIRANGE_6;    // MSI 4.194 MHz enable   Timer_Init_Receiver();   while(1)   {   }}void Timer_Init_Receiver(void){}

Необходимо настроить пин PB7: включить тактирование порта B, настроить альтернативную функцию пина, настроить скорость и выбрать какую конкретную альтернативную функцию подключить к пину. Всё, как и в предыдущем случае.

void Timer_Init_Receiver(void){//Settings for GPIO PB7RCC->AHBENR |= RCC_AHBENR_GPIOBEN;          // GPIO port B clock enableGPIOB->MODER |= GPIO_MODER_MODER7_1;        // Alternative function mode enableGPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR7_1;// High speedGPIOB->PUPDR |= GPIO_PUPDR_PUPDR7_0;        // pull-up PB7GPIOB->AFR[0] |= 0x20000000;                // Pin PB7 TIM4 alternative function AF2 enable}

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

GPIOB->PUPDR |= GPIO_PUPDR_PUPDR7_0;

Теперь настроим TIM4.

Включим тактирование таймера. Так как у нас второй канал, то будем использовать CCR2. Прескейлер приравняем к 9, чтобы частота тактирования таймера была как у TIM2 передатчика. Для приемника можно приравнять CCR2 и ARR. Как уже рассчитали ранее, вписываем в эти регистры 4200.

RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;// TIM4 clock enable

TIM4->PSC = 9;// Prescaler value

TIM4->ARR = 4200;// Auto-reload value

TIM4->CCR2 = 4200;// Capture/Compare 2 value

Далее настроим режим работы таймера.

Нам нужно, чтобы он просто считал, и всё. Для этого в даташите находим TIMx_CCMR1 и в поле OC2M вписываем 000, что соответствует Frozen mode. Изменим регистр, обнулив все биты:

TIM4->CCMR1 &= ~TIM_CCMR1_OC2M;// Frozen mode enable

Настроим канал на выход, обнулив биты поля CC2S (более подробно написано в даташите), впишем нули в эти биты:

TIM4->CCMR1 &= ~TIM_CCMR1_CC2S; // Output mode

Теперь обратимся к картинке со схемой устройства таймера (см. выше). Нам необходимо, чтобы входной сигнал с пина PB7 был управляющим для таймера TIM4, таким сигналом является TI2FP2. Проследите путь этого сигнала от TIMx_CH2 до TRGI. Чтобы выбрать данный сигнал в качестве управляющего, необходимо в регистр TIMx_SMCR в поле TS вписать 110. Сразу же выберем режим для Slave: Reset mode, вписав 100 в поле SMS. Для этого пропишем строчки:

TIM4->SMCR |= TIM_SMCR_TS_1 | TIM_SMCR_TS_2; // Choosing TI2FP2

TIM4->SMCR |= TIM_SMCR_SMS_2;// Reset mode

Теперь нам нужно определиться, что будет являться триггером обновления таймера: передний или задний фронт входных импульсов (на самом деле не особо важно, но лучше выбрать по переднему фронту, в нашем случае, это фронт спада напряжения). Для этого нужно использовать пару битовых полей регистра CCER: CC2P и CC2NP, они работают в паре, более подробно написано о них в даташите.

Для нашего случая вписываем 1 в CC2P и 0 в CC2NP. Делаем это так:

TIM4->CCER &= ~TIM_CCER_CC2NP;// This bit is used in conjunction with CC2P.

TIM4->CCER |= TIM_CCER_CC2P; // Inverted/falling edge

Разрешим прерывание по переполнению. Для этого в регистре TIMx_DIER в поле CC2IE впишем 1.

Для этого впишем строчку:

TIM4->DIER |= TIM_DIER_CC2IE; // Capture/Compare 2 interrupt enable

Включаем таймер строчкой:

TIM4->CR1 |= TIM_CR1_CEN;// TIM4 enable

Не забываем разрешить глобальное прерывание:

NVIC_EnableIRQ(TIM4_IRQn); // TIM4 global Interrupt enable

Наша функция инициализации таймера выглядит следующим образом:

void Timer_Init_Receiver(void){//Settings for GPIO PB7RCC->AHBENR |= RCC_AHBENR_GPIOBEN;           // GPIO port B clock enableGPIOB->MODER |= GPIO_MODER_MODER7_1;         // Alternative function mode enableGPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR7_1; // High speedGPIOB->PUPDR |= GPIO_PUPDR_PUPDR7_0;         // pull-up PB7GPIOB->AFR[0] |= 0x20000000;                 // Pin PB7 TIM4 alternative function AF2 enable//Settings for TIM4RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;          // TIM4 clock enableTIM4->PSC = 9;                                 // Prescaler valueTIM4->ARR = 4200;                              // Auto-reload valueTIM4->CCR2 = 4200;                             // Capture/Compare 2 valueTIM4->CCMR1 &= ~TIM_CCMR1_OC2M;              // Frozen mode enableTIM4->CCMR1 &= ~TIM_CCMR1_CC2S;              // Output modeTIM4->CCER &= ~TIM_CCER_CC2NP;               // This bit is used in conjunction with CC2P.TIM4->CCER |= TIM_CCER_CC2P;                 // Inverted/falling edgeTIM4->SMCR |= TIM_SMCR_TS_1 | TIM_SMCR_TS_2; // Choosing TI2FP2TIM4->SMCR |= TIM_SMCR_SMS_2;                // Reset modeTIM4->DIER |= TIM_DIER_CC2IE;                // Capture/Compare 2 interrupt enableTIM4->CR1 |= TIM_CR1_CEN;                    // TIM4 enableNVIC_EnableIRQ(TIM4_IRQn);                   // TIM4 global Interrupt enable}

Теперь напишем обработчик прерываний:

Первое что нужно сделать, это снять флаг в регистре статуса TIMx_SR:

TIM4->SR &= ~TIM_SR_CC2IF;

Далее можем делать, что хотим. Хоть ставить какой-нибудь флаг, хоть зажигать диод. Допустим будем зажигать светодиод. Настроим пин PB15 на выход, прописав в мэйне:

GPIOB->MODER |= GPIO_MODER_MODER15_0;// PB15 output mode

Наш обработчик прерываний будет выглядеть следующим образом:

void TIM4_IRQHandler(void){TIM4->SR &= ~TIM_SR_CC2IF;GPIOB->ODR |= GPIO_ODR_ODR_15;  // Led red on}

Готово! Теперь при пересечении ИК луча каким-либо объектом, на приемнике будет зажигаться светодиод.

Полный код

Код передатчика ИК сигнала
void Timer_Init_Transmitter(void);int main(void){   RCC->ICSCR |= RCC_ICSCR_MSIRANGE_6;             // MSI 4.194 MHz enable   Timer_Init_Transmitter();   while(1)   {   }}void Timer_Init_Transmitter(void){  //Settings for GPIO PB6   RCC->AHBENR |= RCC_AHBENR_GPIOBEN;             //GPIO port B clock enable   GPIOB->MODER |= GPIO_MODER_MODER6_1;           //Alternative function mode enable   GPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR6_1;  //High speed   GPIOB->AFR[0] |= 0x2000000;                    //Pin PB6 TIM4 alternative function AF2 enable   //Settings for TIM4 - Slave   RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;            //TIM4 clock enable   TIM4->PSC = 0;                                 //Prescaler value   TIM4->ARR = 75;                                //Auto-reload value   TIM4->CCR1 = 37;                               //Capture/Compare 1 value   TIM4->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; //Output compare PMW mode 1 enable   TIM4->CCER |= TIM_CCER_CC1E;                   //OC3 signal is output on the corresponding output pin   TIM4->SMCR &= ~TIM_SMCR_TS;                    //clear bits   TIM4->SMCR |= TIM_SMCR_TS_0;                   //choosing ITR1   TIM4->SMCR &= ~TIM_SMCR_SMS;                   //clear bits   TIM4->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_2; //Gated Mode   TIM4->CR1 |= TIM_CR1_CEN;                      //TIM4 enable   //Settings for TIM2 - Master   RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;            //TIM2 clock enable   TIM2->PSC = 9;                                 //Prescaler value   TIM2->ARR = 840;                               //Auto-reload value   TIM2->CCR1 = 75;                               //Capture/Compare 1 value   TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; //Output compare PMW mode 1 enable   TIM2->CR2 |= TIM_CR2_MMS_2;                    //OC1REF signal is used as trigger output (TRGO)   TIM2->CR1 |= TIM_CR1_CEN;                      //TIM2 enable}
Код приемника ИК сигнала
#include main.hvoid Timer_Init_Receiver(void);int main(void){   RCC->ICSCR |= RCC_ICSCR_MSIRANGE_6;         // MSI 4.194 MHz enable  GPIOB->MODER |= GPIO_MODER_MODER15_0;        // PB15 output mode   Timer_Init_Receiver();   while(1)   {   }}void Timer_Init_Receiver(void){//Settings for GPIO PB7RCC->AHBENR |= RCC_AHBENR_GPIOBEN;           // GPIO port B clock enableGPIOB->MODER |= GPIO_MODER_MODER7_1;         // Alternative function mode enableGPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR7_1; // High speedGPIOB->PUPDR |= GPIO_PUPDR_PUPDR7_0;         // pull-up PB7GPIOB->AFR[0] |= 0x20000000;                 // Pin PB7 TIM4 alternative function AF2 enable//Settings for TIM4RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;          // TIM4 clock enableTIM4->PSC = 9;                                 // Prescaler valueTIM4->ARR = 4200;                              // Auto-reload valueTIM4->CCR2 = 4200;                             // Capture/Compare 2 valueTIM4->CCMR1 &= ~TIM_CCMR1_OC2M;              // Frozen mode enableTIM4->CCMR1 &= ~TIM_CCMR1_CC2S;              // Output modeTIM4->CCER &= ~TIM_CCER_CC2NP;               // This bit is used in conjunction with CC2P.TIM4->CCER |= TIM_CCER_CC2P;                 // Inverted/falling edgeTIM4->SMCR |= TIM_SMCR_TS_1 | TIM_SMCR_TS_2; // Choosing TI2FP2TIM4->SMCR |= TIM_SMCR_SMS_2;                // Reset modeTIM4->DIER |= TIM_DIER_CC2IE;                // Capture/Compare 2 interrupt enableTIM4->CR1 |= TIM_CR1_CEN;                    // TIM4 enableNVIC_EnableIRQ(TIM4_IRQn);                   // TIM4 global Interrupt enable}void TIM4_IRQHandler(void){TIM4->SR &= ~TIM_SR_CC2IF;GPIOB->ODR |= GPIO_ODR_ODR_15;               // Led red on}

Дополнительно

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

Нужно добавить глобальную переменную, например, написав между "инклюдами" и мэйном такую строчку:

int StatusDiode = 0;// 0 - diode is off, 1 - diode is on

Эта переменная необходима для запоминания статуса светодиода: выключен или включен.

Далее в функции инициализации таймера нужно изменить строчку с разрешением прерывания по переполнению на разрешение прерывания по триггеру, такой строчкой:

TIM4->DIER |= TIM_DIER_TIE;// Trigger interrupt enable

И последнее: изменим обработчик прерываний.

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

Если диод был включен, делаем все наоборот: снимаем соответствующий флаг, выключаем диод, выключаем разрешение прерывания по переполнению, включаем разрешение прерывания по триггеру и изменяем статус диода.

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

Получим такой обработчик прерываний:

void TIM4_IRQHandler(void){if (StatusDiode == 0){    TIM4->SR &= ~TIM_SR_TIF;    GPIOB->ODR |= GPIO_ODR_ODR_15;         // Led red on    TIM4->DIER &= ~TIM_DIER_TIE;           // Trigger interrupt disable    TIM4->DIER |= TIM_DIER_CC2IE;          // Capture/Compare 2 interrupt enable    TIM4->CNT = 0;    StatusDiode = 1;}else{    TIM4->SR &= ~TIM_SR_CC2IF;    GPIOB->ODR &= ~GPIO_ODR_ODR_15;        // Led red off    TIM4->DIER &= ~TIM_DIER_CC2IE;         // Capture/Compare 2 interrupt disable    TIM4->DIER |= TIM_DIER_TIE;            // Trigger interrupt enable    StatusDiode = 0;}}
Полный код для приемника ИК сигнала
#include main.hvoid Timer_Init_Receiver(void);int main(void){   RCC->ICSCR |= RCC_ICSCR_MSIRANGE_6;      // MSI 4.194 MHz enable  GPIOB->MODER |= GPIO_MODER_MODER15_0;     // PB15 output mode   Timer_Init_Receiver();   while(1)   {   }}void Timer_Init_Receiver(void){//Settings for GPIO PB7RCC->AHBENR |= RCC_AHBENR_GPIOBEN;           // GPIO port B clock enableGPIOB->MODER |= GPIO_MODER_MODER7_1;         // Alternative function mode enableGPIOB->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR7_1; // High speedGPIOB->PUPDR |= GPIO_PUPDR_PUPDR7_0;         // pull-up PB7GPIOB->AFR[0] |= 0x20000000;                 // Pin PB7 TIM4 alternative function AF2 enable//Settings for TIM4RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;          // TIM4 clock enableTIM4->PSC = 9;                                 // Prescaler valueTIM4->ARR = 4200;                              // Auto-reload valueTIM4->CCR2 = 4200;                             // Capture/Compare 2 valueTIM4->CCMR1 &= ~TIM_CCMR1_OC2M;              // Frozen mode enableTIM4->CCMR1 &= ~TIM_CCMR1_CC2S;              // Output modeTIM4->CCER &= ~TIM_CCER_CC2NP;               // This bit is used in conjunction with CC2P.TIM4->CCER |= TIM_CCER_CC2P;                 // Inverted/falling edgeTIM4->SMCR |= TIM_SMCR_TS_1 | TIM_SMCR_TS_2; // Choosing TI2FP2TIM4->SMCR |= TIM_SMCR_SMS_2;                // Reset modeTIM4->DIER |= TIM_DIER_TIE;                  // Trigger interrupt enableTIM4->CR1 |= TIM_CR1_CEN;                    // TIM4 enableNVIC_EnableIRQ(TIM4_IRQn);                   // TIM4 global Interrupt enable}void TIM4_IRQHandler(void){if (StatusDiode == 0){    TIM4->SR &= ~TIM_SR_TIF;    GPIOB->ODR |= GPIO_ODR_ODR_15;           // Led red on    TIM4->DIER &= ~TIM_DIER_TIE;             // Trigger interrupt disable    TIM4->DIER |= TIM_DIER_CC2IE;            // Capture/Compare 2 interrupt enable    TIM4->CNT = 0;    StatusDiode = 1;}else{    TIM4->SR &= ~TIM_SR_CC2IF;    GPIOB->ODR &= ~GPIO_ODR_ODR_15;          // Led red off    TIM4->DIER &= ~TIM_DIER_CC2IE;           // Capture/Compare 2 interrupt disable    TIM4->DIER |= TIM_DIER_TIE;              // Trigger interrupt enable    StatusDiode = 0;}}

Заключение

Таким образом, разработали датчик движения, основанный на работе пересечения каким-либо объектом ИК луча. Система состоит из двух устройств, имеет минимум элементов, проста в реализации, имеет низкую стоимость и высокое быстродействие. При этом ядро МК свободно и может использоваться для других нужд.

Подробнее..

Перевод Папа может в си, или Простая и аккуратная кодогенерация для SQLite

02.11.2020 20:06:40 | Автор: admin
image

Совсем скоро, 4 и 5 ноября, у нас стартуют новые потоки курсов SQL для анализа данных и C++ разработчик, специально к их старту мы подготовили этот перевод из блога Facebook Engineering с обзором полезного инструмента.

SQLite используется широко, но написание хорошо протестированных и поддерживаемых уровней доступа к данным в лучшем случае может стать сложной задачей. Многие команды применяют некую генерацию кода, чтобы избежать необходимости менять десятки порядковых номеров каждый раз, когда добавляется столбец, но такой подход приводит к ошибкам. Компилятор CQL в CG/SQL позволяет создавать сложные хранимые процедуры с большими запросами, а с помощью комбинаций синтаксических хелперов и сильной типизации эти процедуры гораздо проще получить и сохранить. Сочетание сильной типизации в языке и средства для хорошего юнит-тестирования может дать уверенность в том, что даже очень сложная логика корректна.



Что это такое


CG/SQL это система генерации кода для популярной библиотеки SQLite, позволяющая разработчикам писать хранимые процедуры в варианте Transact-SQL (T-SQL) и компилировать их в код на языке C, использующий для выполнения операций C API SQLite. CG/SQL позволяет инженерам создавать сложные хранимые процедуры с большими запросами без ручной проверки кода, необходимой существующим методам.

Вся система также включает функции управления и обновления схемы, создания тестирующего кода для хранимых процедур, получения планов запросов для процедур, а также взаимодействия с хранимыми процедурами из других языков, например, Java и Objective-C. Вывод в JSON позволяет делать разбор или интерфейсный код. Пакет содержит обширную документацию по языку и системе.

Что делает инструмент


Компилятор CQL делает большую часть грязной работы. Он читает схему и процедуры, предоставляя язык с сильной типизацией и сотнями ошибок компиляции, разработанных для предотвращения проблем SQLite, возникающих во время выполнения. Компилятор тщательно отслеживает типы данных переменных и типы схем, сообщая о несоответствиях: например о попытке присвоить обнуляемые столбцы необнуляемым выходным переменным, а в противном случае гарантирует, что API SQLite используются последовательно и корректно.

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

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

Зачем это нужно?


SQLite используется широко, но написание хорошо протестированных и поддерживаемых уровней доступа к данным в лучшем случае может стать сложной задачей. Многие команды применяют некую генерацию кода, чтобы избежать необходимости менять десятки порядковых номеров каждый раз, когда добавляется столбец, но такой подход приводит к ошибкам. Компилятор CQL в CG/SQL позволяет создавать сложные хранимые процедуры с большими запросами, а с помощью комбинаций синтаксических хелперов и сильной типизации эти процедуры гораздо проще получить и сохранить. Сочетание сильной типизации в языке и средства для хорошего юнит-тестирования может дать уверенность в том, что даже очень сложная логика корректна. Синтаксические хелперы преобразуют безопасный код в канонический SQL, так инженеры пишут меньше кода, но при этом код корректнее и он выполняется везде. Посмотрим на пример:

create procedure insert_a_row(like your_table)begin  insert into your_table from arguments;end;

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

CG/SQL на Github

CG/SQL конечно полезная вещь, но скидочный промокод HABR не менее полезен, он даст вам получить дополнительные 10% к скидке указанной на баннере.

image




Рекомендуемые статьи


Подробнее..

Разбиваем строку на подстроки по разделяющим символам своими руками

19.03.2021 16:09:40 | Автор: admin

Введение

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

Вообще говоря, сама задача разбиения строк на подстроки, каждая из которых отделена в исходной строке определённым символом, является довольно распространённой. Очень часто необходимо извлечь из строки слова, разделённые пробелами. Конечно, в стандартной библиотеке языка Си уже есть функция strtok (заголовочный файл <string.h>), но она имеет свои побочные эффекты, перечисленные ниже.

  1. Функция модифицирует исходную строку, разбивая её на лексемы. Она возвращает указатель на саму лексему, или NULL, в случае, когда нет больше лексем. Подробнее о ней можно прочитать здесь.

  2. Так как функция модифицирует строку, то при передачи ей строчного литерала, будет получено SEGV, поскольку память для таких литеральных строк выделяется в сегменте кода, доступного только для чтения.

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

  4. Она не учитывает экранирование символов разделителей.

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

  1. Не менять оригинальную строку, в которой ищутся лексемы.

  2. Для каждой найденной лексемы создавать новую строку.

  3. Сохранять свою текущую позицию, а именно - указатель на подстроку, которая ещё не разбиралась.

  4. Иметь однородную последовательность вызовов.

  5. Иметь возможность экранировать символы разделители, при сложных лексемах.

Основные шаги при разделении строк.

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

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

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

Разработка функции.

Приступим к разработке. Для начала определим заголовочный файл "str_utils.h", содержащий все прототипы необходимых функций. Реализации функций положим в файл "str_utils.c".

Начнём с функции нахождения символов в строке. Библиотечная функция strchr могла бы решить эту задачу. Но проблема в том, что она не допускает в качестве аргумента строки, в которой надо искать символ, значения NULL. При попытке компилировать с флагами -Wall -Werror, файл с таким аргументом не скомпилируется. Хотя, такую ситуацию можно было бы обработать, вернув NULL. Поэтому был определён свой вариант данной функции с именем contains_symbol. Её прототип выглядит следующим образом:

size_t contains_symbol(char *src, char symbol);

Её реализация определена следующим образом (файл "str_utils.c"):

size_t contains_symbol(char *src, char symbol){  size_t pos = 1;  if(symbols == NULL)    return -1;  while(*symbols != '\0'){    if(*symbols++ == symbol)      return pos;    pos++;  }  return 0;}

Данная функция возвращает позицию символа в строке, увеличенную на единицу. Она не учитывает нулевой символ. Если символ не был найден, функция вернёт 0, либо -1, если ей передали NULL. Её удобно использовать в цикле while, при проверке текущего символа строки на его наличие в другой строке.

Для инкапсуляции работы с памятью был определён отдельный заголовочный файл "mem.h", содержащий следующие прототипы:

void *alloc_mem(size_t nbytes);void *calloc_mem(size_t nelems, size_t elem_size);#define alloc_str(x) ((char *) alloc_mem(x + 1))

Соответствующие функции реализованы в отдельном файле "mem.c":

#include <string.h>#include <stdlib.h>void *alloc_mem(size_t nbytes){  char *buf = (char *)malloc(nbytes);  if(buf != NULL){    memset(buf, '\0', nbytes);    return buf;  }  exit(-1);}void *calloc_mem(size_t nelems, size_t elem_size){  void *buf = calloc(nelems, elem_size);  if(buf != NULL){    return buf;  }  exit(-1);}

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

Функция обрезки разделителей строки trim_separators выглядит следующим образом:

/* trims symbols from separators at src string *//* returns new trimmed string */char *trim_separators(char *src, char *separators);
char *trim_separators(char *src, char *separators){  if(src == NULL || separators == NULL)    return NULL;  char *sp = src;  while(contains_symbol(separators, *sp)) sp++;    /* if it contains only symbols from separators => NULL */  if(sp - s == strlen(s)) return NULL;    char *sp2 = s + strlen(s) - 1; /* last char at src */  while(contains_symbol(separators, *sp2)) sp2--;    /* if it contains only symbols from separators => NULL */  if(sp2 < s) return NULL;    size_t sz = 0;  if(sp2 - sp == 0 && *sp == '\0') return NULL; /* zero byte is not a character */  else if(sp2 - sp == 0){    sz = 1;  }  else{    sz = (sp2 - sp) + 1;  }  char *res = alloc_mem(sz);  memcpy(res, sp, sz);/* copy all chars except last zero byte */  return res;}

В начале мы проверяем на NULL аргументы функции. Если они нулевые, то возвращаем NULL.

Далее, через указатель sp, проходим строку слева направо, пока мы встречаем символы из строки separators. Если мы прошли всю строку, значит она целиком и полностью состоит из сепараторов, следовательно надо удалить все символы, или же просто вернуть NULL.

char *sp = src;while(contains_symbol(separators, *sp)) sp++;  /* if it contains only symbols from separators => NULL */if(sp - s == strlen(s)) return NULL;

Аналогично, далее через указатель sp2, проходим строку справа налево, проверяя, находится ли текущий символ в массиве separators. Если это не так, то мы прерываем цикл, а указатели будут содержать ссылку на первые символы, не являющимися разделителями. Если мы опять прошли всю строку, значит снова придётся удалять всю строку, следовательно, возвращаем NULL.

char *sp2 = s + strlen(s) - 1; /* last char at src */while(contains_symbol(separators, *sp2)) sp2--;  /* if it contains only symbols from separators => NULL */if(sp2 < s) return NULL;

Наконец, вычисляем длину строки. Если указатели ссылаются на одно и то же место, то в строке был лишь один символ, не являющийся разделителем, а потому размер результата будет равным 1 байту (один лишний байт для нулевого символа учтён в макросе alloc_str). Если же этот единственный символ является нулевым (маркером конца), то возвращаем NULL. Иначе берём разницу между адресами указателями, прибавляем к ней единицу, и получаем длину новой строки. Затем мы просто выделяем память для новой строки и копируем в неё строку, начинающуюся с указателя sp.

Теперь, объединим работу выше написанных функции, в единую функцию get_token().

Код функции get_token дан ниже:

char *get_token(char *src, char *delims, char **next){  if(src == NULL || delims == NULL)    return NULL;  char *delims_p = delims;  /* the end of lexem (points to symbol that follows right after lexem */  char *src_p = trim_separators(src, delims);  /* the begining of the lexem */  char *lex_begin = src_p;  if(src_p == NULL){    *next = NULL;    return NULL;  }    /* flag that indicates reaching of delimeter */  int flag = 0;  while(*src_p != '\0'){    flag = 0;    while(*delims_p != '\0'){      if(*delims_p == *src_p){        flag = 1;        break;      }      delims_p++;    }    if(flag == 1)      break;    delims_p = delims;    src_p++;  }    /* now src_p points to the symbol right after lexem */  /* compute lexem size and reset pointers (from trimmed to the original src) */  char *offset;  size_t tok_size;  offset = (src + strspn(src, delims));  tok_size = (src_p - lex_begin);  free(lex_begin);  lex_begin = offset;  src_p = offset + tok_size;  if(*src_p == '\0')    *next = NULL;  else    *next = src_p;    /* result token */  char *res = alloc_str(tok_size);  memcpy(res, lex_begin, tok_size);  return res;}

В ней используется функция обрезки trim_separators(). Функция обрезки возвращает новую строку, и далее сканирование ведётся по ней. В цикле лишь проверяется, не равен ли текущий символ какому-либо символу разделителю из массива символов delims, и если равен, то выйти из цикла. Указатель src_p проходит по сканируемой строке. После цикла он будет указывать на символ, следующий за лексемой (конец лексемы). А начало лексемы сохраняется в указателе lex_begin, который изначально указывает на начало обрезанной, сканируемой строки. После обнаружения границ лексемы, вычисляется её размер (её число символом), а затем сканируемая строка удаляется из динамической кучи. Затем указатели переустанавливаются на позиции в оригинальной строке (первый аргумент функции get_token()), а часть строки, которая ещё не была разобрана, присваивается в качестве содержимого двойному указателю next. Обратите внимание, что next является ссылкой на другой указатель (в данном случае, на указатель строки). Двойной указатель позволяет менять значение переменной типа char *, записывая новый адрес в next. Для первого вызова данной функции, next должен хранить адрес переменной указателя, которая указывает на строку и хранит адрес первой ячейки строки. Однако, при работе с двойным указателем возможна серьёзная и незаметная ошибка, если в качестве начального значения next передать адрес переменной, которая непосредственно указывает на строку, а не адрес переменной копии, которая содержит копию адреса строки. В следующем разделе подробно описана данная ситуация, и показан пример работы данной функции.

Пример работы get_token().

Ниже дан простой рабочий пример функции get_token(). Оригинальная строка с лексемами хранится в указателе test, копия адреса строки (копия переменной test) хранится в переменной copytest. Указатель tok хранит текущую распознанную лексему, а next - сканируемую часть строки. Данная программа разделяет строку test по пробелу и символу табуляции на подстроки, и выводит их. Также она выводит саму строку test до и после работы функции. Как можно убедиться по выводу, оригинальная строка не меняется.

#include <stdio.h>#include <stdlib.h>#include <string.h>#include "mem.h"#include "str_utils.h"int main(int argc, char **argv){  char *test = "  They have  a    cat.\n \0";  char *copytest = test;  char **next = &copytest; /* has side effect on copytest */  char *tok = NULL;    printf("src:%s\n", test);  printf("copytest:%s\n", copytest);  while(*next != NULL){    tok = get_token(*next, " \t\0", next);    if(tok == NULL)      break;    printf("%s\n", tok);    free(tok);  }  printf("src after:%s\n", test);  printf("copytest after:%s\n", copytest);  return 0;}  

Вывод данной программы:

src:  They have  a    cat.copytest:  They have  a    cat.Theyhaveacat.src after:  They have  a    cat.copytest:(null)

Обратите внимание, что в цикле есть дополнительная проверка на NULL указателя tok. Дело в том, что при получении последнего слова в строке (а именно "cat.\n"), указатель next будет указывать на подстроку, состоящую лишь из одних пробелов (плюс нулевой символ). Функция trim_separators()для таких строк возвращает NULL, так как по логике придётся урезать все символы в строке. В итоге get_token() также вернёт NULL, поскольку уже ничего не осталось для сканирования. Поэтому переменная tok сохранит значение NULL, на последнем шаге.

Теперь снова по поводу двойного указателя next. Как вы могли заметить, в вышеприведённом коде ему передаётся адрес переменной copytest, а не переменной test. Дело в том, что мы можем нечаянно затереть значение переменной test (именно переменной, а не самой строки). Для примера, изменим код следующим образом. Передадим адрес test в указатель next. В итоге мы получим следующий вывод.

src:  They have  a    cat.copytest:  They have  a    cat.Theyhaveacat.src after:(null)copytest:  They have  a    cat.

Как видите, сама строка не меняется, но изменилось значение переменной test. Теперь она содержит NULL, поскольку на последнем шаге, функция присваивает ей соответствующее значение. Отсюда следует, что операции вида:

*next = addr;*next = NULL;

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

Модификация функции get_token(). Экранирование разделителей.

Функция get_token() умеет возвращать новые подстроки (токены) из исходной строки, не меняя её. Однако она совершенно не умеет их экранировать, в случае, когда лексемы представляют собой более сложные объекты. Например, а что если лексема содержит символ, который мы выбрали в качестве разделителя?

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

1233,"John Cenna","Male",4.22,"2004, 2005, 2006",11234,"John Doe","Male",4.24,"2001, 2004, 2007",01235,"Maria Laws","Female",4.23,"2003, 2006, 2008",1

Данные значения формируют следующую таблицу:

Id

Name

Gender

Coefficient

Years

IsActive

1233

John Cenna

Male

4.22

2004, 2005, 2006

yes

1234

John Doe

Male

4.24

2001, 2004, 2007

no

1235

Maira Laws

Female

4.23

2003, 2006, 2008

yes

Заметим, что в данном случае, значения отделяются друг от друга запятой, причём сам разделитель (запятая) не учитывается, когда он находится в кавычках. Это значит, что сам разделитель дополнительно экранирован (escaped) двойными кавычками.

Для таких особых случаев, была определена новая функция get_token_escaped(), которая в качестве дополнительного параметра принимает массив (строку) символов, экранирующих разделители. Символы разделители не должны пересекаться с данным массивом. Т.е. один и тот же символ должен быть либо управляющим, либо разделяющим но не одним и тем же одновременно. Если же это так, то функция будет возвращать NULL. Для контроля начала и конца экранируемой последовательности, была заведена отдельная переменная neflag. Переменная neflag указывает, не является ли текущий символ частью экранируемой последовательности (neflag => not escaped flag). Она равна 0, когда символ должен быть экранируем, и 1, когда символ не экранируется. В цикле сканирования, сначала текущий символ ищется среди экранирующих (escaped). Если он найден и он равен предыдущему управляющему символу, то устанавливаем соответствующий флаг neflag равным 0, так как была найдена пара - начала и конец экранируемой последовательности. Если же это другой экранирующий символ, не равный предыдущему, то если мы уже находимся в экранируемой последовательности, то ничего не надо делать (продолжаем поиск, в надежде, что мы отыщем нужную пару (пред. символ)). Иначе, если мы нашли его впервые, то запоминаем его в переменной esym, и сбрасываем флаг neflag в 0. Символ разделитель будет учтён, если он был обнаружен (flag == 1) и он не является частью экранируемой последовательности (neflag == 1).

Ниже дан код данной процедуры.

char *get_token_escaped(char *src, char *delims, char *escaped, char **next){  if(src == NULL || delims == NULL || escaped == NULL)    return NULL;  char *delims_p = delims;  char *escaped_p = escaped;  /* the end of lexem (points to symbol that follows right after lexem */  char *src_p = trim_separators(src, delims);  /* the begining of the lexem */  char *lex_begin = src_p;  if(src_p == NULL){    *next = NULL;    return NULL;  }    /* check that (delims INTERSECTION escaped) IS NULL. */  /* IF NOT => return NULL */  int err = 0;  while(*delims_p != '\0'){    while(*escaped_p != '\0' && (err = (*escaped_p == *delims_p) ) != 1) escaped_p++;    escaped_p = escaped;    if(err){      return NULL;    }    delims_p++;  }  delims_p = delims;    /* flag that indicates reaching of delimeter */  int flag = 0;  /* flag that indicates that we are not in escaped sequence */  int neflag = 1;  /* previously saved escape character (i.e. the begining of the escaped seq.) */  char *esym = (char)0;  while(*src_p != '\0'){    flag = 0;    while(*escaped_p != '\0'){      if(*src_p == *escaped_p && *src_p == esym){        neflag = 1;        esym = (char)0;        break;      }      else if(*src_p == *escaped_p && neflag){        neflag = 0;        esym = *escaped_p;        break;      }      escaped_p++;    }    while(*delims_p != '\0'){      if(*delims_p == *src_p){        flag = 1;        break;      }      delims_p++;    }    if(flag && neflag)      break;    delims_p = delims;    escaped_p = escaped;    src_p++;  }    /* now src_p points to the symbol right after lexem */  /* compute lexem size and reset pointers (from trimmed to the original src) */  char *offset;  size_t tok_size;  offset = (src + strspn(src, delims));  tok_size = (src_p - lex_begin);  free(lex_begin);  lex_begin = offset;  src_p = offset + tok_size;  if(*src_p == '\0')    *next = NULL;  else    *next = src_p;    /* result token */  char *res = alloc_str(tok_size);  memcpy(res, lex_begin, tok_size);  return res;}

Пример её использования дан ниже:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include "mem.h"#include "str_utils.h"int main(int argc, char **argv){  char *test = "  They have  \"cats dogs mice  \"\n \0";  char *copytest = test;  char **next = &copytest; /* has side effect on copytest */  char *tok = NULL;    printf("src:%s\n", test);  printf("copytest:%s\n", copytest);  while(*next != NULL){    tok = get_token_escaped(*next, " \t\0", "\"\0", next);    if(tok == NULL)      break;    printf("%s\n", tok);    free(tok);  }  printf("src after:%s\n", test);  printf("copytest after:%s\n", copytest);  return 0;}

Вывод:

src:  They have  "cats dogs mice  "copytest:  They have  "cats dogs mice  "Theyhave"cats dogs mice  "src after:  They have  "cats dogs mice  "copytest after:(null)

Заключение

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

Функцию get_token_escaped() можно использовать при работе с CSV файлами. Однако ей должны передаваться непересекающиеся множества символов разделителей, и экранирующих символов. Иначе будет неоднозначность между ними. Функция не умеет пока анализировать такие неоднозначности, поэтому в таких случаях она вернёт NULL. Кроме того, она не допускает вложенных экранируемых последовательностей. Т.е. если был встречен экранируемый символ, всё что следует за ним, включительно до его клона (такого же символа, но в следующей позиции), считается как одна экранируемая последовательность.

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

Подробнее..

Пример полезного комментария

22.03.2021 16:16:43 | Автор: admin

Пример полезного комментария


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


Код, который мы рассмотрим далее, был выписан в процессе работы над статьёй "Обработка дат притягивает ошибки или 77 дефектов в Qt 6".


Анализатор PVS-Studio обратил внимание на этот фрагмент кода, выдав предупреждение: V575 [CWE-628] The 'memcpy' function doesn't copy the whole string. Use 'strcpy / strcpy_s' function to preserve terminal null. qplaintestlogger.cpp 253. Собственно, вот он:


const char *msgFiller = msg[0] ? " " : "";QTestCharBuffer testIdentifier;QTestPrivate::generateTestIdentifier(&testIdentifier);QTest::qt_asprintf(&messagePrefix, "%s: %s%s%s%s\n",                   type, testIdentifier.data(), msgFiller, msg,                   failureLocation.data());// In colored mode, printf above stripped our nonprintable control characters.// Put them back.memcpy(messagePrefix.data(), type, strlen(type));outputMessage(messagePrefix.data());

Обратите внимание на вызов функции memcpy. Сам по себе этот код вызывает сразу два вопроса:


  1. Зачем что-то записывается в буфер, содержимое которого было только что сформировано с помощью printf-подобной функции?
  2. Точно не ошибка, что не копируется терминальный ноль? Это как раз и не нравится анализатору.

К счастью, комментарий сразу всё проясняет. Нужно восстановить некие непечатаемые символы.


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


Для сравнения рассмотрим другой фрагмент кода из этого же файла:


char buf[1024];if (result.setByMacro) {  qsnprintf(buf, sizeof(buf), "%s%s%s%s%s%s\n", buf1, bufTag, fill,            buf2, buf2_, buf3);} else {  qsnprintf(buf, sizeof(buf), "%s%s%s%s\n", buf1, bufTag, fill, buf2);}memcpy(buf, bmtag, strlen(bmtag));outputMessage(buf);

Здесь забыли сделать аналогичный комментарий. И картина радикально меняется. Этот код способен ввести в замешательство нового члена команды, который будет его сопровождать или модифицировать. Совершенно не понятно, зачем нужен этот memcpy. Более того, непонятно, почему в начало строки печаталось содержимое некоего буфера buf1, а затем в начало строки помещается содержимое буфера bmtag. Как много вопросов, как мало ответов. Не стоит писать такой код.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. One Useful Comment.

Подробнее..

Макросы в С и С

14.03.2021 16:17:43 | Автор: admin

Макросы - один из моих самых любимых инструментов в языках С и С++. Умные люди и умные книжки советуют по максимуму избегать использования макросов, по возможности заменяя их шаблонами, константами и inline-функциями, и на то есть веские основания. С помощью макросов можно создавать не только изящный код, но и плодить не менее изящные баги, которые потом будет очень сложно отловить и пофиксить. Но если соблюдать ряд несложных правил при работе с макросами, они становятся мощным оружием, которое не стреляет по твоим собственным коленям. Но сперва давай разберемся, что вообще такое макросы в С и С++?

Что есть макросы?

В языках С и С++ есть такой механизм, как препроцессор. Он обрабатывает исходный код программы ДО того, как она будет скомпилированна. У перпроцессора есть свои директивы, такие как #include, #pragma, #if и тд. Но нам интересна только директива #define.

В языке Си довольно распространенной практикой является объявление глобальных констант с помощью директивы #define:

#define PI 3.14159

А потом, на этапе препроцессинга все использования PI будут заменены указанным значением:

double area = 2 * PI * r * r;

После препроцессинга, который по сути является банальной подстановкой, это выражение превратится в:

double area = 2 * 3.14159 * r * r;

PI - макрос, в самом простом его исполнении. Естественно, макросы в таком виде не работают как переменные. Им нельзя присваивать новое значение или использовать их адрес.

// Так нельзя:PI = 3; // после препроцессинга: 3.14159 = 3int *x = &PI;    // после препроцессинга: int *x = &3.14159

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

#undef PI

После этой строчки обращаться к PI будет уже нельзя.

Макросы с параметрами

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

#define MAX(a, b) a >= b ? a : b

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

#define SWAP(type, a, b) type tmp = a; a = b; b = tmp;

Поскольку мы первым параметром передаем тип, данный макрос будет работать с переменными любого типа:

SWAP(int, num1, num2)SWAP(float, num1, num2)

Макросы так же можно записывать в несколько строк, но тогда каждая строка, кроме последней, должна заканчиваться символом '\':

#define SWAP(type, a, b) type tmp = a; \ a = b; \b = tmp;

Параметр макроса можно превратить в строку, добавив перед ним знак '#':

#define PRINT_VAL(val) printf("Value of %s is %d" #val, val);int = 5;PRINT_VAL(x)  // -> Value of x is 5

А еще параметр можно приклеить к чему-то еще, чтобы получился новый идентификатор. Для этого между параметром и тем, с чем пы его склеиваем, нужно поставить '##':

#define PRINT_VAL (number) printf("%d", value_##number);int value_one = 10, value_two = 20;PRINT_VAL(one)  // -> 10PRINT_VAL(two)  // -> 20

Техника безопасности при работе с макросами

Есть несколько основных правил, которые нужно соблюдать при работе с макросами.

1. Параметрами макросов не должны быть выражения и вызовы функций.

Ранее я уже объявлял макрос MAX. Но что получится, если попытаться вызвать его вот так:

int x = 1, y = 5;int max = MAX(++x, --y);

Со стороны все выглядит нормально, но вот что получится в результате макроподстановки:

int max = ++x >= --y ? ++x : --y;

В итоге переменная max будет равна не 4, как мы ожидали, а 3. Потом можно уйму времени потратить, отлавливая эту ошибку. Так что в качестве аргумента макроса нужно всегда передавать уже конечное значение, а не какое-то выражение или вызов функции. Иначе выражение или функция будут вычислены столько раз, сколько используется этот параметр в теле макроса.

2. Все аргументы макроса и сам макрос должны быть заключены в скобки.

Это правило я уже нарушил при написании макроса MAX. Что получится, если мы захотим использовать этот макрос в составе какого-то математического выражения?

int result = 5 + MAX(1, 4);

По логике, переменная result должна будет иметь значение 9, однако вот что мы получаем в результате макроподстановки:

int result = 5 + 1 > 4 ? 1 : 4;

И переменная result внезапно примет значение 1. Чтобы такого не происходило, макрос MAX должен быть объявлен следующим образом:

#define MAX(a, b) ((a) >= (b) ? (a) : (b))

В таком случае все действия произойдут в нужном порядке.

3. Многострочные макросы должны иметь свою область видимости.

Например у нас есть макрос, который вызывает две функции:

#define MACRO() doSomething(); \ doSomethinElse();

А теперь попробуем использовать этот макрос в таком контексте:

if (some_condition) MACRO()

После макроподстановки мы увидим вот такую картину:

if (some_condition) doSomething();doSomethinElse();

Нетрудно заметить, что под действие if попадет только первая функция, а вторая будет вызываться всегда. Именно для того, чтобы избежать подобных багов, у макросов должна быть объявлена своя область видимости. Для удобства в этих целях принято использовать цикл do {} while (0); .

#define MACRO() do { \ doSomething(); \           doSomethinElse(); \         } while(0)

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

Еще немного примеров

В языке Си при помощи макросов можно эффективно избавляться от дублирования кода. Банальный пример - объявим несколько функций сложения для работы с разными типами данных:

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

DEF_SUM(int)DEF_SUM(float)DEF_SUM(double)int main() {     sum_int(1, 2);  sum_float(2.4, 6,3); sum_double(1.43434, 2,546656);}

Таким образом у нас получился аналог шаблонов из С++. Но стоит сразу обратить внимание, что данный способ не подойдет для типов, название которых состоит более чем из одного слова, например long long или unsigned short, потому что не получится нормально склеить название функции (sum_##type). Для этого сперва придется объявить для них новый тип, состоящий из одного слова.

Но все же, если есть возможность обходится без макросов, лучше обходится без макросов. Даже если ты прекрасно усвоил, как они работают, и понимаешь их силу, это никак не страхует тебя от того, что ты допустишь какую-то глупую ошибку при объявлении макроса, а потом 20 минут потратишь на ее поиски. Так что не стоит оборачивать в макросы вообще все, что только можно.

Мой блог в Телеграме

Подробнее..
Категории: C++ , С++ , C , Си , Macro , Макросы , Define

Часть 3. MPI Как процессы обшаются? Сообщения типа точка-точка

27.03.2021 22:10:41 | Автор: admin

В этом цикле статей речь идет о параллельном программировании с использованием MPI.

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


Предисловие

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

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

Работает это так: один из процессов, назовем его P1, какого то коммуникатора C должен указать явный номер процесса P2, который также должен быть под коммуникатором С, и с помощью одной из процедур передать ему данные D, но на самом деле не обязательно нужно знать номер процесса, но это мы обсудим далее.

Процедуры в свою очередь разделены на 2 типа: с блокировкой и без блокировки.
1. Процедуры обмена с блокировкой просто останавливают процесс который должен принять сообщение до момента выполнения какого-либо условия.
2. Процедуры без блокировки, как их ещё называют асинхронные, они возвращаются сразу после осуществления коммуникации, немедленно.

Оба типа процедур мы затронем подробно, а пока начнем с процедур с блокировкой.

Передаем и принимаем сообщения

Наконец приступим к практической части. Для передачи сообщений используется процедура MPI_Send. Эта процедура осуществляет передачу сообщения с блокировкой. Синтаксис у нее следующий:

int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest,  int msgtag, MPI_Comm comm);

Что тут есть что:
buf - ссылка на адрес по которому лежат данные, которые мы пересылаем. В случае массивов ссылка на первый элемент.
count - количество элементов в этом массиве, если отправляем просто переменную, то пишем 1.
datatype - тут уже чутка посложнее, у MPI есть свои переопределенные типы данных которые существуют в С++. Их таблицу я приведу чуть дальше.
dest - номер процесса кому отправляем сообщения.
msgtag - ID сообщения (любое целое число)
comm - Коммуникатор в котором находится процесс которому мы отправляем сообщение.

А вот как называются основные стандартные типы данных С++ определенные в MPI_Datatype:

Название в MPI

Тип даных в С++

MPI_CHAR

char

MPI_SHORT

signed short

MPI_INT

signed int

MPI_LONG

signed long int

MPI_LONG_LONG

signed long long int

MPI_UNSIGNED_*** (Вместо *** int и т.п.)

unsigned ...

MPI_FLOAT

float

MPI_DOUBLE

double

MPI_LONG_DOUBLE

long double

MPI_INT8_T

int8_t

MPI_INT16_T

int16_t

MPI_C_COMPLEX

float _Complex

Аналогичным способом указываются и другие типы данных определенные в стандартной библиотеке С/С++ - MPI_[Через _ в верхнем регистре пишем тип так как он назван в С]. Еще один пример для закрепления понимания, есть тип беззнаковых 32 битных целых чисел, назван он uint32_t, чтобы получить этот тип данных переопределенным в MPI необходимо написать следующую конструкцию: MPI_UINT32_T. То есть все вполне логично и легко, верхний регистр, вместо пробелов знаки андерскора и в начале пишем MPI.

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

Теперь поговорим о приеме этих сообщений. Для этого в MPI определена процедура MPI_Recv. Она осуществляет, соответственно, блокирующий прием данных. Синтаксис выглядит вот так:

int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status* status);

Что тут есть что:
buf - ссылка на адрес по которому будут сохранены передаваемые данные.
count - максимальное количество принимаемых элементов.
datatype - тип данных переопределенный в MPI(по аналогии с Send).
source - номер процесса который отправил сообщение.
tag - ID сообщения которое мы принимаем (любое целое число)
comm - Коммуникатор в котором находится процесс от которого получаем сообщение.
status - структура, определенная в MPI которая хранит информацию о пересылке и статус ее завершения.

Тут все практически идентично процедуре Send, только появился аргумент статуса пересылки. Зачем он нужен? Не всегда нужно явно указывать от какого процесса приходит сообщение, какой тег сообщения мы принимаем, чтобы избавиться от неопределенности MPI сохраняет информацию которая не указана в процессе-преемнике явно и мы можем к ней обратиться. Например чтобы узнать процесс который отправил сообщение и тэг этого сообщения:

MPI_Status status;MPI_Recv(&buffer, 1, MPI_Float, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status)tag = status.MPI_SOURCEsource = status.MPI_SOURCE

Здесь представлен кусочек возможного кода в процессе который принимает данные не более одного float элемента от любого процесса с любым тегом сообщения. Чтобы узнать какой процесс прислал это сообщение и с каким тэгом нужно собственно воспользоваться структурой MPI_Status.

Заметим появление констант MPI_ANY_SOURCE и MPI_ANY_TAG, они явно указывают, что можно принимать сообщения от любого процесса с любым тэгом.

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

#include <stdio.h>#include <time.h>#include "mpi.h"#define RETURN return 0#define FIRST_THREAD 0int* get_interval(int, int, int*);inline void print_simple_range(int, int);void wait(int);int main(int argc, char **argv){// инициализируем необходимые переменныеint thread, thread_size, processor_name_length;int* thread_range, interval;double cpu_time_start, cpu_time_fini;char* processor_name = new char[MPI_MAX_PROCESSOR_NAME * sizeof(char)];MPI_Status status;interval = new int[2];// Инициализируем работу MPIMPI_Init(&argc, &argv);// Получаем имя физического процессораMPI_Get_processor_name(processor_name, &processor_name_length);// Получаем номер конкретного процесса на котором запущена программаMPI_Comm_rank(MPI_COMM_WORLD, &thread);// Получаем количество запущенных процессовMPI_Comm_size(MPI_COMM_WORLD, &thread_size);// Если это первый процесс, то выполняем следующий участок кодаif(thread == FIRST_THREAD){// Выводим информацию о запускеprintf("----- Programm information -----\n");printf(">>> Processor: %s\n", processor_name);printf(">>> Num threads: %d\n", thread_size);printf(">>> Input the interval: ");// Просим пользователья ввести интервал на котором будут вычисленияscanf("%d %d", &interval[0], &interval[1]);// Каждому процессу отправляем полученный интервал с тегом сообщения 0. for (int to_thread = 1; to_thread < thread_size; to_thread++)       MPI_Send(&interval, 2, MPI_INT, to_thread, 0, MPI_COMM_WORLD);// Начинаем считать время выполненияcpu_time_start = MPI_Wtime();}// Если процесс не первый, тогда ожидаем получения данныхelse     MPI_Recv(&interval, 2, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);// Все процессы запрашивают свой интервалrange = get_interval(thread, thread_size, interval);// После чего отправляют полученный интервал в функцию которая производит вычисленияprint_simple_range(range[0], range[1]);// Последний процесс фиксирует время завершения, ожидает 1 секунду и выводит результатif(thread == thread_size - 1){cpu_time_fini = MPI_Wtime();wait(1);printf("CPU Time: %lf ms\n", (cpu_time_fini - cpu_time_start) * 1000);}MPI_Finalize();RETURN;}int* get_interval(int proc, int size, int interval){// Функция для рассчета интервала каждого процессаint* range = new int[2];int interval_size = (interval[1] - interval[0]) / size;range[0] = interval[0] + interval_size * proc;range[1] = interval[0] + interval_size * (proc + 1);range[1] = range[1] == interval[1] - 1 ? interval[1] : range[1];return range;}inline void print_simple_range(int ibeg, int iend){// Прострейшая реализация определения простого числаbool res;for(int i = ibeg; i <= iend; i++){res = true;while(res){res = false;for(int j = 2; j < i; j++) if(i % j == 0) res = true;if(res) break;}res = not res;if(res) printf("Simple value ---> %d\n", i);}}void wait(int seconds) { // Функция ожидающая в течение seconds секундclock_t endwait;endwait = clock () + seconds * CLOCKS_PER_SEC ;while (clock() < endwait) {};}

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

Делаем прием данных более гибким

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

Первая процедура которую мы обсудим следующая:

int MPI_Get_count(MPI_Status* status, MPI_Datatype datatype, int* count);

По структуре status процедура определяет сколько данных типа datatype передано соответствующим сообщением и записывает результат по адресу count.

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

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

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

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

int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status* status);

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

Теперь на очень простом примере соединим эти процедуры вместе:

#include <iostream>#include "mpi.h"using namespace std;void show_arr(int* arr, int size){for(int i=0; i < size; i++) cout << arr[i] << " ";cout << endl;}int main(int argc, char **argv){int size, rank;MPI_Init(&argc, &argv);MPI_Comm_size(MPI_COMM_WORLD, &size);MPI_Comm_rank(MPI_COMM_WORLD, &rank);if(rank == 0){int* arr = new int[size];for(int i=0; i < size; i++) arr[i] = i;for(int i=1; i < size; i++) MPI_Send(arr, i, MPI_INT, i, 5, MPI_COMM_WORLD);}else{int count;MPI_Status status;MPI_Probe(MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);MPI_Get_count(&status, MPI_INT, &count);int* buf = new int[count];MPI_Recv(buf, count, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);cout << "Process:" << rank << " || Count: " << count << " || Array: ";show_arr(buf, count);}MPI_Finalize();return 0;}

Что тут происходит?
В данной программе первый процесс создает массив размером равным количеству процессов и заполняет его номерами процессов по очереди. Потом соответствующему процессу он отправляет такое число элементов этого массива, какой номер у этого процесса. Напрмер: процесс 1 получит 1 элемент, процесс 2 получит 2 элемента этого массива и так далее.

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

Process:1 || Count: 1 || Array: 0 Process:2 || Count: 2 || Array: 0 1 Process:4 || Count: 4 || Array: 0 1 2 3 Process:3 || Count: 3 || Array: 0 1 2 

Собственно 4 результата потому что нулевой процесс занимается отправкой этих сообщений.

И еще несколько типов процедур посылки

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

Для того чтобы повысить степень определенности существуют еще несколько процедур которые никак не отличаются по синтаксису от MPI_Send, но отличаются по типу взаимодействия процессов и в способе отправки.

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

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

И еще одна процедура - MPI_Rsend. Она осуществляет передачу сообщения по готовности. Такой процедурой нужно пользоваться аккуратно, так как она требует чтобы процесс-получатель мыл уже готов принять это сообщение. Для того чтобы она выполнилась корректно необходимо заранее позаботиться о синхронизации процессов, либо явно знать, что этот процесс будет находиться в стадии ожидании получения сообщения к моменту вызова Rsend.


Резюме

Ну вот мы и ознакомились(а для опытных освежили в памяти) основные процедуры передачи сообщений типа точка-точка с блокировкой. В следующей статье я постараюсь показать на практике все изложенные ранее принципы и объяснить как написать программу которая будет выяснять одни из основополагающих характеристик техники используемой при параллелизации вычислений - латнетность и пропускная способность между процессами. А пока вот краткая сводка того что я здесь изложил:

Процедура/Константа/Структура

Назначение

MPI_Send

Отправка сообщения

MPI_Recv

Прием сообщения

MPI_Status

Структура статуса сообщения

MPI_ANY_SOURCE

Константа "Любому процессу"

MPI_ANY_TAG

Константа "Любой тег сообщения"

MPI_Get_count

Получить количество данных по статусу

MPI_Get_elements

Получить количество базовых элементов по статусу

MPI_Probe

Получить данные о сообщении без его приема

MPI_PROC_NULL

Константа-идентификатор не существующего процесса

MPI_Ssend

Отправка сообщения которая осуществляет синхронизацию процессов

MPI_Bsend

Отправка сообщения которая осуществляет буферизацию

MPI_Rsend

Отправка сообщения по готовности. Требует инициализации приема у процесса-получателя.

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

Подробнее..

Часть 3. MPI Как процессы общаются? Сообщения типа точка-точка

28.03.2021 00:13:26 | Автор: admin

В этом цикле статей речь идет о параллельном программировании с использованием MPI.

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


Предисловие

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

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

Работает это так: один из процессов, назовем его P1, какого-то коммуникатора C должен указать явный номер процесса P2, который также должен быть под коммуникатором С, и с помощью одной из процедур передать ему данные D, но на самом деле не обязательно нужно знать номер процесса, но это мы обсудим далее.

Процедуры в свою очередь разделены на 2 типа: с блокировкой и без блокировки.
1. Процедуры обмена с блокировкой просто останавливают процесс который должен принять сообщение до момента выполнения какого-либо условия.
2. Процедуры без блокировки, как их ещё называют асинхронные, они возвращаются сразу после осуществления коммуникации, немедленно.

Оба типа процедур мы затронем подробно, а пока начнем с процедур с блокировкой.

Передаем и принимаем сообщения

Наконец приступим к практической части. Для передачи сообщений используется процедура MPI_Send. Эта процедура осуществляет передачу сообщения с блокировкой. Синтаксис у нее следующий:

int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest,  int msgtag, MPI_Comm comm);

Что тут есть что:
buf - ссылка на адрес по которому лежат данные, которые мы пересылаем. В случае массивов ссылка на первый элемент.
count - количество элементов в этом массиве, если отправляем просто переменную, то пишем 1.
datatype - тут уже чутка посложнее, у MPI есть свои переопределенные типы данных которые существуют в С++. Их таблицу я приведу чуть дальше.
dest - номер процесса кому отправляем сообщения.
msgtag - ID сообщения (любое целое число)
comm - Коммуникатор в котором находится процесс которому мы отправляем сообщение.

А вот как называются основные стандартные типы данных С++ определенные в MPI_Datatype:

Название в MPI

Тип даных в С++

MPI_CHAR

char

MPI_SHORT

signed short

MPI_INT

signed int

MPI_LONG

signed long int

MPI_LONG_LONG

signed long long int

MPI_UNSIGNED_*** (Вместо *** int и т.п.)

unsigned ...

MPI_FLOAT

float

MPI_DOUBLE

double

MPI_LONG_DOUBLE

long double

MPI_INT8_T

int8_t

MPI_INT16_T

int16_t

MPI_C_COMPLEX

float _Complex

Аналогичным способом указываются и другие типы данных определенные в стандартной библиотеке С/С++ - MPI_[Через _ в верхнем регистре пишем тип так как он назван в С]. Еще один пример для закрепления понимания, есть тип беззнаковых 32 битных целых чисел, назван он uint32_t, чтобы получить этот тип данных переопределенным в MPI необходимо написать следующую конструкцию: MPI_UINT32_T. То есть все вполне логично и легко, верхний регистр, вместо пробелов знаки андерскора и в начале пишем MPI.

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

Теперь поговорим о приеме этих сообщений. Для этого в MPI определена процедура MPI_Recv. Она осуществляет, соответственно, блокирующий прием данных. Синтаксис выглядит вот так:

int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status* status);

Что тут есть что:
buf - ссылка на адрес по которому будут сохранены передаваемые данные.
count - максимальное количество принимаемых элементов.
datatype - тип данных переопределенный в MPI(по аналогии с Send).
source - номер процесса который отправил сообщение.
tag - ID сообщения которое мы принимаем (любое целое число)
comm - Коммуникатор в котором находится процесс от которого получаем сообщение.
status - структура, определенная в MPI которая хранит информацию о пересылке и статус ее завершения.

Тут все практически идентично процедуре Send, только появился аргумент статуса пересылки. Зачем он нужен? Не всегда нужно явно указывать от какого процесса приходит сообщение, какой тег сообщения мы принимаем, чтобы избавиться от неопределенности MPI сохраняет информацию которая не указана в процессе-преемнике явно и мы можем к ней обратиться. Например чтобы узнать процесс который отправил сообщение и тэг этого сообщения:

MPI_Status status;MPI_Recv(&buffer, 1, MPI_Float, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status)tag = status.MPI_SOURCEsource = status.MPI_SOURCE

Здесь представлен кусочек возможного кода в процессе который принимает данные не более одного float элемента от любого процесса с любым тегом сообщения. Чтобы узнать какой процесс прислал это сообщение и с каким тэгом нужно собственно воспользоваться структурой MPI_Status.

Заметим появление констант MPI_ANY_SOURCE и MPI_ANY_TAG, они явно указывают, что можно принимать сообщения от любого процесса с любым тэгом.

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

#include <stdio.h>#include <time.h>#include "mpi.h"#define RETURN return 0#define FIRST_THREAD 0int* get_interval(int, int, int*);inline void print_simple_range(int, int);void wait(int);int main(int argc, char **argv){// инициализируем необходимые переменныеint thread, thread_size, processor_name_length;int* thread_range, interval;double cpu_time_start, cpu_time_fini;char* processor_name = new char[MPI_MAX_PROCESSOR_NAME * sizeof(char)];MPI_Status status;interval = new int[2];// Инициализируем работу MPIMPI_Init(&argc, &argv);// Получаем имя физического процессораMPI_Get_processor_name(processor_name, &processor_name_length);// Получаем номер конкретного процесса на котором запущена программаMPI_Comm_rank(MPI_COMM_WORLD, &thread);// Получаем количество запущенных процессовMPI_Comm_size(MPI_COMM_WORLD, &thread_size);// Если это первый процесс, то выполняем следующий участок кодаif(thread == FIRST_THREAD){// Выводим информацию о запускеprintf("----- Programm information -----\n");printf(">>> Processor: %s\n", processor_name);printf(">>> Num threads: %d\n", thread_size);printf(">>> Input the interval: ");// Просим пользователья ввести интервал на котором будут вычисленияscanf("%d %d", &interval[0], &interval[1]);// Каждому процессу отправляем полученный интервал с тегом сообщения 0. for (int to_thread = 1; to_thread < thread_size; to_thread++)       MPI_Send(&interval, 2, MPI_INT, to_thread, 0, MPI_COMM_WORLD);// Начинаем считать время выполненияcpu_time_start = MPI_Wtime();}// Если процесс не первый, тогда ожидаем получения данныхelse     MPI_Recv(&interval, 2, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);// Все процессы запрашивают свой интервалrange = get_interval(thread, thread_size, interval);// После чего отправляют полученный интервал в функцию которая производит вычисленияprint_simple_range(range[0], range[1]);// Последний процесс фиксирует время завершения, ожидает 1 секунду и выводит результатif(thread == thread_size - 1){cpu_time_fini = MPI_Wtime();wait(1);printf("CPU Time: %lf ms\n", (cpu_time_fini - cpu_time_start) * 1000);}MPI_Finalize();RETURN;}int* get_interval(int proc, int size, int interval){// Функция для рассчета интервала каждого процессаint* range = new int[2];int interval_size = (interval[1] - interval[0]) / size;range[0] = interval[0] + interval_size * proc;range[1] = interval[0] + interval_size * (proc + 1);range[1] = range[1] == interval[1] - 1 ? interval[1] : range[1];return range;}inline void print_simple_range(int ibeg, int iend){// Прострейшая реализация определения простого числаbool res;for(int i = ibeg; i <= iend; i++){res = true;while(res){res = false;for(int j = 2; j < i; j++) if(i % j == 0) res = true;if(res) break;}res = not res;if(res) printf("Simple value ---> %d\n", i);}}void wait(int seconds) { // Функция ожидающая в течение seconds секундclock_t endwait;endwait = clock () + seconds * CLOCKS_PER_SEC ;while (clock() < endwait) {};}

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

Делаем прием данных более гибким

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

Первая процедура которую мы обсудим следующая:

int MPI_Get_count(MPI_Status* status, MPI_Datatype datatype, int* count);

По структуре status процедура определяет сколько данных типа datatype передано соответствующим сообщением и записывает результат по адресу count.

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

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

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

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

int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status* status);

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

Теперь на очень простом примере соединим эти процедуры вместе:

#include <iostream>#include "mpi.h"using namespace std;void show_arr(int* arr, int size){for(int i=0; i < size; i++) cout << arr[i] << " ";cout << endl;}int main(int argc, char **argv){int size, rank;MPI_Init(&argc, &argv);MPI_Comm_size(MPI_COMM_WORLD, &size);MPI_Comm_rank(MPI_COMM_WORLD, &rank);if(rank == 0){int* arr = new int[size];for(int i=0; i < size; i++) arr[i] = i;for(int i=1; i < size; i++) MPI_Send(arr, i, MPI_INT, i, 5, MPI_COMM_WORLD);}else{int count;MPI_Status status;MPI_Probe(MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);MPI_Get_count(&status, MPI_INT, &count);int* buf = new int[count];MPI_Recv(buf, count, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);cout << "Process:" << rank << " || Count: " << count << " || Array: ";show_arr(buf, count);}MPI_Finalize();return 0;}

Что тут происходит?
В данной программе первый процесс создает массив размером равным количеству процессов и заполняет его номерами процессов по очереди. Потом соответствующему процессу он отправляет такое число элементов этого массива, какой номер у этого процесса. Напрмер: процесс 1 получит 1 элемент, процесс 2 получит 2 элемента этого массива и так далее.

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

Process:1 || Count: 1 || Array: 0 Process:2 || Count: 2 || Array: 0 1 Process:4 || Count: 4 || Array: 0 1 2 3 Process:3 || Count: 3 || Array: 0 1 2 

Собственно 4 результата потому что нулевой процесс занимается отправкой этих сообщений.

И еще несколько типов процедур посылки

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

Для того чтобы повысить степень определенности существуют еще несколько процедур которые никак не отличаются по синтаксису от MPI_Send, но отличаются по типу взаимодействия процессов и в способе отправки.

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

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

И еще одна процедура - MPI_Rsend. Она осуществляет передачу сообщения по готовности. Такой процедурой нужно пользоваться аккуратно, так как она требует чтобы процесс-получатель мыл уже готов принять это сообщение. Для того чтобы она выполнилась корректно необходимо заранее позаботиться о синхронизации процессов, либо явно знать, что этот процесс будет находиться в стадии ожидании получения сообщения к моменту вызова Rsend.


Резюме

Ну вот мы и ознакомились(а для опытных освежили в памяти) основные процедуры передачи сообщений типа точка-точка с блокировкой. В следующей статье я постараюсь показать на практике все изложенные ранее принципы и объяснить как написать программу которая будет выяснять одни из основополагающих характеристик техники используемой при параллелизации вычислений - латнетность и пропускная способность между процессами. А пока вот краткая сводка того что я здесь изложил:

Процедура/Константа/Структура

Назначение

MPI_Send

Отправка сообщения

MPI_Recv

Прием сообщения

MPI_Status

Структура статуса сообщения

MPI_ANY_SOURCE

Константа "Любому процессу"

MPI_ANY_TAG

Константа "Любой тег сообщения"

MPI_Get_count

Получить количество данных по статусу

MPI_Get_elements

Получить количество базовых элементов по статусу

MPI_Probe

Получить данные о сообщении без его приема

MPI_PROC_NULL

Константа-идентификатор не существующего процесса

MPI_Ssend

Отправка сообщения которая осуществляет синхронизацию процессов

MPI_Bsend

Отправка сообщения которая осуществляет буферизацию

MPI_Rsend

Отправка сообщения по готовности. Требует инициализации приема у процесса-получателя.

На этом пока все, приятного отдыха, хабравчане.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru