All streams
Search
Write a publication
Pull to refresh
69
0.8
Иван Савватеев @SIISII

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

Send message

Оверлеи - так это вообще малая часть всей истории. К тому времени, когда программы выросли до таких размеров, что перестали помещаться в ОЗУ, уже появились аппаратные (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ах, надо полагать, требуется ручками чистить кэш, чтоб гарантированно выполнить обмен с устройством правильно.

Я тоже не один десяток лет работал и работаю. Аппаратура сколько-нибудь существенно не усложняется, все необходимые линии индикации, какие байты, записываются, имеются. См., например, шину AXI -- основную на ARMах (кроме не шибко мощных микроконтроллеров, где основной будет AHB -- но там та же история):

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

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

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

Почему Вы решили, что строка кэша не отслеживает изменения отдельных байтов? Всё она отслеживает, поэтому никаких проблем не возникает.

IDA Pro умеет генерировать сишный код из результата дизассемблирования, но тут вопрос ещё в том, может ли она сделать это для конкретного случая (мало ли какие ограничения имеются).

Плюс, сишный код, полученный таким путём -- он, конечно, сишный, но логика зачастую остаётся непонятной: вполне могут быть if и goto вместо циклов и т.п. вещи.

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

Но если Вы имеете в виду, что один процессор работает только с одними байтами, а другой -- с другими байтами, т.е. имеющими разные адреса, то абсолютно никаких проблем не возникает: им же без разницы, что творится с байтами, которые они не используют.

Это в любом случае нужно для когерентности кешей в любой модели памяти (записи в разные части кеш линеек должны корректно мёрджиться).

Не нужно. Когда производится сериализация, в память записываются не целые строки кэша, а только те байты, которые реально были изменены. Соответственно, если один процессор изменяет байты 0-3, а другой -- 4-7 в одном и том же блоке памяти, занимающем одну строку кэша (скажем, 64-байтовом), то в память всё запишется корректно.

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

Что же касается производительности... Судя по всему, именно производительность у мэйнфреймов вполне себе на уровне. Однако скорость реакции на прерывания для них никогда не была определяющей (несмотря на рекламу ИБМ про "360 градусов" охвата задач, которые они могут решать -- это было, естественно, неверным даже в 1964-м, когда она их анонсировала): исторически они рассчитывались на пакетную обработку данных. В классической OS/360 оно ухудшалось ещё больше самой системой: изрядное время её код выполнялся при полностью запрещённых прерываниях, что, понятное дело, не ускоряло реакцию на их появление. Так что, думаю, обработкой срочных запросов занимаются машины других архитектур, а на мэйнфреймах лежит, так сказать, бэкенд -- гигантские базы данных и всё такое, где важней общая производительность, а не время ответа. Ещё одним вариантом может быть обработка прерываний (шире -- организация всего ввода-вывода) на одних процессорах и обработка данных -- на других, причём для уведомления других процессоров о появлении для них работы использовать не прерывания, а изменение всяких семафоров и прочих мьютексов в памяти: система команд располагает командами, осуществляющими лишь частичную сериализацию и поэтому позволяющими реализовывать быстрые примитивы синхронизации без необходимости каждый раз дёргать систему. Но это создаёт дополнительные проблемы для программиста (впрочем, они есть у любой архитектуры со слабой моделью -- на тех же ARM, где в определённых случаях нужно ручками вставлять барьеры, в то время как на IA-32 это требуется намного реже).

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

Во-вторых, и x86, и ARM -- понятия очень сильно растяжимые. У последнего, в частности, выпускаются процессоры с ядрами, где даже простой суперскалярности нет, не говоря уже о внеочередном выполнении, а среди микроконтроллеров таких явно больше половины -- а ведь они тоже ARM.

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

Information

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

Specialization

Embedded Software Engineer
Lead