Главная » Правописание слов » Как написать свой printf

Слово Как написать свой printf - однокоренные слова и морфемный разбор слова (приставка, корень, суффикс, окончание):


Морфемный разбор слова:

Однокоренные слова к слову:

Самый правильный безопасный printf

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

История

Еще в далекий 2009 год в интернете появились мифы о грядущих пользовательских литералах, которые разрешат делать абсолютно все, а именно парсить строку во время компиляции. (Кстати, спасибо ikalnitsky за апетит — рекомендую посмотреть перед прочтением.) Имеется ввиду тот их вариант, что с шаблоном. Но не тут то было. Реализация с шаблоном разрешена только для цифровых литералов. А это значит, что во время компиляции таким образом можно парсить только цифры.

Завязка. Первые шаги на пути решения

Тут я и расстроился. Но погуглив, узнал что можно и без шаблонов парсить строку во время компиляции.

Итак, часть решения задачи безопасного printf.

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

Но мой gcc-4.7 не хочет это кушать! Я вдруг решил расстроиться еще раз, но пришло озарение. Для продвижения дальше нам необходимо понять constexpr. Ниже, я думаю, наиболее интересная часть статьи.

Кульминация. Понимание constexpr

Что было раньше? Раньше были этап компиляции и этап выполнения, нужно также заметить (хотя и все это знают), что типизация происходит на этапе компиляции.
Что есть сейчас? Сейчас есть constexpr, который разрешает выполнять функции на этапе компиляции — какой-то каламбур выходит. Нам нужно ввести уточняющие определения: будем рассматривать не просто компиляцию и выполнение, а компиляцию и выполнение конкретных частей программы (в нашем случае функций, потому как еще можно и объекты во время компиляции использовать). Например «компиляция функции f», «время выполнения функции f», «компиляция всего проекта», «время выполнения проекта».
То есть теперь этап компиляции всего проекта разбился на компиляции и выполнения различных единиц проекта. Рассмотрим пример

Сразу скажу, что оно компилится но ничего полезного не делает. Рассмотрим ближе процесс компиляции функции main(). Сначала переменной i0 присваивается значение, далее эта переменная используется для вычисления значения переменной i1, но для того чтобы его вычислить нам нужно выполнить функцию f (i0), но для этого нужно ее скомпилировать, а для компиляции ей нужно значение i0. Аналогично с f (i1). То есть мы имеем следующее: Процесс компиляции функции main() содержит в себе последовательною компиляцию функции f (int), затем ее выполнение, затем компиляцию функции f (int), и, соответственно, ее выполнение.
Что же получается? Функция обозначенная как constexpr ведет себя как самая обычная функция. Посмотрим на функцию f: N известно на этапе ее компиляции, а n — на этапе ее выполнения.

Развязка. Реализация безопасного printf

Вот почему это не хотелось компилироваться!

static_assert разрешается на этапе компиляции функции safe_printf, а format будет известен только во время ее выполнения (даже если для чего-то другого в этот момент буде этап компиляции).
И как же это обойти? А ни как, или вставить символы формата в параметры шаблона, чтоб они были видны на этапе компиляции (а как мы помним применение пользовательских литералов не дает возможности это сделать) или вспомнить о том, что когда все супер крутые, могучие и непобедимые средства С++ (и даже С++11) становятся беспомощными, на сцену выходят макросы!

Развязка — что случилось на самом деле или впихнуть невпихиваемое

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

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

Но длина литерала и длина pack‘а должны совпадать, а поскольку pack можно ввести только снаружи, то и литерал должен быть передан снаружи, а если он передается снаружи (но еще НЕТ механизма засунуть его в шаблон как параметр), то он передается как простой аргумент функции, и поэтому не известен на этапе компиляции функции в которой он должен завернуться в шаблон, поскольку шаблоны — это типы, а типы — это компиляция — короче, так нельзя.
Но вспомним снова о макросах. Можно попросить boost::preprocessor сгенерировать список номеров. Конечно же их количество будет статическое, а изменить его можно будет только на этапе препроцессинга. Еще нужно предусмотреть что взятие элемента по индексу у литерала на этапе компиляции контролируется, по-этому нужно предусмотреть какой-то защитный механизм, и, также, нужно будет почистить конец строки. А еще нужно как то проверять все ли строка захватилась, т.е. не ввел ли программист слишком длинный литерал. Ниже код.

Кстати, очень для меня было интересно посмотреть в boost::preprocessor — я не представлял себе что такое им можно делать (как, например, арифметические операции). Так что макросы действительно страшная сила.

Невошедшие кадры. Реабилитация пользовательских литералов

Пришло время показать за что я все же начал их (литералы) уважать. Когда-то очень давно, около двух лет назад, я узнал о кортежах. Очень уж удобными они мне показались, НО кортежи эти были из Питона, Немерла и Хаскеля. А когда я узнал о кортежах из С++, меня очень расстроил std::get (tuple) — фу как громоздко, подумал я, и с тех пор хотел разработать механизм для получения элемента, но через оператор квадратных скобок. И вот тут вот на помощь пришли пользовательские литералы.

UPDATE: Перенес топик в «Ненормальное программирование», думаю тут ему будет уютней.

Источник

Printf Oriented Programming

Intro

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

Приступим?

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

Функции printf()-like работают примерно следующим образом:

Что мы можем с этим сделать? Давайте соберем наш код и запустим. Здесь и далее работать будем с x86-32.

Интересно, откуда же взялось 47? Мы ведь просили вывести «%d». На самом деле функция была написанна на C. Так как перегрузки операторов там нет, то и не знает, сколько ей аргументов было подано, поэтому ориентируется она на первый аргумент, который парсит строку и с каждым % забирает очередной аргумент со стека.

Немного поигравшись можно получить заветный ключ.

Почему именно 6 %d?

Давайте посмотрим на дизасемблированный листинг функции f с помощью objdump:

По адресу 0x80484d0 хранится наш ключ и записывается он в стек по адресу ebp-0xc. Наш первый аргумент лежит по адресу ebp+0x8.

По инструкции sub esp,0x** выделяется нужное место на стеке. Причем выделяется явно много лишнего. Это выравнивание данных(padding) и делается это автоматически компиляторами, для производительности.

Итого если посмотреть на стек перед вызовом printf то становится ясно откуда эти 6 %d.

Непопулярные фичи printf

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

получим такой вывод:

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

Но что, если число, которое нам нужно записать очень большое? Например адрес функции. Первое, что приходит в голову подавать строку соответствующих размеров. Скажем, у нас есть адресс шеллкода, а также есть управление над printf, что же нам делать?

Интересующий адрес шелла после компиляции — 0x80484d4. Выведем столько раз произвольный символ, а затем перепишем указатель на функцию.

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

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

А что все таки «%1$134513876.0X%7$n» значит?

Он представляет собой два исполняющих символа «%1$134513876.0X» и «%7$n».

%1$134513876.0X — вывод на stdout первого переданного аргумента, с длинной поля 134513876(это и есть адрес нашего шеллкода). Что там выведется значения не имеет, главное — количество символов.

%7$n — выполняет запись в 7 аргумент. Записывает он как раз то количество символов, которое мы вывели, т.е. адрес шеллкода.

В заключении

Как вы уже могли заметить, printf()-like функции обладают колосальной мощью. Более того абсолютной, ибо как оказалось они и еще тьюринг-полные, а значит потенциально могут содержать все, что будет угодно хакеру.

Как? Достигается это достаточно длинными и сложными последовательнотями, с которыми можете поиграться например вот тут. Ребята из usenix сделали компиляцию brainfuck кода в format-string последовательности. В репозитории есть примеры вроде чисел фибоначчи, 99 бутылок пива и много чего еще интересного.

Источник

Форматный вывод на Си для микроконтроллеров.

Форматированный ввод-вывод применяется очень широко, в первую очередь это, конечно, взаимодействие с пользователем, а так-же отладочный вывод, логи, работа с различными текстовыми протоколами и многое другое. В этой статье рассматриваются особенности применения форматированного вывода (ввод оставим на потом) для микроконтроллеров.
Первая программа написанная для ПК традиционно называется «Hello, world» и естественно пишет в стандартный вывод эту знаменитую фразу:

Первая программа для микроконтроллера обычно зовётся «Blinky» и она просто мигает светодиодом. Дело в том, что заставить работать традиционный «Hello, world» на микроконтроллере не так уж и просто для начинающего. Во первых, нет стандартного устройства вывода и его функциональность ещё нужно реализовать. Во вторых, не всегда бывает очевидно как подружить стандартные функции вывода с целевым устройством. Ситуация усугубляется тем, что в каждом компиляторе (тулчейне) это делается каким-то своим способом.

Форматный вывод.

Что-же, в общем, за зверь такой форматный вывод? Упрощенно говоря, это — вывод значений различных типов в виде текстовых полей.

Текстовое поле состоит из собственно значения, преобразованного в строку символов, и заполняющих символов для получения нужной ширины поля. Заполняющие символы могут находится слева или справа от значения, с помощью этого получается выравнивание по правому или левому краю соответственно. Заполнение также может находится внутри значения между определёнными его элементами, например между знаком и числом (как на рисунке), или между базой шестнадцатеричного числа (0x) и числом.
В качестве заполняющего символа обычно используется пробел, однако иногда могут использоваться нули, например в качестве ведущих нулей, или ещё что-нибудь.
Таким образом, форматированный вывод это — преобразование значений различных типов в текстовую форму и вывод их с определённым выравниванием и определённым заполнением.

Требования и особенности ввода-вывода для МК.

1. Гибкость. В отличии от старших братьев, для МК нет и не может быть стандартного устройства ввода-вывода. В каждой системе будет что-то своё, с уникальными требованиями и особенностями. В одной системе будет вывод в USART, во второй — на ЖК дисплей, в третей — в файл на SD карточке, в четвёртой — всё сразу. Значит система форматированного ввода-вывода должна быть достаточно гибкой, настраиваемой и независимой от аппаратных особенностей целевой платформы.

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

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

4. Доступность. Библиотека ввода-вывода должна быть в наличии для целевой платформы.

5. Функциональность часто ставится на последнее место в угоду скорости и компактности.

Стандартная библиотека Си.

Стандартным и единственно доступным средством форматированного вывода в Си является семейство функций: printf, sprintf, snprintf и fprintf.
Рассматривать будем функцию printf, как наиболее типичного и часто используемого представителя функций форматного вывода, при необходимости переходя к другим. Итак фунция printf имеет следующий синтаксис:

Это обычная функция с переменным числом параметров. Здесь первый аргумент fmt является форматной строкой, которая содержит, как обычный текст, так и специальные управляющие последовательности. Троеточие (. ) обозначает список дополнительных параметров, их может быть от нуля и до сколько поместится в стеке. Да, да все параметры в функцию printf передаются преимущественно в стеке (за исключением архитектуры ARM, где первые 4 параметра передаются в регистрах). Запихивает их в стек вызывающая функция, она-же инициализирует кадр стека перед вызовом и очищает после. Сама printf узнать сколько и каких параметров ей было передано может узнать только из форматной строки. При передачи параметров размером меньше (signed char, unsigned char, char и на некоторых платформах signed/unsigned short), чем int, они будут расширенны соответствующим расширением(знаковым или без-знаковым) до int-a. Это сделано для того, чтобы стек был всегда выровнен по границе машинного слова, а так-же уменьшает количество возможных ошибок при нестыковке размера фактического параметра и ожидаемого из форматной строки. Так-же параметры типа float при передаче в функции с переменным числом аргументов, приводятся к типу double.
Форматная строка содержит как символы непосредственно выводимые в поток, так и специальные управляющие последовательности, которые начинаются со знака «%». Управляющие последовательности имеют следующий формат:

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

Флаги определяют дополнительные параметры форматирования (их может быть несколько):

Ширина — десятичное число, задаёт минимальное количество символов выводимых для соответствующего параметра. Если выводимое значение содержит меньше символов, чем указано, то оно будет дополнено пробелами (или нулями если есть флаг «0») до нужной ширины слева или справа, если указан флаг «-«. Например:

Если вместо десятичного числа указать «*», то значение ширины будет считанно из дополнительного целочисленного параметра. Это позволяет задавать значение ширины поля вывода из переменной:

Здесь первый раз i передаётся в качестве ширины поля, второй — значения.
Точность или длинна — десятичное число после знака точки «.». В случае вывода целых чисел этот элемент означает минимальное количество записанных знаков, если выводимое число короче указанной длинны, то оно дополняется ведущими нулями, если число длиннее, то оно не урезается.

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

Для форматов «g» и «G» это — общее количество выведенных значимых цифр.

Для строк «s» этот элемент называется длинна и означает максимальное количество выведенных символов, обычно строки выводятся пока не встретится нулевой завершающий символ.

Также как и в элемента «ширина», в место точности можно поставить звёздочку и передать её значение в виде дополнительного целочисленного параметра:

Дополнительный модификатор служит для указания размерности типа:
h — применяется со спецификаторами i, d, o, u, x и X для знаковых и без-знаковых коротких целых short и unsigned short).
l — совместно со спецификаторами i, d, o, u, x и X означают длинные целые long и unsigned long).
l — совместно со спецификаторами s и c «широкие» многобайтные строку и символ соответственно.
L — обозначает тип long double, применяется со спецификаторами e, E, f, g и G.
В компиляторах поддерживающих тип long long, таких как GCC и IAR, часто есть для него нестандартный модификатор ll.
В стандарте С99 добавлены модификаторы «t» и «Z» для типов ptrdiff_t и size_t соответственно.

Работа над ошибками

Основным недостатком функций семейства printf считается вовсе не громоздкость и неторопливость — размер кода и накладные расходы на запихивание параметров в стек и разбор форматной строки обычно считаются приемлемыми, а подверженность ошибкам кодирования. Самое неприятное в этих ошибках то, что они могут быть неочевидными и проявляться не всегда, или не на всех платформах.
Большинство ошибок связано с несоответствием спецификаторов указанных в форматной строке с количеством и типами фактических аргументов. При этом можно выделить следующие ситуации:
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров совпадают с ожидаемыми. В этом случае просто выведутся параметры указанные в форматной строке, а лишние будут проигнорированы.
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров не совпадают с ожидаемыми. В этом случае параметры просто будут интерпретированы в соответствии с форматной строкой и в общем случае будет выведен мусор.
— размер фактических параметров меньше ожидаемых. Здесь поведение не определено и зависит от кучи разных факторов — от платформы, от компилятора, от содержимого стека на момент вызова printf. Поведение printf при этом может быть от внешне корректной работы и до чтения и даже записи произвольных участков памяти со всеми вытекающими последствиями.
Многие ошибки возникают при переносе кода с одной платформы на другую у которых отличаются размеры типов. Например:

На платформах, где int имеет 32 бита этот код работает правильно, а где int — 16 бит — будут выведены только 2 младших или старших (в зависимости от порядка следования байт) байта.
К счастью некоторые компиляторы, например GCC, знают printf «в лицо» и выдают предупреждения в случае несовпадения фактических параметров с форматной строкой. Однако это работает только если форматная строка задана литералом. А если она хранится в переменной (например extern указатель инициализируемый в другом модуле), то компилятор бессилен и проследить соответствеи параметров может быто очень не просто.

Особенности реализаций

AVR-GCC он-же WinAVR

В AVR-GCC, а точнее в avr-libc самая удачная, на мой взгляд, реализация стандартной библиотеки ввода-вывода Си. В ней имеется возможность выбирать необходимый функционал функций семейства printf. Они прекрасно работают со строками как RAM так и во Flash. Все функции семейства, включая snprintf и fprintf разделяют общий код и очень хорошо оптимизированы как по скорости, таки по объёму кода.
Для поддержки находящихся во Flash памяти строк введен новый спецификатор форматной строки %S — S — заглавная, строчная s по-прежнему означает строку в RAM. Но во Flash памяти может быть и сама форматная строка, для этого есть специальные модификации функций с суффиксом «_P» printf_P, fprintf_P, sprintf_P и т. д., которые ожидают форматную строку во Flash.
Для того чтобы printf заработала, нужно написать функцию вывода символа и определить файловый дескриптор стандартного вывода.

Помимо stdout есть стандартные дескрипторы stdin и stderr для стандартного ввода и стандартного вывода ошибок соответственно. Используя функцию fprintf можно явно указывать нужный файловый дескриптор и при необходимости можно определить сколько угодно устройств вывода:

В avr-libc имеется три уровня функциональности библиотеки ввода-вывода:
1. нормальный — поддерживается вся упомянутая выше функциональность, кроме вывода чисел с плавающей точкой. Этот режим включен по умолчанию и каких либо опций для него указывать не надо. Поддержка чисел с плавающей точкой занимает много места, а нужна сравнительно редко. Приведенный выше пример, скомпилированный с этим уровнем функциональности, занимает порядка 1946 байт Flash памяти.
2. минимальный — поддерживаются только самые базовые вещи: целые, строки, символы. Флаги, ширина поля и точность, если они есть в форматной строке, разбираются корректно, но игнорируются, поддерживается только флаг «#». Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash памяти. Включается указанием в командной строке компоновщика («Linker options» в AVRStudio, а не компилятора, как расплывчато указано в документации avr-libc) следующих опций:

3. максимальный — полная функциональность, включая поддержку чисел с плавающей точкой. Включается опциями

Скомпилированный пример занимает при этом 3488 байт.
Функции семейства printf из avr-libc не используют буферизацию, не считая буферов для конвертации чисел, и выводят символы в устройство по мере их обработки. Поэтому, если буферизация нужна, то ее можно реализовать самостоятельно в функции вывода символа, там можно реализовать и кольцевой буфер и все, что угодно. Также в этих функциях не используется динамическая память, что в нашем случае очень хорошо, зато активно используется стек. Попробуем определить максимальное использование стека в них. Для этого в приложенном архиве заходим в каталог AvrGccPrintf, компилируем проект посредством AVRStudio и запускаем симуляцию с помощью runSimul.cmd.

После открывает образовавшийся файл trace.log, находим значение указателя стека (SP) после входа в main (после пролога), находим минимальное значение SP (стек растёт вниз) и вычитаем из первого второе. У меня получилось 0x455 — 0x429 = 0x2c = 44 байта использует сама функция fprintf, плюс еще 8 байт в стеке занимают ее параметры, итого 52 байта. Еще 14 байт занимает один файловый дескриптор и ещё 6 байт три стандартных указателя на файловые дескрипторы (stdout, stdin, stderr). Итого 72 байта RAM только на вызов fprintf, без учета всего остального.
Также из файла trace.log можно узнать общее время выполнения функций и где процессор проводит его больше всего.

Подробнее о стандартной библиотеке ввода-вывода avr-libc можно здесь:
www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html

IAR for AVR

Здесь есть несколько версий Си-шных библиотек, с отличающейся функциональностью:
— CLIB — относительно маленькая, но ограниченная библиотека. Нет поддержки файловых дескрипторов, локалей и многобайтных символов. Считается устаревшей.
— Normal DLIB — более новая. Так-же нет поддержки файловых дескрипторов, локалей и многобайтных символов, но есть некоторые плюшки из стандарта С99.
— Full DLIB — полная библиотека Си. Поддерживает всё согласно стандарту С99, но при этом очень объёмная. Рассматривать этот вариант не буду, так, как размер функции printf отсюда превышает доступные 4 Кб кода для бесплатной версии IAR KickStart for AVR.
В IAR имеется возможность выбирать возможности для printf. Для этого заходим с меню Project->Options далее в диалоге General Options->Library Configuration. В списке «Printf formatter» можно выбрать необходимый уровень функционала. Для CLIB это Large, Small, Tiny, для DLIB добавляется еще Full. По возможностям эти уровни примерно соответствуют аналогичным в avr-gcc, поэтому расписывать их не буду.

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

Разберём полный пример. Он также предназначен для запуска на симуляторе.

Тут выясняются две неприятные особенности. Во-первых функции printf и printf_P видимо не разделяют общий код и printf_P всегда использует максимальный форматтер, независимо от того, что выбрали в настройках для printf, занимая порядка 3500 байт кода. Поэтому приведенный пример не помещается в четыре бесплатных килобайта. Для проверки одну из функций надо закомментировать.
Во-вторых, ни printf, ни printf_P не умеют читать строки из flash памяти — для них нет спецификатора.
Размеры printf для различных уровней функциональности примерно соответствуют аналогичным у avr-gcc, где чуть меньше, где чуть больше — непринципиально. А вот использование стека в разы выше, минимальный размер стека данных, при котором printf заработала, составил 200 байт для DLIB и около 150 для CLIB. Так, что на контроллерах с 2 кб flash, 128 RAM использовать эти функции не получится.
Демо-проект находится в каталоге AvrIarPrintf. Для запуска симуляции, а точнее преобразования генерируемого IAR-ом hex-а в пригодный для потребления simulavr-ом elf, на машине должен быть установлен WinAVR (и прописан в переменной окружения PATH, естественно).

Mspgcc

В стандартной библиотеке mspgcc для форматного вывода реализованы только функции printf и sprintf, плюс еще нестандартная функция uprintf, которая принимает указатель на функцию вывода символа в качестве первого аргумента. Файловых дескрипторов там нет и в помине, выбирать уровень функциональности форматтеров тоже нельзя. При этом printf «весит» порядка двух килобайт так, что запустить её, скажем, на MSP43 Launchpad-е не получится.
Для работы printf нужно, вполне ожидаемо, определить функцию int putchar(int c):

IAR for MSP430, IAR for ARM и может еще какой IAR

Вообще в реализациях стандартной библиотеки от IAR Systems всё довольно однообразно для различных платформ, что не может не радовать. Как правило есть минимум две версии стандартной библиотеки — одна урезанная без поддержки файловых дескрипторов, вторая — полная, соответственно, с их поддержкой. Зовутся они как правило «Normal» и «Full» соответственно. Так-же в каждой из них можно выбирать необходимый функционал разбора форматной строки, поддерживаемый функциями семейства printf. Варианты уже уже описаны для IARfor AVR: Large, Small, Tiny и Full.
Если выбрать вариант библиотеки без файловых дескрипторов, то для работы функций вывода нужно определить лишь функцию int putchar(int outchar).
Если используем вариант с дескрипторами, то определить нужно функции __write, __lseek, __close и remove.
Минимальная их реализация, например, для STM32 может выглядеть так:

ARM + NewLib

Большинство сборок GCC под ARM используют NewLib в качестве стандартной библиотеки Си. Это достаточно взрослый проект и хорошо соответствует Си-шным стандартам, но при этом она относительно «тяжела» и требовательна к ресурсам. Для настройки библиотеки под свои нужды используется механизм системных вызовов. О нём уже немного писалось тут ispolzuem-libc-newlib-dlya-stm32, поэтому подробно на них останавливаться не буду, а упомяну об особенностях.
Первое это требования к памяти. Все stdio функции из NewLib используют хитрую буферизацию и динамически распределяют память для своих внутренних нужд. А значит им нужна куча и не маленькая, а в целых 3 Кбайт. Плюс примерно 1500 байт на статические структуры и плюс около 500 байт стека. Итого только чтоб напечатать «Hello, world!» нужно порядка 5 Кб оперативки. Что как-бы чуть больше, чем много для STM32-Discovery, на которой я запускал тестовый пример, с её 8 килобайтами. Также при использовании таких функций как printf по зависимостям тянется практически вся stdio плюс функции поддержки плавающей точки. В итоге тестовая прошивка занимает чуть меньше 30 Кб памяти программ. Если отказаться от использования чисел с плавающей точкой, то вместо printf можно использовать её облегченный аналог iprintf. В этом случае объём тестовой прошивки будет около 12 Кб.
Если какая либо из функций stdio не сможет выделить необходимую её память из кучи, то она тихонечко свалится в HardFault без объяснений причин.
Еще один момент это буферизация. Она может несколько запутать. Вызываем:

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

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

При этом потребление памяти кучи уменьшится более чем на 1.5 Кб.

Xprintf

Это реализация функций похожих на printf от товарища Chan-а. Качаем отсюда:
elm-chan.org/fsw/strf/xprintf.html
Библиотека содержит следующие функции для форматированного вывода — аналоги функций стандартной библиотеки Си:

Все функции написаны целиком на Си и их можно использовать практически на любом микроконтроллере. Однако они не учитывают особенностей некоторых МК, например, нет никакой поддержки строк во Flash памяти для семейства AVR, что не добавляет удобства использования. Для использования xprintf необходимо инийиализировать указатель xfunc_out, присвоив ему адрес функции вывода символа. Рассмотрим пример. Компилятор avr-gcc, проект AvrStudio, рассчитан на запуск в симуляторе simulavr.

Здесь для вывода строк из Flash памяти их приходится предварительно скопировать в оперативку.
Из достоинств этой библиотеки можно выделить компактность кода (

1600 байт для приведённого примера) и лёгкость использования на различных платформах и модифицировать. На этом достоинства заканчиваются. Из недостатков стоит отметить
относительно медленную работу, примерно в полтора-два медленнее чем стандартная printf из avr-libc, и несоответствие стандартам — не поддерживаются некоторые флаги (‘пробел’, ‘ #’, ‘+’ ), спецификаторы (i, p n), не считая флагов для чисел с плавающей точкой и т.д.
Потребление стека порядка 38 байт не считая аргументов.

Описание примеров.

AvrGccPrintf
AvrIarPrintf
AvrXprintf
Msp430GccPrintf
Msp430IarPrintf
Stm32Format
IarArm

Работает на STM32 Discovery. Собирается с помощью IAR for ARM.

Итоги.

Преимущества использования стандартной библиотеки Си для форматного вывода:
— Стандартность. Этим всё сказано.
— Доступность. В каком-то виде есть практически в любом Си компиляторе.
— Разделение формата вывода и выводимых данных. Форматную строку легко вынести в отдельный файл/модуль, например для дальнейшей локализации.
— Хорошая функциональность.
Недостатки:
— Полностью стандартная реализация в большинстве случаев слишком «тяжела» для микроконтроллеров.
— Использование лишь одной функции, например printf, тянет за собой значительную часть библиотеки вывода, даже если реально используются только ограниченные возможности.
— Неаккуратное использование функций с форматной строкой может привести к трудно обнаруживаемым ошибкам кодирования.
— В каждом компиляторе используется какой-то свой способ для определения низкоуровневых функций вывода.

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

Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash

100 байт свободно 🙂
Сам форматный вывод в примерно 1000 с небольшим байт помещается (и для AVR и для MSP430).
Об этом следующая статья будет. С шаблонами и выносом мозга.

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

Соответственно в коде для вывода отладки вместо printf используем _DBG, например

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

Источник

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

Какие вы еще знаете однокоренные слова к слову Как написать свой printf:



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

Ваш адрес email не будет опубликован. Обязательные поля помечены *