Отличия формата для printf windows и linux

Обновлено: 04.07.2024

Глубокое понимание функции printf с разных точек зрения

Функция printf () принадлежит функции стандартной библиотеки языка C и используется для вывода отформатированной строки в стандартный вывод stdout. Стандартный вывод, стандартный выходной файл. printf () - это функция, связанная с платформой. На ПК printf () выводится на экран терминала, который является соответствующим стандартным выводом, на встроенных устройствах printf () обычно выводится на последовательный порт, а последовательный порт - соответствующий стандартный вывод.

1. Общие функции прототипов

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

3. Формат звонка

Таблица параметров: Содержит 0 или более параметров, каждый параметр может быть переменной, константой, выражением, функцией и т. Д.

Строка формата: Содержит описание преобразования, соответствующее каждому элементу параметра.

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

Среди них% d означает перевод данного значения в десятичный целочисленный текст и его распечатку.

4. Возвращаемое значение

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

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


Компилятор и компоновщик

1. Целевой код

Код машинного языка исходного кода (concrete.c).

2. Код библиотеки

Код объекта библиотечной функции.

3. Код запуска

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

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

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

1. Связь между оперативной памятью и стеком

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


Распределение ОЗУ

Площадь РАО: Сохраняются глобальные переменные и локальные статические переменные, которые были инициализированы и не инициализированы в 0.

ЗИ область: Храните глобальные переменные и локальные статические переменные, которые не инициализированы или не установлены в 0.

стек: Сохраните локальные переменные программы, сохраните переданные параметры и возвращаемое значение вызова функции и увеличьте с высокого адреса до низкого адреса памяти.

Положение указателя стека SP вычисляется в соответствии с кодом и размером, установленным стеком, и не фиксируется в конце адреса.

Распределение ОЗУ определяется после того, как компилятор его скомпилирует, и его можно проверить, просмотрев файл скомпилированной карты в keil.

Вы можете просмотреть начальный адрес sp, установленный с помощью __initial_sp в файле карты, и просмотреть положение указателя на вершине стека.

2. Зачем мне стек, если у меня есть ОЗУ?

Предположим, что в языке Си у нас есть такая функция: int function (int a, int b)

При вызове вы можете использовать эту функцию до тех пор, пока result = function (1,2). Однако, когда язык высокого уровня компилируется в машинный код, который может распознавать компьютер, возникает проблема: в ЦП у компьютера нет возможности узнать, сколько и каких параметров требует вызов функции, и нет аппаратного обеспечения для сохранения этих параметров. , Другими словами, компьютер не знает, как передать параметры этой функции, и работа по передаче параметров должна координироваться вызывающей функцией и самой функцией. По этой причине компьютер предоставляет структуру данных, называемую стеком, для поддержки передачи параметров.

3. Протокол вызова функции

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

(1) Общие случаи вызова протокола

__stdcall: протокол вызова функций по умолчанию Windows API

__cdecl: C / C ++ протокол вызова функций по умолчанию

(2) Способ суммирования параметров функции

__stdcall: параметры функции помещаются в стек справа налево

__cdecl: параметры функции помещаются в стек справа налево

(3) метод очистки данных в стеке

__stdcall: после вызова функции вызываемая функция очистит данные в стеке

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

(4) Правила изменения имени функции компилятора языка C

__stdcall: после компиляции имя функции изменяется на "_functionname @ number"

__cdecl: после компиляции имя функции изменяется на "_functionname"

4. Функция в стек

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

Следующая структура стека:


структура стека функции fun ()

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

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

1. Передача параметров

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

Компьютер или микросхема помещают значение переменной в стек в соответствии с типом переменной, а затем управление передается в функцию printf (), которая получает значение из стека в соответствии с инструкциями преобразования. Спецификация преобразования% d указывает, что printf () должна прочитать 4 байта, поэтому printf () считывает первые 4 байта в стеке как первое значение и так далее.

2. Реализация переменных параметров

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

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

Так что для следующей функции

Мы можем получить

Принимая во внимание выравнивание байтов, мы можем определить x_sizeof как

В «Языке программирования C» Ритчи предоставляет простую версию функции printf:

va_list: Можно понимать как символ *.

va_start: Его можно понимать как адрес первого параметра переменной.

va_arg: Можно понимать как адрес следующего параметра.

va_end: Можно понимать как освобождение указателя ap.

Определив различные источники log_src, журнал можно распечатать иерархически.

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

На примере системы Linux, отслеживая printf () слой за слоем, мы можем получить следующие отношения вызовов:


Подобно остальным уязвимостям, нужны Format String Attacks для того, чтобы получить неправомерный доступ к программе и делать с ней все, что захочется. Одна из важных особенностей этой уязвимости — безразличие к дополнительным меры защиты вроде w^x и ASLR . И самое главное — она позволяет обойти и относительно новую защиту CFI .

Приступим?

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

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

  • Вывести строку
  • Заменить спец.символы начинающиеся с %
  • Вернуть количество успешно выведенных символов

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


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

Дело в том, что для производительности значения на стеке не обнуляются. Выделение/освобождение памяти происходит путем уменьшения/увеличения соответствующего указателя на стеке. Так что 47 — просто некоторое произвольное число каких-то побочных вычислений

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

Почему именно 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 бутылок пива и много чего еще интересного.

Эта команда была создана для замены всем знакомой команды echo. Возможности команды echo ограничены, кроме того, в разных ветках Unix'а оказались разные ее версии, что приводит к несовместимости. Поэтому Posix рекомендует пользоваться командой printf. Команда printf выводит АРГУМЕНТ на стандартный вывод (как правило, экран дисплея), используя при этом определенный ФОРМАТ.

Эта команда была создана для замены всем знакомой команды echo (настолько древней, что даже имя ее автора не известно).


Самое первое, что следует прояснить: по умолчанию команда printf не переводит строку по завершении. Это выглядит так:

Следующее приглашение командной строки находится на той же самой строке что и вывод предыдущей команды. Это очень неудобно, поэтому в ФОРМАТ приходится добавлять символ новой строки (\n):

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

Вот как выглядит команда printf в виде, полностью дублирующем команду echo:

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

Если нам нужно использовать команду printf для просмотра переменных окружения, то кавычки не нужны:

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

Но довольно о замене команды echo, займемся непосредственно командой printf.

Описание и использование

Представление аргумента

N Нормальное десятичное число. Например 177

0N Восьмеричное число. Например 024.

0xN и 0XN Шестнадцатеричные числа. Например 0?41.

"А (обычные кавычки перед любой буквой) Интерпретируется как кодовый номер этой буквы в текущей кодировке. С кириллицей не работает.

'А (одинарные кавычки перед любой буквой) Интерпретируется как кодовый номер этой буквы в текущей кодировке. С кириллицей не работает.

Форматы команды printf

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

ФОРМАТ пишется в командной строке непосредственно после самой команды и заключается в кавычки - двойные или одинарные. Собственно говоря, кавычки нужны не всегда, но лучше их ставить, во избежание недоразумений. Это полезная привычка. (А ведь большинство привычек - вредные).

%b Рассматривает аргумент как строку, при этом интерпретирует все управляющие символы, содержащиеся в ней.

%s Рассматривает аргумент как просто как строку.

%c Рассматривает аргумент как символ, при этом берется первый символ выражения или строки.

%q Преобразует строку к виду, пригодному к использованию в качестве ввода в шелл.

Здесь необходимы пояснения. Допустим, мы хотим напечатать на экране фразу:

Для этого дадим следующую команду:

А теперь вместо формата %b применим формат %q:

Кажется, произошла какая-то ошибка или сбой. Но это не так; подставим вывод команды в качестве аргумента команды printf '%b\n', но уже безо всяких дополнительных кавычек (все необходимые кавычки содержатся в выводе команды):

Больше того, этот же вывод можно использовать в качестве аргумента для других команд, скажем echo -e (опция -e позволяет команде echo интерпретировать специальные символы, в частности символ новой строки (\n):

Вот какой интересный формат %q! Однако продолжим.

Дальше идут всевозможные форматы представления чисел.

%d Представляет аргумент в виде десятичного числа, могущего иметь знак (+ или -).

%u Представляет аргумент в виде десятичного числа, не имеющего знака.

%i То же, что и предыдущее.

%o Представляет аргумент в виде не имеющего знака восьмеричного числа.

%x Представляет аргумент в виде не имеющего знака шестнадцатеричного числа. Буквы пишутся в нижнем регистре.

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

%f Интерпретирует аргумент как число с плавающей запятой.

%e Интерпретирует аргумент с удвоенной точностью (double precision), при этом выводит его в формате <N>+/-e<N>.

%E То же, что и предыдущее, только с заглавной буквой Е.

Вот и все форматы, я привел их для встроенной в bash команды printf.

Модификаторы форматов

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


(С кириллицей не работает).

Модификаторы: Любое число Обозначает минимальную ширину колонки, если текст короче, то строки дополняются пробелами. (См. пример выше).

- Выравнивание текста по левому краю (стандарно - по правому краю)

0 Дополняет числа нулями, а не пробелами. Применяется при заданной ширине колонки (в примере 50). Пример:

space Дополняет положительные числа пробелами, а отрицательные знаками "минус" (-).

+ Пишет все числа со знаками плюс или минус.

Альтернативный формат для чисел

Точность разрешения

Точность разрешения чисел с плавающей запятой и чисел с удвоенной точностью (double precision) можно регулировать следующим образом:

Внимание: не прозевайте точку в записи формата!

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

Для строк точность разрешения определяет максимальное число выводимых символов (максимальную ширину колонки текста).

Для целых чисел - задает число выводимых знаков (добавляет нули).

Управляющие символы языка Си, работающие с командой printf

\" Двойные кавычки

\NNN Символ с восьмеричным значением NNN (от 1 до 3 цифр)

\\ Обратный слэш (\)

\a Звуковой сигнал

\c Не производить дальнейшую обработку данных

\f Перевод страницы

\n Новая строка

\r Возврат каретки

\v Вертикальная табуляция

\xHH Символ с шестнадцатеричным кодом HH (1 или 2 цифры)

\uHHHH Символ Unicode (ISO/IEC 10646) с шестнадцатеричным кодом HHHH (4 цифры)

\UHHHHHHHH Символ Unicode с шестнадцатеричным кодом HHHHHHHH (8 цифр)

%% Символ %

Еще несколько примеров

1. Вывести шестнадцатеричное число в десятичной форме:

2. Вывести десятичное число в восьмеричной форме:

3. Узнать кодовый номер буквы А (англ):

4. Пример, служащий домашним заданием:

Кто понял, в чем тут дело, сообщите мне, пожалуйста. Я лично не понял.

Опции команды printf

Опция -v ПЕРЕМЕННАЯ_ОКРУЖЕНИЯ

Записывает аргумент в указанную переменную окружения. При этом затирает все, что там было прописано. Осторожно!


Хорошо, что в начале статьи я привел пример, в котором была записана моя $PATH, поэтому смог восстановить.

Опции --help и --version общеизвестны и рассмотрены не будут.

Примечание: В мане GNU-версии команды вы не найдете и половины этой информации, а будете отосланы к синтаксису языка Си.

Резюме команды printf

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

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

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

printf (от фразы «print formatted») изначально был разработан для языка программирования C и был реализован во многих языках программирования, включая оболочку. Фактически, в bash printf является встроенной командой.

printf работает так:

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

Строка формата может содержать буквальный текст (например, «Я отформатировал строку:»), escape-последовательности (например, \n, символ новой строки) и последовательности, начинающиеся с символа %, которые называются спецификациями преобразования. В приведённом выше примере спецификация преобразования %s используется для форматирования строки «аргумент» и помещения её в выходные данные команды. И снова:

Как мы видим, спецификация преобразования %s заменена строкой «аргумент» в выводе команды. Преобразование s используется для форматирования строковых данных. Есть и другие спецификаторы для других видов данных. В этой таблице перечислены наиболее часто используемые типы данных:

Общие спецификаторы типов данных printf

Спецификатор Описание
d Форматировать число как десятичное целое со знаком.
f Форматировать и вывести число с плавающей запятой.
o Форматировать целое число как восьмеричное число.
s Форматировать строку.
x Форматировать целое число как шестнадцатеричное, используя строчные буквы a-f, где необходимо.
X То же, что и x, но с прописными буквами.
e Научная нотация (экспоненциальная запись) в форме [-]d.ddde±dd, например, 1,233300e+04
E То же, что и e, но с прописной буквой E, например, 1,233300E+04.
% Вывести буквальный символ (т.е. укажите «%%»)

Мы продемонстрируем влияние каждого из спецификаторов преобразования на строку «380»:

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

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

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

Компоненты спецификации преобразования printf

Есть пять разных флагов:

0 – (ноль) означает заполнит вывод нулями. Это означает, что поле будет заполняться ведущими нулями, как в «000380».

- – (тире) означает выровнять вывод по левому краю. По умолчанию printf выравнивает выход по правому краю.

‘ ’ – (пробел) создаёт начальный пробел для положительных чисел.

+ – (знак плюса) добавит знак к положительным числам. По умолчанию printf пишет знак только для отрицательных исел.

Вот несколько примеров использования различных форматов:

Примеры спецификации преобразования printf

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

Вставив \t (escape-последовательность для табуляции), мы достигаем желаемого эффекта. Далее несколько чисел с аккуратным форматированием:

Это показывает влияние минимальной ширины поля на расстояние между полями.

А как насчёт форматирования крошечной веб-страницы:


Если при использовании printf вы столкнулись с ошибкой «недопустимое число», например:

bash: printf: 3.14156295: недопустимое число

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

Суть в том, что в разных странах в качестве десятичной точки используется либо точка, либо запятая. В компьютере принимаемый в качестве десятичной точки символ зависит от локали ОС. Если вы получили указанную ошибку, то просто попробуйте заменить точку в числе на запятую (или наоборот, запятую на точку), например, вместо числа 3.14156295 используйте чисто 3,14156295.

Есть ли способ (с небольшим изменением кода) получить тот же результат в Linux и Windows (и других) или мне нужно использовать одну и ту же реализацию во всех системах? Я боюсь иметь собственную реализацию для моих платформ Windows / Linux / Linux-ARM / VxWorks / Solaris.

3 ответа

На этих платформах функция printf реализована по-разному.

Взгляните на этот код:

Эта программа, скомпилированная на VC ++, дает:

А та же программа, скомпилированная с g ++, дает:

Есть ли способ (с небольшим изменением кода) получить тот же результат в Linux и Windows?

Используйте gcc в Windows. Под "Windows", безусловно, OP имеет в виду компилятор Visual Stdio или связанный с ним продукт. gcc доступен для Windows, Linux и многих других платформ и дает более стабильные результаты, чем примеры OP. На самом деле это проблема компилятора , а не ОС .

Используйте вывод base-2/16.

Используйте "%.e" с ограниченной точностью. Спецификация C указывает только минимальную точность в 6 цифр для float и 10 для double , иначе используйте FLT_DIG/DBL_DIG .

Подробнее о "%.e" . Есть смысл в том, чтобы не печатать более FLT_DECIMAL_DIG/DBL_DECIMAL_DIG значащих цифр из-за крайних случаев, когда обратное сканирование числа приведет к следующему числу FP. По сути, проблема с двойным округлением - несколько глубокая для этого поста - поэтому никаких подробностей.

Конечно, все это спорный вопрос, если различные системы используют совершенно разные форматы FP, что весьма вероятно с long double . Точная согласованность FP сложно, но вышеизложенное, безусловно, поможет минимизировать различия.

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

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

FLT_MAX составляет около 3,40282346639e + 38. Следующее меньшее число, которое можно представить как float , составляет примерно 3,40282326356e + 38. Это разница на 2,02824096037e + 31 или на десять порядков больше, чем ваша предполагаемая ошибка.

Даже если очевидная разница между числами кажется огромной, оба напечатанных значения намного ближе к FLT_MAX , чем к любому другому числу с плавающей запятой одинарной точности, и преобразовывают текстовое представление в «плавающее» > FLT_MAX`.

Читайте также: