Компиляция в Си происходит как минимум четырьмя отдельными программами.1. Макропроцессор. Он изменяет исходный код ТОЛЬКО с помощью своих команд. Он НЕ смотрит в сами исходники и ему в целом по барабану на всё, что не #... На его работу можно посмотреть с помощью флага -E
2. Компилятор. Он обрабатывает уже отредактированный макропроцессором код. Он производит все оптимизации и превращает Си в ассемблерный код, НО ЕЩЁ НЕ В БИНАРЬ. На его работу можно посмотреть флагом -S
3. Ассемблер. Он собирает всё ещё читабельный ассемблерный код в понятный для процессора бинарь. Это ещё не готовая программа, поскольку в бинаре нету всяких данных, даже названия функций к которым делается call не определены, но это уже оптимизированный код, который имеет смысл хранить отдельно, чтобы не заниматься каждый раз компиляцией всего проекта заного. Флаг -c
4. Линковщик. Он превращает все объекты в ИСПОЛНЯЕМЫЙ файл (для линукса и макоси ELF). По сути он собирает из всех .o файлов архив и подставляет всю метадату: откуда брать функцию read, какие библиотеки подгружать из /usr/lib, и т.д. Само собой, этот файл будет работать ТОЛЬКО на системе, которая во первых умеет читать ELF (например, уже рантайм линкером ld-linux-x86-64), и во вторых если после копипаста файлика все пути до библиотек и их версии совпадут (сверяются тупо строки внутри ELF и библиотек, гляньте objdump). Можно собирать статичные файлики, т.е. те, которые весь код берут с собой и в рантайм линковке не нуждаются, но и весить они будут как ваш код + все библиотеки, которые использовались (причём рекурсивно, библиотеки тоже могут использовать библиотеки), в сумме.