В современных Unix-дистрибутивах часто проводят формальную границу между API, предоставляемыми пользовательскому пространству ядром, и Unix API, которые предоставляет программам «стандартная библиотека», под которой подразумевается стандартная библиотека C. Кое-кого, включая меня, это не вполне устраивает (я уже писал на эту тему). Но, независимо от того, что я об этом думаю, в Unix уже давно существует одно место, в котором видна разница между обычным API, которым пользуются все, и API, который реализован в ядре. Я говорю о традиционной точке входа в программы, написанные на языке C, о функции
Все знакомы с простой формой функции
Но это, на самом деле, не реальная точка входа в программу, которую ядро Unix V7 использует при запуске программы. Реальная точка входа имеет API, отличный от
(В Research Unix V6 тоже был файл crt0.s, но несколько иной. Полагаю, тут, например, нет циклов. Если бы я понимал язык ассемблера PDP-11, то я лучше бы разобрался с тем, что тут, на самом деле, происходит.)
В V7 между API пользовательского пространства для
(Некоторый код тут присутствует из-за того, что среда выполнения C нуждается в предварительной настройке (и да, в современном C есть среда выполнения), но определённый объём этого кода предназначен для согласования того, как ядро вызывает программы, с тем, как хочет быть вызвана функция
В конце каждой версии файла
Оказалось, что он резервирует два байта в начале раздела данных. Unix V7 работает на компьютерах PDP-11, которые поддерживают разделение адресного пространства инструкций и данных. В результате раздел данных начинается с адреса (данных) 0. Резервирование двух байтов в начале адресного пространства позволяет обеспечить то, что ни переменную, ни что-то другое в разделе данных нельзя расположить по адресу 0. В результате
Приходилось ли вам сталкиваться с различиями 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?