惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

E
Exploit-DB.com RSS Feed
O
OpenAI News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hacker News - Newest:
Hacker News - Newest: "LLM"
N
News and Events Feed by Topic
博客园 - Franky
F
Fortinet All Blogs
Hugging Face - Blog
Hugging Face - Blog
T
Tailwind CSS Blog
Forbes - Security
Forbes - Security
S
Security Affairs
S
Security @ Cisco Blogs
Engineering at Meta
Engineering at Meta
Recorded Future
Recorded Future
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
H
Hacker News: Front Page
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
Netflix TechBlog - Medium
The Register - Security
The Register - Security
G
Google Developers Blog
S
Secure Thoughts
Cloudbric
Cloudbric
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Project Zero
Project Zero
V
Visual Studio Blog
GbyAI
GbyAI
宝玉的分享
宝玉的分享
Schneier on Security
Schneier on Security
T
Tor Project blog
T
Threat Research - Cisco Blogs
S
Schneier on Security
小众软件
小众软件
C
Cisco Blogs
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
Stack Overflow Blog
Stack Overflow Blog
L
Lohrmann on Cybersecurity
D
DataBreaches.Net
P
Proofpoint News Feed
A
About on SuperTechFans
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Scott Helme
Scott Helme
S
SegmentFault 最新的问题
Recent Announcements
Recent Announcements
P
Privacy & Cybersecurity Law Blog
Hacker News: Ask HN
Hacker News: Ask HN
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
OSDEV: полная реализация стандартной vsnprintf без поддержки чисел с плавающей точкой
GNU_Dimarik · 2026-06-15 · via Все публикации подряд на Хабре

Приветствую!

Код лежит тут

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

В первую очередь нужно сказать что ориентироваться будем на этот документ, но реализации расширений gnu в духе $, m$ не будет за исключением всего двух: вывод строки (null) вместо nullptr для спецификатора %s и строки (nil) вместо nullptr для спецификатора %p. Я уже не помню в какой версии glibc я видел (Nil) но мне это очень понравилось. Так же форматирование %p будет реализовано как в glibc.

Сначала посмотрим на это графически:

Теперь текстом:

Список поддерживаемых нами спецификаторов следующий: d, i, u,x, X, o, c, s, p, n

Список поддерживаемых нами модификаторов размера следующий: h, hh, l, ll, z, t,

Общий вид строки форматирования следующий:

%[flags][field_width][.precision][size modifier]conversion specifier

Начнем с флагов:

Флаг

Описание

Спецификатор для которого поддерживается флаг

#

Альтернативная форма. Это строка 0x или 0X для %x, %X или символ '0' для %o перед непосредственно значением

%x, %X, %o

0

Значение должно быть дополнено нулями (до ширины поля). Иначе пробелами

%d, %i, %o, %u, %x, %X

-

Значение выравнивается по левой стороне. Это поведение по умолчанию

%d, %i, %o, %u, %x, %X

+

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

%d, %i

' '

Выводить пробел перед положительным (знаковым) числом (если нет флага +)

%d, %i

enum FormatFlags: int
{
    F_NONE = 0,
    F_MINUS = 1 << 0,
    F_PLUS = 1 << 1,
    F_SPACE = 1 << 2,
    F_ALT = 1 << 3,
    F_ZERO = 1 << 4,
    F_HUGE = 1 << 5,
};

Флаг 0 игнорируется, если задан флаг - (минус)или указана точность.

В случае если мы передаем nullptr для %p у нас выводится строка (Nil) которая форматируется как %s.

Что касается ширины поля, важно понимать: что ширина поля никогда не обрезает вывод она может только добавлять пробелы или нули если установлен флаг '0' и соблюдены все остальные условия.

Точность указывает лишь минимальное количество символов которые нужно вывести.

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

Для строк точность означает максимальное количество символов которое будет выведено, т.е. строка может быть обрезана если она длинее.

Модификаторы размера:

Модификатор

Тип аргумента

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

hh

signed char / unsigned char

d, i, u, o, x, X

h

short int / unsigned short int

d, i, u, o, x, X

(нет)

int / unsigned int

d, i, u, o, x, X

l

long int / unsigned long int

d, i, u, o, x, X

ll

long long int / unsigned long long

d, i, u, o, x, X

j

intmax_t / uintmax_t

d, i, u, o, x, X

z

size_t / ssize_t

d, i, u, o, x, X

t

ptrdiff_t

d, i, u, o, x, X

enum class LengthSpec: int
{
    null,
    h,
    hh,
    l,
    ll,
    z,
    t
};

Сразу же обращаю ваше внимание что если вы считываете из стека значение не того типа, то все сломается.

Спецификаторы преобразования:

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

Тип аргумента

Описание

d

int

Знаковое десятичное целое число (base 10).

i

int

Знаковое целое число эквивалентно d

u

unsigned int

Беззнаковое десятичное целое число (base 10).

x

unsigned int

Беззнаковое шестнадцатеричное число (a–f в нижнем регистре).

X

unsigned int

Беззнаковое шестнадцатеричное число (A–F в верхнем регистре).

o

unsigned int

Беззнаковое восьмеричное число (base 8).

c

int

Символ (значение приводится к unsigned char).

s

char *

Строка, заканчивающаяся нулевым символом \0.

p

void *

Указатель выводится в шестнадцатеричном виде с точностью sizeof(void *) * 2 символов без префикса 0x

n

int *

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

enum class LengthSpec: int
{
    null,
    h,
    hh,
    l,
    ll,
    z,
    t
};

// ...

static long long read_signed_number(const FormatSpec &formatSpec, va_list_wrapper &w)
{
    long long value = 0;

    switch (formatSpec.lengthSpec) {
        case LengthSpec::l:
            value = va_arg(w.ap, long);
            break;

        case LengthSpec::z:
            value = static_cast<int64_t> (va_arg(w.ap, int64_t));
            break;

        case LengthSpec::hh: {
          // необходимо обрезать значение до char.
          // буквально, как требует модификатор!
          // Иначе будет работать некорректно!
            char v = va_arg(w.ap, int);
            value = v;
        }
            break;

        case LengthSpec::h:
            value = va_arg(w.ap, int);
            break;

        case LengthSpec::t:
            value = va_arg(w.ap, ptrdiff_t);
            break;

        case LengthSpec::ll:
        default:
            value = va_arg(w.ap, long long);
            break;
    }

    return value;
}

static unsigned long long read_unsigned_number(const FormatSpec &formatSpec, va_list_wrapper &w)
{
    unsigned long long value = 0;

    switch (formatSpec.lengthSpec) {
        case LengthSpec::l:
            value = va_arg(w.ap, unsigned long);
            break;

        case LengthSpec::z:
            value = static_cast<size_t> (va_arg(w.ap, size_t));
            break;

        case LengthSpec::hh: {          
          // необходимо обрезать значение до unsigned char.
          // буквально, как требует модификатор!
          // Иначе будет работать некорректно!
            unsigned char v = va_arg(w.ap, unsigned int);
            value = v;
        }
            break;

        case LengthSpec::h:
            value = va_arg(w.ap, unsigned int);
            break;

        case LengthSpec::ll:
        default:
            value = va_arg(w.ap, unsigned long long);
            break;
    }

    return value;
}

В конечном итоге мы преобразуем любое число в unsigned long long. Это упрощает нам работу, в последующем мы можем не думать про знак.

Что бы работать с этими сущностями мы введем структуру FormatSpec вида

struct FormatSpec
{
    int flags{FormatFlags::F_NONE};
    LengthSpec lengthSpec{LengthSpec::null};
    int total{0};
    int field_width{-1};
    int precision{-1};
    bool negative{false};
    int radix{10};

  // не обходимо вызывать на каждой итерации так как для каждого спефикиатора
  // формата это параметры свои, напрмер % 05hhi %+-0.5llu %-+ 06.3zd
    inline void reset() noexcept
    {
        flags = FormatFlags::F_NONE;
        lengthSpec = LengthSpec::null;
        field_width = -1;
        precision = -1;
        negative = false;
        radix = 10;
    }
};

Далее вот набросок общего алгоритма действий:

Это очень слишком схема :-)

Это очень слишком схема :-)

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

1/ Парсинг флагов, ширины поля и точности

2/ Парсинг модификатора размера

3/ Получение строкового представления аргумента

4/ Форматирование в соответствии с шириной поля и точностью

5/ Собственно вывод

Разберем каждый шаг более подробно:

1/ Шаг 1: парсинг флагов, ширины поля и точности

static const char *parse_flags_fw_precision(const char *fmt, FormatSpec &formatSpec,
                                            va_list_wrapper &w)
{
  // Забираем флаги пока они есть
  // Цикл нужен например для таких случаев: % #12x
  // пока встречаем флаги continue всегда возвращает нас в начало цикла
  // забрав флаг
    do {
        switch (*fmt) {
            case '#':
                formatSpec.flags |= FormatFlags::F_ALT;
                continue;

            case ' ':
                formatSpec.flags |= FormatFlags::F_SPACE;
                continue;

            case '0':
                formatSpec.flags |= FormatFlags::F_ZERO;
                continue;

            case '-':
                formatSpec.flags |= FormatFlags::F_MINUS;
                continue;

            case '+':
                formatSpec.flags |= FormatFlags::F_PLUS;
                continue;

            default: // тут выходим из цикла
                break;
        }

        break;
    }
    while (*fmt++);

  // Далее может идти символ * что означает что ширина поля передается аргументом
  // например sprintf("%*d", 12);
    if (*fmt == '*') {
        ++fmt;
        formatSpec.field_width = va_arg(w.ap, int);
    }
    else if (isdigit(*fmt)) {
        fmt = parse_number(fmt, formatSpec.field_width);
    } // тут обрабатываем случай если ширина поля передается числом сразу за %
  // например %12d
    else if (*fmt == '-') {
// отрицательная ширина поля обрабатывается как флаг - и положительная ширина поля
// TODO: проверить, возможно мы никогда сюда не придем из за парсера флагов
        formatSpec.flags |= FormatFlags::F_MINUS;
        
        ++fmt;

        if (isdigit(*++fmt)) {
            fmt = parse_number(fmt, formatSpec.field_width);
        }
    }
// за точкой следует точность, например %.12d
    if (*fmt == '.') {
// если за точкой ничего нет, то мы считаем что точность равна нулю
        formatSpec.precision = 0;
// не забываем что если указана точность, то флаг '0' игнорируется
        formatSpec.flags &= ~FormatFlags::F_ZERO;
        ++fmt;
// тоже самое: если точность равна символу '*' то она передается аргументом функции
        if (*fmt == '*') {
            ++fmt;
            formatSpec.precision = va_arg(w.ap, int);
        }
        else if (*fmt == '-') {
// если точность отрицательная, то она полностью игнорируется
            while (isdigit(*++fmt)) {}
            formatSpec.precision = -1;
        }
        else if (isdigit(*fmt)) {
            fmt = parse_number(fmt, formatSpec.precision);
        }
    }

    return fmt;
}

Шаг 2

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

static const char *parse_size_modifier(const char *fmt, FormatSpec &formatSpec)
{
    switch (*fmt) {
        case 'h':
            formatSpec.lengthSpec = LengthSpec::h;

            if (*(fmt + 1) == 'h') {
                formatSpec.lengthSpec = LengthSpec::hh;
                ++fmt;
            }
            ++fmt;
            break;

        case 'l':
            formatSpec.lengthSpec = LengthSpec::l;

            if (*(fmt + 1) == 'l') {
                formatSpec.lengthSpec = LengthSpec::ll;
                ++fmt;
            }
            ++fmt;
            break;

        case 'z':
            formatSpec.lengthSpec = LengthSpec::z;
            ++fmt;
            break;

        case 't':
            formatSpec.lengthSpec = LengthSpec::t;
            ++fmt;
            break;
    }

    return fmt;
}

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

static char *format_number(char *buf, unsigned long long number, FormatSpec &formatSpec,
                           size_t size)
{
  // Если число было отрицательное, сохраняем знак
    char sign = formatSpec.negative ? '-' : 0;
    int number_len = 0;
    int offset = 0;
    char number_str[MAX_NUMBER_LEN];

    if (!formatSpec.negative) {
// обрабатываем флаги, для положительного числа флаг '+' требует вывода знака
        if ((formatSpec.flags & FormatFlags::F_PLUS)) {
            sign = '+';
        } // а флаг ' ' требует вывести пробел вместо знака
        else if ((formatSpec.flags & FormatFlags::F_SPACE)) {
            sign = ' ';
        }
    }

    // emulate zero flag via precision
// тут я понял что допустил архитектурную ошибку, но чинить было лениво
// так что если флаг '0' установлен, то от нас требуется заполнить все нулями
// по ширине поля, по сути эффект эквивалентен результату обработки точности
// т.е. дополнительные нули перед числом, а значит это можно имитировать через точность
    if (formatSpec.flags & FormatFlags::F_ZERO) {
        if (formatSpec.precision < formatSpec.field_width) {
// только в том случае если точность меньше ширины поля, иначе точность побеждает
            formatSpec.precision = formatSpec.field_width;
            formatSpec.field_width = 0;
        }

        if (sign > 0) {
            --formatSpec.precision;
        }
    }
// если у нас есть знак, то нужно передать наш массив таким образом что бы он не был
// перетерт строковым представлением числа
    offset = sign == 0 ? offset : ++offset;
    number_str[0] = sign;
    number_len = number_to_string(&number_str[offset],
                                  number,
                                  formatSpec,
                                  MAX_NUMBER_LEN);
  // тут у нас уже обработанное число в виде строки, нам нужно вывести его
  // в выходной буфер с учетом ширины поля, это сделает для нас функция
  // put_with_field_width
    number_len += offset;
    return put_with_field_width(buf, number_str, number_len, formatSpec, size);
}

Шаг 3

number_to_string() делает три вещи:

1. Получает цифры числа в обратном порядке.
2. Добавляет нули, если нужна точность.
3. Добавляет альтернативный префикс 0 / 0x / 0X.
4. Переворачивает результат в выходной буфер.

int
number_to_string(char *buf, unsigned long long number, const FormatSpec &formatSpec,
                 int size)
{
    const char *digits = (formatSpec.flags & FormatFlags::F_HUGE) > 0 ? "0123456789ABCDEF" : "0123456789abcdef";
    char num_str[MAX_NUMBER_LEN];
    int number_len = 0;
    int p = formatSpec.precision;
    int zero_count = 0;
    auto nr = number;
// Для числа 0 если точность установлена в 0 ничего не выводим
    if (formatSpec.precision == 0 && number == 0) {
        return 0;
    }
// обычное преобразование числа в строку
    if (nr != 0) {
        while (nr != 0) {
            unsigned long long n = nr % formatSpec.radix;
            nr /= formatSpec.radix;
            num_str[number_len++] = digits[n];
        }
    }
    else {
        num_str[number_len++] = '0';
    }
// Точность для целых чисел задаёт минимальное количество цифр.
// Поэтому из precision вычитаем уже полученную длину числа:
// остаток p — это количество ведущих нулей.
    p -= number_len;

    if (formatSpec.flags & FormatFlags::F_ZERO) {
      //учитываем длину специальных символов если установлен соответствующий флаг
      // что бы вывести меньше нулей для точности
        if (formatSpec.flags & FormatFlags::F_ALT) {
            if (number > 0) {
                switch (formatSpec.radix) {
                    case 8:
                        --p;
                        break;

                    case 16:
                        p -= 2;
                        break;
                }
            }
        }
    }
// считаем сколько нулей нужно вывести для соответствия точности
    zero_count = min(p, size);
// и заполняем все нулями до соответствия ей
    while (zero_count > 0) {
        num_str[number_len++] = '0';
        --zero_count;
    }
// добавляем 0 к восьмеричному целому или 0x/0X к шестнадцатиричному
    if (formatSpec.flags & FormatFlags::F_ALT) {
        if (number > 0) {
            switch (formatSpec.radix) {
                case 8:
                    num_str[number_len++] = '0';
                    break;

                case 16:
                    num_str[number_len++] = (formatSpec.flags & FormatFlags::F_HUGE) > 0 ? 'X' : 'x';
                    num_str[number_len++] = '0';
                    break;
            }
        }
    }

    number_len = min(number_len, size);
// собственно выводим число
    for (size_t i = 0; i < number_len; ++i) {
        *buf++ = num_str[number_len - i - 1];
    }

    return number_len;
}

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

static char *
put_with_field_width(char *buf, const char *value, int len, FormatSpec &formatSpec,
                     size_t size)
{
// нам нужно понимать как много символов нам нужно вывести
// ширина поля должна учитывать длину аргумента
    int padding_len = formatSpec.field_width - len >= 0 ? formatSpec.field_width - len : 0;
//функция различает фактическую запись в буфер и гипотетическую длину результата. 
// поэтому  все операции записи ограничиваются size, но счётчики и 
// указатель продвигаются так, как будто бы места в буфере достаточно
// я сам был удивлен, gpt мне сказал что так работает glibc и действительно
// glibc возвращает не written, а formatSpec.total, т.е. то, что мы записали бы
// если бы хватило места.
// почему так я не знаю, но у меня тоже реализовано именно так, это один из моментов
// который я скопировал с поведения glibc
    formatSpec.total += padding_len;
// если флаг '-' не установлен то дополняем пробелами слева, например
    if (!(formatSpec.flags & FormatFlags::F_MINUS)) {
// sprintf("'%5d'",12) даст результат '   12'
// как видно аргумент выравнивается по правому краю
        memset(buf, ' ', min(padding_len, size));
        buf += padding_len;
        size = size > padding_len ? size - padding_len : 0;
        padding_len = 0;
    }

    buf = static_cast<char *>(mempcpy(buf, value, min(len, size)));
    formatSpec.total += len;
// если флаг '-' установлен то дополняем пробелами справа
    if (formatSpec.flags & FormatFlags::F_MINUS) {
// sprintf("'%-5d'",12) даст результат '12   '
// как видно аргумент выравнивается по левому краю
        memset(buf, ' ', min(padding_len, size));
        buf += padding_len;
        padding_len = 0;
    }

    return buf;
}

Получается что шаги 3, 4 и 5 уже выполнены тут же

Теперь мы знаем как работает format_number. По сути нам остается только выставить флаги в корректное состояние и функция все сделает сама.

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

Вот собственно код:

// эта структура существует для того что бы
// что бы безопасно и переносимо передавать va_list
// между разными функциями
struct va_list_wrapper
{
    va_list ap;
};
// ...
int vsnprintf(char *buf, size_t max_size, const char *fmt, va_list ap)
{
    FormatSpec formatSpec;
    va_list_wrapper wrapper{};
    va_copy(wrapper.ap, ap);

    if (max_size > 0) {
        char *pos = buf; // текущая позиция в буфере
// резервируем один символ для конца строки
        size_t size = max_size > 0 ? max_size - 1 : max_size;
// количество фактически записанных символов
        ptrdiff_t written = 0;
        // %[flags][field_width][.precision][size specifier]specifier
        for (size_t i = 0; *fmt && written < size; ++i, ++fmt) {
// обязательно сбрасываем на каждой итерации
            formatSpec.reset();
// выводим в строку все что не является %
            if (*fmt != '%') {
                *pos++ = *fmt;
// количество гипотетически записанных символов
                ++formatSpec.total;
                ++written;
                continue;
            }
          
// поймали %
            ++fmt; // идем дальше, к следующему за % символу
// парсим флаги, ширину поля и точность как объяснялось выше
            fmt = parse_flags_fw_precision(fmt, formatSpec, ap);
// парсим модификатор размера как объяснялось выше
            fmt = parse_size_modifier(fmt, formatSpec);
// не забываем, что если установлен флаг '-', то флаг '0' игнорируется
            if (formatSpec.flags & FormatFlags::F_MINUS) {
                formatSpec.flags &= ~FormatFlags::F_ZERO;
            }

            switch (*fmt) {
// для %% выводим %
                case '%':
                    *pos++ = *fmt;
                    ++formatSpec.total;
                    break;

                case 'd':
                case 'i': {
// это знаковые целые, d, i, ld, lld, li, lli
// значит флаг '#' для них мы игнорируем и убираем его что бы не смущать нашу
// функцию number_to_string
                    formatSpec.flags &= ~FormatFlags::F_ALT;
                    formatSpec.radix = 10;
// для i и d читаем просто int
                    auto value =
                        formatSpec.lengthSpec == LengthSpec::null ? va_arg(wrapper.ap, int) : read_signed_number(formatSpec,
                                                                                                         ap);
                    formatSpec.negative = value < 0;

                    if (value < 0) {
// для отрицательных чисел флаг ' ' игнорируется
                        value = -value;
                        formatSpec.flags &= ~FormatFlags::F_SPACE;
                    }
// проеобразуем все в строку
                    pos = format_number(pos, value, formatSpec, size - i);

                }
                    break;

                case 'u': {
// просто убираем флаг альтернативного представления, дальше магия сработает сама
                    formatSpec.flags &= ~FormatFlags::F_ALT;
                    pos = read_format_unsigned_int(pos, formatSpec, size - i, ap);
                }
                    break;

                case 'o': {
// просто выставляем radix или base кому как угодно. Далее все как в Хогвардсе,
// работает само
                    formatSpec.radix = 8;
                    pos = read_format_unsigned_int(pos, formatSpec, size - i, ap);

                }
                    break;
// тоже самое
                case 'X':
                    formatSpec.flags |= FormatFlags::F_HUGE;
                case 'x': {
                    {
                        formatSpec.radix = 16;
                        pos = read_format_unsigned_int(pos, formatSpec, size - i, ap);
                    }
                }
                    break;

                case 'p': {
                    auto value = read_unsigned_number(formatSpec, ap);
// тут я повторил поведение glibc, сторонний тест сломался ... я поправил его
// тут никакие флаги не работают, только '-' для nullptr
                    if (value > 0) {
                        formatSpec.flags &= ~FormatFlags::F_ZERO;
                        formatSpec.flags &= ~FormatFlags::F_SPACE;
                        formatSpec.flags &= ~FormatFlags::F_PLUS;
                        formatSpec.flags &= ~FormatFlags::F_ALT;
                        formatSpec.radix = 16;

                        if (formatSpec.precision == -1 && formatSpec.field_width == -1) {
                            formatSpec.precision = kPointerHexDigits;
                        }

                        pos = format_number(pos, value, formatSpec, size - i);
                    }
                    else {
// и то только в том случае когда мы работаем с указателем как со строкой
// мы выводим строку (Nil)
                        pos = format_string(pos, formatSpec, size - i, ap, kNil);
                    }
                }
                    break;
// далее все просто
                case 'n': {
                    size_t *ptr = va_arg(wrapper.ap, size_t*);

                    if (ptr) {
                        *ptr = written;
                    }
                }
                    break;

                case 'c':
                    pos = format_char(pos, formatSpec, size - i, ap);
                    break;

                case 's':
                    pos = format_string(pos, formatSpec, size - i, ap);
                    break;

                default:
                    *pos++ = *fmt;
                    ++formatSpec.total;
                    break;
            }

            written = pos - buf;
        }

        *pos = '\0';
    }

    return formatSpec.total;
}

Для полного понимания нам осталось рассмотреть только функцию read_format_unsigned_int.

Она достаточно простая и просто настраивает флаги и читает нужно значение из va_list

static char *read_format_unsigned_int(char *buf, FormatSpec &formatSpec,
                                      size_t size, va_list_wrapper &w)
{
    formatSpec.flags &= ~FormatFlags::F_SPACE;
    formatSpec.flags &= ~FormatFlags::F_PLUS;
    auto value =
        formatSpec.lengthSpec == LengthSpec::null ? va_arg(w.ap, unsigned int) : read_unsigned_number(
            formatSpec,
            ap);

    return format_number(buf, value, formatSpec, size);
}

Вот собственно и все!

В репозитории прилагаются тесты. Так же там реализованы ctype и строковые функции string.h, но они простые и рассмотрены тут не будут.

Следующий этап это минимальный abi itanium C++. Нужно будет реализовать минимум:
Потокобезоасную инициализацию локальных статических объектов, массивы переменного размера и перегрузки new/delete. Теперь мы к этому готовы, у нас есть аллокатор

До новых встреч!