Что не так с Hello World?

Казалось бы, современный С++ дает столько возможностей… Давайте попробуем начать постигать всю эту необъятную мощь с написания Hello World:

#include <iostream>
int main(){ std::cout << "Hello World" << std::endl; }

Какой там сейчас последний компилятор… Давайте возьмем какой-нибудь GCC 15.2.0, запускаем компиляцию g++ -static -O2 hello.cpp -o hello.exe и… получили 2,30 МБ.

Что же пошло не так? Почему для отображения 11 символов понадобилось раздуть бинарник до >2МБ? Давайте разбираться.

Флаги компилятора?

Что же может влиять на размер бинарника? Первым на ум приходят флаги компилятора, давайте попробуем поочередно добавлять флаги:

  • -s - (-~1,25 МБ) -> Размер: 1,05 МБ

Пояснение: Флаг -s удаляет отладочную информацию из исполняемого файла.

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

Интересно выходит, что для обычной компиляции даже Hello World компилятор почему-то пихает по умолчанию информацию, которая не нужна для вывода 11 символов.

А что, если?

А что там у нас в кодовой базе? #include <iostream> ага, iostream. Хм, а что если заменить на printf?

#include <stdio.h>
int main(){ printf("Hello World"); }

И получаем… 42,5 КБ — уменьшение еще на ~1 МБ. Что же в этом iostream такого, что он просит 1 метр вашего бинарника? А по факту он тянет за собой инициализацию целой цепочки зависимостей, начиная с глобального std::cout -> std::stringstream и заканчивая локалями, виртуальными функциями и шаблонами, и всё это ради 11 символов. Для стандартной библиотеки языка, который строится вокруг эффективности как-то избыточно выходит.

Может конкретная версия компилятора виновата? Что ж, попробуем собрать на разных версиях GCC:

  • 15.2.0: 1,05 МБ

  • 13.1.0: 1,03 МБ

  • 11.2.0: 1,00 МБ

  • 10.3.0: 930 КБ

  • 4.9.2: 577 КБ

  • 3.4.2: 260 КБ

Тенденция явно показывает, что чем выше версия компилятора, тем жирнее зависимости которые тянет iostream. Если также пройтись по версиям уже для printf?

  • 15.2.0: 42,5 КБ

  • 13.1.0: 41,5 КБ

  • 10.3.0: 86,0 КБ

  • 4.9.2: 15,5 КБ

  • 3.4.2: 5,50 КБ

Любопытно что тут версия 10.3.0 - выбивается из тенденции. Возможно есть отличия в портах между TDM и MinGW-W64.

Выходит, что Hello World может весить всего 5,50 КБ при выборе правильной версии компилятора.

Меняем тактику

Как же мы можем приблизиться к этому результату на более современных компиляторах? Если даже printf может тянуть лишнее, какой самый что ни на есть прямой способ вывода? Для Windows можем попробовать использовать системный вызов WriteFile для записи в stdout:

#include <string.h>
#include <windows.h>
void print(const char* cptr, DWORD len){ WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), cptr, (DWORD)len, NULL, NULL); }
void print(const char* cstr){ print(cstr, strlen(cstr)); }

int main(){ print("Hello World\n"); }

Сравнительная таблица для WriteFile будет уже такой:

  • 15.2.0: 14,0 КБ

  • 13.1.0: 14,5 КБ

  • 10.3.0: 14,0 КБ

  • 4.9.2: 11,5 КБ

  • 3.4.2: 5,50 КБ

Любопытно, что тенденция тут будто бы нелинейная по сравнению с printf: Если для новых версий отличие в несколько раз (42,5/14,0=303%), то для старых версий она уже ближе к району погрешности (15,5/11,5=34,7%), а для 3.4.2 размер вовсе идентичен.

Как выжать максимум?

Что если хочется еще меньше? Текущие ~11-15 кб это минимум чего можно добиться со стандартным рантаймом компилятора. Если мы хотим чтобы наш Hello World весил еще меньше, придется уже вручную инициализировать CRT:

// ====== Minimal CRT ======
#define NULL 0
#define STD_OUTPUT_HANDLE ((unsigned long)-11)
int main(); typedef unsigned long long size_t; typedef unsigned int DWORD;
extern "C" __declspec(dllimport) void __stdcall ExitProcess(DWORD uExitCode) __attribute__((noreturn));
extern "C" __declspec(dllimport) int __stdcall WriteFile(void* hFile, const void* lpBuffer, DWORD nNumberOfBytesToWrite, DWORD* lpNumberOfBytesWritten, void* lpOverlapped );
extern "C" __declspec(dllimport) void* __stdcall GetStdHandle(unsigned long nStdHandle);
extern "C" void _start(){ ExitProcess(main()); } //Наша точка входа: только для 64 битной архитектуры
extern "C" void __main(){} //Заглушка для компилятора
// ====== User-Space Code ======
extern "C" size_t strlen(const char* c){ size_t len=0; while(*c!='\0'){ len++; c++; } return len; }
void print(const char *cptr, size_t len){ WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), cptr, (DWORD)len, NULL, NULL); }
void print(const char *cstr){ print(cstr, strlen(cstr)); }
	
int main(){ print("Hello, min-CRT\n"); }

Для компиляции без стандартного CRT нужно использовать следующие флаги:

g++ -static -s -O2 hello.cpp -o hello.exe -nostdlib -Wl,--entry=_start -lkernel32

Пояснение: Флаг -nostdlib объединяет действия:
-nostartfiles (отключает инициализацию CRT)
-nodefaultlibs (не линковать либы автоматически)

-Wl,--entry=_start: опция линковщика, явно описывающая нашу точку входа _start()

Пример инициализации CRT несет ознакомительный характер.

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

В итоге для всех упомянутых версий 15.2.0, 13.1.0, 10.3.0, 4.9.2 получаем стабильные 3,50 КБ (3 584 байт)

«Не плати за то, что не используешь»

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

Выходит чем меньше мы доверяем свои функции на откуп реализаций компилятора, тем стабильнее будет вес наших исполняемых файлов при обновлении компилятора. Или же проще использовать старый добрый 3.4.2? :)

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


  1. diakin
    08.05.2026 20:41

    Бумага Железо все стерпит!


    1. santer_koder
      08.05.2026 20:41

      Память нынче не та. И это не временный тренд, это с нами надолго


      1. Vlad441 Автор
        08.05.2026 20:41

        Это же касается только RAM, кризис HDD пока еще не наступил, так что экономия места там мало кого волнует.


        1. edik-petrof
          08.05.2026 20:41

          Это если смотреть в разрезе LLM и прочих нейросеток. А вот криптовалюта Chia и ~полдесятка других Proof-of-Space токенов с этим утверждением явно не согласны.


          1. Grey_Box
            08.05.2026 20:41

            И долго Чиа была на хайпе? Кроме того, после майнинга чиа диски в негодность вроде быстро приходят, в отличии от видюх, которыми биток копают. Кому такое счастье надо?


            1. RTFM13
              08.05.2026 20:41

              Видюхами биток не копают, видюхами копали эфир до перехода на pos.


              1. HiItsYuri
                08.05.2026 20:41

                И процами копали, и видюхами, пока асиков не было. Правда было это в 10-12 годах.


          1. YaSRus
            08.05.2026 20:41

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


        1. LAutour
          08.05.2026 20:41

          кризис HDD пока еще не наступил

          Кризис HDD по пропускной способности из-за объема софта и подгружаемых ресурсов уже давно наступил.


          1. WASD1
            08.05.2026 20:41

            Кризис HDD наступил из-за IOPS (100-200 IOPS на шпиндель при рандомном чтении), а не из-за пропускной способности.

            ~200 МБайт \ сек на обычных пользовательских дисках вполне хватает для рядовых нагрузок.


            1. RepppINTim
              08.05.2026 20:41

              База, именно из-за рандомного чтения винда и грузилась по пять минут. Линейная скорость там всегда была более-менее


              1. HardWrMan
                08.05.2026 20:41

                И это не смотря на развитие интерфейса от тупого но быстрого PATA до умного SATA в режиме AHCI с поддержкой мультизадачных запросов с сортировкой по номеру LBA для уменьшения необходимого движения головой над блинами. Не считая удобство с проводами (как массогабаритного так и простого во включении: 1 провод - 1 устройство).


                1. Tamashii
                  08.05.2026 20:41

                  1 провод — 1 устройство

                  Это Вы про провод данных (в PATA 1 шина — 2 устройства)?

                  Или в целом про все кабели (PATA: 1 устройство — 2 провода, 2 устройство — 3-4 провода; SATA: 1 устройство — всегда 2 провода)?


                  1. HardWrMan
                    08.05.2026 20:41

                    Я про провод данных. Т.е. PATA кабели были неудобные для менеджмента, достаточно хрупкие и они подключали 2 устройства, которые ещё надо было настроить на соответствующий режим MASTER/SLAVE любым из доступных способов. А у SATA кабель один, он маленький и достаточно прочный. Естественно в обоих случаях требуется ещё и дополнительный кабель питания, но изначальный посыл был про неудобство PATA для пользователя.


        1. ImagineTables
          08.05.2026 20:41

          Это же касается только RAM, кризис HDD пока еще не наступил, так что экономия места там мало кого волнует.

          Вот с этой точки зрения интересен был бы анализ, т.е. в динамике, а не в статике.

          Как объясняли в MSDN архитекторы из Майкрософта, по-моему, один был Раймонд Чен, а кто был второй — не помню, но тоже очень известный чувак. Виртуальную память надо рассматривать как диск, имеющий кеш в виде оперативной памяти. Важно не сколько программа скушает, а сколько она реально использует. Поэтому если вы пишете на языке, где хранение каждого объекта сопровождается оверхедом (метаданные, неотключаемая таблица виртуальных вызовов и все вызовы виртуальные, и пр.), то это ж. А если вы аллоцировали гигантский кусман, но вообще им не пользуетесь, то это означает только одно: вы зарезервировали кусок в файле подкачки. Хотя цифры в task manager’е могут быть устрашающими.

          Если закольцевать ::Sleep() перед выходом, какое у файла с инициализацией объектов стримов отношение PB/WS/VM? Может, и хрен с ним?


      1. dunmaksim
        08.05.2026 20:41

        Да как так-то? Буквально пару лет назад слышал кукареканье "Просто памяти докинем, и всё, час моей работы дороже стоит!"


        1. Moog_Prodigy
          08.05.2026 20:41

          Пару лет назад, все правильно. А кризис памяти пошел с прошлой осени. Уже так не орут и притихли даже чуваки с ЗП 300к \ наносек.


          1. netch80
            08.05.2026 20:41

            А кризис памяти пошел с прошлой осени.

            Нет.

            Сравним:

            Начало 2012 – около 5$/ГБ. Была такая страничка https://jcmit.net/memoryprice.htm (сейчас на archive.org остались копии), там сводка с 1957 до середины 2024.

            До 2024 произошло падение до 1.5$/ГБ. Такое снижение за 12 лет (2012÷2024) это уже кризис сам по себе, потому что раньше скорость падения относительной цены была даже больше, чем ожидалось по закону Мура. Развитие технологий типа DDR3→DDR4→DDR5 ситуацию не исправляло.

            Середина 2024 – 1.5$/ГБ – возьмём за точку отсчёта кризиса. Далее: смотрим на https://pcpartpicker.com/trends/price/memory/ – уже декабрь 2024 это около 3-4$/ГБ, против 1.5$/ГБ летом 2024, когда последняя цифра у первой ссылки. 4-6$/ГБ посредине 2025. Осенью, когда начало рости и до 10$/ГБ, только тогда журналюшки и блохеры проснулись и начали свой хай.


    1. kuzzmenka
      08.05.2026 20:41

      Не стерпит. Например, мы не заметили, как нам резко и без причин перестало хватать 32 Гб памяти на андроид приложения, сегодня 256 Гб памяти считается адекватным минимумом для устройства, хотя принципиально функционал приложений не изменился за последние 8 лет


      1. diakin
        08.05.2026 20:41

        Современное железо все стерпит. А современное это 256 Гб. А то, что было вчера уже не современное.
        А айтишникам вообще не с руки жаловаться на софт, как говорится, "кто чего породил, так тому и надо!" \(^v^)/


        1. diakin
          08.05.2026 20:41

          Айтишники обиделись ))


  1. feelamee
    08.05.2026 20:41

    «Не плати за то, что не используешь»

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

    А так… тут напрашивается хотя бы сравнение с {fmt} (в официальном readme есть пара бенчмарков)


    1. Keva
      08.05.2026 20:41

      На моей взгляд, вся система стримов для ввода-вывода - это т. н. "C++ ради C++", то есть самый большой грех в разработке на этом языке.

      То же могу сказать и про относительно свежие range.


      1. feelamee
        08.05.2026 20:41

        не пойму, что именно там “ради C++”.

        То же могу сказать и про относительно свежие range.

        да, ренжи могут выглядеть страшно, и дебажить их тяжело. Но как сделать композитные алгоритмы лучше? Думаю, пока дело не доходит до всяких transform и сложных filter, они очень даже удобны - например ппробежаться по контейнеру с индексами, или попарно, или пройтись только по ключам/значениям. Без ренжей для этого приходилось возвращаться к старому сишному циклу. Теперь это намного более читаемо


        1. OlegMax
          08.05.2026 20:41

          не пойму, что именно там “ради C++”

          Форматирование строк, манипуляторы - в стримах это сделано ужасно неудобно и люди стараются пользоваться чем-нибудь printf-подобным вместо этого. То есть, дизайн стримов скорее определялся "смари что я умею!!! у меня везде классы!!!", чем удобством использования


          1. feelamee
            08.05.2026 20:41

            не думаю что это была причина такого дизайна. Скорее тогда не придумали ничего лучше. Были тогда уже языки, которые делали вывод через форматную строку вроде print(“Hello, {}!”, “World”) ? - не уверен.

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


            1. vadimr
              08.05.2026 20:41

              Были тогда уже языки, которые делали вывод через форматную строку вроде print(“Hello, {}!”, “World”) ? - не уверен.

              print '"Hello, ", A', "World"

              Фортран 77. Compile-time (при возможности). Стандарт принят за год до публикации K&R C.

              А вне мейнстрима с 1962 года существовал язык Снобол, где уже и сопоставление с образом (по-нынешнему – регулярные выражения) вовсю использовалось.


          1. Kripiron
            08.05.2026 20:41

            Лично для меня printf пиздец неудобен


    1. Astroscope
      08.05.2026 20:41

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

      Кто "вы"? Программист или пользователь? Потому что получается, что за удобство программиста платит пользователь - потребными вычислительными ресурсами, как в узком, так и в широком смысле слова, платит за возможность запускать поделки, которые программисту было удобнее написать быстро вместо оптимизировать. И после этого находятся те, кто искренне возмущается т.н. вайбкодингу - там, хотя бы, программист вообще не нужен (ну, в идеале, к которому рано или поздно придут), а значит понятен компромисс.


      1. TechExpert
        08.05.2026 20:41

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


      1. feelamee
        08.05.2026 20:41

        почему вы решили что реализации iostream написаны “как удобнее автору std”? Почему решили что реализацию не оптимизировали?


      1. linux-over
        08.05.2026 20:41

        по большому счёту все языки выше ASM — это про то, что пользователь будет платить за скорость разработки


        1. Artyomcool
          08.05.2026 20:41

          Да и асм в каком-то смысле тоже, проблема может быть на стороне линковки например. Так что предлагаю начинать сравнение с машинных кодов (хотя и там не всё так однозначно).


    1. RepppINTim
      08.05.2026 20:41

      Платит пользователь, а не программист. Своим местом на диске, своей оперативкой и своим временем


  1. AndreyDmitriev
    08.05.2026 20:41

    Отчего ж вы не заглянули внутрь исполняемых файлов и детально не разобрались, откуда берутся эти самые (мега)байты? Там же относительно несложно всё. Очевидно, что для того, чтобы вывести что-то на экран, таки придётся позвать из ядра GetStdHandle, WriteConsole, и так далее до выхода через ExitProcess.

    Вот если на асме:

    EUROASM
    hello PROGRAM Format=PE, Entry=Start, IconFile=
    
    INCLUDE winapi.htm
    
    Start: 	nop
    	StdOutput =B"Hello, Habr"
    	TerminateProgram
    ENDPROGRAM

    И StdOutput и TerminateProgram тут макросы, и если всё дизассемблировать, то будет 80 строк на ассебмлере, вот как на самом деле выглядит вывод в консоль:

    Привет, Хабр!
    401000                 public start
    401000 start           proc near
    401000                 nop
    401001                 push    0
    401003                 push    offset aHelloHabr ; "Hello, Habr"
    401008                 push    0FFFFFFFFh
    40100A                 push    0FFFFFFF5h
    40100C                 call    sub_401018
    401011                 push    0
    401013                 call    ExitProcess
    401013 start           endp
    401013
    401018 ; =============== S U B R O U T I N E ====================
    401018 sub_401018      proc near               ; CODE XREF: start
    401018 var_28          = dword ptr -28h
    401018
    401018                 pusha
    401019                 mov     ebp, esp
    40101B                 sub     esp, 8
    40101E                 mov     [esp+28h+var_28], esp
    401021                 push    dword ptr [ebp+24h]
    401024                 call    GetStdHandle
    401029                 mov     ebx, eax
    40102B                 inc     eax
    40102C                 stc
    40102D                 jz      short loc_401099
    40102F                 mov     ecx, [ebp+28h]
    401032                 mov     edi, [ebp+2Ch]
    401035                 mov     eax, [ebp+30h]
    401038                 mov     edx, offset WriteFile
    40103D                 test    al, 2
    40103F                 jz      short loc_40104F
    401041                 mov     edx, offset WriteConsoleA
    401046                 test    al, 1
    401048                 jz      short loc_40104F
    40104A                 mov     edx, offset WriteConsoleW
    40104F loc_40104F:                             
    40104F                 test    al, 4
    401051                 jz      short loc_401065
    401053                 lea     edi, [ebp-4]
    401056                 mov     dword ptr [edi], 0A000Dh
    40105C                 test    al, 1
    40105E                 jnz     short loc_401065
    401060                 mov     word ptr [edi], 0A0Dh
    401065 loc_401065:                             
    401065                 xor     eax, eax
    401067                 mov     esi, edi
    401069                 test    byte ptr [ebp+30h], 1
    40106D                 jz      short loc_40107A
    40106F                 shr     ecx, 1
    401071                 repne scasw
    401074                 jnz     short loc_40107F
    401076                 dec     edi
    401077                 dec     edi
    401078                 jmp     short loc_40107F
    40107A loc_40107A:                             
    40107A                 repne scasb
    40107C                 jnz     short loc_40107F
    40107E                 dec     edi
    40107F loc_40107F:                             
    40107F                 sub     edi, esi
    401081                 mov     eax, [ebp+30h]
    401084                 and     al, 3
    401086                 xor     al, 3
    401088                 jnz     short loc_40108C
    40108A                 shr     edi, 1
    40108C loc_40108C:                             
    40108C                 push    0
    40108E                 push    dword ptr [ebp-8]
    401091                 push    edi
    401092                 push    esi
    401093                 push    ebx
    401094                 call    edx ; WriteFile
    401096                 cmp     [ebp-8], edi
    401099 loc_401099:                             
    401099                 mov     esp, ebp
    40109B                 popa
    40109C                 retn    10h
    40109C sub_401018      endp

    Занимает этот исполняемый файл 2596 байт, и там на самом деле не только start().

    Ну а дальше берётся любой анализатор заголовка и смотрится куда утекают оставшиеся байты (80 строк на ассемблере обычно не занимают два килобайта):

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

    IDA
    IDA


    1. Vlad441 Автор
      08.05.2026 20:41

      Я пытался сделать обзор проблемы именно с точки зрения разработчика С++, а не как ревер-инженер на ASM. Целью этого мини-исследования было именно практически добиться минимально возможного размера компилятором С++ (в данном случае GCC) и показать развитие тенденции вместе с обновлением компилятора, конкретное содержание PE заголовков для этого мне показалось излишним. Хотя это вполне имеет место быть, и спасибо вам за дополнение!

      Проблема в том, что более высокоуровневые языки почему-то не выкидывают лишнее в очевидных ситуациях, когда исключения попросту не требуются. Например компилятор GCC догадывается не прикручивать исключения для Hello Worldно как только в коде появляется malloc()/free() компилятор уже может их включить, и тут уже нужно явно указывать флаг что стоит отключить добавление механизма исключений.

      В вашем примере для того чтобы добиться размера меньше, вам уже пришлось использовать ASM, что уже не совсем разработка на С++ :)

      Компилятор по идее для того и нужен чтобы упростить жизнь разработчику и не заставлять его писать на ASM из-за того что компилятор не справился со своей задачей лучше программиста.


      1. AndreyDmitriev
        08.05.2026 20:41

        Компилятор по идее для того и нужен чтобы упростить жизнь разработчику и не заставлять его писать на ASM из-за того что компилятор не справился 

        Тут я согласен по части "упростить", но не очень по части "не справился", ведь прогресс тоже на месте не стоит, и количество зависимостей растёт, и аккуратно избавиться от них или уменьшить бывает непросто, да и к чему, ведь количество памяти тоже? Я начинал программировать больше тридцати лет назад на ДВК, у меня было 56 килобайт оперативной памяти (и, кстати, там была RT11FB - это Foreground/Background операционка, я запускал там две задачи для дифрактометра - одна, на ассемблере Macro-11 снимала данные, а на второй (на Си) оператор мог прошлые обсчитывать, всё параллельно), и надо было программировать "экономно" ценой долгих вечеров в лаборатории. Но эти времена прошли, хотя, конечно есть "перегибы". Даже если взять Раст, у которого с кодогенерацией всё более-менее норм, то текстовый редактор Zed, на нём написанный, тянет за собой под две тысячи зависимостей, в число которых входят Фурье, MP3 и даже Flac, а исполняемый файл занимает отнюдь не два, а триста пятьдесят мегабайт, при этом довольно резво запускается (чуть быстрее чем VS Code), так как интерфейс там не на электроне, а собственный gpui. Обычно при выпуске продукта конечно имеет смысл оценить используемые ресурсы с точки зрения дискового пространства, занимаемой памяти и общей производительности, но некоторые оптимизации могут довольно "дорого" стоить. А компиляторы современные неплохие, особенно интеловский на своих процессорах показывает очень неплохую производительность (кстати, там объём исполняемого файла и библиотек также растёт за счёт раздельного кода под раздельные архитектуры типа AVX2/AVX512 и это норм). Оптимизировать надо "бутылочные горлышки", при этом объём кода может даже вырасти при том же разворачивании циклов, и современные компиляторы с этим справляются.


        1. rukhi7
          08.05.2026 20:41

          ведь прогресс тоже на месте не стоит, и количество зависимостей растёт

          а разве это прогресс? Может дело в том что прогресс перестали отличать от регресса?


          1. AndreyDmitriev
            08.05.2026 20:41

            Это прогресс, который в том, что меньшими усилиями я теперь получаю "больший" результат. Просто для многих это само собой разумеется, но я в программировании больше четверти века и могу окинуть взглядом весь путь, местами может он и заруливает не туда, но в целом я доволен как аппаратным развитием процессоров, они фантастические, так и программных средств. Я ещё помню, как я писал медианный фильтр на MMX в конце девяностых и потом вкорячивал это в четвёртый Дельфи. Я и теперь умею на ассемблере, но только на AVX512. А желающие могут всегда взять старую железку и ПО и попробовать.

            Впрочем эксперимент ваш меня заинтересовал, я решил повторить на Visual Studio Professional 2026 апрельское обновление v.18.5.2.

            Вот код:

            #include <iostream>
            
            int main()
            {
                std::cout << "Hello World!\n";
            }

            При компиляции в релиз со всеми настройками по умолчанию (я вообще ничего не трогал, только в релиз переключился) получаем исполняемый файл 64-бит размером 11264 байт, или же 10752 байт в 32-бит, то есть двух мегабайт и близко нет, это в общем претензии к линковщику. Листингом утомлять не буду, там 73 функции. Но всё же это заметно меньше того, что Раст выдавал, давайте позволим ему реабилитироваться и сделаем без std, вот код, тут чуть длиннее, так как println!() без std мы теряем, нам надо задействовать три функции:

            #![no_std]
            #![no_main]
            
            use core::panic::PanicInfo;
            
            #[link(name = "kernel32")]
            unsafe extern "system" {
                fn GetStdHandle(nStdHandle: i32) -> *mut u8;
                fn WriteConsoleA(
                    hConsoleOutput: *mut u8, lpBuffer: *const u8,
                    nNumberOfCharsToWrite: u32,
                    lpNumberOfCharsWritten: *mut u32,
                    lpReserved: *mut core::ffi::c_void,
                ) -> i32;
                fn ExitProcess(exit_code: u32) -> !;
            }
            
            const STD_OUTPUT_HANDLE: i32 = -11;
            
            #[unsafe(no_mangle)] // Точка входа:
            pub extern "C" fn _start() -> ! {
                let msg = b"Hello, World!\r\n";
            
                unsafe {
                    let handle = GetStdHandle(STD_OUTPUT_HANDLE);
                    WriteConsoleA(handle, msg.as_ptr(), msg.len() as u32,
                        core::ptr::null_mut(), core::ptr::null_mut(),
                    );
                    ExitProcess(0);
                }
            }
            
            #[panic_handler]
            fn panic(_info: &PanicInfo) -> ! {
                unsafe { ExitProcess(1) }
            }

            .cargo\Config.toml, тут надо пробросить опции в линковщик и указать точку входа:

            [target.x86_64-pc-windows-msvc]
            rustflags = [
                "-C", "link-args=/ENTRY:_start /SUBSYSTEM:CONSOLE"
            ]

            Cargo.toml:

            [package]
            name = "r-hello-nostd"
            version = "0.1.0"
            edition = "2024"
            
            [dependencies]
            
            [profile.release]
            panic = "abort"
            lto = true
            opt-level = "z"
            codegen-units = 1
            strip = true

            И вот — 3072 байт всего исполняемый файл (одинаковый размер для 32 и 64 бит).

            Если дизассемблировать исполняемый файл, там вообще красиво всё:

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

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


            1. Nikolay734
              08.05.2026 20:41

              Ваш комментарий интереснее статьи.


            1. osmanpasha
              08.05.2026 20:41

              получаем исполняемый файл 64-бит размером 11264 байт, или же 10752 байт в 32-бит

              Он наверное динамически слинкован с Visual C++ Runtime - той штуки, нужной версии которой никогда нет на компьютере?


              1. AndreyDmitriev
                08.05.2026 20:41

                Да, конечно, вот зависимости:

                Но проблему с "отсутствующей нужной версией" я уже давно не видел, разве что для какого-нибудь знатного легаси...


                1. osmanpasha
                  08.05.2026 20:41

                  Но проблему с "отсутствующей нужной версией" я уже давно не видел, разве что для какого-нибудь знатного легаси

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

                  Да, конечно, вот зависимости:

                  Ну, сравнивать числа из статьи было бы правильно со статичным билдом в MSVC


            1. Viacheslav01
              08.05.2026 20:41

              Компилятор от MS хорош ))) Иногда...


            1. rukhi7
              08.05.2026 20:41

              При компиляции в релиз со всеми настройками по умолчанию (я вообще ничего не трогал

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


              1. Belarus
                08.05.2026 20:41

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


              1. netch80
                08.05.2026 20:41

                а что они компилируют, релиз или дебаг.

                А почему вы думаете, что на этот вопрос есть ответ? Для того же gcc таких понятий нет, зато есть 1) общий уровень оптимизации, 2) частные опции оптимизации и 3) включение отладочной информации. Может быть -O3 с отладочной информацией, а может быть -O0 без оной. Или посмотрите, например, у ядра Linux опцию сборки “readable-asm”.

                то есть даже не считают нужным продемонстрировать (как-то показать) каким образом код был скомпилирован.

                Или вы не понимаете, как именно они описывают режим сборки?


          1. AndreyDmitriev
            08.05.2026 20:41

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

            У меня был gcc 15.1.0 (Rev5) и только что проапгрейдился до 16.1.0 (Rev2). Win 11/MSYS2.

            с вашими ключами g++ -static -O2:

            15.1.0: 2 873 918 байт / 4014 функций

            16.1.0: 2 977 540 байт / 4009 функций

            Стало на сотню килобайт больше, зато на пять функций меньше — какой-никакой, а прогресс. Кстати, несмотря на указанный -static, я всё равно вижу зависимость от ucrtbase.dll:


            1. wander
              08.05.2026 20:41

              я всё равно вижу зависимость от ucrtbase.dll

              Обычно это означает, что для нее не нашлось статической версии.


            1. Vlad441 Автор
              08.05.2026 20:41

              Винда грешит тем, что даже при -static, она может неявно подключать динамические зависимости. Хотя для того же msvcrt.dll и kernel32.dll это не столь большая проблема. Так как вендор единый, они 100% будут в системе начиная с Win2000.

              А вот ucrtbase.dll относительно новый, и уже могут быть системы где его еще не существовало.


              1. AndreyDmitriev
                08.05.2026 20:41

                Да, но я static честно ни разу практически не пользовался. Кстати, коль скоро я обновил gcc, то если посмотреть в зависимости (я соберу через g++ под Windows используя MSYS64), то разницей будет зависимость от libstdc++-6.dll, которой нет при сборке с ключом --static, а файл этот как раз два с лишним мегабайта, что намекает о том, что нам в исполняемый файл тупо влетает вся рантайм плюсплюсов целиком. Кстати, содержимое компилята удобнее всего чере Relyze посмотреть, там граф всех функций в файле можно увидеть, я чуть ниже коммент со скриншотами оставил.


              1. JustMoose
                08.05.2026 20:41

                Я попробовал повторить Ваш эксперимент.... Под Линуксом действительно получается бинарник под 2МБ. А вот под виндой даже с ключиком static получается 212КБ.

                ЗЫ: Правда, у меня не было gcc под винду, пришлось собрать clang-ом.


      1. osmanpasha
        08.05.2026 20:41

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


    1. Johnny_Depp
      08.05.2026 20:41

      WriteConsole - это функция API Console и не как не ядра, просто не большая поправка)


      1. AndreyDmitriev
        08.05.2026 20:41

        Да, вы правы, просто она вызывается из Kernel32.dll


      1. Vlad441 Автор
        08.05.2026 20:41

        Так вроде бы и называл это "системным вызовом" :)


    1. HardWrMan
      08.05.2026 20:41

      Меня Зубков учил так:

      ; hello-l.asm
      ; Выводит на экран сообщение "Hello World!" и завершается
              .model     tiny              ; модель памяти, используемая для СОМ
              .code                        ; начало сегмента кода
              org        100h              ; начальное значение счетчика - 100h
      start:  mov        ah,9              ; номер функции DOS - в АН
              mov        dx,offset message ; адрес строки - в DX
              int        21h               ; вызов системной функции DOS
              ret                          ; завершение СОМ-программы
      message db         "Hello World!",0Dh,0Ah,'$' ; строка для вывода
              end        start             ; конец программы
      Для WIN32/64, конечно, чуть сложнее, но если из примера под спойлером выкинуть всё после call output_string получим тот же консольный Hello World и он явно будет чуть больше необходимого минимума для PE формата, но килобайты?...

      Исполнимые программы для Windows делятся на два основных типа — консольные и графические приложения. При запуске консольного приложения открывается текстовое окно, с которым программа может общаться функциями WriteConsole()/ReadConsole() и другими (соответственно при запуске из другого консольного приложения, например, файлового менеджера FAR, программе отводится текущая консоль и управление не возвращается к FAR, пока программа не закончится). Графические приложения соответственно не получают консоли и должны открывать окна, чтобы вывести что-нибудь на экран.

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

      MASM:

      ml /с /coff /Cp winurl.asm
      link winurl.asm /subsystem console

      TASM:

      tasm /m /ml /D_TASM_ winurl.asm
      tlink32 /Тре /ар /с /x winurl.obj

      WASM:

      wasm winurl.asm
      wlink file winurl.obj form windows nt runtime console op с

      Попробуйте скомпилировать программу winurl.asm этим способом, чтобы увидеть, как отличается работа консольного приложения от графического.

      В качестве примера полноценного консольного приложения напишем программу, которая перечислит все подключенные сетевые ресурсы (диски и принтеры), используя системные функции WNetOpenEnum(), WNetEnumResource() и WNetCloseEnum().

      ; netenum.asm
      ; Консольное приложение для win32, перечисляющее сетевые ресурсы
      include def32.inc
      include kernel32.inc
      include mpr.inc
      
              .386
              .model     flat
              .const
      greet_message      db  'Example win32 console program',0Dh,0Ah,0Dh,0Ah,0
      error1_message     db  0Dh,0Ah,'Could not get current user name',0Dh,0Ah,0
      error2_message     db  0Dh,0Ah,'Could not enumerate',0Dh,0Ah,0
      good_exit_msg      db  0Dh,0Ah,0Dh,0Ah,'Normal termination',0Dh,0Ah,0
      enum_msg1          db  0Dh,0Ah,'Local ',0
      enum_msg2          db  ' remote - ',0
              .data
      user_name          db  'List of connected resources for user '
      user_buff          db  64 dup (?)    ; буфер для WNetGetUser
      user_buff_l        dd  $-user_buff   ; размер буфера для WNetGetUser
      enum_buf_l         dd  1056          ; длина enum_buf в байтах
      enum_entries       dd  1             ; число ресурсов, которые в нем помещаются
              .data?
      enum_buf  NTRESOURCE  <?,?,?,?,?,?,?,?>  ; буфер для WNetEnumResource
                         dd  256 dup (?)   ; 1024 байт для строк
      message_l          dd  ?             ; переменная для WriteConsole
      enum_handle        dd  ?             ; идентификатор для WNetEnumResource
              .code
      _start:
      ; получим от системы идентификатор буфера вывода stdout
              push       STD_OUTPUT_HANDLE
              call       GetStdHandle      ; возвращает идентификатор STDOUT в eax
              mov        ebx,eax           ; а мы будем хранить его в EBX
      
      ; выведем строку greet_message на экран
              mov        esi,offset greet_message
              call       output_string
      
      ; определим имя пользователя, которому принадлежит наш процесс
              mov        esi,offset user_buff
              push       offset user_buff_l    ; адрес переменной с длиной буфера
              push       esi                   ; адрес буфера
              push       0                     ; NULL
              call       WNetGetUser
              cmp        eax,NO_ERROR          ; если произошла ошибка
              jne        error_exit1           ; выйти
              mov        esi,offset user_name  ; иначе - выведем строку на экран
              call       output_string
      
      ; начнем перечисление сетевых ресурсов
              push       offset enum_handle    ; идентификатор для WNetEnumResource
              push       0
              push       RESOURCEUSAGE_CONNECTABLE ; все присоединяемые ресурсы
              push       RESOURCETYPE_ANY      ; ресурсы любого типа
              push       RESOURCE_CONNECTED    ; только присоединенные сейчас
              call       WNetOpenEnum          ; начать перечисление
              cmp        eax,NO_ERROR          ; если произошла ошибка
              jne        error_exit2           ; выйти
      
      ; цикл перечисления ресурсов
      enumeration_loop:
              push       offset enum_buf_l       ; длина буфера в байтах
              push       offset enum_buf         ; адрес буфера
              push       offset enum_entries     ; число ресурсов
              push       dword ptr enum_handle   ; идентификатор от WNetOpenEnum
              call       WNetEnumResource
              cmp        eax,ERROR_NO_MORE_ITEMS ; если они закончились
              je         end_enumeration         ; завершить перечисление
              cmp        eax,NO_ERROR            ; если произошла ошибка
              jne        error_exit2             ; выйти с сообщением об ошибке
      
      ; вывод информации ресурсе на экран
              mov        esi,offset enum_msg1    ; первая часть строки
              call       output_string           ; на консоль
              mov        esi,dword ptr enum_buf.lpLocalName ; локальное имя устройства
              call       output_string           ; на консоль
              mov        esi,offset enum_msg2    ; вторая часть строки
              call       output_string           ; на консоль
              mov        esi,dword ptr enum_buf.lpRemoteName ; удаленное имя устройства
              call       output_string           ; туда же
      
              jmp        short enumeration_loop  ; продолжим перечисление
      ; конец цикла
      end_enumeration:
              push       dword ptr enum_handle
              call       WNetCloseEnum           ; конец перечисления
      
              mov        esi,offset good_exit_msg
      exit_program:
              call       output_string           ; выведем строку
              push       0                       ; код выхода
              call       ExitProcess             ; конец программы
      ; выходы после ошибок
      error_exit1:
              mov        esi,offset error1_message
              jmp        short exit_program
      error_exit2:
              mov        esi,offset error2_message
              jmp        short exit_program
      
      ; процедрура output_string
      ; выводит на экран строку
      ; ввод: esi - адрес строки
      ;       ebx - идентификатор stdout или другого консольного буфера
      
      output_string proc near
      
      ; определим длину строки
              cld
              xor        eax,eax
              mov        edi,esi
              repne      scasb
              dec        edi
              sub        edi,esi
      ; пошлем ее на консоль
              push       0
              push       offset message_l ; сколько байт выведено на консоль
              push       edi              ; сколько байт надо вывести на консоль
              push       esi              ; адрес строки для вывода на консоль
              push       ebx              ; идентификатор буфера вывода
              call       WriteConsole ; WriteConsole(hConsoleOutput,lpvBuffer,cchToWrite,
                                      ;              lpcchWritten,lpvReserved)
              ret
      output_string endp
      
              end        _start
      

      В файл kernel32.inc надо добавить между ifdef TASM и else строки:

                        extrn   GetStdHandle:near
                        extrn   WriteConsoleA:near
      WriteConsole      equ     WriteConsoleA

      и между else и endif:

                        extrn   __imp__GetStdHandle@4:dword
                        extrn   __imp__WriteConsoleA@20:dword
      GetStdHandle      equ     __imp__GetStdHandle@4
      WriteConsole      equ     __imp__WriteConsoleA@20

      Кроме того, надо создать файл mpr.inc:

      ; mpr.inc
      ; включаемый файл с определениями функций из mpr.dll
      ;
      ifdef _TASM_
        includelib import32.lib
      ; имена используемых функций
                        extrn   WNetGetUserA:near
                        extrn   WNetOpenEnumA:near
                        extrn   WNetEnumResourceA:near
                        extrn   WNetCloseEnum:near
      ; присваивания для облегчения читаемости кода
      WNetGetUser       equ     WNetGetUserA
      WNetOpenEnum      equ     WNetOpenEnumA
      WNetEnumResource  equ     WNetEnumResourceA
      else
        includelib mpr.lib
      ; истинные имена используемых функций
                        extrn   __imp__WNetGetUserA@12:dword
                        extrn   __imp__WNetOpenEnumA@20:dword
                        extrn   __imp__WNetEnumResourceA@16:dword
                        extrn   __imp__WNetCloseEnum@4:dword
      ; присваивания для облегчения читаемости кода
      WNetGetUser       equ     __imp__WNetGetUserA@12
      WNetOpenEnum      equ     __imp__WNetOpenEnumA@20
      WNetEnumResource  equ     __imp__WNetEnumResourceA@16
      WNetCloseEnum     equ     __imp__WNetCloseEnum@4
      endif
      
      

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

      ; def32.inc
      ; файл с определениями констант и типов для примеров программ под win32
      
      ; из winbase.h
      STD_OUTPUT_HANDLE               equ   -11
      
      ; из winerror.h
      NO_ERROR                        equ   0
      ERROR_NO_MORE_ITEMS             equ   259
      
      ; из winnetwk.h
      RESOURCEUSAGE_CONNECTABLE       equ   1
      RESOURCETYPE_ANY                equ   0
      RESOURCE_CONNECTED              equ   1
      NTRESOURCE struc
                      dwScope         dd    ?
                      dwType          dd    ?
                      dwDisplayType   dd    ?
                      dwUsage         dd    ?
                      lpLocalName     dd    ?
                      lpRemoteName    dd    ?
                      lpComment       dd    ?
                      lpProvider      dd    ?
      NTRESOURCE ends
      
      

      Этот пример, разумеется, можно было построить более эффективно, выделив большой буфер для WNetEnumResource(), например при помощи LocalAlloc() или GlobalAlloc() (в Win32 это одно и то же), и затем, прочитав информацию обо всех ресурсах из него, пришлось бы следить за тем, кончились ресурсы или нет, и вызывать WNetEnumResource() еще раз.


      1. KukarekusUltra
        08.05.2026 20:41

        Учился в ИГУ?)


        1. HardWrMan
          08.05.2026 20:41

          Нет, по книжке, которую купил сам.


    1. TimurZhoraev
      08.05.2026 20:41

      А как там строка Cannot be run in dos mode поживает, проще её заменить на Hello World и сделать указатель на неё прямо из экзешника


      1. HardWrMan
        08.05.2026 20:41

        Вот именно. Эта заглушка - часть стандартного РЕ. И если нам надо просто вывести строку, нам расширенный РЕ и не нужен, достаточно стандартного.


      1. s-a-u-r-o-n
        08.05.2026 20:41

        Некоторые не только убирают эту заглушку, но и уменьшают размер исполняемого файла путём наложения заголовков DOS и PE друг на друга.

        http://www.phreedom.org/research/tinype/


        1. HardWrMan
          08.05.2026 20:41

          Ух, ловкачи!


    1. TechExpert
      08.05.2026 20:41

      уважаемый, не много не по теме вопрос, тоже начинал программирование очень давно, еще на асме для z80, но потом не было такой необходимости низко программить. У вас очень хорошо получается объяснять. Могли бы вы посоветовать достойную литературу по реверс, интернет ресурсы, статьи, которые считаете полезными. Последнее время, читаю книги и понимаю, что они являются очень плохим переводом английских людьми далекими от программирования. в 90-х, 0-х литература как-то более дружелюбная и системная была.


      1. antofik
        08.05.2026 20:41

        Practical Reverse Engineerying by Bruce Dang (ссылка)


      1. AndreyDmitriev
        08.05.2026 20:41

        в дополнение к упомянутой книге можете полистать Даниэль Куссвюрм Профессиональное программирование на ассемблере x64, там довольно базово всё, но начинается с регистров и основ. А так — зависит от целей. Если просто поупражняться с ассемблером, то надо попробовать все ассемблеры, какие найдёте и выбрать "свой", FASM популярен, а мне больше ЕвроАссемблер нравится, там есть примеры всякие. Некоторые и MASM используют. По реверсу дизассемблер понадобится, можно IDA, можно Ghidra. Ну там Hex Editor, я 010 использую. Написать на Си несложное крякми приложение, самому же его дизассемблировать, пройти под отладчиком (windbg, x64dbg) и поломать. Криса Касперски можно почитать. Ну и самому пробовать так и сяк. ИИ не стесняться пользовать — он несложные примеры а асме легко генерит и отвечает на вопросы.


        1. TechExpert
          08.05.2026 20:41

          Ассемблер боль мень мне знаком. Интересует имеено способы реверса, инструменты, подходы. Например, иногда некоторые вещи трудно найти. Искал недавно описание elf файла для qnx4 x86. Как происходит инициализация при запуске программ и распределение адресов для библиотечных функций. В ида есть отличный инструмент, она для знакомых компиляторов может найти известные сишные функции и дать им имена. Интересно каким образом создать подобную базу самому для другого компилятора. Подобные технологии мне и интересны. Желательно кросплатформенные и по железу. Сейчас реверсю qnx 6 под процессор sh4. Или вот часто нужно прям до байта разбирать команды ассемблера, чтобы точно понимать что за операнды и почему используются. Попадаются не всегда удобные таблицы или ссылки на сайт интел. Спасибо за информацию, буду изучать.


  1. Gapon65
    08.05.2026 20:41

    [ Я совершено не оспариваю неявное утверждение о том, что iostream в C++ это полнейшее "гуано". ]

    Вопрос лишь - в чем, собственно, смысл делать сборку без динамических/разделяемых библиотек (флаг -static который при сборке передается в ld)? Если Вы строите бинарник для встроенных приложений (микроконтроллеров, систем реального времени) то C++ Standard Library вам точно не нужна поскольку эта библиотека (по умолчанию, если Вы не используете специальные аллокаторы) динамически аллокирует/освобождает память. Во всех осталных случаях удобнее использовать разделяемые библиотеки.

    Если уберете этот флаг и скомпилируете:

    g++ -O2 hello.cpp -o hello.exe

    То результат будет совсем иной:

    ls -al 
    total 39
    drwxr-xr-x 2 xxx yyy     4 May  8 15:23 .
    drwxr-xr-x 8 xxx yyy     8 May  8 15:05 ..
    -rw-r--r-- 1 xxx yyy    75 May  8 15:05 hello.cpp
    -rwxr-xr-x 1 xxx yyy 18192 May  8 15:23 hello.exe
    
    ldd hello.exe 
    	linux-vdso.so.1 (0x00007ffed21c3000)
    	libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f4b17c00000)
    	libm.so.6 => /lib64/libm.so.6 (0x00007f4b17b25000)
    	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f4b17eca000)
    	libc.so.6 => /lib64/libc.so.6 (0x00007f4b17800000)
    	/lib64/ld-linux-x86-64.so.2 (0x00007f4b17ef0000)
    
    nm hello.exe
    0000000000403de0 d _DYNAMIC
    0000000000404000 d _GLOBAL_OFFSET_TABLE_
    0000000000401140 t _GLOBAL__sub_I_main
    0000000000402000 R _IO_stdin_used
                     w _ITM_deregisterTMCloneTable
                     w _ITM_registerTMCloneTable
                     U _ZNKSt5ctypeIcE13_M_widen_initEv@GLIBCXX_3.4.11
    0000000000401260 W _ZNKSt5ctypeIcE8do_widenEc
                     U _ZNSo3putEc@GLIBCXX_3.4
                     U _ZNSo5flushEv@GLIBCXX_3.4
                     U _ZNSt8ios_base4InitC1Ev@GLIBCXX_3.4
                     U _ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4
                     U _ZSt16__throw_bad_castv@GLIBCXX_3.4
    0000000000404080 B _ZSt4cout@GLIBCXX_3.4
    0000000000404191 b _ZStL8__ioinit
                     U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@GLIBCXX_3.4
    0000000000402118 r __FRAME_END__
    000000000040201c r __GNU_EH_FRAME_HDR
    0000000000404060 D __TMC_END__
    000000000040037c r __abi_tag
    000000000040405c B __bss_start
                     U __cxa_atexit@GLIBC_2.2.5
    0000000000404058 D __data_start
    0000000000401220 t __do_global_dtors_aux
    0000000000403dd8 d __do_global_dtors_aux_fini_array_entry
    0000000000402008 R __dso_handle
    0000000000403dc8 d __frame_dummy_init_array_entry
                     w __gmon_start__
                     U __libc_start_main@GLIBC_2.34
    00000000004011a0 T _dl_relocate_static_pie
    000000000040405c D _edata
    0000000000404198 B _end
    0000000000401264 T _fini
    0000000000401000 T _init
    0000000000401170 T _start
    0000000000404190 b completed.0
    0000000000404058 W data_start
    00000000004011b0 t deregister_tm_clones
    0000000000401250 t frame_dummy
    00000000004010b0 T main
    00000000004011e0 t register_tm_clones
    


    1. Vlad441 Автор
      08.05.2026 20:41

      Как только вы начинаете полагаться на то что libstdc++ будет предустановлена в системе, есть риск, что на другом компьютере вы можете столкнуться c

      Запуск программы невозможен, так как на компьютере отсутствует libstdc++.dll

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

      ldd hello.exe 
        linux-vdso.so.1 (0x00007ffed21c3000)
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f4b17c00000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f4b17b25000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f4b17eca000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f4b17800000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4b17ef0000)

      В вашем примере вы полагаетесь на то, что в системе будут и libstdc++ и libc и libm и еще от компилятора libgcc_s.

      Если хоть чего-то из этого не будет, или же будет не той версии которой нужно (привет от GLIBC) - ваш файл превращается в тыкву. Он не запуститься в среде с musl или же где нет компилятора с его libgcc_s.so.


      1. DustCn
        08.05.2026 20:41

        Ну так на другой системе будет компилятор. Вы перенесете на неё исходники, мейкфайл, и все. А если захочется "по серьезному" - делаете инсталлятор.
        Если для портабильности все будут носить с собой полные библиотеки, то зачем тогда придумывали .so/dll?


        1. saege5b
          08.05.2026 20:41

          А потом придумали Appimage и прочие :)

          Компиляция на месте под конкретное железо это хорошо, но порой уж очень долго и заморочно :(


      1. feelamee
        08.05.2026 20:41

        как иронично. Автор статьи, который ратует за размер бинарника, считает правильным паковать все зависимости вместе с приложением…


        1. vadimr
          08.05.2026 20:41

          Ну так зависимости и являются частью приложения. Методически правильно делает.


          1. feelamee
            08.05.2026 20:41

            зависимости это не часть приложения.

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

            Зависимости являются частью приложения только если их нет)


      1. Gutt
        08.05.2026 20:41

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


        1. HardWrMan
          08.05.2026 20:41

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

          Так что это настраиваемый связанный параметр на одной оси: слева статика, справа динамика. Ну а истина где-то посередине.


        1. valdemar-const
          08.05.2026 20:41

          Идея с динамическим связыванием с системными библиотеками не работает. В случае Windows, у которой хорошая поддержка обратной совместимости вплоть до версий ОС конца прошлого тысячелетия, это не проблема. DLL на Windows действительно работают хорошо. А вот с Linux — это боль. Потому что отдельный дистрибутив — это мир в себе, никакой совместимости там нет на концептуальном уровне. И если вы хотите распространять свою предкомпилированную программу, которая имеет максимальный охват, вам нужно полагаться на более старые версии системных библиотек и glibc, компилятор можно взять самый новый, но скомпилированный под всё то же древнее окружение. Упаковать ваш исполняемый файл и shared-библиотеки в AppImage и никогда не полагаться на то, что в системе или пакетном менеджере хост-дистрибутива будет в наличии какой бы то ни было пакет. Динамическая линковка с libstdc++ — это роскошь, которую могут себе позволить создатели пакетов отдельно взятой версии конкретного дистрибутива. Не вы выбираете, на какой версии стандарта C++ вам писать программу, а разработчик дистрибутива. Например, на современных Astra Linux или ALT системно установлен GCC 13. Если я хочу C++23 — всегда статическая линковка и сборка всех зависимостей из исходников со старой версией glibc. Благо, с Docker/CMake это очень просто реализовать.

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

          ИМХО, разумеется. По работе намучался с этим вопросом. Есть ещё всякие Flatpak и аналоги, но это слишком сложно для простых пользователей, которым просто надо вашу программу запустить.


          1. Sap_ru
            08.05.2026 20:41

            Ну да ну да, а потом вылезает VCRUNTIME140 и подобное. Поверх этого работает адски шаманская система манифестов и версионирования библиотек, которая, вроде как помогает, но если что-то пошло не так... Достаточно заглянуть в хранилище установленных бибилиотек, чтобы увидеть, что там может лежать по 50 версий системных библиотек.
            В GCC и libstd, кстати, есть ещё более интересная штука, когда библиотека хранит в себе все предыдущие версии, а нужный API выбирается динамически при runtime подключении. Вот только очень мало кто это использует из программистов, а автоматически оно всегда к последней версии вяжется. Но в приниципе, можно на новом GCC можно скомпилировать программу, которая будет использовать более старый API и потому будет соместима с большинством линуксов со времён царя Гороха. libc++ это, к сожалению, не работает, так как библиотека завязана на версию C++, но и у MS с этим тоже всё сложно.


      1. mshonich
        08.05.2026 20:41

        так именно поэтому нормальные библиотеки и собираются под несколько таргетов, включая экзотику типа musl)

        зато для ОС загрузка бинарника с динамической линковкой максимально быстрая - все нужные либы уже в памяти почти 100%, только докинуть пару сегментов в карту памяти и приложение готово к работе. а эта ваша статическая линковка только занимает лишнюю физическую память да портит кеш


      1. Gapon65
        08.05.2026 20:41

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

        • Docker containers (DockerHub, GitHub CR, etc.)

        • source code (GitHub)

        Собственно для них лучше подходят разделяемые библиотеки.

        В первом случае, все взаимо-согласованные внешние зависимости (LIBC, библиотеки исполнения компилятора, и прочее, что не является продуктом компиляции Вашего кода) включаются в базовый образ (image) run-time контейнерa. Run-time контейнер обеспечивает правильную среду для приложения. На основании такого контейнера вы уже строите следующий уровень - application container, который содержит лишь Ваше приложение скомпилированное для версий библиотек базового контейнера. Помимо собственно бинарников, Вы можете включить в свой контейнер все, что нужно для их работы - конфигурационные файлы, и прочее. Контейнеры это очень удобный механизм позволяющий максимально упростить application deployment либо через Docker run-time либо в Kubernetes.

        Второй способ обычно требует CMake, который используется для сборки приложения там где оно и будет работать.


  1. Sap_ru
    08.05.2026 20:41

    Во-первых и прежде всего, вы непонятно чего хотите добиться.
    Во-вторых, есть ощущение, что вы это делаете неправильно.

    Вы зачем-то смело прыгнули в "nostdlib", но при этом не используете оптимизацию линкера!
    Никогда ни в коем случае не нужно на десктопе использовать "nostdlib" и писать свои CRT. Там всё намного-намного сложнее, чем кажется на первый взгляд. Вы совершенно точно что-то неуловимо поломаете и потом месяцами будете ошибки ловить.
    Вы уверены, что ваш куцый CRT корректно отрабатывает работу с исколючениями и все имеющиеся в stdlib способы инициализации/деинициализации?
    А математика вся во всех случаях будет работать? Точно-точно? А где тогда тонкая настройка компилятора?
    У вас же в такой конфигурации масса всего работать не будет (или что хуже - будет работать некорректно) - от многопоточности, до особенностей работы с плавающей точкой и выделением памяти!
    И, кстати, почему для начала не отключили исключения в настройках компилятора? Это сразу бы уменьшило размер файла, без вот этого всего. Всё равно вы практически наверняка поломали их, выкинув стандартный startup.

    И ещё раз: почему вы даже не посмотрели в стону оптимизации линкером, которая во многом и была придумана, чтобы решить вашу "проблему": lto и тому подобное?

    Я три раза портировал stdlib и crt на новые платформы, много раз писал на C++ под "голое железо" и поэтому повтрю: совершенно абсолютно никогда так не делайте на десктопе!!! Такие оптимизации требуют глубочайшего знания особенностей реализации crt и stdlib на конкретной платформе, компиляторе и линкере! На десктопе это применяется только для загрузчиков исполняемых файлов и ядра ОС, в который масса сил тратится на обеспечение совместимости с кокретными версиями компиляторов, линкеров и платформ!

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


    1. Vlad441 Автор
      08.05.2026 20:41

      Я упоминал, что:

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

      То есть ни -flto/-flto=thin, ни -fno-exceptions ни даже специальный флаг GCC -fwhole-program не помогли выкинуть лишнее в случае включения iostream. Видимо линкер считает все его зависимости используемыми.

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

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

      Что на счет отказа от CRT - я это использовал в качестве сравнительного примера именно для Hello World и только под Windows.

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

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


    1. TimurZhoraev
      08.05.2026 20:41

      Вроде как в UART посылать гораздо проще - делается виртуальный порт, туда программа без какой либо библиотеки льёт данные. На многих операционках исторически работа с портом не требует обвеса. ну а в shell всё это ловится. Вообщем как будто имеется виртуальная ардуина в которой есть этот cout<< и printf, запускаем putty и наслаждаемся printf с сотней байт.


  1. AnSa8
    08.05.2026 20:41

    Добавьте флаг -fuse-ld=lld(если установлен lld) и сэкономите еще 1 килобайт.


  1. hren_sobachiy
    08.05.2026 20:41

    Как мы докатились до того что язык, вотчина которого bare metal, превратился в ещё один клей для свистоперделок. А всё остальное это уже не принципиальное но неизбежное следствие.


    1. HiItsYuri
      08.05.2026 20:41

      вотчина которого bare metal

      А это кстати неправда. С++ предоставляет низкоуровневый доступ, но никогда не планировался как замена С в железках.


      1. Dooez
        08.05.2026 20:41

        Спорное утверждение.

        Развитием языка занимается комитет, в который входят люди из разных индустрий.


        1. HiItsYuri
          08.05.2026 20:41

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


    1. TimurZhoraev
      08.05.2026 20:41

      Лучший cout<< тогда будет на VHDL/Verilog. Кстати был бы интересный printf с использованием многоядерного вывода, CUDA, нейросетевых-ядер и SIMD расширений процессора, для полноты ощущений консоли.


      1. hren_sobachiy
        08.05.2026 20:41

        Можно, а зачем? (c)

        Задача C++ (исторически, и как более удобный C) не дёргать printf, а максимально быстро и на любом чипе рассчитывать какие пиксели зажечь и с какой яркостью. А попытки сделать из него модный-молодёжный клей привели к jack of all trades, master of none: нишу железа по-прежнему занимает C, а как клей C++ со своим UB просто непригоден и опасен.


      1. vanxant
        08.05.2026 20:41

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


      1. Fedottt
        08.05.2026 20:41

        CUDA? Легко!

        Код :)
        #include <stdio.h>
        
        __global__ void helloKernel( void )
        {
          printf("Hello World!\n");
        }
        
        int main( void )
        {
          helloKernel<<<1>>>();
        
          return 0;
        }

        Всего

        Собираем в Developer PowerShell for VS и запускаем:

        Вуаля. Итоговый исполняемый файл - всего 169 КБ.


        1. TimurZhoraev
          08.05.2026 20:41

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

          Гарантия что данные прогонятся через GPU
          #include <stdio.h>
          #include <ctype.h>
          #include <cuda_runtime.h>
          
          // Ядро CUDA для применения шаблона HeLlOwO
          __global__ void applyTemplateKernel(char* data, int len) {
              int i = threadIdx.x; // Получаем индекс текущего потока
              
              if (i < len) {
                  // Если индекс четный — делаем букву заглавной, если нечетный — строчной
                  if (i % 2 == 0) {
                      if (data[i] >= 'a' && data[i] <= 'z') data[i] -= 32;
                  } else {
                      if (data[i] >= 'A' && data[i] <= 'Z') data[i] += 32;
                  }
              }
          }
          
          int main() {
              const char* h_input = "hello world";
              int len = 11;          // Длина строки "hello world"
              size_t size = len + 1; // +1 байт для нулевого терминатора (\0)
          
              // 1. Выделяем память на GPU (Device memory)
              char* d_str;
              cudaMalloc(&d_str, size);
          
              // 2. ГАРАНТИЯ: Копируем данные с CPU на GPU
              // Эта функция блокирует выполнение CPU, пока копирование не завершится
              cudaMemcpy(d_str, h_input, size, cudaMemcpyHostToDevice);
          
              // 3. Запуск ядра: 1 блок, количество потоков равно длине строки
              applyTemplateKernel<<<1, len>>>(d_str, len);
          
              // 4. Ждем завершения работы GPU
              // Без этого CPU может пойти дальше до того, как GPU закончит запись
              cudaDeviceSynchronize();
          
              // 5. Копируем результат обратно с GPU на CPU
              char h_output[12]; 
              cudaMemcpy(h_output, d_str, size, cudaMemcpyDeviceToHost);
          
              printf("Оригинал: %s\n", h_input);
              printf("Результат: %s\n", h_output); // Должно вывести: HeLlO WoRlD
          
              // Освобождаем выделенную память на GPU
              cudaFree(d_str);
              
              return 0;
          }

          Аналогично, ещё более интересная тема, можно даже попробовать прогнать через шейдер (лучше вообще говоря сделать отдельный "объектник" для шейдера чтобы узнать его размер в виде байткода gpu)

          ПринтФ через шейдер
          #include <GL/glew.h>
          #include <GLFW/glfw3.h>
          #include <stdio.h>
          #include <stdlib.h>
          #include <string.h>
          
          // --- GPU КОД (Compute Shader) ---
          const char* shaderSource = 
          "#version 430 core\n"
          "layout(local_size_x = 1) in;\n"
          "layout(std430, binding = 0) buffer DataBuffer {\n"
          "    uint data[];\n"
          "};\n"
          "void main() {\n"
          "    uint i = gl_GlobalInvocationID.x;\n"
          "    uint c = data[i];\n"
          "    if (i % 2 == 0) {\n" // Четные -> ВЕРХНИЙ
          "        if (c >= 97 && c <= 122) data[i] = c - 32;\n"
          "    } else {\n"           // Нечетные -> нижний
          "        if (c >= 65 && c <= 90) data[i] = c + 32;\n"
          "    }\n"
          "}\n";
          
          void checkShader(GLuint shader) {
              GLint success;
              glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
              if (!success) {
                  char infoLog[512];
                  glGetShaderInfoLog(shader, 512, NULL, infoLog);
                  printf("Shader Error: %s\n", infoLog);
              }
          }
          
          int main() {
              // 1. Инициализация контекста (нужен для доступа к GPU)
              if (!glfwInit()) return -1;
              glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // Окно не нужно
              GLFWwindow* window = glfwCreateWindow(1, 1, "Hidden", NULL, NULL);
              glfwMakeContextCurrent(window);
              glewInit();
          
              // 2. Компиляция шейдера
              GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
              glShaderSource(shader, 1, &shaderSource, NULL);
              glCompileShader(shader);
              checkShader(shader);
          
              GLuint program = glCreateProgram();
              glAttachShader(program, shader);
              glLinkProgram(program);
              glUseProgram(program);
          
              // 3. Подготовка данных
              char text[] = "hello world from shaders";
              int len = strlen(text);
          
              // 4. ГАРАНТИЯ: Создаем буфер и копируем данные на GPU (Host -> Device)
              GLuint ssbo;
              glGenBuffers(1, &ssbo);
              glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
              glBufferData(GL_SHADER_STORAGE_BUFFER, len, text, GL_DYNAMIC_DRAW);
              glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
          
              // 5. Запуск вычислений
              glDispatchCompute(len, 1, 1);
          
              // 6. ГАРАНТИЯ: Ждем завершения записи в память GPU
              glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
          
              // 7. ГАРАНТИЯ: Читаем данные обратно в CPU (Device -> Host)
              char* result = (char*)malloc(len + 1);
              glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, len, result);
              result[len] = '\0';
          
              printf("Input:  %s\n", text);
              printf("Output: %s\n", result); // HeLlO WoRlD FrOm sHaDeRs
          
              // Очистка
              free(result);
              glDeleteBuffers(1, &ssbo);
              glDeleteProgram(program);
              glfwDestroyWindow(window);
              glfwTerminate();
          
              return 0;
          }


  1. rumtex
    08.05.2026 20:41

    Кроме -s есть ещё -Osize и ещё какой-то, не помню. Хелло ворлд скомпилированный fasm у меня весит 2025 байт.


    1. AndreyDmitriev
      08.05.2026 20:41

      Если переложить данные из .data в .rdata, то при компиляции евро ассемблером вот этого:

      EUROASM
      hello2 PROGRAM Format=PE, Entry=Start, IconFile=
      
      [.rdata]
      Msg D "Hello, World!",10,13,0
      
      INCLUDE winapi.htm
      
      [.text]
      Start: 	nop
          WinAPI GetStdHandle, 0FFFFFFF5h
          WinAPI WriteConsoleA, eax, Msg, 0Fh 
      	WinAPI ExitProcess 0
      ENDPROGRAM

      станет секцией меньше, вот сравнение было/стало:

      и получится 1564 байта. Я выигрываю, ваш ход. ;) Хотя вроде на хабре уже делали минимальный хелло мир на асме, да что-то найти не могу.


      1. rumtex
        08.05.2026 20:41

        извиняюсь, я на дату просто посмотрел - там 164 байта. этож ассемблер. на эту тему больше конфузий вызывают компиляторы arm64 (zephirOS на 32битных инструкциях столько весит сколько ваш хелло ворлд)


  1. stanukih
    08.05.2026 20:41

    Интересно, а в родной среде - Linux ситуация та же? А clang в Windows/Linux? А так же как в этом же контексте ведёт себя MSVC именно в Windows. Не так же разных версий по годам, а хотя бы текущие/последние. И плюс что по занимаемой оперативной памяти.


    1. TimurZhoraev
      08.05.2026 20:41

      Для Линкус можно создать следующий вайб: хотеть минимальный ассемблер линукс убунту x86 вывод консоль ассемблер эльф.

      Код:

      section .data
          msg db "Hello, World!", 10      ; Строка с символом новой строки (10)
          msglen equ $ - msg              ; Вычисление длины строки
      
      section .text
          global _start                   ; Точка входа для линковщика
      
      _start:
          ; Системный вызов sys_write (вывод в консоль)
          mov rax, 1                      ; Номер вызова write для x86_64
          mov rdi, 1                      ; Дескриптор файла: 1 = stdout
          mov rsi, msg                    ; Указатель на строку
          mov rdx, msglen                 ; Длина строки
          syscall                         ; Вызов ядра
      
          ; Системный вызов sys_exit (завершение программы)
          mov rax, 60                     ; Номер вызова exit для x86_64
          xor rdi, rdi                    ; Код возврата 0 (успех)
          syscall                         ; Вызов ядра
      

      Пререквистисы: sudo apt update && sudo apt install nasm binutils

      Далее транслируем nasm -f elf64 hello.asm -o hello.o, линкуем ld hello.o -o hello

      Размеры. Итого исполняемый 8.9 кБ а объектник 880 байт

      Результат

      Плюс ещё для DOS были упаковщики exe, сжимающие обычными zip-ом секции и потом разворачивающие в памяти

      Также можно удалить различные артефакты командой strip --strip-all hello, после чего размер уменьшается 8872 -> 8488 байт

      Можно оптимизировать далее, убрав выравнивание, символы ld -s -n -z max-page-size=4096 hello.o -o hello. Размер получился после этого 4560 байт.

      Ну и хардкодить заголовок вручную вместо ассемблера

      bits 64
      org 0x00400000
      
      ; --- ELF Заголовок (EHDR) ---
      ehdr:
          db 0x7F, "ELF", 2, 1, 1, 0      ; Магические байты, класс (64-бит), порядок байтов
          times 8 db 0                    ; Паддинг (не используется)
          dw 2                            ; e_type: исполняемый файл
          dw 62                           ; e_machine: x86-64
          dd 1                            ; e_version
          dq _start                       ; Точка входа (адрес начала кода)
          dq phdr - ehdr                  ; Смещение до программного заголовка
          dq 0                            ; Смещение до заголовка секций (отсутствует)
          dd 0                            ; Флаги
          dw 64                           ; Размер ELF заголовка
          dw 56                           ; Размер записи программного заголовка
          dw 1                            ; Количество программных заголовков
          dw 0, 0, 0                      ; Информация о секциях (не нужна для запуска)
      
      ; --- Программный заголовок (PHDR) ---
      ; Описывает, как загрузить файл в память
      phdr:
          dd 1                            ; p_type: PT_LOAD (загружаемый сегмент)
          dd 5                            ; p_flags: R+X (чтение и исполнение)
          dq 0                            ; p_offset: смещение в файле
          dq 0x00400000                   ; p_vaddr: виртуальный адрес в памяти
          dq 0x00400000                   ; p_paddr: физический адрес
          dq filesize                     ; p_filesz: размер в файле
          dq filesize                     ; p_memsz: размер в памяти
          dq 0x1000                       ; p_align: выравнивание страниц
      
      ; --- Основной код ---
      _start:
          ; Системный вызов sys_write (вывод строки)
          mov al, 1                       ; rax = 1 (write)
          mov edi, 1                      ; rdi = 1 (stdout - дескриптор консоли)
          lea rsi, [rel msg]              ; rsi = указатель на строку (относительный)
          mov dl, msg_len                 ; rdx = длина строки
          syscall
      
          ; Системный вызов sys_exit (завершение работы)
          mov al, 60                      ; rax = 60 (exit)
          xor edi, edi                    ; rdi = 0 (код возврата)
          syscall
      
      msg db 'Hello, World!', 10          ; 10 — это символ переноса строки (\n)
      msg_len equ $ - msg                 ; Вычисляем длину строки автоматически
      
      filesize equ $ - ehdr               ; Полный размер файла
      

      Далее транслируем это дело nasm -f bin tiny.asm -o tiny, затем делаем файл исполняемым chmod +x tiny

      Рекорд! Для Линукс 158 байт

      Аналогично думается под Windows можно вручную собрать заголовок, вместо текста Cannot be run in dos mode - свою любую строку


      1. 40kTons
        08.05.2026 20:41

        Хардкодить заголовок? Потужно. У меня на дурачка так не получилось


        1. TimurZhoraev
          08.05.2026 20:41

          Попробуем завайбкодить. Кстати пример выше - это в чистом виде не вручную, а вот таким вот промптом сгенерированный пример. Эмбеддер-llm-хардварщик. Вообще говоря это супер, он знает весь мой 30-ти летний "компьютерный" опыт, а по факту всё наверное до 1957 года.

          Первый промпт. Устанавливаем компилятор для Винды (всё-таки пришлось расчехлить мне комп с 10-кой). Ай командная строчка ой не могу дай бат-филе мне срочно BuildTools чтобы компилятор был сию секунду с проверкой оной winget запущу её из под админа проверь что это так а не просто юзер угрюмый, да так чтобы cl.exe был как надо! Туда же потом запусти vs vars для путей хороших.

          Ставим MS C++
          @echo off
          setlocal
          
          :: 1. Проверка прав администратора
          net session >nul 2>&1
          if %errorLevel% neq 0 (
              echo [!] Ошибка: Запустите скрипт от имени АДМИНИСТРАТОРА.
              pause
              exit /b
          )
          
          echo [+] Права подтверждены. Работаем...
          
          :: 2. Установка Visual Studio Build Tools через winget
          echo [+] Проверка и установка BuildTools...
          winget install --id Microsoft.VisualStudio.2022.BuildTools --silent --override "--wait --add Microsoft.VisualStudio.Workload.VCTools;includeRecommended"
          
          if %errorLevel% neq 0 (
              echo [!] Что-то пошло не так при установке через winget.
          )
          
          :: 3. Поиск vcvars64.bat для настройки путей (cl.exe)
          echo [+] Настройка окружения cl.exe...
          
          set "vcvars_path=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
          
          if exist "%vcvars_path%" (
              call "%vcvars_path%"
              echo [+] Окружение настроено. Проверка:
              cl.exe
          ) else (
              echo [!] vcvars64.bat не найден по стандартному пути. 
              echo Попробуйте найти его вручную или проверьте установку.
          )
          
          pause
          

          Промпт: Ты bare metal программер супер хард рок Металлика, такой же хардкод. Давай MZ ударные правь exe заголовок чётко в видe db asm под компилятор ml.exe только. Посчитай длину заголовок кастом PE Header фарш. А cannot be run in dos mode смело меняй на превед мир туда указатель строка хотеть мэссадж бокс А тоже. Хоти файл асм туда полный для запуска прога.эксэ, минимум байт стаб только вызов систем или винапи 32 бит винда десяточка, действуй!

          Результат программы для ЭВМ на языке "Ассемблер"

          Слушай сюда, кодер! Сейчас накидаем сырой байт-код, который ml.exe проглотит и не подавится. Мы собираем олдскульный MZ-заголовок (DOS-стаб), который прыгает прямо в PE-секцию. Никакого лишнего жира, только металл и регистры. Это — настоящий олдскульный фарш в духе Metallica, забудь про линкеры, мы пишем заголовок руками. Машинный код — наш закон! Твой EXE будет звучать как Master of Puppets — чётко по долям, байт к байту.

          Лови фарш для ml.exe. Я выкинул всё лишнее, оставил только чистую ярость. Вместо стандартного нытья про DOS теперь там твой «Превед мир».

          Custom MZ + PE Header (ASM Style)

          assembly

          .386
          .model flat, stdcall
          
          ; --- ИМПОРТЫ И ЛИБЫ (The Backstage) ---
          MessageBoxA PROTO :DWORD, :DWORD, :DWORD, :DWORD
          ExitProcess PROTO :DWORD
          
          includelib user32.lib
          includelib kernel32.lib
          
          ; --- ДАННЫЕ (Объявлены заранее, чтобы не было A2006) ---
          .data
              text    db "Превед мир! Это RAW ASM.", 0
              caption db "Hardcore Metal Edition", 0
          
          .code
          
          ; --- MZ HEADER (The Kick Drum) ---
          mz_start:
              db 4Dh, 5Ah             ; e_magic: 'MZ'
              dw 0090h                ; e_cblp
              dw 0003h                ; e_cp
              dw 0000h                ; e_crlc
              dw 0004h                ; e_cparhdr
              dw 0000h                ; e_minalloc
              dw 0FFFFh               ; e_maxalloc
              dw 0000h                ; e_ss
              dw 00B8h                ; e_sp
              dw 0000h                ; e_csum
              dw 0000h                ; e_ip
              dw 0000h                ; e_cs
              dw 0040h                ; e_lfarlc
              dw 0000h                ; e_ovno
              dw 4 dup(0)             ; e_res
              dw 0000h                ; e_oemid
              dw 0000h                ; e_oeminfo
              dw 10 dup(0)            ; e_res2
              dd 00000080h            ; e_lfanew -> Указатель на PE Header (128 байт)
          
          ; --- DOS STUB (The Solo) ---
          ; Твой кастомный стаб
          msg_stub db 'Превед мир!', 24h ; '$' для DOS
          stub_code:
              push cs
              pop ds
              mov dx, offset msg_stub - offset mz_start
              mov ah, 09h
              int 21h
              mov ax, 4C01h
              int 21h
          
          ; --- ВЫРАВНИВАНИЕ ---
          ; Добиваем до 0x80 (128 байт), где начинается PE
          db (80h - (offset $ - offset mz_start)) dup(0)
          
          PE_Header:
              db 'PE', 0, 0         ; Signature
              dw 014Ch              ; Machine: x86
              dw 0001h              ; NumberOfSections
              dd 663D5C00h          ; TimeDateStamp
              dd 0, 0               ; Symbols
              dw 00E0h              ; SizeOfOptionalHeader
              dw 0102h              ; Characteristics: EXECUTABLE | 32BIT
          
          ; --- ВЫХОД НА СЦЕНУ ---
          start:
              ; Теперь 'text' и 'caption' определены заранее
              invoke MessageBoxA, 0, addr text, addr caption, 0
              invoke ExitProcess, 0
          
          end start
          

          ml.exe флаги дай

          Для ml.exe (MASM), чтобы собрать этот хардкорный фарш под Win10, тебе нужны флаги, которые не задают лишних вопросов и делают чистый объектник.

          Базовый набор для 32-bit (x86):

          ml /c /coff main.asm

          А теперь вторая стадия — Линковка (Лязг железа):

          Чтобы твой кастомный заголовок и стаб взлетели на Десятке, link.exe нужно приструнить:

          link /subsystem:windows /entry:start /nodefaultlib main.obj kernel32.lib user32.lib

          Вообщем игры с кастомным заголовком не прошли под среду построения Microsoft. Выход только один - использовать ещё более железный FASM

          Промпт. Йоу чел, bare metal! программер супер чёткий бит ага. такой же хардкод. Давай MZ раскачай правь exe заголовок коммон в видe db asm под компилятор FASM только. Посчитай длину заголовок кастом PE Header ага. А cannot be run in dos mode смело меняй на превед мир туда указатель строка хотеть мэссадж бокс А тоже. Хоти файл асм туда полный для запуска прога.эксэ, минимум байт стаб только вызов систем или винапи 32 бит винда десяточка, действуй йоу!

          Используем FASM

          Вот тебе самый чёткий и минималистичный хардкод на FASM. Настоящий bare metal стайл: MZ-заголовок, самописный PE-заголовок и минимум байтов для Win10.

          Вместо скучного текста про DOS — твой «превед мир», который вылетит через MessageBoxA.

          Код компиируется но винда ругается, ей нужны кратные 512
          format binary as 'exe'
          
          ; ========================================================
          ; MZ HEADER (DOS STUB)
          ; ========================================================
          db 'MZ'                      ; Signature
          dw 0x0090, 0x0003, 0x0000, 0x0004, 0x0000, 0xffff, 0x0000
          dw 0x00b8, 0x0000, 0x0000, 0x0000, 0x0040, 0x0000, 0x0000, 0x0000
          times 8 dw 0
          dd 0x00000080                ; Offset to PE Header
          
          ; --- DOS Code (Stub) ---
          ; Вместо "This program cannot be run..." фигачим месседж
          stub_start:
              push    cs
              pop     ds
              mov     dx, msg_hello - stub_start
              mov     ah, 0x09
              int     0x21
              mov     ax, 0x4c01
              int     0x21
          
          msg_hello db 'превед мир!', 0x0d, 0x0a, '$'
          align 16
          
          ; ========================================================
          ; PE HEADER
          ; ========================================================
          org 0x80
          _pe_header:
          db 'PE', 0, 0                ; Signature
          dw 0x014c                    ; Machine (i386)
          dw 1                         ; Number of Sections
          dd 0                         ; TimeDateStamp
          dd 0, 0                      ; PointerToSymbolTable, NumberOfSymbols
          dw _op_size                  ; SizeOfOptionalHeader
          dw 0x0102                    ; Characteristics (Executable, 32-bit)
          
          _optional_header:
          dw 0x010b                    ; Magic (PE32)
          db 8, 0                      ; Major/MinorLinkerVersion
          dd _code_size, 0, 0          ; SizeOfCode, SizeOfInitializedData, SizeOfUninitializedData
          dd _start, _start            ; AddressOfEntryPoint, BaseOfCode
          dd 0                         ; BaseOfData
          dd 0x00400000                ; ImageBase
          dd 0x1000                    ; SectionAlignment
          dd 0x200                     ; FileAlignment
          dw 4, 0                      ; Major/MinorOSVersion
          dw 0, 0                      ; Major/MinorImageVersion
          dw 4, 0                      ; Major/MinorSubsystemVersion
          dd 0                         ; Win32VersionValue
          dd _image_size               ; SizeOfImage
          dd _header_size              ; SizeOfHeaders
          dd 0                         ; CheckSum
          dw 2                         ; Subsystem (Windows GUI)
          dw 0                         ; DllCharacteristics
          dd 0x100000, 0x1000          ; SizeOfStackReserve/Commit
          dd 0x100000, 0x1000          ; SizeOfHeapReserve/Commit
          dd 0                         ; LoaderFlags
          dd 16                        ; NumberOfRvaAndSizes
          
          ; Data Directories
          dd 0, 0                      ; Export
          dd _import_table, 0          ; Import
          times 14 dd 0, 0             ; Остальные нули
          
          _op_size = $ - _optional_header
          _header_size = $
          
          ; --- Section Table ---
          db '.text', 0, 0, 0
          dd _code_size                ; VirtualSize
          dd 0x1000                    ; VirtualAddress
          dd _code_size                ; SizeOfRawData
          dd _start                    ; PointerToRawData
          dd 0, 0                      ; Relocations, Linenumbers
          dw 0, 0                      ; NumberOfRelocations, NumberOfLinenumbers
          dd 0x60000020                ; Characteristics (Code, Execute, Read)
          
          align 512
          _start:
          ; ========================================================
          ; CODE SECTION (WIN32)
          ; ========================================================
              push 0                   ; uType (MB_OK)
              push _caption            ; lpCaption
              push _text               ; lpText
              push 0                   ; hWnd
              call [MessageBoxA]
          
              push 0
              call [ExitProcess]
          
          _text    db 'Йоу, это Win10 Bare Metal!', 0
          _caption db 'FASM Power', 0
          
          align 16
          ; --- Import Table ---
          _import_table:
          dd _kernel32_import, 0, 0, _kernel32_name, _kernel32_thunks
          dd _user32_import, 0, 0, _user32_name, _user32_thunks
          dd 0, 0, 0, 0, 0
          
          _kernel32_name db 'KERNEL32.DLL', 0
          _user32_name   db 'USER32.DLL', 0
          
          align 4
          _kernel32_thunks:
          ExitProcess dd _exit_proc_name
          dd 0
          
          _user32_thunks:
          MessageBoxA dd _msgbox_name
          dd 0
          
          _kernel32_import = _kernel32_thunks
          _user32_import   = _user32_thunks
          
          _exit_proc_name dw 0
          db 'ExitProcess', 0
          _msgbox_name    dw 0
          db 'MessageBoxA', 0
          
          _code_size = $ - _start
          _image_size = 0x2000
          

          Что тут происходит, бро:

          1. MZ Header: Вручную прописаны байты db 'MZ'. Смещение до PE указано как 0x80.

          2. DOS Stub: Вместо стандартной ошибки — код на асме 16-бит, который выводит "превед мир!" и выходит.

          3. PE Header: Полностью расписан через db/dw/dd. Считает длину опционального заголовка автоматически (_op_size).

          4. Imports: Прямая привязка к USER32.DLL для MessageBoxA и KERNEL32.DLL для выхода.

          5. Alignment: Всё выровнено под 512 байт (FileAlignment) и 4096 байт (SectionAlignment), чтобы Винда не ругалась.

          Как собрать: Сохрани в hack.asm и жахни fasm hack.asm. На выходе будет чистейший экзешник.

          Бро, я вижу на скрине, что файл hack.exe весит 674 байта. Это и есть главная проблема для Windows 10. Она видит, что файл не выровнен по границе 512 байт (FileAlignment), и считает его битым или вирусным.

          Для «десятки» критично, чтобы SizeOfHeaders и физический размер файла были кратны 512. Давай сделаем финальный рывок — добавим жесткое выравнивание и поправим версию подсистемы на 5.1 (XP) или 6.0 (Vista), чтобы точно пролезло.

          Вот код, который обязательно должен выдать файл размером ровно 1024 байта (или 1536).

          Давай применим «тяжёлую артиллерию» FASM. Вместо ручного db/dw, будем использовать стандартные макросы, но с максимально жёстким контролем байтов. Это гарантирует, что заголовки будут правильными, а выравнивание — идеальным.

          Вот код, который точно создаст валидный PE-файл. Сохрани его и скомпилируй:

          fasm

          include 'win32a.inc' ; Используем стандартные макросы для стабильности
          
          format PE GUI 4.0     ; Указываем подсистему и версию (Win 95+)
          entry start           ; Точка входа
          
          ; ========================================================
          ; MZ HEADER & DOS STUB (Твой кастомный стайл)
          ; ========================================================
          ; Примечание: FASM сам соберет PE, если использовать 'format PE', 
          ; но мы добавим твой текст через секцию или оставим стандарт.
          ; Чтобы сделать именно "превед мир" в DOS, нужно шаманить с бинарным форматом,
          ; но давай сначала заставим его просто работать в Win10.
          
          section '.text' code readable executable
          
          start:
              invoke MessageBox, 0, _message, _caption, MB_OK
              invoke ExitProcess, 0
          
          _message db 'Йоу! Это Bare Metal на Win10!', 0
          _caption db 'FASM Hack', 0
          
          section '.idata' import data readable
          
          library kernel32, 'KERNEL32.DLL', \
                  user32,   'USER32.DLL'
          
          import kernel32, \
                 ExitProcess, 'ExitProcess'
          
          import user32, \
                 MessageBox, 'MessageBoxA'
          
          ; ========================================================
          ; ВЫРАВНИВАНИЕ (Секретный ингредиент)
          ; ========================================================
          ; Этот блок добьет файл до нужного размера, чтобы Винда не ругалась.
          db 512 dup (0) 
          

          Use code with caution.

          Почему этот вариант сработает:

          1. format PE GUI 4.0: FASM сам правильно высчитает длину Optional Header, SizeOfImage и SizeOfHeaders. Тебе не нужно гадать с dd и dw.

          2. invoke: Это чистый системный вызов через таблицу импорта, без лишнего мусора.

          3. Alignment: Использование макросов section гарантирует, что данные будут лежать по правильным адресам, которые ожидает загрузчик Windows.

          Как проверить:После компиляции размер файла должен быть кратно 512 (например, 1024, 1536 или 2048 байт).

          Обратите внимание на расположение папок FASM для инклудов

          2 килобайта. (2048)

          Подменяем всё-таки заголовок

          Как ни крути с заголовком, винда 10-ка и старше походу думает что это малварь
          format binary as 'exe'
          
          ; --- Константы ---
          IMAGE_BASE = 0x00400000
          RVA_BASE   = 0x1000
          
          FILE_START:
          ; ========================================================
          ; MZ HEADER (0x00 - 0x3F)
          ; ========================================================
          db 'MZ'
          dw 0x0090, 0x0003, 0x0000, 0x0004, 0x0000, 0xffff, 0x0000
          dw 0x00b8, 0x0000, 0x0000, 0x0000, 0x0040, 0x0000, 0x0000, 0x0000
          times 8 dw 0
          dd 0x80                      ; PE заголовок на 128-м байте
          
          ; ========================================================
          ; DOS STUB (0x40 - 0x7F)
          ; ========================================================
          db (0x40 - ($ - FILE_START)) dup 0 ; Жесткий переход на 0x40
              use16
              push    cs
              pop     ds
              mov     dx, msg_dos - FILE_START
              mov     ah, 9
              int     21h
              mov     ax, 4C01h
              int     21h
          msg_dos db 'превед мир!$', 0
          
          ; ========================================================
          ; PE HEADER (0x80 - 0x1FF)
          ; ========================================================
          db (0x80 - ($ - FILE_START)) dup 0 ; Жесткий переход на 0x80
          _pe_header:
          db 'PE', 0, 0
          dw 0x014c, 1, 0, 0, 0
          dw _op_size, 0x0102
          
          _opt_header:
          dw 0x010b
          db 12, 0
          dd 0x200, 0, 0
          dd RVA_BASE, RVA_BASE
          dd IMAGE_BASE
          dd 0x1000, 0x200
          dw 6, 0, 0, 0, 6, 0
          dd 0, 0x2000, 0x200, 0, 2, 0
          dd 0x100000, 0x1000, 0x100000, 0x1000, 0, 16
          
          ; Directories
          dd 0, 0
          dd (_import_table - CODE_START + RVA_BASE), _import_size
          times 14 dd 0, 0
          _op_size = $ - _opt_header
          
          ; Section Table
          db '.text', 0, 0, 0
          dd 0x1000, RVA_BASE, 0x200, 0x200, 0, 0, 0
          dd 0x60000020
          
          ; Добиваем ПЕРВЫЙ блок до 512 байт (0x200)
          db (0x200 - ($ - FILE_START)) dup 0
          
          ; ========================================================
          ; CODE & DATA (Блок 2, старт с 0x200 в файле)
          ; ========================================================
          CODE_START:
              use32
              push    0
              push    dword (_caption - CODE_START + RVA_BASE + IMAGE_BASE)
              push    dword (_text - CODE_START + RVA_BASE + IMAGE_BASE)
              push    0
              call    dword [(_iat_msg - CODE_START + RVA_BASE + IMAGE_BASE)]
          
              push    0
              call    dword [(_iat_exit - CODE_START + RVA_BASE + IMAGE_BASE)]
          
          _text    db 'Йоу, дамп теперь чистый!', 0
          _caption db 'Bare Metal', 0
          
          align 4
          _import_table:
          dd (_k_names - CODE_START + RVA_BASE), 0, 0, (_k_dll - CODE_START + RVA_BASE), (_k_thunks - CODE_START + RVA_BASE)
          dd (_u_names - CODE_START + RVA_BASE), 0, 0, (_u_dll - CODE_START + RVA_BASE), (_u_thunks - CODE_START + RVA_BASE)
          dd 0, 0, 0, 0, 0
          _import_size = $ - _import_table
          
          _k_dll db 'KERNEL32.DLL', 0
          _u_dll db 'USER32.DLL', 0
          
          _k_names: dd (_aExit - CODE_START + RVA_BASE), 0
          _u_names: dd (_aMsg - CODE_START + RVA_BASE), 0
          
          _k_thunks: _iat_exit dd (_aExit - CODE_START + RVA_BASE), 0
          _u_thunks: _iat_msg  dd (_aMsg - CODE_START + RVA_BASE), 0
          
          _aExit dw 0
                 db 'ExitProcess', 0
          _aMsg  dw 0
                 db 'MessageBoxA', 0
          
          ; --- ХВОСТ ---
          ; Добиваем до ровного КБ (1024 байта / 0x400)
          db (0x400 - ($ - FILE_START)) dup 0
          

          Вообщем винда походу детектирует битый заголовок и не даёт запускаться.

          Теперь жмём секции

          Бро, Десятка — та ещё вредина. Если файл весит 1.5 КБ, но «молчит», значит, ей не нравится, что импорты лежат в одной куче с кодом в режиме PE 6.0. Она считает такую структуру небезопасной и тихо гасит процесс.

          Давай сделаем каноничный вариант. Мы разделим код и импорт на две секции.

          Держи код, который 100% выкинет бокс:

          include 'win32a.inc'
          
          ; Ставим 6.0 для совместимости с Win10 x64
          format PE GUI 6.0
          entry start
          
          ; --- СЕКЦИЯ КОДА ---
          section '.text' code readable executable
          start:
              invoke MessageBox, 0, _message, _caption, MB_OK
              invoke ExitProcess, 0
          
          _message db 'Йоу! На 2КБ всё летает!', 0
          _caption db 'FASM Win10', 0
          
          ; --- СЕКЦИЯ ИМПОРТА ---
          ; Разделение секций — ключ к успеху на свежих билдах Винды
          section '.idata' import data readable
          
          library kernel32, 'KERNEL32.DLL', \
                  user32,   'USER32.DLL'
          
          import kernel32, \
                 ExitProcess, 'ExitProcess'
          
          import user32, \
                 MessageBox, 'MessageBoxA'
          

          Use code with caution.

          Почему этот вариант — «золотой»:

          1. Чистые права: Секция кода — только для чтения и выполнения. Секция данных — только для чтения. Win10 это любит.

          2. Разделение: Загрузчик Windows чётко видит, где искать таблицу функций (IAT), и не путается в твоих байтах.

          3. Стабильность: Этот шаблон — база. Если он не запустится, значит, твой антивирус или защитник (Windows Defender) считает самописные EXE без цифровой подписи угрозой.

          4. Красава, бро! 1536 байт — это «золотое сечение» для FASM на Десятке. Значит, загрузчик всё-таки проглотил комбинированную секцию, и мы вписались ровно в три блока по 512 байт.

            Это чистая победа: файл максимально поджат, импорты живые, Win10 не ругается. Настоящий Bare Metal стайл, как ты и хотел.

            Что мы имеем в итоге:

            • Заголовок: Стандартный PE (1024 байта), который не триггерит защиту системы.

            • Секция: Одна общая .all (512 байт), где и код, и данные, и таблица импорта.

            • Вес: Ровно 1.5 КБ.

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

          Итак, диалог с 1,5 кБ кто меньше? Объявляется конкурс на не битый заголовок содержащий что то вроде

          Для ранних версий винды вполне возможно что ок, речь идёт о 10ке и старше. Итак, рекордсмен ровно 1536 байт. Идём к 1024 или 512.


      1. AnSa8
        08.05.2026 20:41

        Размеры. Итого исполняемый 8.9 кБ ...

        Чего так много?

        
        __attribute__ (( section( ".text" ) ))
        static char str[] = "Hello!\n";
        
        extern "C" void _start() {
        	asm volatile ( "syscall" :: "a"( 1/*SYS_write*/), "D"(1/*STDOUT_FILENO*/), "S"(str), "d"(sizeof( str )) );
        	asm volatile ( "syscall" :: "a"(60/*SYS_exit */), "D"(0) );
        }
        wsl -e g++ -O2 -fno-exceptions -fno-rtti -fno-asynchronous-unwind-tables -nostartfiles -s -static -fuse-ld=gold -Wl,--gc-sections main.cpp -o main.elf

        main.elf - 824 байта.

        А если ещё сделать:

        wsl -e objcopy -j .text main.elf

        То остаётся всего 528 байтов.


      1. s-a-u-r-o-n
        08.05.2026 20:41

        Под Windows не получится сделать аналогично, поскольку в нём плавают номера syscall-ов от версии к версии.


        1. d3d14
          08.05.2026 20:41

          Они и в Линуксе плавают.


          1. netch80
            08.05.2026 20:41

            Нет, однажды добавленное уже остаётся навеки. Вы, наверно, с разными платформами путаете, вот тут номера между даже x86-32 и x86-64 несовместимы.


            1. d3d14
              08.05.2026 20:41

              Разными версиями Linux. Даже высокоуровневый код нужно перекомпилировать, а ABI тем более разное.


              1. netch80
                08.05.2026 20:41

                Вы ответили именно на

                поскольку в нём плавают номера syscall-ов от версии к версии.

                и я именно про это и говорю: с номерами syscall-ов такого не происходит, они не плавают. Для конкретной платформы они постоянны, и могут только добавляться новые; старые поддерживаются. Это правило зафиксировано жёстко при его разработке, как и все прочие детали syscalls ABI (местами к сожалению – например, диверсия с автоназначением управляющего терминала, если для этого сложились условия).

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

                А то, о чём вы говорите, это, я предполагаю, ABI уже на уровне libc и прочих библиотек, а также системных демонов вроде systemd, nscd, syslogd и прочих. Этот вопрос в мире Linux принципиально отдан на откуп построителям дистрибутивов, и спрос должен идти именно с них.


          1. AnSa8
            08.05.2026 20:41

            Не "плавают" они там.


    1. ivanrt
      08.05.2026 20:41

      Нет, ответил ниже. 15kb и та и другая прога.


  1. d3d14
    08.05.2026 20:41

    Не нужно вручную инициализировать CRT.
    В MSVS собирается с опцией NODEFAULTLIB вообще без CRT - размер 2.5 кБ.


  1. CoolCmd
    08.05.2026 20:41

    у visual c++ 2022 размер 64-битного exe равен 220 КБ. из зависимостей - только kernel32.dll. видимо, качество библиотеки g++ для винды (???) не слишком хорошее.


    1. Nemoumbra
      08.05.2026 20:41

      А при чём тут g++, если MSVC? g++ и под Винду есть.


      1. CoolCmd
        08.05.2026 20:41

        А при чём тут g++, если MSVC?

        я сравниваю библиотеки msvc и g++.

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


        1. Vlad441 Автор
          08.05.2026 20:41

          Таргет вроде бы явный - hello.exe а не просто hello. Да системные вызовы чисто виндовые.


  1. titbit
    08.05.2026 20:41

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


    1. TimurZhoraev
      08.05.2026 20:41

      это как раз-таки очень правильно, так как уже пришла пора отказываться от файловых библиотек в пользу объектных. Когда линкуется только то что необходимо а не то что нагрузили. Вплоть до того что в библиотеках прописываются условные пути по идентификаторам как их обходить. Вообще говоря эта кухня, структура эльфов, exe-шников, dll-so уже не менялась лет так 40, там капитальный ремонт нужен с учётом современных требований IDE и LLM, плюс довозка в компиляторы, линкёры нормального рефакторинга и работой вот с такими синтаксическими деревьями и кодогенерацией на нормальном уровне а не скрытой от пользователя


  1. RedEyedAnonymous
    08.05.2026 20:41

    Блин, как же в лохматом году борманы в своём дельфе умели выкидывать лишнее при линковке, а авторы современного, моднючего ц++ разучились?


    1. d3d14
      08.05.2026 20:41

      Лишнее выкидывается. Вопрос в том, что они считают лишним.


    1. LittleAlien
      08.05.2026 20:41

      У “борманов” те же проблемы, если в стандартных библиотеках прописана какая-то инициализация (а она прописана в VCL, в Classes и т.д.), никакой смарт линкер не поможет и прицепится всё.


  1. Ewoke
    08.05.2026 20:41

    Самая дикость, что этих printf-ов готовых, полно в сишных рантайм-библиотеках msvcrt, ТЫСЯЧИ ИХ. Причем они есть уже в винде изкаробки, но обязательно все равно нужно с собой dll-hell возить в каждом дистрибутиве каждого приложения, добавляя очередной Visual C Runtime 2026.


  1. ilgbs
    08.05.2026 20:41

    import <iostream>;
    
    int main()
    {
    	std::cout << "Hello World";
    }

    Только что в Windows скомпилировал данный код в последней версии Visual Studio 2026 с флагом -O2, у меня получился исполняемый файл размером 11 Кб.


    1. osmanpasha
      08.05.2026 20:41

      Дак оно и в gcc столько же весит, если динамически слинковать с рантаймом, как MS VC делает по умолчанию (выше в коментах есть).


  1. titbit
    08.05.2026 20:41

    В итоге для всех упомянутых версий 15.2.0, 13.1.0, 10.3.0, 4.9.2 получаем стабильные 3,50 КБ (3 584 байт)

    Если достаточно поиграться опциями и отключить исключения например, то у меня тот же файл выдает уже 2560 байт именно на g++. Чтобы сделать меньше надо сливать секции уже, что в случае gnu ld не так тривиально как для msvc link: надо аккуратно править скрипты линкера.

    Вы можете спросить, ну а зачем компилятор сует обработку исключений в код, если из кода очевидно, что никаких исключений не предвидется? Ответ прост: специально никто ничего не сует, просто вариант runtime библиотеки один - и он на все, даже самые тяжелые, случаи. Потому что иначе пришлось бы иметь два варианта библиотек - для исключений и без них. И два варианта кажутся ерундой, но потом кто-то скажет, а мне не нужно utf8, а кому-то rtti, а кому-то потоки не нужны, а кому-то еще не нужна поддержка новых фич процессоров ну и т.д. и число вариантов билиотек устремляется к тысячам, а это банально никто не сможет поддерживать. Так что придется смириться с тем, что runtime библиотека обычно есть в паре-тройке вариантов покрывающих 97% всех программ и да, размер этой библиотеки обычно приличный, ну а для оставшихся экзотических вариантов придется выкручиваться самостоятельно.


    1. osmanpasha
      08.05.2026 20:41

      Хех, с компилятором для микроконтроллеров ARM (arm-gcc-none-eabi), такая ситуация как раз. Там на каждый вариант архитектуры свой рантайм (типа "arm v8-m c вещественными числами двойной точности"). В итоге с каждой новой версией компилятор толстеет на сотню-другую мегабайт, gcc 5 весил 100Мб, gcc 7 - 450Мб, gcc 10 - 730Мб, gcc 15 - 1.1 Гб.


    1. Vlad441 Автор
      08.05.2026 20:41

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

      Чтобы все эти паре-тройке вариантов можно было выкинуть ели они явно не нужны.


      1. titbit
        08.05.2026 20:41

        Линкер и так выбрасывает неиспользуемое. Просто например, поддержка исключений - она либо есть везде, либо ее нет нигде, поэтому чтобы выбросить всю поддержку надо пересобрать значительную часть runtime библиотеки, вот и получается “еще один вариант”. Ну и так со многими вещами. И даже если вы нигде не используете какую-то возможность языка, это не значит что ее не использовали авторы библиотеки и таким образом она и попадает в код исполняемого файла, к тому же всегда можно попросить линкер объяснить зачем он потащил какой-то объект в код.


  1. isumix
    08.05.2026 20:41

    Ну вот, оказывается не npm единый - является черной дырой зависимостей, а даже православные плюсы этим грешат.


  1. SilverTrouse
    08.05.2026 20:41

    import std?


  1. Akon32
    08.05.2026 20:41

    Тем временем, hello world на Python занимает 23 байта. Естественно, если вы не хотите тянуть рантайм, то приходится тянуть реализации "std::cout <<" в каждый бинарник. Особенно показателен Rust - без специальных (и известных всем, проблема не новая) оптимизаций по размеру бинарник легко оказывается больше 100МБ. С оптимизациями - вполне приличные ~500КБ.


    1. TimurZhoraev
      08.05.2026 20:41

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

      .class public HelloWorld
      .super java/lang/Object
      
      ; Стандартный конструктор (необходим для любого класса)
      .method public <init>()V
         aload_0
         invokenonvirtual java/lang/Object/<init>()V
         return
      .end method
      
      ; Точка входа в программу (main)
      .method public static main([Ljava/lang/String;)V
         .limit stack 2   ; Указываем максимальную глубину стека
         
         ; Получаем статический объект System.out
         getstatic java/lang/System/out Ljava/io/PrintStream;
         
         ; Кладем строку на вершину стека
         ldc "Hello JVM Bytecode!"
         
         ; Вызываем метод println
         invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
         
         return
      .end method
      

      Создаём ^^ этот файл HelloWorld.j, скачиваем jasmin.jar отсюда компилируем

      java -jar jasmin.jar HelloWorld.j

      должно получиться: Generated: HelloWorld.class

      запускаем


      размер байткода для ява: 384 байт


      1. Akon32
        08.05.2026 20:41

        Да, сначала хотел привести в пример Java, но формат .class - это минимум сотни байт.


  1. TimurZhoraev
    08.05.2026 20:41

    Запускаем эмулятор DOS. Берём TASM (Turbo assembler) компилируем следующее для работы с консолью через прерывание 21h

    .model tiny        ; Указываем модель tiny для создания .com файла
    .code
    org 100h           ; Точка входа для .com файлов всегда начинается с адреса 100h
    
    start:
        mov ah, 09h    ; Функция DOS 09h: Вывод строки на экран
        mov dx, offset msg ; Помещаем адрес сообщения в регистр DX
        int 21h        ; Вызываем прерывание DOS
    
        mov ax, 4C00h  ; Функция DOS 4Ch: Завершение программы (код возврата 0)
        int 21h        ; Вызываем прерывание DOS
    
    msg db 'Hello World!$' ; Строки для функции с символом '$'
    
    end start
    

    Транслируем tasm hello.asm и потом tlink /t hello.obj
    Получаем файл .com размером 25 байт

    Можно также без DOS сразу используя BIOS прерывание 10h для вывода на экран (записать по 0 дорожка 0 цилиндр 0 сектор только аккуратно)

    .model tiny
    .code
    org 100h
    
    start:
        mov ah, 0Eh    ; Функция BIOS 0Eh: Вывод символа в режиме телетайпа
        mov si, offset msg ; Указатель на начало строки в регистр SI
    
    print_loop:
        lodsb          ; Загружаем следующий байт из DS:SI в регистр AL и двигаем SI
        cmp al, 0      ; Проверяем, не дошли ли мы до конца строки (нуля)
        je done        ; Если встретили 0, выходим из цикла
        int 10h        ; Вызываем прерывание BIOS для печати одного символа в AL
        jmp print_loop ; Переход к следующему символу
    
    done:
        ret            ; В .com файлах простая команда ret завершает программу
    
    msg db 'Hello World!', 0 ; Строка заканчивается нулем (null-terminated)
    
    end start
    

    размер не сильно отличается, до 50 байт в любом исполнении


    1. HardWrMan
      08.05.2026 20:41

      О Зубкове я написал выше. :)

      PS Из COMы можно выходить по RET, потому что перед запуском система кладёт туда адрес возврата на вызов сервиса 0x4C@INT21. Кстати, можно ещё тупо INT 20h вызвать без параметра.


      1. TimurZhoraev
        08.05.2026 20:41

        Можно даже ещё круче - вылететь с ошибкой (без ret) но сделать вывод на экран, плюс ещё без терминатора $

        Функционально железобетонный вариант под dos, который громит всё вокруг но выводит, плюс с элементами машинных кодов 0xCD, 0x21 - это int21h в ASCII формате это =!

        [org 0x100]
        
        start:
            mov dx, msg        ; Адрес строки
            mov ah, 0x09       ; Функция печати DOS
        
            ; Провал в код. Процессор начнет исполнять строку.
        msg:  
            db "═!Hello World"


        1. HardWrMan
          08.05.2026 20:41

          Это уже на грани фола.

          PS

          0xCD, 0x21 - это int21h в ASCII формате это =!

          Желательно уточнить, что тут не знак "равно" (=) а двойная горизонтальная рамка псевдографики. Потому что "равно" это 0x3D.


          1. TimurZhoraev
            08.05.2026 20:41

            да, так и есть. Если уж DOS = и ═ (тут он так отображается)это чуть разные вещи, то про Unicode уже и говорить не приходиться. А так в принципе там просто можно вполне дописать db 0xCD, 0x21. Если нет транслятора ассемблера, то можно уж прям сразу в hex, в машинных кодах. Не помню кто умел печатать из хекса в символы - вроде как Dos Navigator, Notron не умел, только смотреть в виде hex.

            B4 09 BA 0B 01 CD 21 B8 00 4C CD 21 Hello World!$

            Вспоминаются странички с таким содержимым из разряда Радио-86 РК.

            На Спектруме можно было шить ассемблер в машинных командами PEEK/POKE прямо в ОЗУ. Из интерпретатора Бэйсика )). Да и в DOS что-то такое можно было делать с таблицей векторов. Это уже потом HIMEM и прочие config,sys подвезли почти что с виртуалкой


            1. Viacheslav01
              08.05.2026 20:41

              Помню подобную фигню "B4 09 BA 0B 01 CD 21 B8 00 4C CD 21" писал для z80 на салфетках лежа в больнице. И читал и понимал, сейчас не предстаавляю как мозг с этим справлялся, сернуть бы его )


              1. HardWrMan
                08.05.2026 20:41

                Я до сих пор в состоянии понять бинарные опкоды от MCS80, хотя прошло уже 30 лет с момента когда я реально этим пользовался. Помогает при починке всяких Корветов и прочих ПК/ЛК на ВМ80.


                1. Viacheslav01
                  08.05.2026 20:41

                  Ооо корветы, я как раз пытаюсь найти себе 8020 за вменяемые деньги в нормальном состоянии, закрыть недоделанное в школе дело )))


                  1. HardWrMan
                    08.05.2026 20:41

                    Тогда, вам точно сюда.


                    1. Viacheslav01
                      08.05.2026 20:41

                      Спаисбо


              1. Akon32
                08.05.2026 20:41

                C9 это ret. Больше не помню.


                1. Viacheslav01
                  08.05.2026 20:41

                  00 nop


              1. d3d14
                08.05.2026 20:41

                Я, помнится, для диплома, писал для Z80 прошивку переводя асм в hex коды на листочке, по таблице опкодов :)


            1. HardWrMan
              08.05.2026 20:41

              Не помню кто умел печатать из хекса в символы - вроде как Dos Navigator, Notron не умел, только смотреть в виде hex.

              Здрасьте. Иметь такие познания и забыть про hiew?

              Да и в DOS что-то такое можно было делать с таблицей векторов.

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


              1. TimurZhoraev
                08.05.2026 20:41

                нуу это знаменитый, он потом уже только появился когда с друзьями обменивались 3.5'' вместо 5.25'', не всё сразу, на пятёрке как раз умещались norton utilites. Помню windows 3.11 занимал что то в районе с десяток мелких дискет. Там уже и borland c подоспел и masm/tasm, а дос навигатора вполне хватало смотреть hex по F3, потом FAR конечно же (сейчас Midnight в принципе хватает), дизассемблер тоже был вроде как из этих пакетов, hiew это конечно классика, помню как можно было весело менять команды выравниванием до неузнаваемости или попытка им посмотреть сжатые exe-файлы, были ещё такие exe-шники которые ловили int 03 дебаггер и вываливались ругаясь ая яй нельзя меня ломать.

                Самое интересное - это попытка написать com резидентный (TSR) вирус )) который сканил по 21h и заражал другой com, встраиваясь в код. Exe вирусы это уже другое дело, особенно шифрующиеся, понравился тогда Civil Defence, который матом на принтер ругался. Потом как-то попался этот текстовик (он не оригинальный, что-то не ищется, наверное уже в забвении). Тогда был drWeb, полифаг Лозинского и ещё какой-то. Плюс ещё эксперименты с прямым чтением цилиндр-сектор-голова по int 13.


        1. vanxant
          08.05.2026 20:41

          поясните нафига тут этот мусор в строке? что мешало сделать честное int 21h и только потом msg: db "Hello world" ? Т.е. в двоичном виде изменится только адрес msg


          1. TimurZhoraev
            08.05.2026 20:41

            Это из того времени, когда tasm/masm нужно было школьнику где-то разыскать на дискете. Тогда "кнопка download" была разве что в институтах или FIDO (и то это для тех у кого был модем). Поэтому приходилось использовать подручные средства, это так, дань истории, когда можно было переводить ASCII в машиный код и обратно, глядя в те самые 80 строк и 25 столбцов на 13'' ЭЛТ мониторе. Первый 386й, 4 МБ, SVGA до 800 на 600, 1994й год, 9-ти игольный Эпсон. Первые проги были .com, умещающиеся в сегмент 64 кБ. Их действительно можно было делать прямо в машкодах, взяв книжку по опкодам 8086 (тогда тоже надо было ещё раздобыть), вычислять смещение для jmp/jc вручную. Всегда забавляли вот эти вот карты - смайлики и это была как "таблица умножения" и тот самый 0x9 пищащий в спикер, int21 как раз умел это делать.

            для кириллицы как сейчас помню: mode con cp select=866 или prepare как то так


    1. czz
      08.05.2026 20:41

      Запускаем эмулятор DOS

      Ну не совсем спортивно, вы фактически, пишете код для виртуальной машины и тянете рантайм в виде эмулятора :)


  1. ivanrt
    08.05.2026 20:41

    Проблемы портирования posix на винду? На Linux всё нормально, потестировал на Gentoo:

    Linux -Os: c + stdio: 15kb, c++ + iostream: 15kb


    1. d3d14
      08.05.2026 20:41

      Какой нафиг посикс...


      1. KseandI
        08.05.2026 20:41

        Скорее всего про CRT имелось ввиду.


    1. KseandI
      08.05.2026 20:41

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

      Вы динамическую программу собираете, а в статье про статическую говорится.


  1. brat_viktor
    08.05.2026 20:41

    Собрал под Ubuntu в Qt Creator и Qt6.4.2 проект С++, без использования Qt и размер исполняемого файла 16 кб. Переносил чисто файл в голую папку /tmp, чтобы быть уверенным что не тянутся либы из относительных путей - запускается нормально.

    Самое сложное в метрологии - не намерять фигни!


    1. 100h
      08.05.2026 20:41

      Сделал то же самое (и внизу запостил коммент ) - подтверждаю!


    1. HardWrMan
      08.05.2026 20:41

      чтобы быть уверенным что не тянутся либы из относительных путей

      А что с абсолютными?


    1. fshp
      08.05.2026 20:41

      Если не статически линковали и не шаманили с настройками линкера, то проверьте через ldd


    1. bogolt
      08.05.2026 20:41

      Чтобы понять что он тянет нужно выполнить `ldd <бинарник>`

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


  1. 0xdead926e
    08.05.2026 20:41

    попробовала все то же самое на clang'e (с вижловой стл и вижловым линкером)- 212 килобайт в случае iostream и 124 килобайта в случае std::print. видать, g++ там всю свою библиотеку насильно тащит.


  1. TimurZhoraev
    08.05.2026 20:41

    Что касается C++ то можно навайбить следующее (вместе со стандартной библиотекой)

    #include <iostream>
    
    int main() {
        // Отключаем синхронизацию с stdio для небольшого ускорения
        std::ios_base::sync_with_stdio(false);
        
        std::cout << "Hello, World!" << std::endl;
        return 0;
    }
    

    Далее небольшой бэш для компиляции

    g++ -Os -s -fno-ident -fno-asynchronous-unwind-tables -DNDEBUG main.cpp -o hello_cpp
    # 1. Удаляем все лишние секции (комментарии, заметки)
    strip --strip-all -R .comment -R .note.ABI-tag -R .note.gnu.build-id hello_cpp
    

    В итоге файл 14192 байт

    Можно использовать упаковщик исполняемых upx, который sudo apt install upx-ucl

    g++ -Os main.cpp -o hello_cpp

    /usr/bin/upx -9 hello_cpp

    После чего размер становится 6332 байт

    Ну или трюк, компиляция C++ файла со C-вставкой

    Делаем "С++" файл со следующим содержимым без main

    extern "C" {
        void _start() {
            // static гарантирует, что строка в секции .rodata, а не на стеке
            static const char msg[] = "Hello from C++!\n";
            
            asm volatile (
                "mov $1, %%rax;"     // sys_write
                "mov $1, %%rdi;"     // stdout
                "mov %0, %%rsi;"     // адрес строки
                "mov $16, %%rdx;"    // длина
                "syscall"
                :
                : "r" (msg)
                : "rax", "rdi", "rsi", "rdx", "memory"
            );
    
            asm volatile (
                "mov $60, %%rax;"    // sys_exit
                "xor %%rdi, %%rdi;"  // код 0
                "syscall"
                :
                :
                : "rax", "rdi"
            );
        }
    }
    

    Делаем файл команд bmetal.ld линкёру

    ENTRY(_start)
    SECTIONS
    {
        . = 0x400000;
        .text : { *(.text) }
        .rodata : { *(.rodata) }
        .data : { *(.data) }
        /DISCARD/ : { *(.comment) *(.note*) }
    }
    

    Компилируем

    # 1. Компиляция объектного файла
    # -fno-stack-protector критически важен при использовании -nostdlib,
    # так как в системе нет стандартной библиотеки для проверки стека.
    g++ -c -Os -fno-exceptions -fno-rtti -fno-asynchronous-unwind-tables \
        -fno-stack-protector bmetal.cpp -o bmetal.o
    
    # 2. Линковка с использованием упрощенного скрипта
    # -s: Удалить все символы и отладочную информацию (заменяет команду strip)
    ld -T bmetal.ld -s bmetal.o -o bmetal_cpp
    
    # 3. Установка прав на исполнение
    chmod +x bmetal_cpp
    
    # 4. Запуск готовой программы
    ./bmetal_cpp

    Итого получили файл "C++" размером 4.5 кБ


    1. d3d14
      08.05.2026 20:41

      MSVS 2022

      #include <Windows.h>
      
      INT main()
        {
        WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), "Hello, world.", 13, NULL, NULL);
        }
      
      

      NODEFAULTLIB, обьединение секций, уменьшение выравнивания
      зависимостей нет
      EXE x64 - 800 Байт


      1. TimurZhoraev
        08.05.2026 20:41

        О, а вот это интересно, выше написал коммент как раз как на винде уменьшить, а тут файл 800 байт, хотя вроде как пишет что начиная с 10-ки где-то необходима кратность (выравнивание) в 512 байт иначе не запустится. Минимальный размер 1024 байта. Скорее всего это наверное под более ранние версии винды

        Код сделал таким, с указанием точки входа:

        #include <Windows.h>
        
        void mainCRTStartup() {
            DWORD written;
            WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), "Hello, world.", 13, &written, NULL);
            ExitProcess(0);
        }
        

        Линковка:

        cl.exe /O1 /GS- /GR- /c hello.c
        link.exe /NODEFAULTLIB /SUBSYSTEM:CONSOLE /ALIGN:16 /MERGE:.rdata=.text hello.obj kernel32.lib

        Результат


        1. d3d14
          08.05.2026 20:41

          Не, как раз раньше (windows 2000 и более ранние) не работали с alignment < 512 байт. А начиная с XP поддерживается < 512.
          Там еще можно убрать DOS stub и Rich signature, встраиваемую MS линкером, это уберет еще 100-200 байт, но я уж не стал заморачиваться.

          • CreateFile допускает указание NULL в качестве указателя на возвращаемый размер.

          • ExitProcess не обязателен, процесс корректно завершается (если стек не сбит где-нибудь).


    1. fshp
      08.05.2026 20:41

      У вас первый вариант динамически слинкован.


      1. TimurZhoraev
        08.05.2026 20:41

        Это какой, для Линукса или для Винды?
        Для Линукса первый - динамически (вариант статики при таких ключах не рассматривается так как он втянет в себя мир std::),
        второй - уже статически

        Для Винды проверил дополнительно:
        вывод дампа командой dumpbin /DEPENDENTS
        Dump of file my.exe File Type: EXECUTABLE IMAGE

        Image has the following dependencies:

        KERNEL32.dll - он в любом случае будет даже для 100% статического файла.

        Summary 80 .idata10 .reloc140 .text
        Скомпилирован с флагом /MT
        Плюс в объектном файле /DEFAULTLIB:"LIBCMT"


        1. fshp
          08.05.2026 20:41

          вариант статики при таких ключах не рассматривается так как он втянет в себя мир std::

          Но ведь именно про это и речь в статье.


        1. d3d14
          08.05.2026 20:41

          KERNEL32.dll - он в любом случае будет даже для 100% статического файла.

          При желании, можно и без него ) Ручное нахождение адресов АПИ функций. Это позволит совсем исключить импорт.


  1. DanielKross
    08.05.2026 20:41

    Хорошая статья, а комменты вообще огонь!!11 Спасибо парни, в избранное.


  1. 100h
    08.05.2026 20:41

    Хм... Что я делаю не так?
    /bin/g++ -v
    gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1)

    Программа из начала статьи компилируется в
    file ./helloworld

    Версия с отладочной информацией - 31896  байт:

    ./helloworld: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, with debug_info, not stripped

    Без отладочной информации - 16224 байт:

    ./helloworld: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, not stripped

    Дело, наверное, не в С++ ))))

    P.S. The gcc options:
    COLLECT_GCC_OPTIONS=‘-Os’ ‘-D’ ‘NDEBUG’ ‘-std=gnu++17’ ‘-v’ ‘-MD’ ‘-MT’ ‘CMakeFiles/helloworld.dir/main.cpp.o’ ‘-MF’ ‘CMakeFiles/helloworld.dir/main.cpp.o.d’ ‘-o’ ‘CMakeFiles/helloworld.dir/main.cpp.o’ ‘-c’ ‘-shared-libgcc’ ‘-mtune=generic’ ‘-march=x86-64’ ‘-dumpdir’ ‘CMakeFiles/helloworld.dir/’


    1. Vlad441 Автор
      08.05.2026 20:41

      Так у вас же dynamically linked :)
      Использовали ли вы флаг -static? На линухе оно неизбежно будет тащить либо glibc либо musl .


      1. 100h
        08.05.2026 20:41

        Я это прекрасно понимаю ))
        Согласитесь, если бы статья называлась "Я написал хелло ворлд на Си++ и статически прилинковал стандартную библиотеку. C++, остановись, 2Мб! ", то комментарии были бы совсем другие )


        1. Vlad441 Автор
          08.05.2026 20:41

          В третьем же абзаце было упоминание -static

          Выбрать удачное название статьи так, чтобы было и эффектно, и просто, и чтобы все всё правильно поняли - тот еще квест.


          1. AndreyDmitriev
            08.05.2026 20:41

            Я честно говоря, (используя gcc сильно эпизодически) static не сразу и заметил, а вы мастер хайповых заголовков, на этом внимание не акцентировали, при этом залезать в содержимое двух мегабайт вообще не стали, и оставили вопрос "почему так" в общем открытым. Но я думаю, что если бы вы таки залезли в потрошки компилята и детально во всём разобрались, то статья стала бы техничнее и скучнее, а так куча народу ломанулись в комменты, и я сам с удовольствием покрутил код так и сяк, и поупражнялся до кучи в ассемблере и Расте (узнал много нового про линковку и оптимизацию), так что в общем спасибо.


    1. Gutt
      08.05.2026 20:41

      Дело в dynamically linked.


  1. Autochthon
    08.05.2026 20:41

    Библиотека iosteam была написана Страуструпом на спор. Некто высказал ему мысль, что написать нормальную библиотеку ввода вывода без поддержки языка невозможно и он воспринял это как вызов. Я не помню точно откуда это, но скорее всего из книги дизайн и эволюция С++. Естественно что пользоваться вещами написанными на спор невозможно. По хорошему все это надо объявлять deprecated и выкидывать из стандарта, но оно там живет годами совершенно бесполезное, пока не появляются вещи типа std::quoted для реанимации трупа. И такого там полно одно написано на спор, другое (stl) чья-то диссертация. Сама философия разработки выпустить какое-то нечто типа std::valarray потом десятилетиями доводить до ума.

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


  1. iamkisly
    08.05.2026 20:41

    Вспоминается анекдот про балабановскую спичечную фабрику

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


    1. DanielKross
      08.05.2026 20:41

      В реальном мире ресурсы таки важны. В вашем может быть нет. Расписать почему, или сами догадаетесь? ;)


    1. CitizenOfDreams
      08.05.2026 20:41

      производительное железо позволяет писать плохой код..

      Так ведь не позволяет. Хоть 10-гигагерцевый 32-ядерный процессор возьми, все равно стартовое меню Windows будет открываться с видимой задержкой.


      1. vvzvlad
        08.05.2026 20:41

        Так дело вовсе не в 2мб неиспользуемого кода на каждый бинарник


        1. DanielKross
          08.05.2026 20:41

          Так вроде никто и не утверждал, что именно в этом :)


    1. rybkin_kotik
      08.05.2026 20:41

      "Хорошие времена создают слабых людей".


  1. SilverTrouse
    08.05.2026 20:41

    Если include iosream заменить на import std;

    import std;

    int main(){

    std::cout << "Hello World" << std::endl;

    return 0;

    }

    g++16 -std=c++23 -fmodules --compile-std-module main.cpp

    Сгенерированый бинарник весит всего 16,5 кб

    Показываю пример на gcc16 так как только с этой версии появилась удобная опция

    для подключения import std; ( До этого делается чуть сложнее но резлуьтта должен быть таким же)

    Если заменить на std::println то размер бинарника составляет ~241КБ

    Но это и логично так как тащится уже и std::format и к нему еще сверху


    1. SilverTrouse
      08.05.2026 20:41

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


      1. valdemar-const
        08.05.2026 20:41

        А lsp подсвечивают import директивы как ошибку... В блокноте кодить?


        1. SilverTrouse
          08.05.2026 20:41

          clangd 21 уже хорошо справляется с этим, а поддержка включена с 19 c использованием опции --experimental-modules-support


  1. ExoticHadron
    08.05.2026 20:41

    Чертовски интересно! Я вот не программист ни разу, объясните, пожалуйста, почему сам компилятор не делает эту работу? Например, зачем тащить зависимости потоковой обработки, если понятно, что она здесь точно не нужна? Вроде век всех этих ИИ, прочей лабуды вроде умных IDE, динамических анализаторов кода. Не понимаю, где умная оптимизация? Почему б не предположить сразу ASM-вставку под работу со stdout? Хотя б на уровне IDE.


    1. iamkisly
      08.05.2026 20:41

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


    1. tenzink
      08.05.2026 20:41

      Да потому что это нафиг никому не нужно. Как выше в коментах уже отписывались - компиляция с параметрами по-умолчанию делает нормально - 16Кб. Автор топика работает со специфическим сценарием - полностью статическая линковка, где большой размер, это ожидаемый side-effect.


      1. Vlad441 Автор
        08.05.2026 20:41

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


        1. CoolCmd
          08.05.2026 20:41

          Динамическая библиотека грузится полностью в память

          нет, загружаются только используемые страницы (размер страницы обычно 4 КБ).


          1. denis_iii
            08.05.2026 20:41

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


        1. tenzink
          08.05.2026 20:41

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

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


  1. webcounters
    08.05.2026 20:41

    Я чаще использовал С, С++ для микроконтроллеров и там это не прощается, так-как озу у среднего МК от 2к до 128к.

    Ещё в Qt юзал, но не задумываясь о размере.

    Сейчас думаю что лучший Си это Rust и начал его изучать.


    1. artemisia_borealis
      08.05.2026 20:41

      Rust это всё же не С.

      Вот тут есть Better C, и это действительно C


  1. vanxant
    08.05.2026 20:41

    а под -О3 не пробовали? интересно чё как


    1. Vlad441 Автор
      08.05.2026 20:41

      Для данного примера -О3 не дает эффекта. Это флаг оптимизации отвечает за скорость программы в рантайме, а не за размер бинарника. Хотя есть флаг и для этого -Os но обычно он выигрыша не дает.


  1. JuPk
    08.05.2026 20:41

    Какие мегабайты? Оба компилятора дают ~16Kb.
    Какие мегабайты? Оба компилятора дают ~16Kb.


    1. Vlad441 Автор
      08.05.2026 20:41

      Вы забыли указать -static - вам откроется портал кроличей норы :)


      1. JuPk
        08.05.2026 20:41

        Да, внатуре) Обычно статически линковать нужды нет, проглядел.
        Да, внатуре) Обычно статически линковать нужды нет, проглядел.


  1. RepppINTim
    08.05.2026 20:41

    iostream всегда был жирным монстром, который тянет за собой локали и обработку ошибок

    Кто в здравом уме использует его там, где важен размер


    1. firegurafiku
      08.05.2026 20:41

      тянет за собой локали

      Кажется, это первый комментарий в треде, который попытался объяснить, почему же всё-таки такая огромная разница. Все остальные заняты переписыванием на Rust и ассемблер.


      1. AndreyDmitriev
        08.05.2026 20:41

        почему же всё-таки такая огромная разница.

        Ну тут то как раз несложно — при сборке с ключом -static и без мы очевидным образом получаем части рантайма в наше приложение. Если посмотреть в зависимости (я соберу через g++ под Windows используя MSYS64), то разница вот здесь, это сборка без -static:

        и оно зависит от libstdc++-6.dll, а вот при сборке -static этой зависимости не будет. Библиотека эта, кстати те самые два с лишним мегабайта и что-то мне подсказывает, что она просто целиком переезжает в наше приложение (с подзависимостями, но их там немного).

        Посмотреть всё это, кстати, удобнее всего через Relyze, там можно визуализировать зависимости по функциям.

        Вот как выглядит граф "здорового" приложения (без -static), это то, что попало в наш исполняемый файл — основная функция - точка входа и ещё чутка, остальное вызывается динамически, размер этого добра примено 10 килобайт:

        А вот что произойдёт, если включить -static, теперь на этой окружности больше четырёх тысяч точек-функций, и они связаны друг с другом, а файл больше двух мегабайт:

        Кстати, если от gcc перейти в Visual Studio, то всё выглядит не так трагично, но тоже много "ненужного" для примитивного "привет мир", вот статическая линковка,если я её правильно включил:

        Тут безусловно, есть где покопаться и поисследовать детали, но для меня С++ не очень входит в круг интересов, просто "ассемблер и Раст" на данный момент чуть интереснее.


        1. firegurafiku
          08.05.2026 20:41

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

          Вот смотрите, я подготовил два файла:

          main-iostream.cpp
          #include <iostream>
          int main() {
              std::cout << "Hello world\n";
              return 0;
          }
          
          main-printf.cpp
          #include <cstdio>
          int main() {
              std::printf("Hello world\n");
              return 0;
          }
          

          Компилирую их в Ubuntu 24.04 со -static и прочими флагами:

          $ g++ -static -Os -s -flto -o out-iostream main-iostream.cpp
          $ g++ -static -Os -s -flto -o out-printf main-printf.cpp
          

          И получаю out-iostream размером 1862288 байт (1819 кБ) и out-printf размером 706584 байт (690 кБ). Вопрос: на что ушёл мегабайт?


          1. AndreyDmitriev
            08.05.2026 20:41

            Если вкратце, то мегабайт ушёл на функционал, который тащит за собой std::cout << "Hello world\n";, на самом деле его тут на порядки больше, чем в тривиальном std::printf("Hello world\n");, ведь это стрим прежде всего, соответственно буферизация, и вот мы получаем ios_base, basic_streambuf, затем надо не забыть про темплейты ostream, ну и до кучи locale, ctype, форматирование и разбор num_put/num_get; опять же исключения нельзя сбрасывать со счетов, каждый раз когда мы там try...catch нам в соответствующую секцию ложится структура с адресом возврата из обработчика (и их там сотни если не тысячи) и так далее. Тут реально на порядки больше функционала.

            Вот смотрите, допустим мы можем написать так:

            #include <iostream>
            
            int main() {
                std::cout.imbue(std::locale("English_United States.1252"));
                std::cout << 1234567 << "\n";
            
                std::cout.imbue(std::locale("German_Germany.1252"));
                std::cout << 1234567 << "\n";
            
                return 0;
            }

            (в линуксах имена будут чуть другие, но суть та же)

            И получим

            1,234,567
            1.234.567

            И вот код этот, который вкорячивает нам точечки или запятые в нужные места — он тут и с нами всегда (а локалей много всяких разных и правила группировки у всех разные), а линковщик поятия не имеет, используем мы это в коде или нет, он тупо обходит все зависимости и добавляет это в компилят, вот откуда мегабайт, грубо говоря. Так что std::cout << — это просто-напросто верхушка айсберга.

            Я тут снова со своим Растом — вот в нём есть несколько промежуточных стадий компиляции, начиная от AST, которое Abstract Syntax Tree мы идём в высоуровневое HIR (High‑level Intermediate Representation), затем промежуточное MIR ( Mid‑level...), потом LLVM, и уже затем стадия ассемблера, и все листинги можно получить и проанализировать и лучше понять почему код разворачивается в более или менее компактные наборы команд.


            1. netch80
              08.05.2026 20:41

              ведь это стрим прежде всего, соответственно буферизация, и вот мы получаем ios_base, basic_streambuf, затем надо не забыть про темплейты ostream, ну и до кучи locale, ctype, форматирование и разбор num_put/num_get; опять же исключения нельзя сбрасывать со счетов, каждый раз когда мы там try…catch нам в соответствующую секцию ложится структура с адресом возврата из обработчика (и их там сотни если не тысячи) и так далее.

              Буферизация и пр. – копейки, а вот хитрое форматирование и особенно локализация в основном и съедают этот объём.

              Можно было бы их завязать на слабые ссылки линковки, подставляя стабы, если, например, не вызван ни один setlocale(). Но не сделали.


              1. AndreyDmitriev
                08.05.2026 20:41

                Буферизация и пр. – копейки, а вот хитрое форматирование и особенно локализация в основном и съедают этот объём.

                Да в общем так. В теории можно, конечно "упороться" и детально разобрать какой функционал или секция дают какой вклад в объём:

                сделать красивые диаграммы, там всё в общем открыто, и собственно кода меньше половины от двух мегабайт:

                Но оно надо? И хотя статья и не даёт ответы на большинство вопросов, но дала свои плоды в виде кучи комментов.


  1. sir66
    08.05.2026 20:41

    Я бы все таки отметил, что все еще зависит от платформы. Зачем нам на x86 всякая там грошовая экономия. Вот Arduino Nano, правда все равно многовато, но все таки:

    Совсем пустая программа - 444 байта:


    1. HardWrMan
      08.05.2026 20:41

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


    1. HardWrMan
      08.05.2026 20:41

      Совсем пустая программа - 444 байта:

      Она не пустая. setup() и loop() это функции, которые вызываются основным кодом инициализации. Это как startup.s файл с буткодом и таблицей векторов у STM32. По-настоящему пустую программу для AVR можно написать только в IDE вроде AVRStudio.


      1. vvzvlad
        08.05.2026 20:41

        По-настоящему пустую программу для AVR можно написать только в IDE вроде AVRStudio.

        “только в IDE”… avr-gcc?


        1. HardWrMan
          08.05.2026 20:41

          avr-gcc основные массы знают именно как AVRStudio, потому что этот тулчейн по сути комплектный для него из коробки.


  1. vmpru
    08.05.2026 20:41

    Меряться размером исполняемого файла хорошо на писюках. А вот на микроконтроллерах куда важнее размер используемого ОЗУ. (Пояснение для незнакомых с проблемой - обычно на микроконтроллерах размер ОЗУ (для данных) на один-два порядка меньше, чем размер ПЗУ (сегмент кода)).

    И вот тут-то стандартный printf представляет большую (иногда очень большую - больше, чем размер ОЗУ) проблему. Стандартная реализация printf создаёт в стеке буфер на несколько килобайт, чтобы там формировать строки для вывода. Для микроконтроллеров это жуткое расточительство. Поэтому хорошие реализации исполняющих систем (библиотек) имеют возможность выбора вариантов printf, от максимальных до минимальных. А многие микроконтроллерщики имеют свой собственный велосипед вместо стандартного printf, который практически не трогает стек.

    (Ну и возвращаясь к размеру кода - бывают реализации printf, не поддерживающие вывод float/double. Это тоже сильно снижает размер кода).


    1. HardWrMan
      08.05.2026 20:41

      Добавлю ещё проблему новичков, что объявляют константы по привычке как на PC, без атрибута __PROGMEM__.


      1. vmpru
        08.05.2026 20:41

        Это что-то на AVRском? Забыл как страшный сон лет 20 назад, с появлением MSP430. А потом и их тоже забыл, перейдя на различные ARMы.

        Сугубо Гарвардская архитектура, с абсолютно разными адресными пространствами, к которым даже доступ идёт разными командами и которая требует нестандартных расширений компилятора.

        Кстати, возвращаясь к большим микроконтроллерам. Там тоже всё не так просто. Например, для того, чтобы разместить в ПЗУ массив указателей на строки в ПЗУ, нужно в объявлении переменной поставить два const, иначе строки лягут в ПЗУ, а вот указатели на них останутся в ОЗУ.


        1. HardWrMan
          08.05.2026 20:41

          Это что-то на AVRском?

          Нет. Для ARM та же проблема. Суть в том, что обычные const компилятор кладёт во флеш а потом при ините копирует в ОЗУ и уже оттуда использует как константы. Хотя они вполне себе доступны и из FLASH. На AVR понятное дело вообще разные команды для доступа к FLASH и ОЗУ, но у ARM команды общие, как и адресное пространство. Но вот такой заёб присутствует, у arm-none-eabi-gcc по крайней мере.


          1. LAutour
            08.05.2026 20:41

            Суть в том, что обычные const компилятор кладёт во флеш а потом при ините копирует в ОЗУ и уже оттуда использует как константы

            В некторых микроконтроллерахсодержимое FLASH (медленного) по дефолту копируется\кэшируется в ОЗУ при старте.


            1. HardWrMan
              08.05.2026 20:41

              Он это делает на всех уровнях оптимизации. Не думаю, что скорость доступа к FLASH является ключевым фактором для этого. Ведь, когда ты даёшь константе атрибут, он кладёт таблицу поближе к использующему коду (кроме откровенно больших массивов) и, получается, prefetch успевает всё закешировать через read ahead.


          1. vmpru
            08.05.2026 20:41

            Странное поведение. Такое впечатление, что некорректно написан скрипт линкера.


            1. HardWrMan
              08.05.2026 20:41

              Девственный, от создателей STM32Cube. Понимаю, что не показатель, но всё же.


          1. Dancho67
            08.05.2026 20:41

            А можно подробнее как у вас такое вышло, что const в ram попадает? На моем опыте без шаманства с линкером или атрибутами в программе const никак не попадает в .data, а все время откисает в .rodata. Забавно когда видишь PROGMEM где-то кроме AVR.


            1. HardWrMan
              08.05.2026 20:41

              Некоторое уточнение конкретно про STM32: PROGMEM это действительно для AVR и такого атрибута для arm-none-eabi нет, я просто указал образно. Конкретно для STM32 под вышеупомянутый arm-none-eabi если указать просто const то я в отладке вижу все константы в ОЗУ при штатной остановке отладчика после инита (это когда отрабатывает весь инит-стартап и срабатывает автоматическая бряка в main()), а вот если указать static const тогда всё как надо располагается в ПЗУ.


    1. LAutour
      08.05.2026 20:41

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

      Или вместо printf обходиться легким puts. Числа только в строки придется вручную преобразовывать.


  1. codecity
    08.05.2026 20:41

    Просто никто ж не думал что ОЗУ так подорожает, по этому перестали считать байты...


  1. mittorn
    08.05.2026 20:41

    #include <iostream>

    дальше можно не читать, такой код гарантированно развозит


  1. KseandI
    08.05.2026 20:41

    Выше уже писали, но это как-то странно статически линковать iostream. Библиотека всё же не лёгкая, а нужна прям очень редко в случаях, когда пара мегабайт так важны.

    Ну и сравнивать размеры на пустых бинарях естественно не очень хороший вариант, libc++ очень большой, а вы его только ради вывода в консоль добавили, получается.


  1. viordash
    08.05.2026 20:41

    Подобные вопросы возникают регулярно, и я могу припомнить несколько:
    - 25 лет назад удивляло, почему пустая форма Delphi или MFC весит так много килобайт.
    - 8 лет назад обсуждали, почему почти пустые приложения на Angular занимают столько же места.
    - периодически всплывают вопросы о весе обычного Hello World.

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


    1. Vlad441 Автор
      08.05.2026 20:41

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

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


  1. ADEXITUM
    08.05.2026 20:41

    Стоит отдать должное, что именно благодаря hello world на 2МБ мы живём в мире, где по одному клику завтра в ПВЗ приезжает любой товар, попутно на карте доступен маршрут до любой шаурмечной в половине стран мира и многое что ещё.

    Я имею ввиду и использование более высокоуровневых ЯП и обмен "оптимизировано" на "быстрее в прод"


    1. HardWrMan
      08.05.2026 20:41

      Но твой бы смартфон не звонил бы, чёрт возьми

      Если б программисты были бы нормальными людьми.

      (С) НТР


  1. Samhuawei
    08.05.2026 20:41

    Помнится на спор написал для винды просмотрщик картинок размером в 500 байт. Через com дёргал соответствующий интерфейс системы. Ну и выпилил ненужные секции из исполняемого файла… короче ящик пива выиграл.


  1. KMiNT21
    08.05.2026 20:41

    ~ 24 года прошло, как я писал статью "Написание экстра-маленьких Win32 приложений на С++ от 1 КБ используя лишь API, на примере программы Windows Hider". Но там я "уходил" от 32Кб до 1 Кб пустышки и 4 Кб полезной программы.


    1. Vlad441 Автор
      08.05.2026 20:41

      Тогда и 32 кб было много, а сегодня и 2 МБ считается “Ну, сойдет. Железо стерпит.”