Пользователь
Информация
- В рейтинге
- 101-й
- Откуда
- Петропавловск, Северо-Казахстанская обл., Казахстан
- Зарегистрирован
- Активность
Специализация
Десктоп разработчик, Инженер встраиваемых систем
Pure C
Assembler
X86 asm
Win32 API
Visual Basic
MySQL
Git
ООП
Разработка электроники
Обратная разработка
Я не увидел бирки, что это перевод. Но если это таки галлюцинаторный перевод, тогда у меня и к планам питания вопрос. Возможно это power planes, а power planes это совсем не планы питания...
На слове «проводники-инсуляторы» я чуть лицо фейспалмом не пробил...
Это единственное, что стоило бы контролировать антивирусом, аудитам. Без полученного хендла с правом на соответствующие операции никакие ни WriteProcessMemory, CreateRemoteThread, ни SetThreadContext ничего сделать не могут.
А если контролировать последние — это лишние накладные расходы, потому что подтормаживаются совершенно легитимные вызовы указанны системных сервисов.
Так что ключевой момент это как раз OpenProcess.
Комментарий я обычно строчу даже не перечитывая. А статья это 300 итераций с перепрочтением, перестановкой абзацев, укорочением одних предложений, уточнением других, рисованием картинок.
Но это не совсем так.
Windows в user-mode создаёт небольшой сегмент с базой, указывающей на thread environment block (TEB) / thread information block (TIB), а селектор этого сегмента загружает в регистр FS.
Отсюда обращения к FS:[0] для работы с SEH-цепочкой, работа виндового TLS на это завязано, да и куча кода в kernel32.dll и иже с ними лежет в TEB для множества разных целей.
В Linux-е FS используется для TLS, а GS доступен для использования.
В этом объёме сегментацию не выпилили даже в AMD64.
Но в целом, тот факт, что Intel предложила миру эту классную концепцию, а её в полной мере никто не заценил и не использовал, вызывает у меня моральные страдания. У меня даже есть мысль написать статью о том, каким мог бы быть мир, если бы сегменты использовали в хвост и в гриву (в защищённом режиме и 32-битном режиме).
Да, может постоянная замена значения сегментных регистров и вызвала бы проблемы с производительностью (из-за необходимости процессора обращаться к таблицам дескрипторов, чтобы занести что-то в теневую часть сегментных регистров), но уверен, Intel могла это красиво оптимизировать, будь на технологию ответный спрос, как она оптимизировала трансляцию линейных адресов в физические (речь о TLB).
Подумать только: у нас был бы мир с аппаратной защитой от ошибки переполнения буфера, мир, где программу можно было бы разбить на множество изолированных друг от друга микро-песочниц, где функция не могла бы из себя вызвать какую угодно другую функцию процесса и обратиться к каким угодно другим данным процесса, а «видела» бы только минимально необходимый для своей работы набор данных и других функций/процедур. Ошибки переполнения буфера, шеллкод и инжект перестали бы существовать или потеряли бы смысл...
/del
Но затем у вас идёт
Как будто бы это не шумно для защиты :-D
С SetThreadContext в общем-то очевидно, а вот из способов протащить собственный код в чужой процесс незаметно, меня до сих пор очаровывает вот этот.
Никто не спорит, но приводя пример ARM-кода вы всё же используете константы
GPIO0DIRиGPIO0DATAвместо магических чисел0x50008000и0x50003FFC, в примере с AVR8 вы используете константыPORTDиDDRBвместо магических0x12и0x17и так далее, но именно в случае x86 почему-то используются магические числа вместо констант с внятными именами. Вот именно к этой непоследовательности и вопрос.Мешает гнетущение ощущение, что такая статья окажется никому не нужной и не интересной, недостаток времени и почти что неспособность бороться с тенденцией «и тут Остапа понесло» при написании статей (и комментариев тоже, но с комментариев спрос не такой строгий и ответственность не такая сильная).
На мобильных устройствах редактор комментариев это просто ад и пытка.
Если начать нумерованный или ненумернованный список, и за абзацем со спиком заренее не поставить какой-нибудь ненужный абзац-плейсхолдер, то не существует никакого способа закончить список, выйти из режима ведения списка и начать писать просто последующий абзац. Всё, вы навеки заперты в списке.
А при создании спойлера, например, бекспейс в заголовке спойлера срабатывает ровно на одну букву, после чего курсор перескакивае в тело спойлера. Хочешь поменять 10 символов за головке раскрывающегося спойлера — будь добр 10 раз рукой переставь курсор из тела спойлера в заголовок.
Не важно, что бОльшая часть посвящена ассемблерам для МК-шных архитектур. Вопрос в том, что как только вы коснулись x86-ассемблера, то сразу всплывают
Во-первых, это создаёт ощущение того, что вы пересказываете статьи 30-летней давности, для которых такие сниппеты-примеры были актуальными. Хотя как раз вот такое подобное API (между прикладным софтом и ДОС-ом и между гипотетической ОС реального режима и БИОС-ом) морально устарело, а вот сам x86-ассемблер актуален как никогда, покуда жива x86-архитектура, пусть и в виде x86-64.
Во-вторых, это чисто эстетически не самый приятный и разумный способ организации API. Он исторически сложился, но не стоит начинать знакомство а написанием кода на ассемблере именно с демонстрации такого подхода. Почему бы в качестве примера не привести что-то вроде
Это и актуально, и не так запутанно, и универсально, и пример можно скомпилировать и запустить как под Windows 95, так и под современной виндой. А потом уже в качестве шок-контента показать, как делалось взаимодействие с системными API в стародавние времена.
В-третьих, даже если закрыть глаза на неактуальность DOS-прерываний в реальном режиме, а представить, что это супер-актуально и что ничего другого нет — опять же, именно такой код в таком виде подаёт дурной пример. И за этим делом стоит, на самом деле, большой философский вопрос. Да, я знаю, что именно так (int 10h) в большинстве своём все и пишут (или писали). Но правильно ли так писать?
Подумайте вот над каким глубоким вопросом: что самое главное в ассемблере, что первостепеннее всего в ассемблере и почему ассемблер вообще называется ассемблером (а не мнемо-транслятором или машкод-транслятором или чем-то таким)?
Когда я был юным и глупым, я бы уверенно ответил, что в первую очередь ассемблер это инструмент, который транслирует инструкции для процессора, написанные в человеко-читаемом виде (в виде текста из мнемоник инструкций и операндов) в машинный код в его непосредственном виде. То есть я бы сказал, что написать свой ассемблер — значит написать трансялятор, который для каждой строчки вроде
xor eax, eaxилиnopилиint 10hвыплюнет в ответ байты33С0,90илиCD10. Но прошли годы, мне довелось и свой собственный x86-ассемблер написать, и написать just-for-fun реализацию компилятора Си , и я могу с уверенностью сказать, что трансляция текстового представления инструкций в бинарное представление это вообще ерунда и абсолютно тривиальная задача, а самое главное в ассемблере совсем не это.А что же тогда? Тут стоит задаться вопросом: вот есть ассемблер для x86, для MIPS, для ARM, для AVR, а может ли существовать ассемблер вообще ни под какую архитектуру, то есть ассемблер, вообще не знакомый ни с одной из архитектур, не знающий ни одной машинной команды? Будет ли хоть какой-то смысл существования такого инструмента? Тогда можно поставить вопрос иначе: могу ли я взять какой-нибудь x86-ассемблер (например FASM, MASM, NASM) и с его помощью породить на свет машинный код для AVR8? Ведь набор инструкций совсем другой, а даже если какие-то мнемоники и совпадают, опкоды совсем другие и принцип кодирования операндов тоже совсем другой. На самом деле — могу, если просто проведу трансляцию текстового представления машинных инструкций в бинарное и в своём ассемблерном листинге запишу всё в виде директив .db. Нечестная игра? Очень тяжело? На самом деле, не очень-то и тяжело — написано один раз и работает всегда. А есть в ассемблерах (для любых архитектур) кое что ещё, без чего писать код, править и дорабатывать его было бы на порядок или на несколько порядков более тяжело, чем если бы конвертировать инструкции в бинарный вид пришлось бы вручную.
Это всевозможные директивы процессора, это возможность объявлять константы, это возможность ставить метки, это возможность в коде и в данных (записываемых в виде других директив) ставить упоминания этих меток, это способность ассемблера самостоятельно вычислять абсолютные адреса и относительные адреса или смещения в нужных местах, это возможность в своём коде указать не просто метку, но и сделать арифметику с метками и константами и заставить ассемблер посчитать корректный адрес или смещение одной сущности относительно другой.
Вот это самое главное (по крайней мере я так считаю) в ассемблерах и самое сложное при написании своего ассемблера для какой бы то ни было архитектуры.
Почему сложное? Что здесь сложного?
А вот, например, что. Возьмём для простоты архитектуру x86. С точки зрения человеко-читаемого представления кода всё очень легко:
С точки же зрения машинного кода как для безусловного джампа так и для всех условных джампов существует две версии: одна версия инструкции после которой процессор ожидает увидеть относительное смещение в виде 8-битного знакового числа, в другой версии смещение будет закодировано уже в виде 32-битного или 16-битного знакового числа (в зависимости от того, в 32-битном или 16-битном режиме выполняется задача, код которой выполняет процессор + к этому к инструкции может быть примерён префикс 66h переопределяющий размер адреса на противоположный).
Так вот, если мы пишем свой собственный x86-ассемблер, который идёт по строкам, перебирая одну за другой, и для каждой новой инструкцией делает трансляцию в машинный код, то здесь возникает проблема. Как ассемблеру обработать машинную команду вида jmp some_label? Выбрать для этого короткий форму кодирования с опкодом EB или длинную форму с опкодом E9? Предположим, мы генерируем код для 32-битного режима. Тогда инструкция с опкодом EB будет занимать 2 байта, а инструкция с опкодом E9 — 5 байт, из которых четыре это относительный адрес места назначения прыжка. Можно было бы поступать тупо и везде для всех джампов, как безусловных так и условных, использовать длинный вариант инструкции. Но это бы безмерно разувало машинный код, потому что ветвлений в коде чуть ли половина от всего объёма команд.
Можно поступать умнее: если jump destination лежит относительно недалеко и смещение относительно адреса следующей инструкции лежит в пределах от –128 до +127, то нужно использовать компактный двухбайтовый вариант инструкции, в противном случае — длинный трёх или пятибайтовый.
Проблема, однако, в том, что если встречаем в джампе упоминание метки, до которой мы ещё не добрались (т.е. джамп вперёд, а не назад), то мы пока ещё не знаем расстояние до неё и не знаем, укладывается ли это расстояние/смещение в вышеупомянутый диапазон. А значит мы не знаем, как получится длина инструкции, которую мы прямо сейчас генерируем.
Что мы тут можем сделать как авторы гипотетического ассемблера? Мы можем рекурсивно запустить ассемблирование (точнее трансляцию) далее следующих инструкций с указанием вложенноу рекурсивному вызову делать её до тез пор, пока не доберёмся до нужной метки — по возврату мы уже будем знать точное расстояние до интересующей нас метки, сможем выбрать между коротким джампом и длинным джампом, и, сгенерировав его, продолжим трансляцию с того места, до которого добрался только что сделанный рекурсивный вызов. Либо мы можем в условиях неопределённости применять всегда длинный вариант джамп-инструкции, но заносить все такие инструкции в особый список, а по завершению первого прохода, когда взаимное расположение всех меток станет окончательно ясным — провести второй проход и пройтись по всем джамп-инструкциям из списка, и те их них, где можно было бы обойтись коротким вариантом вместо длинного, заменить на короткий вариант.
Проблема в том, что ни первый ни второй подход не работают. Если метка лежит впереди и джамп ведёт вперёд и мы не знаем, какое до него расстояние и у нас возникает неопределённость относительно длины текущей транслируемой инструкции, мы не можем тупо взять и прямо сейчас попробовать ассемблировать/транслировать последующие инструкции (с целью определить величину смещения), либо отложенным проходом расставить все точки над i, потому внутри этого прыжка могут оказаться другие джамп-инструкции с неопределённой длиной, а у тех, в свою очередь, destination-метка может быть выбрана так, что уже длина тех инструкций тоже будет в состоянии неопределённости и попадёт в зависимость от длины транслируемой в данной момент джамп-инструкции. Иными словами: длины джамп-инструкций зависят от того, насколько далеко нужно прыгать, а если перепрыгивать нужно через другие джамп-инструкции, которые тоже в свою очередь непонятно какой длины в конечном итоге получатся, то зависимость между длинами джамп-инструкций может стать циркулярной, замкнутой. И тогда в первом подходе мы получаем бесконечную рекурсию, а во втором подходе мы получаем бесконечный цикл на втором проходе (компактификации длинных джампов), потому что компактификатор будет заменять длинный джамп на короткий в одном конкретной месте, а маленькое изменение будет приводить к сдвигу всех (или многих) последующих инструкций на несколько байт назад, сдвиг куска машинного кода на несколько байт переведёт к необходимости пересчёта кучи других смещений, стоящих в разных местах кода, и где-то в результате такого пересчёта величина смещения увеличится настолько, что короткий вариант джамп-инструкции перестанет переходить, а это в свою очередь вызовет необходимость поменять короткий джамп на длинный, а это вызовет новый сдвиг точек/меток, новый пересчёт смещений, то есть это запускает цепную реакцию, конца у которой может не быть.
Конечно, и от этого можно защититься, если для каждого такого места, которое мы собираемся перекраивать в плане замены длины инструкций в пользу большей компактности, ввести флаг или счётчик попыток перекроить инструкцию: если мы сгенерировали сначала длинный джамп (но занесли его в особый список, чтобы потом попробовать применить компактный), потом заменили на компактный (чем вызвали пересчёт всех, а точнее многих прочих меток и смещений), и оказалось, что это вызвало волну пересчёта смещений и вынудило нас где-то в другом месте поменять короткий джамп на длинный, что в свою очередь вынуждает нас опять первоначальный джамп поменять на короткий, то не нужно бесконечно менять короткую вариацию инструкции на длинну и наоборот — уже после второй попытки стоит ставить крест и оставлять длинную вариацию инструкции.
Тогда, конечно, ассемблер не зависнет в процессе ассемблирования, но такой наивный подход к решению проблемы не даёт гарантии, что мы сгенерировали оптимальный в плане размера код: возможно мы отказались заменить одну джамп-инструкцию с длинной на короткую, но в результате 50 других джамп-инструкций, которые могли бы существовать в короткой форме, пришлось оставить в длинной вариации.
Смысл этого опуса в том, что процесс вычисления меток, их взаимного расположения в некоем адресному пространстве, вычисления смещения между ними и текущей точкой ассемблирования/трансляции и подбор нужного варианта кодирования одной и той же инструкции (но с разной длиной, которая в конечном итоге зависит от расположения меток друго относительно друга) — это очень нетривиальный процесс, по сравнению с которым тупое транслирование текстового представления в бинарный машинный код становится супер-тривиальной задачей.
Тогда смысл ассемблера, который вообще ни в курсе ни про одну архитектуру и не знает ни одной мнемоники очень простой — с помощью такого ассемблера по прежнему можно создать код машинный код под любых архитектуру, просто забивая опкоды и кодируя операнды ручками, но при этом вычислять и подставлять адреса и относительные смещения за нас будет инструмент. Нам не придётся от одной правки где-то в середине кода пересчитывать десяток адресов/смещений и менять их в коде. С другой стороны, если такой столь странный инструмент умеет работать с метками и директива, мы можем с помощью такого ассемблера сформировать какой-нибудь не исполняемый, но при этом бинарный файл со сложной структурой. Какой-нибудь PDF, или x509-сертификат, или MBR с таблицей разделов или что-то ещё табличное и с древовидной структурой. Нам не придётся вручную считать смещения, адреса и индексы, за нас это сделает ассемблер.
К слову, наличие таких директив, меток и адресной арифметики с ним даёт нам возможность на выходе ассемблера получить, например, PE-файл или ELF-файл корректного формата даже в том случае, если ассемблер вообще ни сном ни духом ни про PE-формат, ни про ELF-формат и не поддерживает из коробки генерацию исполняемых файлов в таком формате, а может выдавать на выходе только raw-бинарники. Разве что с полем типа CRC/checksum будет проблема — его вычисления распространными директивами типа .org/.align и адресной арифметикой на добьёшься.
Так вот, к чему я об этом всём? Во-первых, как мне показалось, вы мало внимания уделяете обзору именно этой стороны ассемблера: всем этим директивам, константам и трюкам с адресной арифметикой. А это, между прочим, присуще в той или иной степени всем ассемблерам, и с другой стороны, жизнь без именно этих фишек была бы адом — гораздо большим адом, чем если бы int 10h пришлось бы превращать в .db CDh, 10h вручную, вооружившись табличкой. Хотя у вас пример с применением этой адресной арифметики в коде проскальзывает (
len = . - msg), но я вном виде об этом в тексте статьи упоминания нигде нет.Ну а потом, это собственно, то с чего я начал третью претензию. Раз вы не собираетесь делать секрет из того, что помимо собственно трансляции машинных команд ассемблеры понимают ещё и директивы, объявления констант, какие-то ассемблеры понимают макросы, почему вы собственно не пользуетесь этими константами?
Почему mov AH, 9? Почему int 20h? Почему вы не объявляете константы для прерываний, функций и подфункий, не даёте им внятные человеко-читаемые имена? Вам не кажется, что эти 9 и 20h это то, что отталкивает от мысли освоить ассемблер, потому что пугает необходимостью зазубривать все это номера прерываний и функций?
Почему авторы статей базового уровня по ассемблеру x86 так любят реальный режим и DOS?
По-моему, эта комбинация — одна из самых неприятных и отталкивающих. У многих вообще формируется порочная ассоциативная связь: я как-то общался с одним человеком, который работал преподавателем программирования в колледже — когда я сказал, что, в числе прочего, пишу код на ассемблере, он скривил лицо и сказал что-то вроде «фу, ассемблер это же какой-то допотопный язык времен ДОСа». То, что любой современный исполняемый файл состоит из таких же машинных команд, которые можно записать в виде человеко-читаемого ассемблерного листинга, в его картину мира не укладывалось.
Виноваты в этом статьи про ассемблер, которые все дружно начинают знакомство читателя с ассемблером приводя в пример 16-битный реальный режим, 16-битные регистры и прерывания DOS/BIOS.
Ещё больший процент не знают и не понимает концепцию сегментов в защищенном режиме, имея представление о сегментах на основе сегментов реального режима :-(
Оффтопик
Знакомый ник с форума VBStreets. И аватара всё та же, что и 20 лет над :-)
Не адрес, а селектор. А селектор (в защищённом режиме) это флаговые биты плюс индекс в таблице — таблице дескрипторов сегментов.
И уже в дескрипторе сегмента хранится базовый адрес сегмента.
А чем неполноценна многозадачность в 286? В 386 в плане многозадачности появился только режим Virtual 8086 mode. Остальные новые фишки касаются в основном MMU, например, введение страничной организации и дополнительного уровня трансляции адресов (линейных) в физические.
Так это проблема китайской комнаты.
Женщину вынули — автомат засунули.
Ютуб уже давно чем-то таким пользуется, пытаясь выделять хейт-спич и не пропускать такие комменты. Приходится проявлять чудеса находчивости в иносказательности, чтобы преодолеть бездумную машинную цензуру. Ведь слово «шлак» может понизить шансы коммента на долгую жизнь — и плевать, что это комментарий к видео про сварку или металлургию.
Почему неведомая хрень? Можно для вас и Википедия неведомая хрень?
У вас узкоспециализированный софт. А general-purpose есть? Или не general purpose, а для инженерных систем бытовых и общественных зданий, главным образом для отопления.
У Valtec-а есть расчётный комплекс, но это не то: в нём нет возможности нарисовать схему, он считает только параллельные не ветвящиеся контуры исходя из предположения, что они запитаны, условно говоря, от коллекторов с безграничной возможностью по поставке/поглощению воды.
Кстати, раз уж вы занимаетесь этой тематикой. Неужели нет специализированного софта подобно тому, как в электронике есть симуляторы, которым даёшь схему, а они тебе посчитают токи и напряжения в любой точке?
Почему в маткаде приходится считать?
Увидев в новостной подборке заголовок, думал, автором будет @iMonin...