Как эта программа C компилируется и выполняется с двумя основными функциями?

Сегодня, работая с одной пользовательской библиотекой, я обнаружил странное поведение. Статический библиотечный код содержал функцию отладки main(). Он не был внутри флага #define. Так что он присутствует и в библиотеке. И используется ссылка на другую программу, содержащую реальный main().
Когда оба они связаны друг с другом, компоновщик не набрасывает ошибку объявления с несколькими объявлениями для main(). Мне было интересно, как это может произойти.

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

$ cat prog.c
#include <stdio.h>
int main()
{
        printf("Main in prog.c\n");
}

$ cat static.c
#include <stdio.h>
int main()
{
        printf("Main in static.c\n");
}

$ gcc -c static.c
$ ar rcs libstatic.a static.o
$ gcc prog.c -L. -lstatic -o 2main
$ gcc -L. -lstatic -o 1main

$ ./2main
Main in prog.c
$ ./1main
Main in static.c

Как найти бинарную "2main", которую main выполняет?

Но компиляция обоих из них дает множественную ошибку объявления:

$ gcc prog.c static.o
static.o: In function `main':
static.c:(.text+0x0): multiple definition of `main'
/tmp/ccrFqgkh.o:prog.c:(.text+0x0): first defined here
collect2: ld returned 1 exit status

Может кто-нибудь объяснить это поведение?

Ответ 1

Цитата ld (1):

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

При связывании 2main главный символ решается до того, как ld достигнет -lstatic, потому что ld выбирает его из prog.o.

При связывании 1main у вас есть undefined main к моменту, когда он попадает в -lstatic, поэтому он ищет архив для main.

Эта логика применяется только к архивам (статическим библиотекам), а не к обычным объектам. Когда вы связываете prog.o и static.o, все символы из обоих объектов включаются безоговорочно, поэтому вы получаете дублируемую ошибку определения.

Ответ 2

Когда вы связываете статическую библиотеку (.a), компоновщик только ищет архив, если есть какие-либо undefined символы, отслеженные до сих пор. В противном случае он вообще не смотрит в архив. Таким образом, ваш случай 2main, он никогда не смотрит в архив, так как у него нет символов undefined для создания единицы перевода.

Если вы включили простую функцию в static.c:

#include <stdio.h>
void fun()
{
      printf("This is fun\n");
}   
int main()
{
      printf("Main in static.c\n");
}

и назовите его с prog.c, тогда линкер будет вынужден смотреть в архив, чтобы найти символ fun, и вы получите ту же самую ошибку основного определения, что и компоновщик, найдя дубликат символа main сейчас.

Когда вы непосредственно компилируете объектные файлы (как в gcc a.o b.o), компоновщик не имеет никакой роли здесь, и все символы включены для создания одного двоичного и явно повторяющихся символов.

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

Ответ 3

После того, как компоновщик загружает любые объектные файлы, он ищет библиотеки для undefined символов. Если их нет, то библиотеки не должны читаться. Поскольку main был определен, даже если он находит основной в каждой библиотеке, нет причин загружать второй.

Однако у линковщиков есть совершенно разные формы поведения. Например, если ваша библиотека включала объектный файл с именами main() и foo() в нем, а foo - undefined, вы, скорее всего, получите ошибку для многозначного символа main ().

Современные (тавтологические) линкеры будут пропускать глобальные символы из объектов, недоступных - например, AIX. Компоненты старого стиля, подобные тем, которые были найдены в Solaris, и Linux-системы по-прежнему ведут себя как линкеры unix с 1970-х годов, загружают все символы из объектного модуля, достижимые или нет. Это может быть источником ужасного вздутия, а также чрезмерного времени соединения.

Также характерным для * nix-линкеров является то, что они эффективно ищут библиотеку только один раз за каждый раз, когда она указана. Это требует от программиста заказывать библиотеки в командной строке компоновщику или в файле make в дополнение к написанию программы. Не требуется упорядоченный список библиотек не является современным. У старых операционных систем часто были компоновщики, которые неоднократно выполняли поиск по всем библиотекам, пока пропуск не смог разрешить символ.