company_banner

Заметки о Unix: С-функция main() — одно из мест, где видны различия между API пользовательского пространства и ядра Unix

Автор оригинала: Chris Siebenmann
  • Перевод
В современных Unix-дистрибутивах часто проводят формальную границу между API, предоставляемыми пользовательскому пространству ядром, и Unix API, которые предоставляет программам «стандартная библиотека», под которой подразумевается стандартная библиотека C. Кое-кого, включая меня, это не вполне устраивает (я уже писал на эту тему). Но, независимо от того, что я об этом думаю, в Unix уже давно существует одно место, в котором видна разница между обычным API, которым пользуются все, и API, который реализован в ядре. Я говорю о традиционной точке входа в программы, написанные на языке C, о функции 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?

RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 14

    +7
    В результате раздел данных начинается с адреса (данных) 0. Резервирование двух байтов в начале адресного пространства позволяет обеспечить то, что ни переменную, ни что-то другое в разделе данных нельзя расположить по адресу 0.


    А в досе можно было положить. И на этот случай в борландовском Turbo C по этому адресу лежала строчка копирайта Borland International. А в досовском Quick C лежала строчка копирайта Microsoft. И при выполнении выхода из main CRT подсчитывало контрольную сумму этой строчки и при несовпадении жаловалось на «null pointer assignment», тем самым сообщая что где-то в программе произошло присваивание по адресу NULL (никакого защищённого режима в те времена естественно не было и никто никакие GPF и прочие иксепшены прямо в рантайме не умел).

    Во какой я старый, во чего я помню!
      0
      Вау, спасибо за интересное дополнение!
        +5
        Естественно «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!», чем вероятно сильно повышало ЧСВ кракера или релизера.
        0
        Никаких строчек копирайта там не было. По адресу 0 жил Program Segment Prefix
          +4
          «Иди отсюда, мальчик, не мешай!» © :-)

          Где жил PSP и что такое PSP я прекрасно знаю. Но к данной теме он никакого отношения не имеет (мы ведь не про модель памяти tiny говорим где и правда по адресу 0 был PSP, а small, medium, compact, large и huge, да? на tiny делалсь столь мало софта, что им можно пренебречь).
            +1
            А по far ptr 0 таблица прерываний лежит.
              0
              Лежала. В real mode DOS. А сейчас она лежать может где угодно, инструкция sidt покажет где.
          0
          Из линкерного файла одного из современных микроконтроллеров NXP:
              __xRAM_data_start = .;
              # do NOT remove this line, as it allocates a Word at address x:0 
              # so no other valid variable gets a NULL address
              WRITEH(0x0BAD);
            0

            В начале еще была надпись Runtime error

              0
              По адресу ds:0000 лежала именно строчка с копирайтом. А всякие «runtime error», «null pointer assignment», «floating point error», "(null)" и прочие строчки ою ошибках лежали по другим адресам. А строчка с копирайтом была как раз ds:0000 и именно по её контрольной сумме CRT определяло что кто-то загадил память по нулевому указателю. Исходники CRT вам в помощь, там всё чётко и ясно откоменнтировано что, где и зачем.
            +2
            Вот только при чем здесь UNIX, когда int main() это особенность Си? Перед запуском пользовательского кода стартап должен записать значения инициализации в переменные, занулить bss и т.д.
            А в случае работы без ОС (в микроконтроллерах, например), нужно еще и базовую настройку провести — регистры стека, прерывания и все остальное.
            И что-то мне подсказывает, что в других языках (кроме ассемблера, конечно) ситуация та же.
              0

              Когда ковыряли crt у ДВК, нашли место где вычисляется частное от деления одного полинома на другой. Так и не поняли, что это было. Может тест flow point?

                0

                Можно еще рекурсивный main() сделать для развлечения, работает.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое