В современных 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?

pfemidi
А в досе можно было положить. И на этот случай в борландовском Turbo C по этому адресу лежала строчка копирайта Borland International. А в досовском Quick C лежала строчка копирайта Microsoft. И при выполнении выхода из main CRT подсчитывало контрольную сумму этой строчки и при несовпадении жаловалось на «null pointer assignment», тем самым сообщая что где-то в программе произошло присваивание по адресу NULL (никакого защищённого режима в те времена естественно не было и никто никакие GPF и прочие иксепшены прямо в рантайме не умел).
Во какой я старый, во чего я помню!
dmitryrf
Вау, спасибо за интересное дополнение!
pfemidi
Естественно «null pointer assignment» говорилось только после выхода из программы, а не при самом присваивании, GPF ведь в те времена не ловили. Так что ни конкретной переменной, ни конкретной функции, ни номера строки в которой произошла эта «бяка» узнать было невозможно, озвучивался только сам факт что «где-то вот кто-то по нулевому указателю что-то нагадил». И естественно таким образом детектировалась только запись в NULL, а не чтение оттуда.
И ещё чего вспомнил. В те времена всякие кракеры да релизеры частенько специально «портили» строчку «Turbo C++ — Copyright 1990 Borland Intl.» или «MS Run-Time Library — Copyright © 1990, Microsoft Corp», прописывая туда строку типа "-=~ Brought to ya by VeryC00l Team ~=-", а вместо строки «Null pointer assignment» вставляли какой-нибудь «Released by !M0n$teR!», в результате при штатном завершении игры CRT проверяла контрольную сумму копирайта, она само собой не совпадала и CRT выводила «Released by !M0n$teR!», чем вероятно сильно повышало ЧСВ кракера или релизера.
Kohelet
Никаких строчек копирайта там не было. По адресу 0 жил Program Segment Prefix
pfemidi
«Иди отсюда, мальчик, не мешай!» © :-)
Где жил PSP и что такое PSP я прекрасно знаю. Но к данной теме он никакого отношения не имеет (мы ведь не про модель памяти tiny говорим где и правда по адресу 0 был PSP, а small, medium, compact, large и huge, да? на tiny делалсь столь мало софта, что им можно пренебречь).
Kohelet
А по far ptr 0 таблица прерываний лежит.
pfemidi
Лежала. В real mode DOS. А сейчас она лежать может где угодно, инструкция sidt покажет где.
DoHelloWorld
Из линкерного файла одного из современных микроконтроллеров NXP:
beerware
В начале еще была надпись Runtime error
pfemidi
По адресу ds:0000 лежала именно строчка с копирайтом. А всякие «runtime error», «null pointer assignment», «floating point error», "(null)" и прочие строчки ою ошибках лежали по другим адресам. А строчка с копирайтом была как раз ds:0000 и именно по её контрольной сумме CRT определяло что кто-то загадил память по нулевому указателю. Исходники CRT вам в помощь, там всё чётко и ясно откоменнтировано что, где и зачем.