Pull to refresh
68
0.6
Иван Савватеев @SIISII

Микроконтроллеры, цифровая электроника, ОС…

Send message

А ещё дифференциальный каскад -- основа ЭСЛ, самой быстрой цифровой схемотехники.

На самом деле, могут быть даже формально независимые вещи, которые, однако, есть смысл писать на ассемблере ради производительности. Скажем, всякие перекодировки между UTF-8/16/32, пересылки строк, шифрование DES/AES и ряд других операций современные IBMовские мэйнфреймы умеют делать одной командой; соответственно, реализующие их подпрограммы для эффективности должны опираться не на "обычную" систему команд, используемую компилятором, а прямо быть написаны на ассемблере (ну или оформлены внутри компилятора как intrinsic, что, в первом приближении, то же самое, только переносит ассемблерный код из библиотеки в компилятор).

Оригинальный компилятор Ричи не был первым компилятором в истории. В частности, на той же самой PDP-11 с теми же самыми ограничениями существовали DECовские компиляторы: Фортран, Паскаль, Кобол и Бейсик (был ещё Бейсик-интерпретатор с совсем другим диалектом). Так вот, все эти компиляторы выдавали сразу объектные файлы. Компилятор Паскаля мог, если ему указать соответствующий параметр, выдать и ассемблерный текст (насчёт остальных не знаю -- я использовал Паскаль и ассемблер). Точно так же обстоит дело и с другими архитектурами.

В интеловских процессорах, как и в большинстве других архитектур, приёмник -- первый операнд. Последний в роли приёмника -- у DECовских машин, что весьма и весьма неудобно (например, путаница в SUB и CMP: первая вычитает справа налево, а вторая -- слева направо). Просто те клоуны, которые сделали древний gas, для чего-то решили натянуть дурацкий DECовский синтаксис на другую (интеловскую) систему команд.

Причём "исторически" -- в GGC и CLANG. Древние трансляторы транслировали сразу в машинный код -- процы и без того хилые были, чтобы выполнять две трансляции вместо одной.

"всё остальное" на сях не сделаешь: всегда остаются вещи, которые можно сделать только на ассемблере -- в частности, переключение контекста.

Чтобы разобраться, "как на самом деле работают программы на уровне процессора и ОС", потребуется не одна статейка на Хабре, а пара толстенных талмудов, как минимум. Ну а статья -- лишь беглый и очень поверхностный обзор всего этого, если исходить из заявленного в заголовке.

И, кстати, неверно сказать, что виртуальная память создаёт для программы впечатление, что ей (программе) доступна вся имеющаяся на машине память. Виртуальная память просто полностью "отвязывает" программу от реально имеющейся памяти. Но программе, во-первых, всё равно доступны не все адреса -- скажем, Винда забирает под себя старшую половину адресного пространства (наследие VAX/VMS, где это было следствием архитектуры машины); в Линухе и других системах ситуация принципиально такая же, различаются лишь детали. А во-вторых, программе благодаря виртуальной может быть доступен больший объём памяти, чем физически имеется на машине, причём иногда, особенно во времена уже достаточно отдалённые, во много раз больше.

Хм... Подзабыл о такой возможности, спасибо, что напомнили.

Оверлеи - так это вообще малая часть всей истории. К тому времени, когда программы выросли до таких размеров, что перестали помещаться в ОЗУ, уже появились аппаратные (exTended) и программные (exPanded) менеджеры дополнительной памяти.

Поправочка: это на ПК они являются малой частью истории. А если брать историю вычислительной техники вообще, то они широчайшим образом использовались все 1960-70-е годы.

Не исключено. Но точно известно, что большой файл с расширением COM, имеющий заголовок MZ, будет загружаться и выполняться как EXE. Вероятно, если ОС смотрит на объём, то он должен быть как раз 64 Кбайта - 256 байт -- предельный размер настоящей COM-программы.

По сути, дело в объёме прямо доступной памяти. Оверлеи широко использовались на машинах, где логический адрес был ограничен 16-ю битами, т.е. программа в принципе не могла адресовать больше 64 Кбайт, даже если физически памяти было больше (скажем, на PDP-11 с MMU физический адрес составлял либо 18, либо 22 бита в зависимости от модели, но виртуальный/логический всё равно оставался 16-битным).

Я на эту тему находил много интересных разногласий, и сам не до конца понимаю мысль.Есть материалы, где говорится, что программа-COM может загружаться с любого места.

Загружаться -- да, в любое место памяти (выровненное по границе параграфа), но это не значит, что у самого файла точка входа может находиться в произвольном месте. EXEшники, кстати говоря, тоже могут загружаться в любое место памяти (собственно, и загружаются: заранее ведь нельзя сказать, с какого физического адреса будет производиться загрузка).

Я сколько понимаю, (почему я писал именно про binary interface, а не API):

Ну, когда говорят о вызовах системных сервисов из прикладных программ, говорят именно про API. Про ABI вспоминают при обсуждении правил передачи параметров между подпрограммами и тому подобных вещей, что к вызовам системных сервисов отношения не имеет (если подпрограмма A вызывает подпрограмму B, то они используют определённый ABI, при этом обе подпрограммы обычно являются частью одной программы). В общем, ABI -- про внутреннее устройство программы и взаимодействие её частей между собой, API -- про внешние связи между программами и ОС (или, шире, между разными программами или сервисами, или между, скажем, драйверами и функциями ядра ОС; во всех этих случаях общая суть в том, что один компонент обращается за услугами к другому компоненту, и оба этих компонента не являются частью одного модуля, т.е. что связи именно внешние).

Но вообще, эти термины, как и 95% других терминов, никак не регламентированы, из-за чего регулярно возникает путаница. Например, микроядерных ОС в природе не существует, если понимать этот термин так, как он понимался изначально; почти не существует сейчас и RISC-процессоров в изначальном смысле (в частности, ни ARM, ни RISC-V не являются RISC-процессорами, если исходить из того, что сделали разработчики первого RISC-процессора), ну и т.п. Лично я это называю "инфляцией терминологии" (да и сам термин "инфляция" тоже подвергся инфляции: зачастую под ним понимают рост цен, хотя это разные вещи).

Было такое дело. Насколько помню, в самой ДОС поддержки оверлеев не было, и они реализовывались программами самостоятельно (в отличие от более вменяемых систем, где они шли "из коробки").

Я приведу таблицу, и там детальнее покажу все особенности.

Точка входа в COM-программу -- не "везде", а строго фиксированная -- её первый же байт. Максимальный размер COM-программы -- не 64 Кбайта, а 64 Кбайта минус 256 байт (для PSP). И, кстати, это не "команда", это именно COM-файл или COM-программа.

А вот у EXE-программы точка входа как раз не фиксированная, а может находиться в произвольном месте, поскольку её положение указывается в заголовке файла или в иной структуре, уж не помню точно.

Раньше ДОСа расширение .EXE для обозначения выполняемых файлов точно использовалось в VAX/VMS -- "мамаше" Винды НТ. Но, вполне может быть, что она тоже не была первой (хотя "бабка" Винды -- RSX-11 -- использовала расширение TSK).

В чуть позже, это будет называться "НеДалёкие указатели" (англ. "Near Pointers").

Вообще-то, эти указатели называются ближними, а не "недалёкими". А far pointers -- дальние указатели.

Поскольку, размер образ .EXE файла может быть значительно больше, (уже за пределами ОЗУ), -- в ОЗУ полностью он просто не поместится, а в "Program Memory" уж и подавно нет.

Неверно. В ДОСе EXEшки целиком загружались в память (точней, целиком загружались секции с кодом и данными; управляющая информация использовалась в процессе загрузки, но потом выкидывалась за ненадобностью для дальнейшей работы). Оверлеи же, если были, находились в другом файле -- о чём, собственно, Вы дальше пишите. Хотя у большинства ОС, поддерживающих оверлеи, они являются составной частью выполняемого файла, а не отдельным файлом.

В самом ABI системы

Это не ABI, это API. API -- набор системных сервисов, которые ОС предоставляет прикладным программам, откуда и его название. ABI же -- это, по сути, соглашения о связях между подпрограммами (через какие регистры передаются параметры и всё такое). В одной и той же системе ABI может быть несколько -- скажем, в 32-разрядной Винде, кажется, было не меньше четырёх способов передачи параметров. А вот API -- он один, и правила передачи параметров в него одни (но могут совсем не совпадать с ABI используемых языков программирования, особенно "в древности", в т.ч. в МС ДОС: тамошние системные вызовы рассчитаны на вызов из ассемблерных программ и не могут быть напрямую вызваны из кода на языке высокого уровня, ведь требуется занесение информации в определённые регистры, причём разные для разных функций, и выдача команды INT, которую трансляторы не используют).

Стоит помнить, что речь идет о временах, когда на уровне двоичных данных
не было понятия "секции"

Ещё очень много лет пройдёт, когда понятие "секции" появится у IBM и Microsoft. Поэтому после заголовков и таблиц релокаций в файле будут разграниченные области, но никаких .CODE или .DATA не будет!

Тоже неверно. Программы делились на секции очень давно, задолго до появления МС. Правда, ИБМ таки была :) В частности, у неё программы точно делились на секции в DOS/360 и OS/360 -- а первая версия первой из них появилась на рынке вместе с первыми машинами Системы 360, в 1965 году (OS/360 в жутко кастрированном виде -- на год позже).

Кстати говоря, формат PE COFF, что в Винде, пошёл с VAX/VMS, т.е. с середины 1970-х.

Ну и relocations -- всё-таки перемещения, а не релокации...

-- Почему ещё плюс 16 байт?!

Объясняю; потому что e_cparhdr (или длина заголовка в блоках) измеряется в блоках, а сегменты смещаются на 16 байт.

Блоки по 16 байт в x86 называются параграфами.

В минимальной ОС терминала может не быть вообще -- он не всегда нужен (да и не только в минимальной). Во всяком случае, терминал в привычном понимании.

И уж точно изучать ОС с терминала -- путь неверный. Хотя б потому, что терминал "стоит сверху" многих фундаментальных вещей типа управления памятью, управления процессами/потоками, управления вводом-выводом... А "оболочка" -- просто прикладная (по своему месту в системе, хотя и не по функциям) программа

Небольшое предупреждение! Я буду говорить не просто о том "Как запускаются комманды?"

В целом статья написана, с точки зрения русского языка, грамотно (что является весьма редким явлением), но вот в её "предисловии" команды почему-то с двумя м :)

Процессор i8080 (полн. "Intel 8080") был 8-разрядным,соответственно объемы обрабатываемой памяти были не такими уж и большими. Все написанные под эту ОС программы помещались в один файл команды и весили не больше чем 64Кб.

Пара замечаний. Во-первых, "у них", когда имеются в виду килобайты, принято сокращать их до KB, ну а Kb означают килобиты. Понятно, что в данном случае проблемы с пониманием вряд ли возникнут, но в других ситуациях такое может произойти (скажем, когда указывают ёмкость микросхемы памяти).

Ну а во-вторых, разрядность процессоров и размеры программ не имеют совсем уж прямой связи. Как, например, транслятор ассемблера в OS/360 мог выполняться в разделе памяти размером порядка 16 килобайт, если он сам суммарно имел куда больший объём и мог транслировать программы, чей размер, по большому счёту, ограничивался только доступным местом на дисках? (Система 360 -- 32-разрядная машина, адрес там был 24-битным, занимая три младших байта слова, т.е. теоретически адресуемый объём памяти достигал 16 Мбайт, но сами машины имели весьма небольшой объём физической памяти; у самой младшей модели 30 он составлял от 8 до 64 Кбайт -- всё ж середина 1960-х).

Так что "помещались в один файл" -- это именно особенности CP/M, а не безусловная необходимость для 8-разрядного процессора.

И, кстати, были же оверлейные программы, суммарный размер кода которых мог существенно превосходить 64 Кбайта. Это сейчас про оверлеи, наверное, мало кто помнит :)

Кстати говоря, если делать так, как сделано, -- считывать подряд N первых секторов, -- это, конечно, облегчает жизнь, но правильней было бы найти нужный файл в каталоге диска и прочитать его...

unsigned char stack[4096] attribute((aligned(16)));

Не знаю, как на чистых современных сях, а на це++ со стандарта 11 года выравнивание можно (и нужно) описывать стандартными средствами, а не с помощью нестандартных атрибутов -- гарантирует переносимость между различными компиляторами и всё такое.

ADD. Посмотрел: в сях добавили тоже, но лишь в стандарте 23 года (https://en.cppreference.com/w/c/language/alignas.html):

struct sse_t { alignas(16) float sse_data[4]; };

А ещё неплохо, чтобы она была таки ОС, а не очередным ХеллоВорлдом прямо из загрузчика :)

там есть AMBA для связи с периферией (и вроде как AXI входит в AMBA)

Да, AXI, как и другие шины (AHB, APB, ещё что-то, вроде бы...) -- часть AMBA.

работа с памятью идёт через интегрированные контроллеры

Угу. И наверняка связь ядро-контроллер памяти -- через AXI (нет смысла специально для этого изобретать какую-то другую шину).

И дело же не только в том, что торчит наружу - на телефонах, скажем, обычно процессорный чип один, но внутри него кучка разнородных ядер со своми кешами, и надо чтобы многопоточный софт корректно работал

Вот для упрощения работы многопоточного софта могут сделать аппаратное согласование содержимого кэшей -- но, строго говоря, архитектура этого не требует (там слабая модель), что позволяет, при необходимости, строить очень крупные системы -- но перекладывая заботу о согласованности на ПО.

Дело не только в операционке, без когерентности кешей обычный код написанный на C или Фортране (скажем, с распараллеливанием через OpenMP) не будет работать корректно, и барьеры не помогут

Почему ж не помогут, если они вставляются там, где нужно? На машинах с сильной моделью барьер лишь приостанавливает работу, пока изменения, внесённые в кэш, не будут гарантированно доставлены всем другим процессорам; на машинах со слабой моделью будет выполнена принудительная запись изменённых байтов из кэша в память и ожидание, пока запись физически не закончится (естественно, на приёмной стороне тоже должен быть соответствующий барьер -- обеспечивающий очистку кэша от старых данных).

Т.е. программно это реализовать вполне можно на, скажем, уровне стандартной библиотеки (атомики и барьеры в современных версиях C++ вполне дают возможность это сделать; вот насчёт Фортрана не уверен). Ну и, в любом случае, это будет обеспечиваться при вызове сервисов ОС.

Кстати, возможен и промежуточный вариант для NUMA. Скажем, процессор (многоядерный, есно) плюс непосредственно окружающая его память обеспечивают аппаратное согласование кэшей, а между узлами NUMA такого согласования уже нет. ОС запускает связанные потоки только на одном узле NUMA, а соответственно, они могут не особо заботиться о дополнительных телодвижениях (кроме обычных барьеров -- без них в любом случае не обойтись). А вот если нужна связь потоков, работающих на разных узлах, -- там уже изволь выполнять все нужные телодвижения (или просить ОС об этом). В этом случае можно обеспечить и относительную лёгкость программирования многопоточных приложений для сильной модели, и (почти) неограниченное масштабирование машины, обеспечиваемое слабой моделью.

Ну, у тех армовских ядер, что я видел, из ядра наружу торчит именно AXI (или AHB, но это на младших и средних микроконтроллерных, на Cortex-M7 -- уже AXI). Полагаю, внутри кристаллов от процессорных ядер до контроллера памяти идёт именно она (на ПЛИС так точно она, но ПЛИС -- всё же специфическая вещь). Вот снаружи микросхемы (кристалла), надо полагать, идёт уже обычная PCI Express, ибо она -- стандарт де-факто для всего современного мира.

Что касается серверных процов (и вообще процов) ARM, то слабая модель памяти не запрещает обеспечивать аппаратное согласование кэшей и т.д. -- она лишь разрешает этого не делать. Что её делают по крайней мере в некоторых реализациях архитектуры, я знаю, и есть предположение, что это для облегчения работы Линуха: если не возложить сию задачу на аппаратуру, придётся много что подпиливать в системе, которая изначально, как Вы не хуже меня знаете, возникла на IA-32, где всё это гарантированно не требуется на уровне архитектуры (сильная модель).

Скажем, если на одном процессоре захватили спин-блокировку и модифицировали некий управляющий блок, то перед освобождением этой блокировки надо гарантированно вытеснить данный блок (и всё, что с ним связано) из кэша в память, а на другом процессоре, ожидающем эту блокировку, после её захвата надо сначала гарантированно очистить свой кэш от старых копий этого участка памяти; очевидно, что это весьма геморройно, особенно если этого нельзя добиться одной командой. На M-профиле нельзя, там надо гулять по всему кэшу и вытеснять его строки в цикле, что само по себе весьма и весьма медленно; про A-профиль, который в телефонах и серверах, я не в курсе, подробно не смотрел доку. Вот на мэйнфреймах полную сериализацию можно сделать одной командой. Замечу попутно, что в таких ситуациях сериализация может оказаться весьма быстрой, если кэш оптимизирован под такие сценарии: скажем, если на упомянутом M-профиле приходится выполнять цикл несколько сотен раз (зависит от объёма и организации кэша), чтоб пройтись по каждой строке, железная поддержка может сразу анализировать множество строк и обеспечивать запись лишь из содержащих грязные байты, а чистые строки просто аннулировать -- что будет довольно быстро (особенно в подобном сценарии, достаточно характерном для ядер ОС, где нет больших объёмов данных, особенно изменяемых, а соответственно, почти все строки кэша между сериализациями остаются чистыми).

Но есть ещё момент, связанный с вводом-выводом. На ПК, вроде как, кэши процов отслеживают не только записи со строноны всех процов, но и доступы, выполняемые со стороны внешних устройств. Соответственно, после завершения операции чтения чистить кэши для гарантированного поступления процессорам новых данных не требуется, а перед записью не нужно записывать грязные строки в память: контроллер памяти, заметив обращение устройства по DMA к кэшированной строке, вернёт данные из кэша, а не из физической памяти (если ошибаюсь, прошу поправить). В ARMах, надо полагать, требуется ручками чистить кэш, чтоб гарантированно выполнить обмен с устройством правильно.

Information

Rating
2,082-nd
Location
Солнечногорск, Москва и Московская обл., Россия
Date of birth
Registered
Activity

Specialization

Embedded Software Engineer
Lead