Как стать автором
Обновить

Комментарии 86

в 23-м году писать 32-битную прогу, да без юникода...

И только под x86...

я не против написания кода на ассемблере, всякое бывает. просто человек потратил немало времени на написание статьи, которая фактически уже устарела.

просто человек потратил немало времени на написание статьи, которая фактически уже устарела

А почему устарела? Да, в реальной жизни вам скорее всего не придётся писать пятнашки на ассемблере под винду, но тем не менее, и сейчас есть немало ИТшных направлений, где хотя бы базовое понимание ассемблера поможет решать сложные проблемы. Не все же на тайпскрипте и пайтоне пишут :)

базовое понимание ассемблера

Это действительно полезно. Но статья о конкретике кодинга в VS2019 под 32-бинтую винду, а по работе может понадобиться сделать, скажем, маленькую вставку в gcc на Aarch64 )

Так а в чем проблема? Базовые знания общие, как раз асм проще учить на старом добром x86 cisc

В статье больше про IDE и работу с Win32 API. Реально на ассемблере сейчас скорее пишется самодостаточный код, который ничего не вызывает. И чтобы поиграться - на мой взгляд проще скомпилировать сишный код через gcc -S и дальше модифицировать сгенерённый ассемблер.

Полезно знать про регистры, calling convention, режимы адресации, атомики и разные подходы к ним на разных архитектурах, SIMD - но ничего этого нет.

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

Спасибо, буду знать))

То, что тут показано, это, конечно, не bleeding edge, но совсем устарелым я б не назвал. Если бы я написал что-нибудь про TASM, far/near и прочий int 21h, тогда да. Про юникод отступление я сделал в статье, наверное надо было подробнее написать, но объем статьи и так вышел намного больше изначального замысла, чем-то пришлось пожертвовать. Но если кратко, то использование юникода ничего принципиально не меняет.

MOP_NABLA                   EQU 2207h
MOP_DOT                     EQU 22c5h
CAP_BETA                    EQU 0392h

englishCaption              db "Hello, world", 0
russianCaption              dw 0041fh, 00440h, 00438h, 00432h, 00435h, 00442h, 0002ch, 00020h, 0043ch, 00438h, 00440h, 0; Привет, мир
maxwellEquationText         dw MOP_NABLA,MOP_DOT,CAP_BETA," ", "=", " ","0",0
maxwellCaption              dw "M", "A", "X", "W", "E", "L", "L", 0

Вот и вся разница. Как именно конвертировать строки, вот на эту тему можно написать статью.

Почему использовал 32-х разрядный ассемблер. Одной из целей было показать как использование директив MASM упрощает жизнь. А в 64-х разрядном MASM нету ни invoke, ни if, ни while. Вообще говоря это проблема решаемая, но это опять же можно целую статью написать только про это. Также calling convention изменился, нужны еще определенные приседания с прологом/эпилогом. Тут опять для простоты пришлось срезать углы. Я хотел показать как можно писать обычные windows-приложения без всяких лишних приседаний, установок и прочего и делать это в привычной многим IDE.

об этом я и говорил. захочет кто-нибудь на основании этой статьи написать полезный код под актуальную конфигурацию железо/ПО, и не сможет это сделать, потому что в masm не директив (чем fasm не угодил?), эпилог как писать не знает, параметры в функции передаются по-другому, и т.д. да, будет сложнее, но люди сразу будут понимать объем работы, который им придется проделать.

Почему не взять тогда fasm? Тот имеет возможность писать platform agnostic код и имеет кучку пахожих плюшек. Ещё и строки паскалевы.

Ассемблеров много есть разных, это один из вариантов. Здесь фишка в том, что используется привычная IDE. С привычным дебагом в том числе. И все это идет из коробки и работает без проблем.

А если взять фасм, там с одним редактором приключений хватит. Фасмовский родной, ну это такое. В masm sdk куча примеров, причем нормально оформленных, а в фасме чуть больше десятка.

Разумеется и на фасме кто-то пишет, но статья не про фасм и не про ассемблер вообще, а про конкретный вариант использования.

Обратная совместимость AMD64 <- IA32? Нет, не слышали.

раньше винда могла запускать 16-битные проги. и где теперь эта совместимость?

Ну, это и в XP достигалось работой через *vdm (в т.ч. и работа dos приложений).

И просто по теме: для запуска 16-битных на текущих системах можно установить winevdm, если сильно надо :) https://github.com/otya128/winevdm Sid Meier’s Colonization недавно так устанавливал поиграть.

Ого, спасибо, даже и не думал что так ещё можно, потому и не искал.

Спасибо! Я занимался настройками IDE где-то в начале лета, тогда еще не было.

MASM лже пророк, только в ARM наше спасение.

Спасибо за статью! В обучающих целях - самое то. Для своих студентов обязательно возьму кое-какие инфы из вашей статьи. В ассемблере в 90-х писал, но не под Виндовсом. С интересом открыл для себя передачу параметров для вызываемой функции через стек. Никогда об этом раньше не думал. Только через регистры, а через стек удобнее, хоть и с риском для ошибок.

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

Как раз через стек, это классика. А через регистры, это уже оптимизация.

Как раз через стек, это классика. А через регистры, это уже оптимизация.

Может быть вы и правы. Всё таки почти 30 лет прошло. Видать настолько всё забыл, что воспринимаю как новую вещь.

Сейчас скорее регистры (когда хватает) - стандарт, а стек - legacy на древних платформах типа 32-битного Интела )

Любой внешний API — только через стек. Включая тот же WinAPI.

Это справедливо для WinAPI x86, а в WinAPI x86-64 немного по другому:


The Microsoft x64 calling convention is followed on Windows and pre-boot UEFI (for long mode on x86-64). The first four arguments are placed onto the registers. That means RCX, RDX, R8, R9 (in that order) for integer, struct or pointer arguments, and XMM0, XMM1, XMM2, XMM3 for floating point arguments. Additional arguments are pushed onto the stack (right to left). Integer return values (similar to x86) are returned in RAX if 64 bits or less. Floating point return values are returned in XMM0. Parameters less than 64 bits long are not zero extended; the high bits are not zeroed.

Под досом в реальном режиме всегда через стек было ?

mov ah,4ch
mov al,[exitCode]
int 21h


Это разве через стек? Да, дос, да, реальный режим.

Думаю, имелся в виду вызов обычной функции (call), а не прерывание

Это сисколл, не вызов функции.

Обычно делали примерно так:

lea dx, msg
push dx
push 0
call printMessage

А внутри printMessage уже форматирование и вызов инт сисколла с передачей через AX как вы описали.

В досе? Нет, в досе, да и в биосе так никогда не делали если программа писалась именно на ассемблере, а не на каком-нибудь языке высокого уровня. Языки высокого уровня передавали через стек, да, а на ассемблере это выглядело бы как-то так:


mov si, offset msg
call printMessage
...
printMessage:
cld
loop:
lodsb
test al,al
jz exitLoop
call printChar
jmp loop
exitLoop:
ret
...
printChar:
mov ah,0eh
int 10h
ret


То есть всё через регистры. Вот если регистров нехватало, тогда да, передавали через стек или через какие-нибудь локальные переменные.


Я с 1987 года по 1993 год программировал преимущественно на ассемблере и исключительно под дос, так что знаю о чём говорю.

В универе нас учили именно через стек передавать. Передача через регистры - это уже более новое и идет рука об руку с flat моделью, 32 битным защищенным режимом и виндой.

Ну и писали пару лет мы портянки со стенами пушей, после чего считали смещение и делали ret N

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

Честно скажу, не знаю как и чему сейчас учат в универе :-) Я получал свою практику программирования на ассемблере в своё время путём изучения дизассемблированых BIOS, DOS и первых вирусов для DOS, и везде там передача была именно через регистры. Через стек параметры передавались только в языках высокого уровня и много позже когда появились первые Windows (ещё не как самостоятельные OS, а как оболочки для DOS) и соответственно WinAPI. Так что я перенял способ передачи параметров через регистры из "взрослых" программ и меня удивляло и поражало в первых Windows — зачем передавать параметры через стек, когда всю жизнь передавали через регистры? Ведь когда пишешь на ассемблере передача параметров через регистры и быстрее, и удобнее! Только после дошло что это для совместимости с HLL того времени в которых никаких __fastcall в то время ещё не было, всё шло тупо через стек, в C справа налево и с очисткой стека после вызывателем, а в паскале слева направо и с очисткой стека вызываемым.


В середине нулевых да, передача параметров была уже преимущественно через стек, на flat модели, 32 битах и винде. А на голом DOS, на голом ассемблере параметры старались по возможности всегда передавать в регистрах.

https://en.wikipedia.org/wiki/X86_calling_conventions#List_of_x86_calling_conventions

даже для 8086 есть варианты с передачей через регистры, но это лишь некие соглашения.

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

FASM гораздо лучше, и главное IDE монструозную не надо ставить.

Ассемблеров много хороших и всяких разных, тут, конечно, частный случай.

Кстати говоря, что используете в качестве IDE для FASM? Раньше там в комплекте шло что-то типа блокнота, но это была жуть.

Интересно бы сравнить размер проги на ASM и на С. Сдаётся мне, что будет одинаковый.

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

НЛО прилетело и опубликовало эту надпись здесь

Это хорошо пишется словами, но трудно делается в реале. Одни графики в реальном времени чего стоят.

Что трудно делается в реале? Вызовы Win32 API? Это элементарно. Другое дело - что вызывать и как вызывать. Плюс было задействовано много недокументированных секретов. За что и ценили.

Графики в реальном времени что? Я сам студентом во времена когда ещё назывались ntinternals рисовал график скорости передачи данных по COM-порту.

Все сами рисовали графики. Я про то, что там очень много функционала для такого маленького размера утилит. И далеко не только вызовы WinAPI.

Да нет, бинарный код непосредственно функционала никогда много не занимал. Экзешник всегда раздували сторонние либы, графические ресурсы (один БМПшник несжатый для красивой кнопки чего мог стоить). И даже если поиграться просто с выравниванием порой можно было много сэкономить. Типичному разрабу всё это нафиг не нужно было конечно. Если только целенаправленно заниматься, со знанием дела.

Через студийный менеджер расширений недоступно, видимо для 2022 только руками. Надо поразбираться.

Спасибо огромное! Шикарная статья! Последний раз на языке ассемблера писал лет двадцать назад, но самые мои приятные воспоминания от профессии связаны именно с ним.

НЛО прилетело и опубликовало эту надпись здесь

Если пользоваться микрософтовскими тулами - то отдельный исходник на MASM, реализующий вызываемые из C/C++ функции; насколько помню, поддержку инлайн ассемблера при компиляции под Win64 убрали. В gcc/clang можно и инлайн вставки делать.

Ну если в самом простейшем случае, то через DLL. Надо знать соглашение о вызовах, чтобы передать параметры, разумеется.

минимальный C++ будет где-то так:

#include <print>

#pragma comment( lib, "ASM" )
extern "C" int fnAsm(int a, int b);

int main(int argc, char* argv[])
{
	int res = fnAsm(2, 3);
	std::println("2 + 3 = {}", res);
}

А код на ассемблере для библиотеки ASM.dll примерно такой:

EXPORT fnAsm
fnAsm PROC ; Calculate a + b
	mov eax, ecx ;eax = a
	add eax, edx ;eax = a + b
	ret 	     ;return result to caller
ENDP fnAsm

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

Ещё есть книжка - Даниэль Куссвюрм - Профессиональное программирование на ассемблере x64 с расширениями AVX, AVX2 и AVX-512:

Там не всё идеально, но для начала - очень неплохо. В принципе можно и не покупать, а просто примерами с гитхаба обойтись.

А если писать на Делфи, будет считаться?)

В начале 90х многие знакомые писали под ДОС на Турбо Паскале в таком стиле )

Можно ещё вспомнить библиотеку Turbo Vision, которая оборачивала вызовы к Windows 16-bit.

Можно было получить 16-битные программы с окошком для Windows 1 - Windows 3.11, но из-за обратной совместимости запускались и в 32-битных Windows и выглядели как родные.

А, память подводит.

Смешались вспоминания про Turbo Pascal for Windows и Turbo Assembler, и почему-то в памяти всплыл Turbo Vision

пральна, эта библиотека называется не TV, а VCL, и дожила она, если не ошибаюсь, до наших дней.

Неа, эта библиотека называлась OWL или как-то так. Прожила она несколько лет вместе с Borland Pascal & Borland C++ for Windows, и ушла в небытие, когда появились Delphi и чуть позже C++ Builder с их VCL

Спасибо за интересную статью. +1 статье, +1 звезда проекту на гитхабе

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

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

Практический кейс — вычисления повышенной точности на FPU, например.

x87 в 21ом веке? seriously? Подозреваю, что хорошая ручная реализация 128битных FP на AVX 512 будет побыстрее.

Да, серьёзно, и статью вы очевидно не читали. Речь идёт именно о 80-битной точности, которая в SSE/AVX недоступна, и об интегральных/итеративных вычислениях, приводящих к катастрофическому накоплению ошибок.

Я понял, и я именно о 128 бит на одно число.

Я сравнивал скорость арифметических операций SSE и FPU. Оно одинаковое. Ускорение SSE/AVX достигается исключительно за счёт 2/4 вычисления за одну команду (если такое явно прописано и позволяет алгоритм) и (в некоторых случаях) выполнения на разных конвейерах.

Любой серьёзный алгоритм работает с большими векторами/матрицами и ускорение от параллелизации не помешает.

и (в некоторых случаях) выполнения на разных конвейерах.

Ну да, соответственно в AVX512 у нас два векторных FMA за такт (на нормальном серверном процессоре), а в x87 - только отдельные скалярные сложения/умножения по одному за такт.

У FMA эффективная область применения тоже ограничена. Для свёртки (небольших размеров) — да, подходит идеально. Ну так для свёртки я его и буду использовать.

Подозреваю, что хорошая ручная реализация 128битных FP на AVX 512 будет побыстрее
Подозреваю обратное, потому что опыт подобного тоже имеется, только на SSE без AVX. Но если у вас есть что-то более конкретное — конечно же с удовольствием обменяю 80-битную точность на более быструю 128-битную с не меньшим диапазоном значений.

Сам не работал, общался с человеком, который подобным занимался (оно и в обратную сторону работает - в каких то случаях можно вместо обычного дабла использовать два сингла для улучшения скорости при похожей точности). Вот тут - https://arxiv.org/abs/2303.04353 - пишут, как использовать два дабла для улучшения точности (эффективно получается 106 бит на мантиссу, что всё равно лучше, чем extended double на x87).

Вот именно это я и программировал. Спойлер: а) FPU значительно быстрее, б) увеличивается только точность, диапазон значений остаётся тем же, в) моя реализация на ассемблере с использованием как FPU, так и SSE оказалась в 10 раз быстрее, чем реализация на си у этих товарищей (а других, более низкоуровневых, не попадалось).

Не уверен, что они в коде на C++ смогли эффективно запользовать векторные инструкции.

А они и не должны были, компилятор должен был. И сама логика алгоритма к векторизации тоже не располагает.

Автовекторизация - дело тонкое, работает не всегда. Собственно, для этого часто и нужен ассемблер в наше время.

И сама логика алгоритма к векторизации тоже не располагает.

То есть обработать параллельно несколько FP64x2 чисел не получается?

Где-то получится, где-то нет. Я же писал, что и то и то использую. Вот мой код для сложения:
спойлер
ddasm_add_dd_dd PROC uses esi edi dd1:PTR DWORD, dd2:PTR DWORD, dd3:PTR DWORD;[+]
local s1s:XMMWORD, s2s:XMMWORD

mov esi, dd1
mov edi, dd2
mov eax, dd3

movupd xmm0, XMMWORD PTR [esi];=a
movupd xmm1, XMMWORD PTR [edi];=b

movapd xmm3, xmm0;=sum
addpd xmm3, xmm1
movapd xmm4, xmm3;=bv
subpd xmm4, xmm0
movapd xmm5, xmm3;=av
subpd xmm5, xmm4
subpd xmm1,xmm4;=br
subpd xmm0,xmm5;=ar
addpd xmm0, xmm1;=err

movupd XMMWORD PTR s1s, xmm3
movupd XMMWORD PTR s2s, xmm0

fld REAL8 PTR s1s
fadd REAL8 PTR s1s+8
fadd REAL8 PTR s2s
fadd REAL8 PTR s2s+8
fstp REAL8 PTR [eax]

fld REAL8 PTR [eax]
fsubr REAL8 PTR s1s
fadd REAL8 PTR s1s+8
fadd REAL8 PTR s2s
fadd REAL8 PTR s2s+8
fstp REAL8 PTR [eax+8]

ret
ddasm_add_dd_dd ENDP

А почему хотя бы AVX2 не используете?

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

Потому что он тут и незачем

Если всё таки нужно не несколько чисел поскладывать, а массивчик - должно помочь.

и на моём домашнем нетбуке отсутствует.

Ради искусства конечно интересно, но для HPC задач платформа так себе )

Ну не выкидывать же рабочую железяку. С ролью плеера и DSP-процессора справляется прекрасно, не шумит и не греется.

Это понятно, для дома сгодится - у меня до мая этого года основной домашней машинкой был Asus 1015PEM. Просто если говорить о реальных промышленных расчётах (когда матрицы могут и в оперативке на одной ноде не помещаться) - то стоит всё таки использовать серверную машинку с полноценным AVX512, и тогда соотношение x87 vs vector instructions будет другим.

В реальных и тяжёлых промышленных расчётах имеет смысл использовать GPU, а не CPU тогда уж.

Не все алгоритмы ложатся на GPU - и как раз реализовывать кастомные вещи типа той же арифметики повышенной точности там сложнее. Ну и в целом CPU не так уж отстают - на втором месте в Top500 до сих пор Fugaku вообще без акселераторов, просто Arm c SVE. Но согласен, во многих случаях GPU экономически оправданнее.

Запользовать, скажем, AMX для своего алгоритма, не покрытого MKL.

Ну неплохо. Я вброшу пару советов:

  1. Искать клетку, в которую кликнули мышкой, путём вычисления координат прямоугольников для клеток в цикле - сойдёт для начала, но как-то неэлегантно, что ли. Я бы сделал наоборот, преобразованием в клеточные координаты, и потом вычислнением индекса клетки.

  2. В коде совместно (вперемешку) используются макросы .IF и инструкции cmp, выглядит странно. Вы в начале статьи задаётесь вопросом: зачем писать на ассемблере, и даёте на него риторические ответы. Мой ответ другой: иногда нужно хакнуть или отреверсить программу, и для этого приходится читать дизассемблированный код, и чтобы его понимать, нужно натренировать глаз читать ассемблер. Для такой тренировки лучше не пользоваться макросами.

  3. Я поглядел код на гитхабе, есть странности. В процедуре отрисовки зачем-то два цикла по клеткам, достаточно было бы одного. И точно не нужно для каждой клетки вызывать CreateFontIndirectA с последующим DeleteObject, шрифт лучше проинициализровать на старте программы. С brush-ами я бы тоже посоветовал так же поступить.

  4. Процедура CalculateTileRect сделана не очень. Я бы из CalculateTileRectPos убрал бы второй параметр additionalTileSize. Тогда из CalculateTileRect можно будет сделать только два вызова CalculateTileRect, а не четыре, как сейчас, и добавлять вот этот additionalTileSize только для нижней и правой граней.

С советами соглашусь, код особо не вылизывал, тем более что на ассемблере что-то писал больше 10 лет назад, да и то для развлечения. Там в паре мест регистры портятся в процедурах, массив для тайлов было бы проще сделать из двойных слов вместо байт, проталкивания hwnd по всем процедурам можно избежать, именование не причесано к единому виду, всякие about-окна сделаны через MessageBox, а не полноценные окна. Для улучшений место есть.

Момент со смешиванием .IF и cmp есть, я прямо долго думал и никак не мог решить в каком стиле все это делать. С одной стороны я хотел показать как масмовские директивы упрощают жизнь, с другой стороны если все писать с их помощью, как наглядно показать разницу? Поэтому кое-где осталось в смешаном стиле.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации