printf

Функция C для форматирования и вывода текста

Пример функции printf

printf — это стандартная библиотечная функция языка C , которая форматирует текст и записывает его в стандартный вывод .

Название printf является сокращением от print formatted , где print относится к выводу на принтер, хотя функции не ограничиваются выводом на принтер.

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

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

Синтаксис и семантика строки формата одинаковы для всех функций семейства printf-подобных.

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

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

Дизайн форматирования был скопирован в других языках программирования .

История

1950-е: Фортран

Ранние языки программирования, такие как Fortran, использовали специальные операторы с синтаксисом, отличным от других вычислений, для построения описаний форматирования. [1] В этом примере формат указан в строке 601, а команда PRINT [a] ссылается на него по номеру строки:

  ПЕЧАТЬ 601 , IA , IB , ОБЛАСТЬ 601 ФОРМАТ ( 4 H A = , I5 , 5 H B = , I5 , 8 H ОБЛАСТЬ = , F10 . 2 , 13 H КВАДРАТНЫЕ ЕДИНИЦЫ )              

Настоящим:

  • 4Hобозначает строку из 4 символов " A= "( Hозначает поле Холлерита );
  • I5указывает на целочисленное поле шириной 5;
  • F10.2указывает поле с плавающей точкой шириной 10 с 2 цифрами после десятичной точки.

Вывод с входными аргументами 100, 200 и 1500.25 может выглядеть следующим образом:

A= 100 B= 200 ПЛОЩАДЬ= 1500,25 КВАДРАТНЫХ ЕДИНИЦ

1960-е: BCPL и ALGOL 68

В 1967 году появился BCPL . [2] Его библиотека включала writefпроцедуру. [3] Пример приложения выглядит так:

WRITEF("%I2-ЗАДАЧА С ФЕРЗЯМИ ИМЕЕТ %I5 РЕШЕНИЙ*N", NUMQUEENS, COUNT)

Настоящим:

  • %I2указывает целое число шириной 2 (порядок ширины и типа поля спецификации формата обратный по сравнению с C printf);
  • %I5обозначает целое число шириной 5;
  • *N— это управляющая последовательность языка BCPL , представляющая символ новой строки (в языке C для этого используется управляющая последовательность \n).

В 1968 году ALGOL 68 имел более функциональный API , но по-прежнему использовал специальный синтаксис ( $разделители окружали специальный синтаксис форматирования):

printf (( $ "Цвет " g ", number1 " 6 d , ", number2 " 4 zd , ", hex " 16 r2d , ", float " - d .2 d , ", беззнаковое значение" -3 d "." l$ , "red" , 123456 , 89 , BIN 255 , 3.14 , 250 ));       

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

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

С тех пор люди на собственном горьком опыте убедились [4] , что это убеждение ложно, что приводит к множеству нежелательных последствий, начиная от уязвимостей безопасности и заканчивая сбоями оборудования (например, сетевые возможности телефона навсегда отключаются после попытки подключения к точке доступа с именем «%p%s%s%s%s%n». [5] ).

Современные языки, такие как C++20 и более поздние, поэтому предпринимают шаги для исправления этой ошибки и включают спецификации формата как часть синтаксиса языка [6] , что в некоторой степени восстанавливает безопасность типов при форматировании и позволяет компилятору обнаруживать некоторые недопустимые комбинации спецификаторов формата и типов данных во время компиляции.

1970-е: С

В 1973 году printfвключен в качестве процедуры C в состав версии 4 Unix . [7]

1990-е: Командование Shell

В 1990 году printfкоманда оболочки была аттестована как часть 4.3BSD-Reno . Она была создана по образцу стандартной библиотечной функции. [8]

В 1991 году printfкоманда была включена в состав GNU shellutils (теперь часть GNU Core Utilities ).

2000-е: безопасность формата W

Необходимость решения целого ряда проблем, возникающих из-за отсутствия безопасности типов, побудила к попыткам сделать компилятор C++ поддерживающим эту printfтехнологию.

Опция -WformatGCC позволяет выполнять проверки вызовов во время компиляции , позволяяprintf компилятору обнаруживать подмножество недопустимых вызовов (и выдавать либо предупреждение, либо ошибку, полностью останавливая компиляцию, в зависимости от других флагов). [9] .

Поскольку компилятор проверяет printfспецификаторы формата, включение этой опции фактически расширяет синтаксис C++, делая форматирование его частью.

2020-е: спецификаторы формата C++20 и печать C++23

Как было сказано выше, многочисленные проблемы [10], связанные с printf()отсутствием безопасности типов, привели к пересмотру [11] подхода к форматированию, а в C++20 и далее в язык были включены спецификации формата [12] , обеспечивающие безопасное форматирование типов.

Подход (и синтаксис) C++20 std::formatвозник в результате эффективного включения API Виктора Зверовича libfmt[13] в спецификацию языка [14] (Зверович написал [15] первый черновик предложения нового формата); следовательно, libfmtон является реализацией спецификации формата C++20.

Функция форматирования была объединена с выводом в C++23, что предоставляет [16] команду std::printв качестве замены printf().

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

Спецификация формата libfmtи std::formatсама по себе является расширяемым «мини-языком» (так называемым в спецификации [17] примером предметно-ориентированного языка ).

Включение в синтаксис языка C++ отдельного, предметно-ориентированного мини-языка, специально предназначенного для форматирования std::print, завершает исторический цикл, возвращая современное состояние (по состоянию на 2024 год) к тому, каким оно было в случае первой PRINTреализации FORTRAN в 1950-х годах, обсуждавшейся в начале этого раздела.

Спецификатор формата

Форматирование значения указывается как разметка в строке формата. Например, следующий код выводит "Ваш возраст составляет ", а затем значение переменной ageв десятичном формате.

printf ( "Ваш возраст %d" , age ); 

Синтаксис

Синтаксис спецификатора формата следующий:

%[ параметр ][ флаги ][ ширина ][. точность ][ длина ] тип

Параметр поля

Поле параметра является необязательным. Если включено, то сопоставление спецификаторов со значениями не является последовательным. Числовое значение n выбирает n-й параметр значения.

ХарактерОписание
н $n — индекс параметра значения для сериализации с использованием этого спецификатора формата.

Это расширение POSIX , а не C99 .

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

Например,

printf ( "%2$d %2$#x; %1$d %1$#x" , 16 , 17 )

выходы: 17 0x11; 16 0x10.

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

В Microsoft Windows поддержка этой возможности осуществляется через другую функцию — printf_p.

Поле флагов

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

ХарактерОписание
-
(минус)
Выровняйте вывод этого заполнителя по левому краю. (По умолчанию вывод выравнивается по правому краю.)
+
(плюс)
Добавляет плюс к положительным знаковым числовым типам. positive = +, negative = -.
(По умолчанию ничего не добавляется перед положительными числами.)

(космос)
Добавляет пробел для положительных знаковых числовых типов. positive = , negative = -. Этот флаг игнорируется, если существует флаг + . (По умолчанию ничего не добавляется перед положительными числами.)
0
(ноль)
Если указан параметр «ширина», для числовых типов добавляются нули. (По умолчанию добавляются пробелы.)
Например, выдает , а выдает .printf("%4X",3) 3printf("%04X",3)0003
'
(апостроф)
К целому числу или показателю десятичной дроби применяется разделитель тысяч.
#
(хеш)
Альтернативная форма:
Для типов g и G конечные нули не удаляются. Для типов f , F , e , E , g , G вывод всегда содержит десятичную точку. Для типов o , x , X текст 0 , 0x , 0X , соответственно, добавляется к ненулевым числам.

Ширина поля

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

Например, указывает ширину 3 и выводит с пробелом слева, чтобы вывести 3 символа. Вызов выводит, который имеет длину 4 символа, поскольку это минимальная ширина для этого значения, даже если указанная ширина равна 3.printf("%3d", 12) 12printf("%3d", 1234)1234

Если поле ширины опущено, выводится минимальное количество символов для значения.

Если поле указано как *, то значение ширины считывается из списка значений в вызове. [18] Например, выводит , где второй параметр, 3, — это ширина (соответствует *), а 10 — это значение для сериализации (соответствует d).printf("%*d", 3, 10) 10

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

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

 1 1 12 12 123 123 1234 123     

Точное поле

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

Поле точности может быть опущено или может быть целочисленным значением или динамическим значением при передаче в качестве другого аргумента, если указано звездочкой *. Например, выводитprintf("%.*s", 3, "abcdef")азбука.

Длина поля

Поле длины может быть опущено или иметь одно из следующих значений:

ХарактерОписание
ччДля целочисленных типов заставляет printf ожидать целочисленный аргумент размера int , который был повышен из char .
часДля целочисленных типов заставляет printf ожидать целочисленный аргумент размера int , который был повышен из short .
лДля целочисленных типов заставляет printf ожидать длинный целочисленный аргумент.

Для типов с плавающей точкой это игнорируется. Аргументы float всегда повышаются до double при использовании в вызове varargs. [19]

ллДля целочисленных типов заставляет printf ожидать длинный целочисленный аргумент.
ЛДля типов с плавающей точкой заставляет printf ожидать длинный двойной аргумент.
зДля целочисленных типов заставляет printf ожидать целочисленный аргумент размером size_t .
джДля целочисленных типов заставляет printf ожидать целочисленный аргумент размером intmax_t .
тДля целочисленных типов заставляет printf ожидать целочисленный аргумент размером ptrdiff_t .

Варианты длины, зависящие от платформы, появились еще до широкого использования расширений ISO C99, в том числе:

ПерсонажиОписаниеЧасто встречающиеся платформы
яДля знаковых целых типов printf ожидает целочисленный аргумент размером ptrdiff_t ; для беззнаковых целых типов printf ожидает целочисленный аргумент размером size_t .Win32/Win64
И32Для целочисленных типов заставляет printf ожидать 32-битный (двойное слово) целочисленный аргумент.Win32/Win64
И64Для целочисленных типов заставляет printf ожидать 64-битный (четверное слово) целочисленный аргумент.Win32/Win64
дДля целочисленных типов заставляет printf ожидать 64-битный (четверное слово) целочисленный аргумент.БСД

ISO C99 включает inttypes.hзаголовочный файл, который включает ряд макросов для платформенно-независимого printfкодирования. Например: определяет десятичный формат для 64-битного знакового целого числа. Поскольку макросы вычисляются как строковый литерал, а компилятор объединяет соседние строковые литералы, выражение компилируется в одну строку.printf("%" PRId64, t);"%" PRId64

Макросы включают в себя:

МакроОписание
PRId32Обычно эквивалентно I32d ( Win32/Win64 ) или d
PRId64Обычно эквивалентен I64d ( Win32/Win64 ), lld ( 32-битные платформы ) или ld ( 64-битные платформы )
ПРИи32Обычно эквивалентно I32i ( Win32/Win64 ) или i
ПРИи64Обычно эквивалентен I64i ( Win32/Win64 ), lli ( 32-битные платформы ) или li ( 64-битные платформы )
ПРИу32Обычно эквивалентно I32u ( Win32/Win64 ) или u
ПРИу64Обычно эквивалентно I64u ( Win32/Win64 ), llu ( 32-битные платформы ) или lu ( 64-битные платформы )
PRIx32Обычно эквивалентно I32x ( Win32/Win64 ) или x
PRIx64Обычно эквивалентно I64x ( Win32/Win64 ), llx ( 32-битные платформы ) или lx ( 64-битные платформы )

Тип поля

Поле типа может быть любым из:

ХарактерОписание
%Печатает буквенный символ % (этот тип не принимает никаких флагов, полей ширины, точности и длины).
д , яint как знаковое целое число . %d и %i являются синонимами для выходных данных, но различаются при использовании с входными данными (при использовании %i число будет интерпретироваться как шестнадцатеричное, если ему предшествует 0x , и как восьмеричное, если ему предшествует 0 ).scanf
тыРаспечатать десятичное беззнаковое целое число .
ж , Жdouble в обычной ( с фиксированной точкой ) нотации. f и F отличаются только тем, как выводятся строки для бесконечного числа или NaN ( inf , infinity и nan для f ; INF , INFINITY и NAN для F ).
е , ЕЗначение double в стандартной форме ( d . ddddd ). Преобразование E использует букву E (а не e ) для представления показателя степени. Показатель всегда содержит не менее двух цифр; если значение равно нулю, показатель равен 00 . В Windows показатель степени по умолчанию содержит три цифры, например, 1.5e002 , но это можно изменить с помощью специфичной для Microsoft функции._set_output_format
гарантированная победаdouble в нормальной или экспоненциальной записи, в зависимости от того, какая из них больше подходит для его величины. g использует строчные буквы, G использует заглавные буквы. Этот тип немного отличается от записи с фиксированной точкой тем, что незначащие нули справа от десятичной точки не включаются. Кроме того, десятичная точка не включается в целые числа.
х , Хunsigned int как шестнадцатеричное число. x использует строчные буквы, а X использует заглавные.
обеззнаковое целое число в восьмеричном формате.
сСтрока с нулевым завершением .
сchar (персонаж).
пvoid* (указатель на void) в формате, определяемом реализацией.
а , Аdouble в шестнадцатеричной системе счисления, начинающейся с 0x или 0X . a использует строчные буквы, A использует заглавные буквы. [20] [21] (в C++11 iostreams есть hexfloat , который работает так же).
нНичего не выводить, а записывать количество символов, записанных на данный момент, в параметр указателя целого числа.
В Java это выводит новую строку. [22]

Пользовательское форматирование типа данных

Распространенным способом обработки форматирования с использованием пользовательского типа данных является форматирование значения пользовательского типа данных в строку, а затем использование %sспецификатора для включения сериализованного значения в более крупное сообщение.

Некоторые функции типа printf допускают расширения мини-языка на основе escape-символов , что позволяет программисту использовать определенную функцию форматирования для не встроенных типов. Одна из них — это register_printf_function() из glibc (теперь устаревшая ). Однако она редко используется из-за того, что конфликтует со статической проверкой формата строки. Другая — это пользовательские форматировщики Vstr, которые позволяют добавлять многосимвольные имена форматов.

Некоторые приложения (например, Apache HTTP Server ) включают собственную функцию, похожую на printf, и встраивают в нее расширения. Однако все они, как правило, имеют те же проблемы, что и register_printf_function().

Функция ядра Linux printk поддерживает несколько способов отображения структур ядра с использованием общей %pспецификации путем добавления дополнительных символов формата. [23] Например, %pI4печатает адрес IPv4 в десятичной форме с точками. Это позволяет проводить статическую проверку строки формата (части %p) за счет полной совместимости с обычной printf.

Семья

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

fprintfвыводит в системный файловый объект, который допускает вывод на устройство, отличное от стандартного вывода.

sprintfзаписывает в строковый буфер вместо стандартного вывода.

snprintfобеспечивает определенный уровень безопасности, sprintfпоскольку вызывающая сторона предоставляет параметр длины (n), который указывает максимальное количество символов для записи в буфер.

Для большинства функций семейства printf есть вариант, который принимает va_listвместо списка параметров переменной длины. Например, есть vfprintf, vsprintf, vsnprintf.

Уязвимости

Атака форматной строки

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

Некоторые компиляторы, такие как GNU Compiler Collection , статически проверяют строки формата printf-подобных функций и предупреждают о проблемах (при использовании флагов -Wallили ). GCC также предупреждает о пользовательских функциях в стиле printf, если к функции применен -Wformatнестандартный «формат» .__attribute__

Неконтролируемый эксплойт форматной строки

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

Запись памяти

Хотя на первый взгляд это функция вывода, printfона позволяет записывать в область памяти, указанную аргументом через %n. Эта функциональность иногда используется как часть более сложных атак форматной строки. [24]

Функциональность %nтакже делает printfслучайным образом Тьюринг-полным даже с хорошо сформированным набором аргументов. Игра в крестики-нолики, записанная в строке формата, является победителем 27-го IOCCC . [25]

Языки программирования с printf

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

Исключены языки, использующие строки формата, которые отклоняются от стиля, описанного в этой статье (например, AMPL и Elixir ), языки, которые наследуют свою реализацию от JVM или другой среды (например, Clojure и Scala ), а также языки, которые не имеют стандартной собственной реализации printf, но имеют внешние библиотеки, эмулирующие поведение printf (например, JavaScript ).

Смотрите также

Примечания

  1. ^ Согласно руководству по Fortran 1956 года [1] , PRINTкоманда печатает на подключенном принтере . В руководстве также представлена ​​команда WRITE OUTPUT TAPE, которая также использует FORMATоператор для записи на ленту .

Ссылки

  1. ^ ab Backus, John Warner ; Beeber, RJ; Best, Sheldon F.; Goldberg, Richard ; Herrick, Harlan L.; Hughes, RA; Mitchell, LB; Nelson, Robert A.; Nutt, Roy ; Sayre, David ; Sheridan, Peter B.; Stern, Harold; Ziller, Irving (15 октября 1956 г.). Sayre, David (ред.). Система автоматического кодирования FORTRAN для IBM 704 EDPM: Справочное руководство программиста (PDF) . Нью-Йорк, США: Отдел прикладных наук и отдел исследований программирования, International Business Machines Corporation . стр. 26–30. Архивировано (PDF) из оригинала 4 июля 2022 г. . Получено 4 июля 2022 г. .(2+51+1 страницы)
  2. ^ "BCPL". cl.cam.ac.uk . Получено 19 марта 2018 г. .
  3. ^ Ричардс, Мартин; Уитби-Стревенс, Колин (1979). BCPL — язык и его компилятор. Cambridge University Press. стр. 50.
  4. ^ «Атака строки формата».
  5. ^ ""Ошибка iPhone приводит к поломке WiFi при подключении к точке доступа с необычным именем".
  6. ^ «Спецификация стандартного формата C++20».
  7. ^ Макилрой, МД (1987). Исследовательский ридер Unix: аннотированные выдержки из Руководства программиста, 1971–1986 (PDF) (Технический отчет). CSTR. Bell Labs. 139.
  8. ^ "printf (4.3+Reno BSD)". man.freebsd.org . Получено 1 апреля 2024 г. .
  9. ^ "Опция -Wformat компилятора gcc".
  10. ^ «Как не надо кодировать: остерегайтесь printf». 10 августа 2016 г.
  11. ^ «Предложение по улучшению формата C++20 для включения проверок во время компиляции».
  12. ^ "C++20 std::format".
  13. ^ "libfmt: современная библиотека форматирования".
  14. ^ «Форматирование текста C++20: Введение».
  15. ^ «История предложений формата C++».
  16. ^ "Печать C++".
  17. ^ «Мини-язык спецификации формата».
  18. ^ "printf". cplusplus.com . Получено 10 июня 2020 г. .
  19. ^ ISO / IEC (1999). ISO/IEC 9899:1999(E): Языки программирования – C §7.19.6.1 пункт 7 .
  20. ^ ""Справочное руководство по библиотеке GNU C", "12.12.3 Таблица выходных преобразований"". Gnu.org . Получено 17 марта 2014 г. .
  21. ^ "printf" ( %a добавлено в C99)
  22. ^ "Форматирование числового вывода на печать". Учебники Java . Oracle Inc. Получено 19 марта 2018 г.
  23. ^ "Linux kernel Documentation/printk-formats.txt". Git.kernel.org. Архивировано из оригинала 29 апреля 2015 г. Получено 17 марта 2014 г.
  24. ^ https://www.exploit-db.com/docs/english/28476-linux-format-string-exploitation.pdf [ пустой URL-адрес PDF ]
  25. ^ "Best of show – abuse of libc". Ioccc.org . Получено 5 мая 2022 г. .
  26. ^ ""The Open Group Base Specifications Issue 7, edition 2018", "POSIX awk", "Output Statements"". pubs.opengroup.org . Получено 29 мая 2022 г. .
  27. ^ "Printf Standard Library". Руководство по языку Julia . Получено 22 февраля 2021 г.
  28. ^ «Встроенные типы: форматирование строк в стиле printf», The Python Standard Library , Python Software Foundation , дата обращения 24 февраля 2021 г.
  • Справочник C++ для std::fprintf
  • краткий справочник спецификаций формата gcc printf
  • printf: вывод в формате печати – Справочник по системным интерфейсам, Единая спецификация UNIX , версия 4 от The Open Group
  • Спецификация Formatter в Java 1.5
  • Встроенная функция GNU Bash printf(1)
Взято с "https://en.wikipedia.org/w/index.php?title=Printf&oldid=1253332260"