main()
, с которой
начинается выполнение таких программ.Все знакомы с простой формой функции
main()
, в которой
используются аргументы argc
и argv
. Такая
функция вызывается с передачей ей количества аргументов и массива
строк. При несколько более продвинутом способе работы с этой
функцией применяется ещё и третий аргумент envp
. Он
представляет собой массив переменных окружения. Этот формат
существует в Linux очень давно. Версия main()
с двумя
аргументами существует, как минимум, со времён
exec(2) Research Unix V4. А форма этой функции с третьим
аргументом, похоже, появилась в
exec(2) V7.Но это, на самом деле, не реальная точка входа в программу, которую ядро Unix V7 использует при запуске программы. Реальная точка входа имеет API, отличный от
main()
. Обычно C-программы в V7
начинают работу с метки, имеющей символическое имя
start
. Самая простая версия ассемблерного кода, в
котором это используется, представлена в файле
crt0.s, и тут, очевидно, выполняется некий объём
подготовительной работы. Есть и другие версии подобного кода, их
можно найти
здесь. Тут выполняется больше вспомогательных операций,
например подготовка к профилированию кода.(В Research Unix V6 тоже был файл crt0.s, но несколько иной. Полагаю, тут, например, нет циклов. Если бы я понимал язык ассемблера PDP-11, то я лучше бы разобрался с тем, что тут, на самом деле, происходит.)
В V7 между API пользовательского пространства для
main()
и API ядра имеется лишь небольшая разница. В
актуальных дистрибутивах Unix там часто происходит очень много
всего, особенно тогда, когда пользуются динамическими загрузчиками
и чем-то вроде вспомогательного
вектора, который имеется в некоторых дистрибутивах. Я
подозреваю, что самую простую современную версию этого механизма
можно найти в musl libc для
Linux, где
crt1.c и
функции libc для подготовки к работе main()
сравнительно просты.(Некоторый код тут присутствует из-за того, что среда выполнения C нуждается в предварительной настройке (и да, в современном C есть среда выполнения), но определённый объём этого кода предназначен для согласования того, как ядро вызывает программы, с тем, как хочет быть вызвана функция
main()
. Например, обратите
внимание на то, что функция musl libc для запуска
main()
не вызывается с передачей ей argc
в виде явно заданного аргумента. Она извлекает argc
из
памяти.)Примечание: V7 и адрес данных 0
В конце каждой версии файла
crt0.s
V7 есть код,
который поначалу меня озадачил:
.data.=.+2 / loc 0 for I/D; null ptr points here.
Оказалось, что он резервирует два байта в начале раздела данных. Unix V7 работает на компьютерах PDP-11, которые поддерживают разделение адресного пространства инструкций и данных. В результате раздел данных начинается с адреса (данных) 0. Резервирование двух байтов в начале адресного пространства позволяет обеспечить то, что ни переменную, ни что-то другое в разделе данных нельзя расположить по адресу 0. В результате
NULL
в C всегда отличается от действительных
указателей.Приходилось ли вам сталкиваться с различиями API пользовательского пространства и ядра Unix?