Мысли о программировании на ассемблере автор Алексей @khett взято здесь здесь После многих лет занятия чем не попадя, решил вернуться к истокам. К программированию. Опять же, ввиду множества «современных достижений» в этой области было трудно определиться, чего же на самом деле не хватает, за что взяться чтобы было и приятно и полезно. Попробовав много чего понемногу, все же решил вернуться туда, куда тянуло с первых дней знакомства с компьютером (еще с копией творения сэра Синклера) – к программированию на ассемблере. На самом деле, в свое время Ассемблер я знал достаточно неплохо (в данном случае говорю про x86), но почти 15 лет ничего на нем не писал. Таким образом это своеобразное возвращение «блудного сына». Но тут поджидало первое разочарование. Найденные на просторах Интернета книги, руководства и прочие справочники по ассемблеру, к моему глубокому сожалению, содержат минимум информации о том, как надо программировать на ассемблере, почему именно так, и что это дает. Если брать в качестве примера бокс, то все подобные руководства учат исполнять удар, перемещаться стоя на полу, но абсолютно отсутствует то, что делает бокс — боксом, а не «разрешенным мордобитием». То есть комбинационная работа, особенности использования ринга, защитные действия, тактическое построение боя и, тем более, стратегия боя не рассматриваются вообще. Научили человека бить по «груше» и сразу на ринг. Это в корне неверно. Но именно так построены практически все «учебники» и «руководства» по программированию на ассемблере. Однако нормальные книги должны быть, скорее всего под горой «шлака» я их просто не нашел. Поэтому прежде чем восполнять знания глобальным описание архитектуры, мнемоники и всяческих фокусов «как слепить фигу из двух пальцев», подойдем к вопросу программирования на ассемблере с «идеологической» точки зрения. Идиллия? Маленькое замечание, далее по тексту будет использована классификация, отличающаяся от распространенной в настоящее время. Однако это не является поводом для «споров о цвете истины», просто в данном виде проще объяснить точку зрения автора на программирование. Итак, на сегодняшний день, казалось бы, для программистов наступила эпоха счастья. Огромный выбор средств на все случаи жизни и пожелания. Тут тебе и миллионы «фреймворков»/«паттернов»/«шаблонов»/«библиотек» и тысячи средств «облегчающих» программирование, сотни языков и диалектов, десятки методологий и различные подходы у программированию. Бери – не хочу. Но не «берется». И дело не в религиозных убеждениях, а в том, что это все выглядит как попытка питаться чем-то невкусным. При желании и усердии можно приноровиться и к этому, конечно. Но, возвращаясь к программированию, в большинстве из предлагаемого не видно технической красоты — видно лишь множество «костылей». Как результат, при использовании этих «достижения», из-под «кисти художников» вместо завораживающих пейзажей выходит сплошная «абстракция», или лубки — если повезет. Неужели большинство программистов такие бездари, неучи и имеют проблемы на уровне генетики? Нет, не думаю. Так в чем же причина? На сегодняшний день имеется множество идей и способов программирования. Рассмотрим наиболее «модные» из них. Императивное программирование — в данном подходе программист задает последовательность действий, приводящих к решению задачи. В основе лежит разделение программы на части, выполняющие логически независимые операции (модули, функции, процедуры). Но в отличии от типизированного подхода (см. ниже) тут есть важная особенность – отсутствие «типизации» переменных. Иными словами отсутствует понятие «тип переменной», вместо него используется понимание, что значения у одной и той же переменной могут иметь различный тип. Яркими представителем данного подхода являются Basic, REXX, MUMPS. Типизированное программирование — модификация императивного программирования, когда программист и система ограничивают возможные значения переменных. Из наиболее известных языков — это Pascal, C. Функциональное программирование — это более математический способ решения задачи, когда решение состоит в «конструировании» иерархии функций (и соответственно создание отсутствующих из них), приводящей к решению задачи. Как примеры: Lisp, Forth. Автоматное программирование – подход, где программист строит модель/сеть, состоящую из обменивающихся сообщениями объектов/исполнительных элементов, как изменяющих/хранящих свое внутреннее «состояние» так и могущих взаимодействовать с внешним миром. Иными словами это то, что обычно называют «объектное программирование» (не объектно-ориентированное). Этот способ программирования представлен в Smalltalk. А как-же множество других языков? Как правило, это уже «мутанты». Например, смешение типизированного и автоматного подхода дало «объектно-ориентированное программирование». Как видим, каждый из подходов (даже без учета ограничений конкретных реализаций) накладывает собственные ограничения на саму технику программирования. Но иначе и быть не может. К сожалению, эти ограничения зачастую созданы искусственно для «поддержания чистоты идеи». В итоге, программисту приходится «извращать» изначально найденное решение в вид, хоть как-то соответствующий идеологии используемого языка или используемому «шаблону». Это даже без учета новомодных методик и способов проектирования и разработки. Казалось бы, программируя на ассемблере, мы вольны делать все и так, что и как пожелаем и позволяет нам «железо». Но как только нам захочется использовать «универсальный драйвер» для какого-либо типа оборудования, мы вынуждены менять свободу «творчества» на предписанные (стандартизированные) подходы и способы использования драйвера. Как только нам понадобилась возможность использовать наработки других коллег или дать им возможность делать тоже самое с плодами нашего труда — мы вынуждены менять свободу выбора взаимодействия между частями программы на некие обговоренные/стандартизированные способы. Таким образом та «свобода», за которой часто рвутся в ассемблер зачастую оказывается «мифом». И этому (пониманию ограничений, и способам их организации), на мой взгляд, должно уделяться повышенное внимание. Программист должен понимать причину вносимых ограничений, и, что отличает ассемблер от многих языков высокого уровня, иметь возможность менять их, при возникновении такой необходимость. Однако сейчас программист на ассемблере вынужден мириться с ограничениями, вводимыми языками высокого уровня, не имея «пряников» доступных программирующими на них. С одной стороны, операционные системы предоставляют множество уже реализованных функций, есть готовые библиотеки и много многое другое. Но способы их использования, как специально, реализованы без учета вызова их из программ, написанных на ассемблере, а то и вообще наперекор логике программирования для x86 архитектуры. В результате, сейчас программирование на ассемблере с вызовом функций ОС или внешних библиотек языков высокого уровня – это «страх» и «ужас».Чем дальше в лес, тем толще Итак, мы осознали, что хотя ассемблер очень прост, но пользоваться им надо уметь. И основная слаженность — это необходимость взаимодействия со средой исполнения, где запускается наша программа. Если программисты на языках высокого уровня уже имеют доступ к необходимым библиотекам, функциям, подпрограммам на многие случаи жизни и им доступны способы взаимодействия с внешним миром, в виде, согласованном с идеей языка, то программисту на ассемблере приходится продираться сквозь чащу всевозможных препонов, водруженных на пустом месте. Когда смотришь на то, что генерируют языки высокого уровня при компиляции, то складывает ощущение, что, те, кто писал компиляторы, либо понятия не имеют, как работает процессор с архитектурой x86, «или одно из двух» ©. Итак, давайте по-порядку. Программирование — это в первую очередь инженерия, то есть научное творчество, направленное на эффективное (по показателям надежности, использования доступных ресурсов, сроков реализации и удобства применения) решение практических задач. И, в основе любой инженерии лежит системный подход. То есть нельзя рассматривать любое решение как некий «неразборный» черный ящик, функционирующий в полном и идеальном вакууме. Как яркий пример системного подхода можно привести производство грузовиков в США. В данном случае, производитель грузовика – это просто изготовитель рамы и кабины + сборщик конструктора. Все остальное (двигатель, трансмиссия, подвеска, электрооборудование и так далее) берется исходя из пожеланий заказчика. Захотел один заказчик получиться себе некий Kenworth с двигателем от Detroit Diesel, ручной коробкой Fuller, рессорной подвеской от какой-нибудь Dana — пожалуйста. Понадобилась другу этого заказчика та же модель Kenworth, но с «родным» двигателем Paccar, коробкой-автоматом Allison и пневмоподвеской от другого производителя – легко! И так делают все сборщики грузовиков в США. То есть грузовик – это система, в котором каждый модуль может быть заменен на другой, того же назначения и беспроблемно состыкован с уже имеющимися. Причем способ стыковки модулей сделан с максимально доступной универсальностью и удобством дальнейшего расширения функционала. Вот к чему должен стремиться инженер.
К сожалению, нам придется жить с тем, что есть, но в дальнейшем подобного следует избегать. Итак, программа — это, по сути, набор модулей (неважно как они называются, и как себя «ведут»), компонуя которые мы добиваемся решения стоящей задачи. Для эффективности крайне желательно, чтобы можно было эти модули использовать повторно. Причем не просто использовать любой ценой, а использовать удобным способом. И вот тут нас ждет очередной неприятный «сюрприз». Большинство языков высокого уровня оперируют такими структурными единицами как «функция» и «процедура». И, как способ взаимодействия с ними, применяется «передача параметров». Это вполне логично, и тут никаких вопросов не возникает. Но как всегда, «важно не то, что делается — важно как делается» ©. И вот тут начинается самое непонятное. На сегодня распространены 3 способа организации передачи параметров: cdecl, stdcall, fastcall. Так вот, ни один из этих способов не является «родным» для x86. Более того, все они ущербны с точки зрения расширения функционала вызываемых подпрограмм. То есть, увеличив количество передаваемых параметров, мы вынуждены менять все точки вызова этой функции/подпрограммы, или же плодить новую подпрограмму с похожим функционалом, которая будет вызываться немного иным способом. Указанные выше методы передачи параметров относительно неплохо работают на процессорах с двумя раздельными стеками (стеком данных, и стеком адресов/управления) и развитыми командами манипулирования стеком (хотя бы индексное обращение к элементам стека). Но при программировании на x86 приходится сначала извращаться при передаче/получении параметров, а потом не забыть «структурное» их удаление из стека. Попутно стараясь угадать/рассчитать максимальную глубину стека. Напомним, что x86 (16/32 битный режим), это процессор, у которого: специализированные регистры (РОНы — регистры общего назначения — как таковые отсутствуют: то есть, мы не можем одной командой умножить содержимое регистра GS на значение из EDI и результат получить в паре EDX:ECX, или же разделить значение из пары регистров EDI:ESI на содержимое регистра EAX); регистров мало; один стек; ячейка памяти не дает никакой информации от типа хранящегося там значения. Иначе говоря, методы программирования, используемые для процессоров с большим регистровым файлом, с поддержкой нескольких независимых стеков и так далее в большинстве своем не применимы при программировании на x86. Следующая особенность взамодействия с готовыми модулями, написанными на «языках высокого уровня»— это «борьба» с «типами переменных». С одной стороны, причина появления типов переменных ясна — программист знает какие значения используются внутри его подпрограммы / модуля. Исходя из этого, видится вполне логичным, что, задав тип значений переменной, мы можем «упростить» написание программы, возложив контроль типов/пределов значений на транслятор языка. Но и тут с водой выплеснули младенца. Потому как любая программа пишется не для генерации сферических коней в вакууме, а для практической работы с пользовательскими данными. То есть очевидное нарушение системного подхода — как будто разработчики языков высокого уровня рассматривали свои системы без учета взаимодействия с внешним миром. В итоге, программируя на типизированном языке разработчик должен предусматривать все возможные виды «неправильных» входных данных, и искать способы обхода неопределенностей. И вот тут на сцену выходят монструозные системы поддержки регулярных выражений, обработки исключительных ситуаций, сигнатуры методов/процедур для разных типов значений и прочая прочая генерация костылей. Как было уже указано выше, для архитектуры x86 само значение, хранимое в ячейке памяти, не обладает никаким типом. Программист на ассемблере получает привилегию и ответственность за определение способа обработки этого самого значение. А уж каким образом определять тип значения и как его обрабатывать — тут на выбор множество вариантов. Но, подчеркнем еще раз, все они касаются только значений, получаемых от пользователя. Как верно заметили разработчики типизированных языков: типы значений внутренних и служебных переменных практически всегда известны заранее. Эта причина (извращенная передача параметров в модули, написанные на языках высокого уровня и необходимость строго следить за типами передаваемых параметров в те же самые модули) видится основной, из-за которой программирование на ассемблере неоправданно затруднено. И большинство предпочитает разбираться в дебрях «языков высокого уровня», чтобы воспользоваться тем, что уже наработано другими, чем мучиться, вставляю одни и те же «типовые» костыли, для исправления того, чего они не делали. И редкий транслятор ассемблера хоть как-то «разгружает» программиста от этой рутины.Что делать?Предварительные выводы с учетом пятнадцатилетного перерыва в программировании на ассемблере. Во-первых, по поводу модулей или частей программы. В общем случае стоит выделить два вида исполнительных модулей программы на языке ассемблера — «операция» и «подпрограмма». «Операцией» будем называть модуль, выполняющий «атомарное» действие и не требующий для своего выполнения множества параметров (например, операция очистки всего экрана, или операция расчета медианы числового ряда и тому подобное.). «Подпрограммой» же стоит назвать функциональный модуль, требующий, для корректного функционирования, множество входных параметров (больше двух-трех). И тут стоит оценить опыт императивных и функциональных языков. Они нам подарили 2 ценных инструмента, которыми стоит воспользоваться: «структура данных» (или, на примере REXX — составные/дополняемые переменные) и «немутабельность данных». Для передачи параметров в подпрограммы удобно использовать «структуры», то есть сформированные наборы параметров, расположенные в некой области памяти, доступной и основной программе и вызываемым подпрограммам. Более того, можно стандартизировать подход, и использовать «нулевой» параметр как битовую маску заполненных/значимых полей структуры. То есть это будет своеобразной сигнатурой вызова, которую подпрограмма может дополнительно анализировать и менять логику работы, в зависимость от фактически используемых параметров. Более того, разработчик может расширять возможности подпрограммы, сохраняя совместимость со старыми вызовами, и увеличивать количество используемых параметров без необходимости плодить множество подобных подпрограмм с одинаковым функционалом в рамках поддерживаемого API. Дополнительным плюсом такого подхода видится уменьшение «паразитной» работы со стеком. Полезно также следовать правилу немутабельности — то есть неизменности передаваемых параметров. Подпрограмма не может (не должна) менять значения в передаваемой ей структуре и результат возвращает либо в регистрах (не более двух-трех параметров), либо также в новой, создаваемой структуре. Таким образом мы избавлены от необходимости делать копии структур, на случай «забытого» изменения данных подпрограммами, и можем использовать уже созданную структуру целиком или основную ее часть для вызова нескольких подпрограмм, оперирующих одним/схожим набором параметров. Более того, практически «автоматом» приходим к очередному «функциональному» правилу — внутренней контекстно-независимости подпрограмм и операций. Иными словами — к разделению состояния/данных от метода/подпрограммы их обработки (в отличие от автоматной модели). В случаях параллельного программирования, а также совместного использования одной подпрограммы мы избавляемся как от необходимости плодить множество контекстов исполнения и следить за их «непересечением», так и от создания множества экземпляров одной подпрограмм с разными «состояниями», в случае нескольких ее вызовов. Что касается «типов» данных, то тут можно как оставить «все как есть», а можно тоже не изобретать велосипеда и воспользоваться тем, что давно используют разработчики трансляторов императивных языков — «идентификатор типа значения». То есть все данные, поступающие из внешнего мира анализируются и каждому полученному значению присваивается идентификатор обрабатываемого типа (целое, с плавающей точкой, упакованное BCD, код символа и так далее) и размер поля/значения. Имея эту информацию, программист, с одной стороны, не загоняет пользователя в излишне узкие рамки «правил» ввода значений, а с другой — имеет возможность в процессе работы выбрать наиболее эффективный способ обработки данных пользователя. Но, повторюсь еще раз, это касается только работы с пользовательскими данными. Это были общие соображения о программировании на ассемблере, не касающиеся вопросов проектирования, отладки и обработки ошибок. Надеюсь что разработчикам ОС, которые пишут их с нуля (а тем более на ассемблере), будет о чем подумать и они выберут (пусть не описанные выше, а любые иные) способы сделать программирование на ассемблере более систематизированным, удобным и приятным, а не будут слепо копировать чужие, зачастую безнадежно «кривые» варианты.
Спасибо за статьи все очень интересно, но такое ощущение что вы пишите сами себе, вот лично мне было бы понятнее, что бы рядом с теорией были примеры, а так читаю и думаю, да вот есть умные люди 15 лет не брал в руки ассемблер, а тут взял и написал статью и не просто статью, а еще и сравнительный анализ по отношению к другим языкам, материал наверно на диссертацию тянет. <и использовать «нулевой» параметр как битовую маску заполненных/значимых полей структуры.> вот взять хотя бы такой фрагмент, как можно понять о какой структуре идет речь? И что из себя представляет нулевой параметр и с чем его едят? А поскольку этим статьям уже год и ответов нет, значит или все все понимают или никто ничего не понял. Сразу хочу сказать, что автора статьи очень уважаю. Знаю по другому форуму, много было и там интересных статей.
а мне статья не понравилась: Автор прям пришёл, узрил проблему, порвал её в клочья и проповедует )) в сложившейся ситуации имеются две части.. 1. Школа прогеров действительно очень сильно пострадала из-за доктрины "элегантных" кодов. 2. с другой стороны, понятие "эффективность" варьируется согласно поставленной задаче.. 2.1. допустим, имеется задача супер быстрого кода ==>> тогда нужно забыть о таких вещах как авто-очистка памяти/перехват ошибок/дамп/портабельность/безопасность/.. 2.2. допустим, нужно соорудить ось достаточно удобную для автоматизации разработки + требуются мин. затраты переноса всей софтины на другие платформы ==>> тогда, увы и ах, придётся забыть о супер оптимазах кода.. 2.2.1. нам нужен наиболее широкий пул разрабов, дабы заданные объёмы кода выдавались на гора хотя бы чуть ранее смерти Вселенной ;D то бишь средний уровень разрабов не может быть очень высоким + компиляторы должны пахать достаточно шустро, то бишь глубине авто-анализа режут осетра. 2.2.2. с любой глубиной авто-анализа код остаётся привязан к конкретной железке, то бишь оптимаза зачастую требует доп. исследований и оные могут выливаться в ОЧЕНЬ ЗВЕРСКУЮ КОПЕЕЧКУ.
Получается вы косвенно меня поддержали, для новичка статья почти ни о чем, примеров нет, осознать написанное трудно/невозможно. Начальным ликбезом тоже нельзя назвать по этой же причине. А для профи эта статья только часть или один из вариантов создания драйвера, без привязки к конкретной задаче/ТЗ, я так понял ваш ответ UbIvItS,
седьмой, автор описал реально коньЫкАа в Вакууме и нисколько не удосужился проверить свои утв против РЕАЛЬНЫХ ЗАДАЧ. Вот, к примеру, пассаж.. стыдно спросить, а что делать, если нужно динамически создавать объекты? чрез какую }|{@пЪу передавать параметры??? наверно, как-то так.. struct head{ int X; int Y; int Z; head *newdata; }; то есть каждый новый экземпляр подпрограммы должен подцепить свои данные к общей голове, то бишь ПОДПРОГРАММА НЕ ИМЕЕТ ПРЯМОГО ДОСТУПА К СВОИМ ДАННЫМ а можь не стоит себя так пужать и использовать в своём коде Асм вставки, если уж и впрямь нужна особая оптимаза? http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html то бишь чел не понимает, что авто-оптимаза есмь ДiAVoЉСКИ ресурсоёмкий процесс, тч зачастую используются дженерик схемы, кои могут генериться сравнительно быстро даже на слабых машинах + обладают достаточно хорошими характеристиками относительно широкого круга машин без надобности их перекомпиляции. ......................................__................................................ .............................,-~*`¯lllllll`*~,.......................................... .......................,-~*`lllllllllllllllllllllllllll¯`*-,.................................... ..................,-~*llllllllllllllllllllllllllllllllllllllllllll*-,.................................. ...............,-*llllllllllllllllllllllllllllllllllllllllllllllllllllll.\.......................... ....... .............;*`lllllllllllllllllllllllllll,-~*~-,llllllllllllllllllll\................................ ..............\lllllllllllllllllllllllllll/.........\;;;;llllllllllll,-`~-,......................... .. ...............\lllllllllllllllllllll,-*...........`~-~-,...(.(¯`*,`,.......................... ................\llllllllllll,-~*.....................)_-\..*`*;..).......................... .................\,-*`¯,*`)............,-~*`~................/..................... ..................|/.../.../~,......-~*,-~*`;................/.\.................. ................./.../.../.../..,-,..*~,.`*~*................*...\................. ................|.../.../.../.*`...\...........................)....)¯`~,.................. ................|./.../..../.......)......,.)`*~-,............/....|..)...`~-,............. ..............././.../...,*`-,.....`-,...*`....,---......\..../...../..|.........¯```*~-,,,, ...............(..........)`*~-,....`*`.,-~*.,-*......|.../..../.../............\........ ................*-,.......`*-,...`~,..``.,,,-*..........|.,*...,*...|..............\........ ...................*,.........`-,...)-,..............,-*`...,-*....(`-,............\....... ......................f`-,.........`-,/...*-,___,,-~*....,-*......|...`-,..........\........ чел, вообще, с Асмом-то дела имел.. где он там УНИВЕРСАЛЬНОСТЬ откопал??? НО ГЛАВНОЕ, ЧТО ЧЕЛ ЗНАЕТ ПРО СИСТЕМНЫЙ ПОДХОД
UbIvItS, Это утверждение верно, структура в данном случае это обьект, доступ к которому происходит через указатель на него. Это максимально быстрая выборка данных из обьекта, так например адресуются все обьекты NT.
Indy_, хорошо == в сущности можно располагать все данные в паблике, но для максимально быстрого доступа по-любому нужно хранить указатель локально. и я уж совсем молчу, что глобальные переменные сильно вредят устойчивости кода и его безопасности.
Это тема моей статейки будет, про то что глобальные переменные можно криптовать и применять пост новые техники, например динамическую базу и ASLR или загон в дамп )) Про безопастность типа.