Вступ
Коли ви пишете просту програму на C, наприклад:
#include <stdio.h>
int main(void) {
printf("Hello, world!\n");
return 0;
}
і компілюєте її (наприклад, gcc hello.c -o hello
), ви можете припустити, що ваша функція main()
є першою частиною коду, що виконується, коли програма запускається. Однак насправді існує кілька спеціальних частин коду, які виконуються до і після main()
, готуючи середовище для вашої програми. Ці частини коду є частиною об'єктів запуску C Runtime (CRT). Серед них ви могли зустрічати імена файлів, такі як crt0.o
, crt1.o
, crti.o
та crtn.o
. У цьому пості ми розглянемо, що кожен з них робить, чому вони існують і як вони працюють разом, щоб забезпечити безперебійну роботу ваших програм на C (і C++).
Що таке C Runtime?
C Runtime (CRT) — це набір процедур запуску, ініціалізаційного коду, підтримки стандартної бібліотеки та інколи оболонок системних викликів, які формують середовище, в якому виконується програма на C. Більшість цього коду знаходиться поза вашим власним вихідним кодом, але автоматично підключається компілятором (наприклад, gcc
або clang
).
Коли ви компілюєте програму за допомогою команди:
gcc main.c -o main
або
clang main.c -o main
компілятор і лінкер неявно включають об'єктні файли запуску і бібліотеки, включаючи один або кілька об'єктних файлів CRT. Ці файли містять вхідні точки на рівні асемблера та процедури, які:
- Ініціалізують регістри та стек.
- Налаштовують аргументи програми (
argc
,argv
,envp
). - Викликають глобальні конструктори (у програмах на C++).
- Викликають вашу функцію
main()
. - Обробляють повернення з
main()
і передають код завершення операційній системі.
Роль crt0.o
(або crt1.o
в сучасних інструментальних ланцюгах)
Історично crt0.o
(C runtime zero) є невеликим об'єктним файлом, який містить реальну процедуру входу, зазвичай звану _start
. Його відповідальності включають:
- Ініціалізація програми
- Ініціалізація стека (на деяких архітектурах і ОС, хоча зазвичай це налаштовує вказівник стека ядро).
- Налаштування сегментів пам'яті, якщо це необхідно (наприклад, дані, BSS).
- Підготовка
argc
,argv
та вказівників на середовище з даних, наданих ядром. - Виклик конструкторів для глобальних і статичних об'єктів (особливо в C++).
- Можливо, виклик функцій ініціалізації бібліотек (для стандартної бібліотеки вводу/виводу тощо).
- Перехід керування до
main()
- Після налаштування середовища,
crt0.o
викликаєmain(argc, argv, envp)
.
- Очищення
- Коли
main()
завершується,crt0.o
(або остання процедура виходу) викликає операційно-системний виклик виходу (наприклад,_exit
або подібний) для завершення процесу з кодом повернення зmain()
.
Оскільки crt0.o
часто був великим, монолітним файлом, багато сучасних інструментальних ланцюгів зараз розділяють його на більш модульні компоненти. Можливо, ви побачите використання crt1.o
замість crt0.o
. Назва crt1.o
зазвичай вказує на те, що це "перше" (або основне) об'єктне файл для запуску. Незважаючи на різницю в іменах, вони виконують ту ж саму основну функцію: містять символ _start
, який є стандартною точкою входу, використовуваною лінкером.
Типовий вміст crt0.o
/ crt1.o
- Низькорівневий асемблерний код, відповідальний за налаштування середовища виконання.
- Символ, званий
_start
(або інколи__start
), що є точкою входу. - Виклик до
main()
(або_main
, залежно від конвенції).
Фаза лінкування
Коли ви лінкуєте вашу програму, лінкер автоматично підключає crt0.o
(або crt1.o
) з реалізації C бібліотеки (наприклад, glibc або musl) або з компіляторського інструментального ланцюга.
Це відбувається за лаштунками, якщо ви явно не вимкнете це (наприклад, за допомогою певних флагів компілятора, таких як -nostartfiles
).
Додаткові файли для запуску: crti.o
, crtn.o
та інші
У сучасних інструментальних ланцюгах C Runtime часто розділений на кілька об'єктних файлів:
crti.o
(C runtime ініціалізація)crtn.o
(C runtime завершення)crt1.o
(C runtime точка входу)
crti.o
: Ініціалізація C Runtime
crti.o
зазвичай містить пролог для ініціалізаційної процедури запуску. Його основні завдання включають:
- Ініціалізація, специфічна для платформи
Наприклад, ініціалізація спеціальних регістрів, можливостей CPU або інших ресурсів, специфічних для архітектури. - Підготовка середовища
Готує все необхідне для виклику конструкторів (.ctors
секція для C++). - Підключення для раннього налаштування
Це можуть бути ініціалізаційні процедури, необхідні для ОС або платформи, такі як налаштування локального сховища потоків (TLS) на деяких системах.
Концептуально, ви можете вважати crti.o
місцем, де середовище запуску програми ініціалізується: «Я починаю налаштування середовища, ось деякий початковий код». Коли все готово, керування переходить до main()
або інших початкових процедур.
crtn.o
: Завершення C Runtime
crtn.o
містить епілог процесу ініціалізації та обробляє фінальні процедури. Він:
- Завершує послідовність ініціалізації
Завершує те, що почавcrti.o
, забезпечуючи виклик усіх глобальних конструкторів. - Керує деструкторами
Для програм на C++ глобальні деструктори (.dtors
) повинні бути викликані наприкінці програми. Огортаючи початкову та кінцеву частини навколо цих секцій,crti.o
іcrtn.o
правильно керують цією логікою.
Коли програма завершується, деструктори глобальних об'єктів викликаються, забезпечуючи очищення ресурсів перед тим, як програма справді завершиться.
Як все це працює разом
Щоб уявити, як ці файли вписуються в потік запуску програми, ось спрощена діаграма:
┌─────────────────────┐
│ Точка входу в програму │ (Визначено в crt1.o або crt0.o)
│ _start() │
└──────────┬──────────┘
│
│ (1) Ініціалізація середовища, пам'яті тощо
│
┌──────────┴──────────┐
│ crti.o (Пролог) │
│ Викликає конструктори │
└──────────┬──────────┘
│
│ (2) Переходимо до main()
│
┌──────────┴──────────┐
│ main() │
└──────────┬──────────┘
│
│ (3) main повертається
│
┌──────────┴──────────┐
│ crtn.o (Епілог) │
│ Викликає деструктори │
└──────────┬──────────┘
│
│ (4) системний виклик виходу
│
┌─────┴──────┐
│ Вихід ОС │
└────────────┘
Ключові етапи:
_start
(зcrt1.o
абоcrt0.o
) виконує низькорівневе налаштування, після чого викликає початковий код зcrti.o
.- Ініціалізаційний код з
crti.o
завершується, і ми переходимо доmain()
. - Коли
main()
повертається, виконується епілог зcrtn.o
, що викликає фіналізатори та деструктори. - Останній системний виклик виходу завершує процес з кодом повернення з
main()
.
Приклад фрагмента асемблера
Нижче наведено спрощений фрагмент асемблерного коду для Linux x86–64, що ілюструє мінімальну процедуру _start
(реальний код у crt1.o
або crt0.o
може бути складнішим). Зверніть увагу, що в реальних реалізаціях будуть додаткові інструкції для управління середовищем, локальним сховищем потоків тощо.
.global _start
_start:
; Вказівник стека вже налаштований ОС.
; Регістри RDI, RSI та RDX можуть містити вказівники на argc, argv та envp.
; Зберігаємо argc, argv та envp у стеку або
; передаємо їх безпосередньо в main() (залежно від конвенції виклику).
mov rdi, [rsp] ; argc знаходиться на верху стека
lea rsi, [rsp+8] ; вказівник на argv після argc
; envp буде після argv тощо.
call main ; Виклик main(argc, argv, envp неявно)
; Збереження коду повернення в eax
mov rax, rax
; Виконання системного виклику виходу
mov rax, 60 ; sys_exit на Linux x86-64
syscall
Дуже спрощена версія crti.o
може виглядати ось так (псевдокод C++ / асемблер):
.section .init
_init:
; Тут ви ініціалізуєте глобальні конструктори або
; налаштовуєте код, необхідний для запуску перед викликом main.
; Наприклад, викликайте __libc_init_array (в деяких інструментальних ланцюгах)
ret
А crtn.o
може мати відповідний код:
.section .fini
_fini:
; Процедури очищення та виклик глобальних деструкторів.
; Наприклад, викликайте __libc_fini_array
ret
У реальних інструментальних ланцюгах ці секції (.init
і .fini
) автоматично виконуються до та після main()
, відповідно, завдяки скриптам GNU linker і механізмам .init_array
/ .fini_array
або .ctors
/ .dtors
.
Практичні зауваження щодо сучасного використання
- Статичне проти динамічного лінкування
Якщо ви створюєте статично лінкований виконуваний файл (-static
), то файли C runtime повністю включаються в кінцевий бінарний файл. Для динамічно лінкованих виконуваних файлів динамічна версія цих об'єктів CRT часто обробляє взаємодію з динамічним завантажувачем перед викликомmain()
. - Різні ОС, різні реалізації
Назви та точні деталі можуть змінюватися. На Linux з glibc ви можете побачитиcrt1.o
,crti.o
,crtn.o
і так далі. На інших системах (наприклад, BSD або macOS) назви можуть відрізнятися, або процес може бути реалізований по-іншому. - Конструктори та деструктори C++
Секції.ctors
і.dtors
(або.init_array
і.fini_array
) є необхідними для автоматичного виклику конструкторів і деструкторів глобальних об'єктів. Окремі файлиcrti.o
таcrtn.o
обгортають ці виклики, щоб вони відбувалися до викликуmain()
і після того, якmain()
повертається (або викликаєтьсяexit()
). - Користувацькі точки входу
Досвідчені розробники іноді заміняють стандартні об'єкти CRT на свої власні мінімалістичні версії (використовуючи-nostdlib
або-nostartfiles
) для автономних або вбудованих середовищ.
Висновок
C runtime (CRT) — це важлива, часто недооцінена частина будь-якої програми на C або C++. Файли на кшталт crt0.o
(або crt1.o
, crti.o
та crtn.o
) гарантують, що ваш код має все необхідне перед виконанням main()
, включаючи налаштування стека, виклик глобальних конструкторів і ініціалізацію бібліотек. Вони також займаються очищенням (наприклад, глобальні деструктори), коли main()
повертається. Хоча ці об'єкти зазвичай включаються автоматично компілятором, знання про них допомагає вам зрозуміти, як ваша програма на C/C++ переходить від сирого процесу до повноцінної програми — і врешті-решт завершиться організовано.
Незалежно від того, чи розробляєте ви компілятори, працюєте з вбудованими системами, або просто цікаво дізнатися, як насправді починається програма на C, ці відомості про C runtime можуть допомогти розвіяти міфи про "невидимий" код за вашою функцією main()
.
Посилання
- Джерело GNU C Library (glibc) Шукайте
crt1.o
,crti.o
,crtn.o
тощо в каталогах sysdeps. - Бібліотека C musl Бібліотека C musl надає спрощену реалізацію цих файлів.
- Документація GCC Шукайте файли запуску, лінкування та питання часу виконання.
- Специфікація базових стандартів Open Group Issue 7 Описує стандартизовані інтерфейси процесів та деталі середовища.
- 👉 Читайте більше на нашому сайті
Перекладено з: Understanding the C Runtime: crt0, crt1, crti, and crtn