Давайте сделаем простой пример. У вас есть три файла:
cnt.h
void inc_counter();
void print_counter();
cnt.c
#include <stdio.h>
#include <cnt.h>
static int counter= 0;
void inc_counter() {
couner++;
}
void print_counter() {
printf("Counter: %d\n", counter);
}
main.c
#include <counter.h>
int main(char** args) {
inc_counter();
print_counter();
return 0;
}
Затем вы компилируете cnt.c
и main.c
для создания cnt.o
и main.o
.
cnt.o
будет содержать исполняемый код для get_counter
и inc_counter
. Для каждого есть точка входа. Но код не исполняемый. Вызов printf
не будет работать, так как адрес printf
еще не известен. Таким образом, файл содержит информацию о том, что это нужно будет исправить позже.
main.o
будет содержать исполняемый код для main
и точку входа для него. Опять же, ссылки для inc_counter
и print_counter
не будут работать.
На втором шаге файлы cnt.o
, main.o
и стандартная библиотека C будут связаны, и будет создан исполняемый файл вывода (с расширением .elf
или без него). Компоновщик создаст недостающие ссылки между вызовом inc_counter
и функцией inc_counter
. И он сделает то же самое для print_counter
и printf
, включая код printf
из стандартной библиотеки.
Таким образом, хотя оба типа файлов в основном состоят из исполняемого кода, файлы .o
содержат только фрагменты кода, а файлы .elf
содержат полную программу.
Примечание. Существуют дополнительные варианты при создании или использовании динамически связанных библиотек. Но ради простоты я их исключил.