[Перевод] Заметки о Unix: С-функция main() — одно из мест, где видны различия между API пользовательского пространства и ядра Unix
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?