Pull to refresh

Comments 50

Мне попадался проект машинного кода ForwardCom, в котором тоже для ассемблера используется C-подобный язык, но симкод вроде бы элегантнее выглядит.

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

a=(b+c)^^2;

Так что с одиночным ^ путаницы нет.

Для циклического сдвига в языке Phi (был такой малоизвестный язык) использовались операторы ~<< и ~>>, например:

a = b ~<< 2;
a ~<<= 1;

Идея использовать для xor eax, eax конструкцию eax = 0 будет выглядеть красиво, пока не появятся макросы-константы. А макросы будут. Если язык не имеет препроцессора, то препроцессор напишут. И тогда конструкция eax = BLACK может вызвать внезапное изменение флагов, хотя операция mov не должна это делать.

На мой взгляд, выражение xor eax, eax проще записать так:

^eax

ForwardCom, в котором тоже для ассемблера используется C-подобный язык

ForwardCom — это экспериментальная архитектура набора команд, а не просто язык ассемблера. Его нельзя взять и скомпилировать в x86-код (можно только сэмулировать, но толку в этом немного).

А макросы будут.

Макросов точно не будет. Симкод не предназначен для написания ассемблерного кода, он нужен для максимального удобства чтения сгенерированного машинного/ассемблерного кода. Покажите мне хоть один компилятор (C++, Rust или другого компилируемого языка), который бы генерировал ассемблерный код с макросами. В 64-разрядной версии MASM поддержку встроенных макросов (.if, .while, invoke и пр.) вообще убрали.

Покажите мне хоть один компилятор (C++, Rust или другого компилируемого языка), который бы генерировал ассемблерный код с макросами.

Visual C++ генерирует ассемблерные листинги с макроконстантами. Приведу фрагмент:

_TEXT	SEGMENT
_hInstance$ = -8					; size = 4
?getInstance@@YAPAUHINSTANCE__@@XZ PROC			; getInstance, COMDAT
...
; 14   : 	HINSTANCE hInstance = GetModuleHandle(NULL);
	mov	esi, esp
	push	0
	call	DWORD PTR __imp__GetModuleHandleW@4
	cmp	esi, esp
	call	__RTC_CheckEsp
	mov	DWORD PTR _hInstance$[ebp], eax
; 15   : 	InterlockedExchangePointer(&s_hInstance, hInstance);
	mov	esi, esp
	mov	eax, DWORD PTR _hInstance$[ebp]
	push	eax
	push	OFFSET _s_hInstance
	call	DWORD PTR __imp__InterlockedExchange@8

Симкод не предназначен для написания ассемблерного кода, он нужен для максимального удобства чтения сгенерированного машинного/ассемблерного кода.

С моей точки зрения, это взаимоисключающие утверждения.
Мне трудно представить, как отказ от макроконстант может облегчить чтение листинга. Вы реально собираетесь в уме держать, какое численное значение что означает? Вот сравните: eax = 11045 или eax = $HEAVY_LASER_ID, что понятнее выглядит?

В 64-разрядной версии MASM поддержку встроенных макросов (.if, .while, invoke и пр.) вообще убрали.

Хоть я и говорил про макроконстанты, а не про .while, но и здесь выскажусь. Большой проект, скорее всего, будет содержать таблицы. Это могут быть, например, наборы поддерживаемых разрешений экрана, карты сообщений: событие - действие, и прочее. Особенность всего этого в том, что эти таблицы понадобиться вставить в нескольких разных местах кода, и согласовать. Например, для каждого разрешения экрана должно быть текстовое описание, чтобы пользователь мог выбрать это разрешение из меню, и, к примеру, своя процедура быстрой очистки заэкранного буфера. И нужно следить, чтобы одно другому соответствовало. Удобнее всего такие таблицы вставлять в код макроциклами. Приведу условный пример:

// Декларация таблицы разрешений экрана
#let resolutions = [{x: 640, y: 480} {x: 1024, y:768}]
...
string resolutionToString[#[resolutions.count]] = {
// Здесь r - курсор, который проходит по всем элементам таблицы resolutions
// Участок #delim ... #endfor выполняется для всех итераций цикла, кроме последней.
// В данном случае он просто выводит запятую между строками.
#for resolutions |> r: \" #[r.x] x #[r.y] \" #delim, #endfor
}
...
void clearOffscreen()
{
    switch(resolutionID)
    {
    #for resolutions |> r:
    case #[indexof(r)]:
        memzero(offscreenBuffer.dataPtr, #[r.x * r.y] * bytesPerPixel);
        break;
    #endfor
    }
}

В книге "Расширяемые программы" Горбунов-Посадов подробнее раскрывает тему о том, что препроцессор должен поддерживать таблицы и циклы. Правда, язык, который он в книге использует, больше академический, чем практический. Но суть от этого не меняется.

Visual C++ генерирует ассемблерные листинги с макроконстантами.

А, вы про именованные константы.
Я говорил про макросы в их традиционном понимании (макрос — символьное имя, заменяемое при обработке препроцессором на последовательность программных инструкций).

Насчёт конструкции eax = BLACK.
Сгенерированный компилятором симкод, скорее всего, будет выглядеть как
eax = 0 // BLACK если в этом месте можно использовать xor, либо как
eax = (0) // BLACK, если нужен именно mov (но такое бывает крайне редко).

UFO just landed and posted this here

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

Автор выше пишет

Симкод не предназначен для написания ассемблерного кода, он нужен для максимального удобства чтения сгенерированного машинного/ассемблерного кода.

И там методов адресации вроде побольше, чем в x86

Это тема именно для Blackfin, и да, Analog Devices сделали круто.

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

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

Forth-ассемблер рассматривали? Он, правда, несёт необходимость написания программы в стиле форта, но программа на нём читается отлично.

Предназначение симкоманд, помимо улучшения читаемости, заключается в более чётком обозначении назначения кода.

А чем не устраивает С, который делает тоже самое?

У вас процессор напрямую Сишный код исполняет?
Да, в процитированном вами предложении перед словом «кода» я не написал уточнение «ассемблерного», т.к. полагал, что из контекста это должно быть очевидно (т.к. статья посвящена языку ассемблера).
Вам разве не доводилось анализировать ассемблерный код, сгенерированный компилятором из программы на Си?
Вот для упрощения такого анализа и предназначен симкод.

У вас процессор напрямую Сишный код исполняет?

Так у вас процессор тоже не симкоманды выполняет ;) Если хочется писать на С - можно сразу писать на С. Говорят, что компиляторы сейчас умные, умеют в оптимизации.

Вам разве не доводилось анализировать ассемблерный код, сгенерированный компилятором из программы на Си?

Не доводилось. Но как тут помогут макросы?

Так у вас процессор тоже не симкоманды выполняет

Именно что симкоманды и исполняет. Можете проверить сами на web-версии консольной утилиты symasm — любой ассемблерной инструкции x86-64 соответствует симкоманда, а любой симкоманде соответствует одна или две ассемблерные инструкции (правда, пока что реализован перевод только в одну сторону).
Если вас смущает фраза «процессор исполняет симкоманды» (или, что то же самое, «процессор исполняет ассемблерные команды»), то, разумеется, это не буквально — процессор исполняет двоичный машинный код, но симкоманды являются прямым отображением этого машинного кода, так же, как и язык ассемблера.

А вот язык программирования Си прямым отображением машинного кода ни разу не является.

Удивительно, что приходится тратить время на разъяснение таких очевидных вещей.

Но как тут помогут макросы?

Ладно, я зачеркнул приставку «макро» в определении симкоманды в начале статьи, если она вас так сбивает с толку. Но продублирую информацию из всплывающей подсказки к слову «макрокоманда»:
Формально, макрокоманда — это мнемоническая команда, которая может разворачиваться более чем в одну машинную инструкцию. Однако, фактически, все симкоманды разворачиваются только в одну машинную инструкцию, за исключением лишь симкоманд условных переходов, которые могут разворачиваться в две машинные инструкции.

xor eax, eax
eax = 0

mov eax, 1
eax = 1

mov eax,0
eax = (0)

"Кон, агон, сол пишется с мягкий знак. Тарэлька, вилька - без мягкий знак. Понять нельзя, нужно запомнить" </а>

Спасибо, но если мне вдруг захочется сопоставить машинный код с операторами С - https://godbolt.org/

mov eax, 0

Данная ассемблерная команда практически не используется компиляторами (при включенных оптимизациях). Поэтому было логично поставить ей в соответствие более длинную симкоманду.

Спасибо, но если мне вдруг захочется сопоставить машинный код с операторами С - https://godbolt.org/

Можно пример?
godbolt.org сопоставляет Сишный код с машинным, а не наоборот.

А если нужно ковырять чужие бинарники без исходников, да после оптимизирующего компилятора.. С какой целью? Замена ассемблерных мнемоник неочевидными кодами никак не приблизит к восстановлению алгоритма. Есть ghidra например. Я бы с нее начал.

Так язык С это и есть своего рода надстройка над ассемблером, который не зависит от конкретного процессора и его набора команд. Что вы будете делать, когда потребуется использовать аппаратно зависимые ассемблерные команды (какие нибудь SIMD)?

А мне почему-то ассемблерный код выглядит более понятным, чем эти шифровки

Симкоманда — это символьная машинная макрокоманда с Си-подобным синтаксисом.

"Сим-команда" это по сути переименование мнемоники, замена буквенной мнемоники на "знаковую". "Макрокоманда" это более сложный инструмент.

Единственно где на мой взгляд оправдано переименование мнемоник, это команды сдвига/вращения. Они сложно воспринимаются, и кроме того у разных процессоров называются по разному. Например у Atmel8 LSR (Logical Shift Right) а в STM8 SRL (Shift Right Logical). Аналогично RRC и ROR. Здесь унификация конечно желательна.
Тем более что на "R" начинаются Right и Rotate, а на "L" Left и Logic. Например RLC Rotate Left а SRL Shift Right Logical. Не очень удобно читать мнемоники при такой неоднозначности.

Еще LD и MOV казалось можно было бы обозначать одной мнемоникой, так как суть одна. Компилятор разберется где что использовать.
Однако, в некоторых случаях результат выполнения LD может влиять на флаги, а результат MOV не влияет. То есть после LD в определенных случаях можно сразу проверить флаг Z, а после MOV в тех же условиях нужно выполнять команду влияющую на Z. Программист должен помнить про такие особенности, и разные мнемоники в этом помогают.

Идея в целом неплохая - унифицировать мнемоники разных семейств процессоров, и сблизить синтаксис ассемблера и Си. Но насколько это востребовано и оправдано, вопрос очень сложный. Программисту нужно будет залезть справочник чтобы найти инструкцию, (допустим add rax, rbx). А затем он встанет перед выбором - записывать "add rax, rbx" или записывать "rax += rbx". Разные варианты порождают лишние сомнения.
А потом этот код будет читать другой программист, и ругаться с разной громкостью, если у него окажется другой взгляд на синтаксис ассемблера.

ассемблерной команде add rax, rbx соответствует симкоманда rax += rbx

"=" здесь лишнее.
В Си "A += B" и "A + B" имеют разный смысл. Первое означает что нужно обязательно модифицировать A, а второе допускает выполнение сложения во временной переменной, например в стеке, без модификации A.
А в ассемблере rax будет модифицирован неизбежно. "rax += rbx" и "rax + rbx" для ассемблера это команда "add rax" без вариантов.
Получается что избыточное "=" добавляется только ради того чтобы не ломать привычку Си-шникам. Но программирование на ассемблере требует иного типа мышления. Иначе оно не будет эффективным.

Получается что избыточное "=" добавляется только ради того чтобы не ломать привычку Си-шникам.

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

eax ^= ecx
cl = 13
eax ~<<= cl // циклический сдвиг влево на cl бит

А программист это может переписать так:

eax = (eax ^ ecx) ~<< 13

Это уже не ассемблерный код, но вполне себе корректное выражение.

вполне себе корректное выражение.

И результат этого выражения будет.. ? ;)

Это вычисление хэша:

DWORD hashDword(DWORD hash, DWORD v)
{
	return rol(hash ^ v, 13);
}

Например, хэш от прямоугольника:

DWORD hashRect(DWORD hash, const RECT &rc)
{
	hash = hashDword(hash, rc.left);
	hash = hashDword(hash, rc.top);
	hash = hashDword(hash, rc.right);
	return hashDword(hash, rc.bottom);
}

Упсь, не досмотрел ;) Да, вы правы.

Но все равно eax ^= ecx не сильно информативнее xor eax, ecx. Даже наоборот, не то С не то ассемблер дезориентирует. Тогда как на выходе ghydra - почти привычный С код.

Но все равно eax ^= ecx не сильно информативнее xor eax, ecx

Для меня информативнее. Из-за того, что в отладчике Visual C++ приёмник операции xor слева, а в Code::Blocks справа. Когда переключаешься между ними - это несколько дезориентирует.

Так и не понял, чем "x+=y" принципиально лучше "add x,y". Не нравятся существующие мнемоники - делайте свой процессор с блекджеком и шлюхами с индексными регистрами и векторными командами, как Zilog в свое время. У них получилось.

Проблема всех этих симкодов состоит в том, что в отличии от ассемблерной мнемоники, они скрывают ряд нюансов происходящих "под капотом". Например, ассемблерная мнемоника как правило содержит указание на тип операндов (mulhsu - умножение знакового на беззнаковое) и метод адресации (add/addi - регистровый и непосредственный). В некоторых системах команд инструкции влияют на флаги. Если все эти подробности втащить в симкод, то он станет абсолютно нечитаем и потеряет всякий смысл. А без этих подробностей симкод не адекватен и может легко ввести в заблуждение. Короче, дети, учите в школе ассемблер, желательно RISC-V. :-)

Подобная система была реализована в Asm80 Медноногова, году так в 1995-97. Я одно время очень активно ей пользовался, нравилось. К тому же там можно было записывать серии этих самых команд (и любых) через двоеточие, целый кусок кода в одну строку. Но со временем всё же отказался в пользу классических мнемоник.

Читаемость у этих альтернативных конструкций похуже, и проблема совместимости с copy/paste существующих решений, приходится читать два разных синтаксиса одновременно, это лишняя нагрузка на голову, плюс передать другим людям кусок своего кода становится проблемой - они же спрашивают «а что это такое»?

search LLVM: 0

search SSA: 0

берем данный опус про изобретение С-- лохматых годов , сворачиваем трубочкой, и отправляем по назначению

Для советской управляющей мини-ЭВМ СМ-2М в СредьМаше (атомная энергетика) был разработан язык SPL (System Programming Language), который был близок по духу к симкоду - этакий ассемблер высокого уроаня. Я писал на нем иногда быстрее, чем на С. И уж точно программы на нем исполнялись быстрее (компиляторы С или Фортрана были не очень умные). К большому удивлению я не смог найти ничего про этот язык в инете. Есть только некий SPL для мини-ЭВМ HP/3000, но это совсем другой язык. Автор языка работал тоже в СредьМаше. Вторая версия языка была просто шикарная - с корутинами и встроенным лексическим анализатором.

Да, да ... Только в последнй версии был добавлен "лексический препроцессор", а не лексический анализатор (который выполняет первый проход при трансляции). Это был машинно-ориентированный язык высокого уровня: все стандартные операторы для высокого уровня (циклы, условное выполнение, селекторы и т.п.), а также можно было использовать ассемблерные команды. На архитектуре без стека корутины до этого можно было писать только на ассемблере и очень скрупулёзно. SPL не был гостирован, поэтому многие для официальных "релизов" писали корутины (там они назывались реентерабельные подпрограммы) на SPL, затем дезассемблированный код втыкали в техническую документацию проекта))). По оценкам разработчиков, эффективность кода по памяти составляла 98-99 % от того, если бы этотже код писали на ассемблере - там была глубокая оптимизация кода. Кому интересна документация на это моё детище, могу выслать PDF (один добрый человек мне сделал :-) )

Рекомендую ознакомиться с С--, идея его создания была плюс минус такой же

Нет. C-- это другое. В чём отличие я уже достаточно подробно написал здесь.

Да, но нет. С-- изначально создавался как промежуточный этап для транслятора Хаскеля. Собственно это написано в Википедии

Мне идея понравилась, но и критику я понимаю.

Первое, проблема "еще" одного стандарта. Те, кто уже годами пишет/читает ассемблер, скажут, что не нужно. Типа еще чего учить тут?

Второе, грамматика против словарного запаса. "Грамматика" прежних видов записи ассемблера предельно проста: название инструкции и в каком-то порядке. Остальное - словарный запас в виде запоминания названий инструкций и того, что они творят. Здесь фокус смещается в сторону расширения грамматики, новые операторы и последовательности символов. Главное не перестараться, а то будет как в некоторых языках программирования: способов десять как сделать одно и то же, пять из которых отродясь не видывал и пришлось искать, что за синтаксис такой. Это влияет на погружение в язык, на сложность.

Третье, все почему-то подразумевают базовый текстовый редактор при работе с этим. А что, если подсказка к каждой строке читаемого ассемблера будет на Симкоде? Или наоборот? Можно же его будет использовать только для прочтения, а не написания.

Далее по идее расширения интерактивных подсказок редактора, почему не развить эту идею дальше? Автор уже думает об организованной документации для подсказок по ассемблерным инструкциям. Это может быть функциональной надстройкой над обычным ассемблерным кодом. Типа LaTeX, но для ассемблера. На Симкоде синтаксический сахар (условно), ручные на/переименования регистров в текущем scope (labels), рядом транслированный "голый" ассемблер.

Еще раз, задумка понравилось, но всё упрется в первый и второй пункт из-за "глаз привык, рука набита", т.е. usability должно кардинально улучшиться.

А что, если подсказка к каждой строке читаемого ассемблера будет на Симкоде?

Уже реализовано в консольной утилите symasm посредством режима --annotate.
Хотя я об этом уже писал в статье, но приведу результат работы этого режима [применительно к примеру с setg/cmovl из статьи] для наглядности:

xor     eax, eax ; eax = 0
cmp     edi, esi ; edi <=> esi
mov     edx, -1  ; edx = -1
setg    al       ; al = 1 if > else 0
cmovl   eax, edx ; eax = edx if <
ret

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

xor eax, eax ; тут зануляется [eax = 0]

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

Учитывая способности редакторов, такой как бы inline текст, наверно, без костылей возможен только в VSCodium. Intellij видел до сих пор только надстрочные аннотации, считаю их недостаточными как средство.

Повторюсь, допустить смену режимов с первичный ассемблер, а вторичный Симкод или наоборот.

Вышенаписанное деградирует идею Симкода до плагина-надстройки IDE вместо самостоятельного языка выражения ассемблера? Не обязательно, но думаю такой путь развития вероятнее выстрелит. Тем более такое сопоставление старое<>новое посодействует беспрепятственному обучению синтаксису.

Как по мне, стало менее читаемо, ибо нагромождение разношёрстных конструкций выглядит хуже и менее интуитивно, чем ровные ряды мнемоник. Да и для других архитектур, отличных от ARM и x86, его будет сложно адаптировать.

А похожую идею уже давно реализовал Wiz, который конкретно заточен под хомубрю-разработки для ретро-консолей, хоть он и будет несколько повыше уровнем, чем Ваш симкод.

Для консолей была ещё попытка высокоуровнего ассемблера NESHLA, на которой разрабатывалась изначальная версия игры Grand Theftendo.

rcx < 0 : palindrome_end

Давайте упрощу синтаксис:

goto palindrome_end when rcx < 0

Впрочем, LLVM IR выглядит наглядее (пример из документации):

define i32 @max(i32 %a, i32 %b) {
entry:
  %retval = alloca i32, align 4
  %0 = icmp sgt i32 %a, %b
  br i1 %0, label %btrue, label %bfalse

btrue:                                      ; preds = %2
  store i32 %a, i32* %retval, align 4
  br label %end

bfalse:                                     ; preds = %2
  store i32 %b, i32* %retval, align 4
  br label %end

end:                                     ; preds = %btrue, %bfalse
  %1 = load i32, i32* %retval, align 4
  ret i32 %1
}

Было много лет назад, не прижилось.

Только заточенную исключительно на х64/86 в отличие от. А сейчас в мире уже преобладают арм устройства

А напомните что стало с C-- есть ли шансы его портировать с i86 вверх хотя бы до i386? В целом Сишный синтаксис функций и Интеловая ассемблерное тело содержания, а так же удобство в упаковке и распаковке параметров и результата. Вот мечта, но с другой стороны это можно сделать и на обычном Си.

Есть исходники компилятора, в проекте КолибриОс

Это не более чем синтаксический сахар. Никакой читаемости он не повышает. Про читаемость есть например тот же masm где есть упрощённые конструкции объявления и вызова процедур, работы со стековыми переменными, ветвления и пр. Такое не только у masm есть. И это действительно повышает читаемость. А это не более чем рюшечки. Красиво, радует глаз, но бесполезно. Т.е. не несёт никаких функциональных улучшений.

Никакой читаемости он не повышает.

Вам вообще приходилось читать ассемблерный код? (Я уж не говорю про «писать».)
Или это так, рассуждения из воздуха?

Если приходилось, то будет очень интересно услышать. Именно про ваш опыт. А не просто мнение. Т.к. в русскоязычной среде специалистов по языку ассемблера (не по интринсикам, а именно по машинным командам) буквально единицы, и на Хабре они, увы, не пишут.

Если вам хоть немного интересно программирование на ассемблере, тогда вы должны были читать вот это:
Understanding Windows x64 Assembly

    cvtsi2sd    xmm8, r8        ; convert n from int to double and store in xmm8
    ...
    vmulsd    xmm3, xmm3, xmm8  ; xmm3 = n * sumYY
    ...
    vsubsd    xmm2, xmm2, xmm6  ; xmm2 = (n * sumXX) - (sumX * sumX)
    ...
    vextractf128    xmm6, ymm4, 1 ; xmm6 = lower 2 doubles of sumXY

И как думаете, комментарии к каждой строке ассемблерного кода там, они для чего вообще? Так, просто по приколу? Или же всё-таки для улучшения читаемости/понимаемости кода.

Вы вообще представляете, что такое современный x86-64 ассемблерный код?
Посмотрите для общего развития на этот список команд. Одних только команд конвертации (у которых мнемоники начинаются на cvt или vcvt) — 82 штуки! И отличаются они между собой порой всего на одну букву. Если ассемблерный код не содержит подсказок/комментариев к каждой строке, то найти в нём ошибку практически невозможно. Поэтому и приходится изобретать язык для таких комментариев. И симкод является попыткой стандартизации такого языка.

Про читаемость есть например тот же masm где есть упрощённые конструкции объявления и вызова процедур, работы со стековыми переменными, ветвления и пр.

Уже нету.
В 64-разрядной версии MASM поддержку встроенных макросов (.if, .while, invoke и пр.) убрали.

Т.к ни один компилятор (C++, Rust или другого компилируемого языка) не генерирует ассемблерный код с макросами. А руками ассемблерный код уже давно не пишут. Поэтому макросы и убрали в MASM64.

Эти идеи развиты в fasm

Sign up to leave a comment.

Articles