printf

Материал из свободной русской энциклопедии «Традиция»
Перейти к: навигация, поиск

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

В общем случае так называют целое семейство функций, различающихся по различным второстепенным особенностям. Например, fprintf записывает вывод в файл, sprintf помещает вывод в строку, vprintf принимает аргументы с помощью va_list, snprintf, vsnprintf имеют дополнительный параметр, ограничивающий максимальный размер отформатированной строки.

Впервые функция появилась в предшественниках языка Си (BCPL и Би), однако полную свою форму (полноценнная строка форматирования с флагами, шириной, точностью и размером) она обрела в стандартной библиотеке Си, откуда попала в Си++ и Objective-C.

Аналогичный используемому в printf синтаксис строки шаблона вывода (называемой иногда строкой форматирования или строкой формата) в дальнейшем начал использоваться другими языками программирования (с учётом возможных изменений в рамках возможностей языка).

Содержание

История появления[править]

Первые следы будущей функции printf появляются в языке BCPL в 1960-х. Функция WRITEF принимает строку форматирования, в которой тип данных указывается отдельно от самих данных в строковой переменной (тип указывался без полей флагов, ширины, точности и размера, но уже предварялся символом процента %)[1]. Появившийся вслед за ним в 1969 году язык Би уже использовал название printf с простейшей строкой форматирования (аналогичной BCPL), указывался только один из трёх возможных типов и два представления числа: десятичные (%d), восьмеричные (%o), строки (%s) и символы (%c)[2].

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

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

Сама функция WRITEF была средством упрощения вывода, вместо набора функций WRCH (вывод символа), WRITES (вывод строки), WRITEN, WRITED, WRITEOCT, WRITEHEX (вывод чисел в различной форме) использовался единый вызов, в котором можно было чередовать «просто текст» с выходными значениями.

Использование в других языках программирования[править]

Помимо Си и его производных (Си++, Objective-C), printf-подобный синтаксис строки форматирования используют многие другие языки программирования:

Помимо этого, благодаря наличию утилиты printf в составе большинства UNIX-подобных систем, printf используется во многих shell-скриптах (для sh, bash, csh, zsh и т. д.).

Именование функций семейства[править]

Все функции имеют в имени основу printf. Префиксы перед именем функции означают:

  • v (vprintf, vsnprintf и т.д.) — функция вместо переменного числа параметров принимает список аргументов va_list.
  • f (fprintf, vfprintf) — вывод результата в передаваемый через параметр функции поток, вместо стандартного вывода.
  • s (sprintf, snprintf, vsprintf, vspnprintf) — запись результата в строку (буфер в памяти), а не поток.
  • n (snprintf, vnsprintf) — наличие параметра, ограничивающего максимальное количество символов для записи результата (используется только вместе с префиксом s). В Maple функция nprintf аналогична sprintf, но возвращает не текстовую строку, а имя.
  • w, перед остальными префиксами (wsnprintf, wvsnprintf) — использующийся фирмой Microsoft префикс для реализаций семейства функций sprintf в операционных системах Windows.
  • w, после остальных префиксов (fwprintf, swprintf, wprintf) — функция использует многобайтовую кодировку (wchar_t) вместо обычных строк. (при этом функция swprintf не имеет префикса «n», хотя и принимает параметр, ограничивающий размер результирующей строки).
  • a (asprintf, vasprintf) — расширения GNU; функции аналогичные sprintf/vsprintf, но выделяющие достаточный объём памяти с помощью malloc для форматированной строки. Вместо указателя на строку эти функции принимают указатель на указатель на строку, освобождение памяти производит вызвавшая функция.

Общие соглашения[править]

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

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

Функции семейства s (sprintf, snprintf, vsprintf, vsnprintf) первым параметром (s) принимают указатель на область памяти, куда будет записана результирующая строка. Функции, не имеющие ограничения по количеству записываемых символов являются небезопасными функциями, так как могут привести к ошибке переполнения буфера, в случае, если выводимая строка окажется больше, чем размер выделенной для вывода области памяти.

Функции семейства f записывают строку в любой открытый поток (параметр stream), в частности, в стандартные потоки вывода (stdout, stderr). fprintf(stdout, format, …) эквивалентно printf(format, …).

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

Функции семейства w (первым символом) являются ограниченной реализацией Microsoft семейства функций s: wsprintf, wsnprintf, wvsprintf, wvsnprintf. Эти функции реализованы в динамических библиотеках user32.dll и shlwapi.dll (n функции). Они не поддерживают вывод значений с плавающей запятой, кроме того, wsnprintf и wvsnprint поддерживают выравнивание текста только по левому краю.

Функции семейства w (wprintf, swprintf) реализуют поддержку многобайтовых кодировок, все функции этого семейства работают с указателями на многобайтные строки (wchar_t).

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

Описание функций[править]

Имена параметров[править]

  • format — строка форматирования (формат описан ниже)
  • stream — файловый поток для вывода
  • s — строка для помещения результата работы функции
  • n — переменная, содержащая максимальное допустимое количество символов для строки s
  • ap — список значений для вывода
  • strp — указатель на указатель на строку для помещения результатов работы функции

Описание функций[править]

  • int printf( const char *format, ... );
    Вывод форматированной строки на стандартный вывод
  • int fprintf( FILE *stream, const char *format, ... );
    Вывод форматированной строки в поток
  • int sprintf( char *s, const char *format, ... );
    Запись форматированной строки в строку без ограничения по размеру строки
  • int snprintf( char *s, size_t n, const char *format, ... );
    Запись форматированной строки в строку с ограничением по размеру строки
  • int vprintf( const char *format, va_list ap );
    Вывод форматированной строки на стандартный вывод, значения для вывода передаются в функцию в виде списка va_list
  • int vfprintf( FILE *stream, const char *format, va_list ap );
    Запись форматированной строки в поток, значения для вывода передаются в функцию в виде списка va_list
  • int vsprintf( char *s, const char *format, va_list ap );
    Запись форматированной строки в строку без ограничения размера; значения для вывода передаются в функцию в виде списка va_list
  • int vsnprintf( char *s, size_t n, const char *format, va_list ap );
    Запись форматированной строки в строку с ограничением на количество выводимых символов. Значения для вывода передаются в функцию в виде списка va_list
  • int fwprintf(FILE *stream, const wchar_t *format, ... );
    Запись форматированной строки многобайтовых символов в файл
  • int swprintf(wchar_t *ws, size_t n, const wchar_t *format, ... );
    Запись форматированной многобайтовой строки в область памяти (Примечание: несмотря на отсутствие буквы n в названии, эта функция принимает параметр, ограничивающий максимальный размер выходной строки).
  • int wprintf(const wchar_t *format, ... );
    Вывод многобайтовой форматированной строки на терминал
  • int wsprintf( LPTSTR s, LPCTSTR format, ... );
    Реализация функции sprintf в операционной системе Windows. В отличие от sprintf не поддерживает вывод значений с плавающей запятой, вывод указателей, дополнительно поддерживает обработку многобайтовых строк в однобайтовой версии функции), не поддерживает флаг '+'.
  • int wsnprintf( LPTSTR s, int n, LPCTSTR format, ... );
    Реализация функции snprintf в операционной системе Windows. Не поддерживает вывод значений с плавающей запятой и указателей, поддерживает только флаг выравнивания по левому краю.
  • int wvsprintf( LPTSTR s, LPCTSTR format, va_list ap );
  • int wvsnprintf( LPTSTR s, int n, LPCTSTR format, va_list ap );
  • int asprintf(char **strp, const char *format, ... );
    функция записывает результат в строку, память для которой выделяется при помощи malloc
  • int vasprintf(char **strp, const char *format, va_list ap);
    функция записывает результат в строку, память для которой выделяется при помощи malloc, значения для вывода передаются в функцию в виде списка va_list

Возвращаемое значение: отрицательное значение - признак ошибки; в случае успеха функции возвращают количество записанных/выведенных байтов (без учёта нулевого байта в конце), функция snprintf выводит количество байт, которые были бы записаны, если бы n было бы достаточного размера.

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

Утилита printf[править]

Icons-mini-icon 2main.png Основная статья: printf (утилита)

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

Утилита имеет следующий формат вызова: printf format [argument …], где

  • format — строка формата, по синтаксису похожая на строку формата функции printf.
  • argument — список аргументов (0 или более), записанных в строковой форме.

Синтаксис строки форматирования[править]

Файл:Printf illustration.svg
пример вызова printf с комментариями

Строка форматирования представляет из себя последовательность символов, заканчивающаяся символом с кодом 0. Все символы, кроме управляющих последовательностей, копируются в итоговую строку без изменений. Стандартным признаком начала управляющей последовательности является символ % (знак процента, ASCII код 37), для вывода знака '%' используется его удвоение '%%'.

Структура управляющей последовательности[править]

%[флаги][ширина][.точность][размер]тип
Обязательными полями являются символ начала управляющей последовательности (%) и тип.

Флаги[править]

  • - (знак минуса; точнее, дефиса, ASCII код 45) — выводимое значение выравнивается по левому краю в пределах минимальной ширины поля (если флаг не указан — по правому)
  • + (знак плюса) — всегда указывать знак (плюс или минус) для выводимого десятичного числового значения (если флаг не указан, то знак выводится только для отрицательных чисел)
  • (пробел) — помещать перед результатом пробел, если первый символ значения не знак. Символ + имеет больший приоритет, чем пробел. Используется только для десятичных числовых значений.
  • # (октоторп) — «альтернативная форма» вывода значения.
  • 0 (символ нуля) — дополнять поле символом 0 до ширины, указанной в поле ширина управляющей последовательности. Символ - имеет больший приоритет, чем 0. Используется для типов d, i, o, u, x, X, a, A, e, E, f, F, g, G. Для типов d, i, o, u, x, X, если точность указана, этот флаг игнорируется. Для остальных типов поведение не определено.

Ширина[править]

Ширина (десятичное число или символ звёздочка) указывает минимальную ширину поля (включая знак для чисел). Если представление величины больше, чем ширина поля, то запись выходит за пределы поля (например, %2i для величины 100 даст значение поля в три символа), если представление величины менее указанного числа, то оно будет дополнено, по-умолчанию, пробелами справа, поведение может меняться предшествующими флагами. Если в качестве ширины указана звёздочка, ширина поля указывается в списке аргументов перед значением для вывода (например, printf( "%0*x", 8, 15 ); выведет текст 0000000f).

Точность[править]

  • указывает на минимальное количество символов, которое должно появиться при обработке типов d, i, o, u, x, X;
  • указывает на минимальное количество символов, которое должно появиться после десятичной запятой (точки) при обработке типов a, A, e, E, f, F;
  • максимальное количество значащих символов для типов g и G;
  • максимальное число символов, которые будут выведены для типа s;

Точность задаётся в виде точки с последующим десятичным числом или звёздочкой (*), если число или звёздочка отсутствует (присутствует только точка), то предполагается, что число равно нулю. Точка для указания точности используется даже в том случае, если при выводе чисел с плавающей запятой выводится запятая.

Если после точки указан символ «звёздочка», то при обработке строки форматирования значение для поля читается из списка аргументов. (При этом, если символ звёздочка и в поле ширины и в поле точности, сначала указывается ширина, потом точность и лишь потом значение для вывода). Например, printf( "%0*.*f", 8, 4, 2.5 ); выведет текст 002.5000.

Размер[править]

Поле размер позволяет уточнить размер данных, переданных функции. Необходимость в этом поле объясняется особенностями передачи произвольного количества параметров в функцию в языке Си: функция не может «самостоятельно» определить тип и размер переданных данных, так что информация о типе параметров и точном их размере должна передаваться явно. С каждым типом ассоциирован свой размер (например, %i — 4 байта на 32 разрядных системах), а модификатор размера позволяет указать иной размер (например, %hi — 2 байта на 32 разрядных платформах).

Влияние размера на целочисленные типы данных:

размер целые (d, i, o, u, x, X) параметр n (см. ниже)
hh signed char или unsigned char указатель на signed char
h unsigned short int или short int указатель на short int
l long int или unsigned long int указатель на long int
ll long long int или unsigned long long int указатель на long long int
j intmax_t или uintmax_t указатель на intmax_t
z size_t (или эквивалентный по размеру знаковый тип) эквивалентный по размеру size_t знаковый тип
t ptrdiff_t (или эквивалентный по размеру знаковый тип) Эквивалентный по размеру ptrdiff_t знаковый тип
  • Для чисел с плавающей запятой (a, A, e, E, f, F, g, G) есть единственный модификатор: L: он указывает, что аргумент имеет тип long double.
  • значение l поля размер для типа «символ» (%c) указывает на размер символа wint_t (многобайтовый символ), для типа «строка» (%s) указывает на многобайтовую строку (тип: *wchar_t)

Тип[править]

Тип указывает не только на тип величины (с точки зрения языка программирования Си), но и на конкретное представление выводимой величины (например, числа могут выводить в десятичном или шестнадцатеричном виде). Записывается в виде одного символа. В отличие от остальных полей является обязательным. Максимальный поддерживаемый по стандартам размер вывода от единичной управляющей последовательности составляет 4095 символов; на практике большинство компиляторов поддерживают существенно большие объёмы данных.

Значения типов:

  • d, i — десятичное знаковое число, размер по-умолчанию, sizeof( int ). По-умолчанию записывается с правым выравниванием, знак пишется только для отрицательных чисел;
  • o — восьмеричное беззнаковое число, размер по-умолчанию sizeof( int );
  • u — десятичное беззнаковое число, размер по-умолчанию sizeof( int );
  • x и X — шестнадцатеричное число, x использует маленькие буквы (abcdef), X большие (ABCDEF), размер по-умолчанию sizeof( int );
  • f и F — числа с плавающей запятой. По-умолчанию выводятся с точностью 6, если число по модулю меньше единицы, перед десятичной точкой пишется 0. Величины ±∞ представляются в форме [-]inf или [-]infinity, Величина Nan представляется как [-]nan или [-]nan(любой текст далее). Использование F выводит указанные величины заглавными буквами (-INF, NAN). Аргумент по-умолчанию имеет размер double.
  • e и E — числа с плавающей запятой в экспоненциальной форме записи (вида 1.1e+44); e выводит символ «e» в нижнем регистре, E — в верхнем (3.14E+0);
  • g и G — число с плавающей запятой; форма представления зависит от значения величины (f или e);
  • a и A — число с плавающей запятой в шестнадцатеричном виде;
  • c — вывод символа с кодом, соответствующим переданному аргументу; переданное число приводится к типу usnigned char (или wint_t, если был указан модификатор длины l);
  • s — вывод строки с нулевым завершающим байтом; если модификатор длины - l, выводится строка wchar_t*;
  • p — вывод указателя, внешний вид может существенно различаться в зависимости от внутреннего представления в компиляторе и платформе (например, 16 битная платформа MS-DOS использует форму записи вида FFEC:1003, 32-битная платформа с плоской адресацией использует адрес вида 00FA0030);
  • n — запись по указателю, переданному в качестве аргумента количество символов, записанных на момент появления командной последовательности, содержащей n;
  • % — символ для вывода знака процента (%), используется для возможности вывода символов процента в строке printf, всегда используется в виде %%.
Вывод чисел с плавающей запятой[править]

В зависимости от текущей локали, при выводе чисел с плавающей запятой может использоваться как запятая, так и точка (а, возможно, и другой символ). Поведение printf в отношении разделяющего дробную и целую часть числа символа определяется использующейся локалью (точнее, переменной LC_NUMERIC)[13].

XSI расширения в стандарте Single Unix[править]

В рамках стандарта Single UNIX (практически эквивалентного стандарту POSIX), определены следующие дополнения printf по отношению к ISO C, в рамках расширения XSI (X/Open System Interface):

  • Добавляется возможность вывода произвольного по номеру параметра (указывается в виде n$ сразу после символа начала управляющей последовательности, например, printf("%1$d:%2$.*3$d:%4$.*3$d\n", hour, min, precision, sec);).
  • Добавлен флаг «'» (одинарная кавычка), который для типов d, i, o, u предписывает разделять классы соответствующим символом.
  • тип C, эквивалентный lc ISO C (вывод символа типа wint_t).
  • тип S, эквивалентный ls ISO C (вывод строки типа wchar_t*)
  • Добавлены коды ошибок EILSEQ, EINVAL, ENOMEM, EOVERFLOW.

Нестандартные расширения[править]

GNU C Library[править]

В рамках GNU C Library (libc) добавлены следующие расширения:

  • тип m выводит значение глобальной переменной errno (код ошибки последней функции).
  • тип C эквивалентен lc.
  • флаг ' (одинарная кавычка) используется для разделения классов при выводе чисел. Формат разделения зависит от LC_NUMERIC
  • размер q указывает на тип long long int (на системах, где не поддерживается тип long long int, это то же самое, что и long int
  • размер Z является псевдонимом для z, был введён в libc до появления стандарта C99, не рекомендуется к использованию в новом коде.
Регистрация собственных типов[править]

GNU libc поддерживает регистрацию пользовательских типов, позволяя программисту определять формат вывода для собственных структур данных. Для регистрации нового типа используется функция
int register_printf_function (int type, printf_function handler-function, printf_arginfo_function arginfo-function), где:

  • type — буква для типа (если type = 'Y', то вызов будет выглядеть как '%Y');
  • handler-function — указатель на функцию, которая вызывается, printf-функциями, если в строке форматирования встречается тип, указанный в type;
  • arginfo-function — указатель на функцию, которая будет вызываться функцией parse_printf_format.

Помимо определения новых типов регистрация позволяет переопределить существующие типы (такие, как s, i).

Microsoft Visual C[править]

В составе Microsoft Visual Studio для языков программирования Cи/Cи++ в формате спецификации printf (и остальных функций семейства) предусмотрены следующие расширения:

  • поле размера:
значение поля тип
I32 signed __int32, unsigned __int32
I64 signed __int64, unsigned __int64
I ptrdiff_t, size_t
w эквивалентно l для строк и символов

Maple[править]

В среде математических вычислений Maple также имеется функция printf, она имеет следующие особенности:

Форматирование[править]
    •  %a, %A: объект Maple будет выдан в текстовой нотации, это работает для всех объектов (например, матриц, функций, модулей и т. д.). Строчная буква предписывает окружать обратными апострофами символы (имена), которые должны быть окружены ими на входе printf.
    •  %q, %Q: то же, что и %a/%A, но обрабатываться будет не один аргумент, а все начиная с того, которому соответствует флаг форматирования. Таким образом, флаг %Q/%q может стоять только последним в строке формата.
    •  %m: форматировать объект в соответствии с его внутренним для maple представлением. Практически используется для записи переменных в файл.

Пример:

> printf("%a = %A", `+`, `+`);
`+` = +
> printf("%a = %m", `+`, `+`);
`+` = I"+f*6"F$6#%(builtinGF$"$Q"F$F$F$F"%*protectedG
Вывод[править]

Функция fprintf в maple в первом аргументе принимает либо дескриптор файла (возвращаемый функцией fopen), либо имя файла. В последнем случае имя должно имет тип "символ", если имя файла содержит точки, то его обязательно нужно заключить в обратные апострофы или преобразовать функцией convert(имя_файла, symbol).

Уязвимости[править]

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

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

Поведение при несоответствии строки форматирования и переданных аргументов[править]

Стек при правильном вызове printf

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

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

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

Избыточное количество аргументов[править]

Файл:Printf stack leftovers.svg
Стек при вызове printf с излишним количеством аргументов

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

Недостаточное количество аргументов[править]

Стек при вызове с меньшим количеством параметров, чем указано в строке форматирования

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

При обработке «лишних» значений стека, возможны следующие ситуации:

  • успешное чтение «лишнего» параметра для вывода (число, указатель, символ и т. д.) — в результаты вывода помещается «почти случайное» значение, прочитанное со стека. Это не представляет из себя опасности для работы программы, но может приводить к компрометации каких-либо данных (вывод значений стека, которые может использовать злоумышленник для анализа работы программы и получению доступа к внутренней/закрытой информации программы).
  • ошибка при чтении значения со стека (например, в результате исчерпания доступных значений стека или доступ к «несуществующим» страницам памяти) — такая ошибка вероятнее всего приведёт к аварийному завершению работы программы.
  • чтение указателя на параметр. Строки передаются с помощью указателя, при чтении «произвольной» информации со стека, прочитанное (почти случайное) значение используется как указатель на случайную область памяти. Поведение программы в этом случае не определено и зависит от содержимого этой области памяти.
  • запись параметра по указателю (%n) — в данном случае поведение аналогично ситуации с чтением, но осложняется возможными сторонними эффектами записи в произвольную ячейку памяти.

Несоответствие размеров аргументов[править]

Если размер переданных аргументов не соответствует ожидаемому (согласно строке форматирования), то поведение функции формально не определено. На практике возможны следующие ситуации:

Состояние стека при вызове функции с параметрами, размер которых больше ожидаемого
  • Передача параметров, размер которых больше ожидаемого (чтение меньшего из большего). При этом на архитектурах с little endian порядком байтов значение будет выведено корректно, на big endian будет выведена бессмысленная информация. При этом последующее значение, даже если его размер соответствует указанному в строке форматирования будет выведено неправильно.
Файл:Printf stack lack.svg
Состояние стека при вызове функции с параметрами, размер которых меньше ожидаемого
  • Передача параметров, размер которых меньше ожидаемого (чтение большего из меньшего). В этом случае возможна ситуация, когда читаются области стека, выходящие за пределы переданных аргументов. Поведение функции в этом случае аналогично поведению в ситуации с недостатком параметров.

Несоответствие типов при совпадении размеров[править]

Если переданные аргументы совпадают по размеру, но имеют отличный тип, то в большинстве случаев работа программы будет «почти правильной» (не вызовет ошибок доступа к памяти), хотя выводимое значение вероятнее всего будет бессмысленным (например, указание %hhi вместо %c даст правильный размер, но приведёт к выводу числа вместо символа). В случае ошибки между знаковым и беззнаковым типом ошибка будет "скрытой" (будет проявляться только на больших беззнаковых и отрицательных числах). В случае ошибки между числовым и строковым типом (вывод строки вместо числа) возможны ошибки доступа к памяти.

Выравнивание значений на стеке[править]

Файл:Printf stack padded.svg
Стек при вызове функции с учётом выравнивания по размеру машинного слова (32 бита, little-endian)
Стек при вызове функции, которая ожидает параметры, размер которых меньше размеров машинного слова

Во многих компиляторах при передаче значений через стек значение выравнивается до размера машинного слова, в этом случае незначительные ошибки в указании размеров (особенно в little-endian архитектуре) приводят к правильному поведению программы, несмотря на неправильные размеры параметров. Например, строка printf("%i=%i", 'a', 'b') отработает правильно на большинстве архитектур (несмотря на то, что размер %i - int (32 или 64 бита), а размер 'a' (char) - 8 бит.

Уязвимость строки форматирования[править]

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

Пример некорректного кода:
printf(" Current status: 99 % stored.");
В этом примере содержится управляющая последовательность «% s», содержащая признак управляющей последовательности (%), флаг (пробел) и тип данных «строка» (s). Функция, приняв управляющую последовательность, попытается прочитать из стека указатель на строку. Так как функции не передавались дополнительные параметры, значение, которое будет прочитано со стека, не определено. Полученное значение будет интерпретировано как указатель на строку с завершающим нулём. Вывод такой «строки» может привести к выводу произвольного дампа памяти, ошибке доступа к памяти и разрушению стека. Такой тип уязвимости называют атакой на строку форматирования (англ. Format string attack)[14].

Переполнение буфера[править]

Функция printf при выводе результата не ограничивается по максимальному количеству выводимых символов. Если в результате ошибки или недосмотра будет выведено больше символов, чем ожидалось, худшее, что может случиться — это «разрушение» картинки на экране. Созданная по аналогии с printf функция sprintf также не ограничивалась в максимальном размере результирующей строки. Однако, в отличие от «бесконечного» терминала, память, которую выделяет приложение для результирующей строки, всегда ограничена. И в случае выхода за ожидаемые рамки, запись производится в области памяти, принадлежащие другим структурам данных (или, вообще, в недоступные участки памяти, что практически на всех платформах означает аварийное завершение программы). Запись в произвольные области памяти приводит к непредсказуемым эффектам (которые, возможно, проявятся много позже и не в форме ошибки программы, а в форме повреждения пользовательских данных). Отсутствие ограничения на максимальный размер строки является принципиальной ошибкой планирования при разработке функции. Именно из-за этого функции sprintf и vsprintf имеют статус небезопасных. Взамен им были разработаны функции snprintf, vsnprintf, принимающие дополнительный аргумент, ограничивающий максимальную результирующую строку. Появившаяся много позже функция swprintf (для работы с многобайтными кодировками) учитывает эту недоработку и принимает аргумент для ограничения результирующей строки. (Именно поэтому нет функции snwprintf).

Пример опасного вызова sprintf:

char buffer[65536];
char* name = get_user_name_from_keyboard();
sprintf(buffer, "User name:%s", name);

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

Сложности в использовании[править]

Функции семейства printf используют типы данных языка Си, размер типов, и соотношение размеров типов может меняться от платформы к платформе (например, на 64-битных платформах в зависимости от выбранной модели (LP64, LLP64 или ILP64) размеры типов int и long могут различаться). При этом "правильно" работавший код на одной платформе начинает выдавать неправильный результат на другой (в ряде случаев, возможно, приводя к повреждению данных).

Например, код printf( "text address: 0x%X", "text line" ); работает правильно на 32-битной платформе (размер ptrdiff_t и размер int 32 бита) и на 64-битной модели IPL64 (где размеры ptrdiff_t и int 64 бита), но даст неверный результат на 64-битной платформе модели LP64 или LLP64, где размер ptrdiff_t 64 бита, а размер int 32 бита [15].

Ссылки[править]

  1. Краткое описание языка BCPL
  2. Руководство по языку Би
  3. Описание функции sprintf в документации Perl
  4. Описание форматирующего оператора для строковых типов в Питоне
  5. Описание функции printf в составе PHP
  6. Описание функции java.io.PrintStream.printf() в Java 1.5
  7. Описание функции printf в документации Ruby
  8. Описание шаблона строки для printf в документации GNU Octave
  9. Описание printf в документации к Maple[Источник?]
  10. R. Fourer, D.M. Gay, and B.W. Kernighan. AMPL: A Modeling Language for Mathematical Programming, 2nd Ed.. Pacific Grove, CA: Brooks/Cole--Thomson Learning, 2003.
  11. GNU Emacs Lisp Reference Manual, Formatting Strings
  12. Описание модуля Printf в документации OCaml
  13. §7.11.1.1 ISO/IEC 9899:TC2, LC_NUMERIC определяет, в частности, форму представления разделителя дробной и целой части.
  14. Описание уязвимостей printf, Robert C. Seacord: Secure Coding in C and C++. Addison Wesley, September, 2005. ISBN 0-321-33572-4
  15. Описание проблем переноса приложений с 32 на 64 битную архитектуру. [1]

Источники[править]

  • printf, fprintf, snprintf, vfprintf, vprintf, vsnprintf, vsprintf в стандарте ISO/IEC 9899:TC2 (ISO C) [2]
  • printf, fprintf, sprintf, snprintf в стандарте Single Unix [3]
  • vprintf, vfprintf, vsprintf, vsnprintf в стандарте POSIX [4]
  • wprintf, swprintf, wprintf в стандарте POSIX [5]
  • vfwprintf, vswprintf, vwprintf в стандарте POSIX [6]
  • wsprintf в MSDN [7]
  • wvnsprintf в MSDN [8]
  • wnsprintf в MSDN [9]
  • wvsprintf в MSDN [10]
  • wnsprintf в MSDN [11]
  • asprintf, vasprintf в man-pages в Linux [12], в документации к libc [13]
  • Описание синтаксиса строки форматирования в руководстве libc [14].
  • Описание строки форматирования в документации к Microsoft Visual Studio 2005 [15]
  • Описание register_printf_function [16], [17]

См также[править]

Первоисточник этой статьи был признан «хорошей статьёй» русского раздела Википедии.