Создаю простейшую программу, выводящую на экран пунктирную стрелку. Код (ASM): ; GUI # include win64a.inc UnitWorld equ 0 LineCapRoundAnchor equ 12h LineCapArrowAnchor equ 14h .code WinMain proc local msg:MSG ;инициализация библиотеки GDIPLUS invoke GdiplusStartup,&gdiplusToken,&gsi,0 xor ebx,ebx mov esi,IMAGE_BASE invoke LoadCursorFromFileA,"br_Rabbit3.cur" mov edi,offset ClassName push rax ;hIconSm push rdi ;lpszClassName push rbx ;lpszMenuName push COLOR_WINDOW ;hbrBackground push rax ;hCursor push rax ;hIcon push rsi ;hInstance push rbx ;cbClsExtra & cbWndExtra pushaddr WndProc ;lpfnWndProc push sizeof WNDCLASSEX;cbSize & style invoke RegisterClassExA,esp ;addr WNDCLASSEX push rbx push rsi ;rsi=400000h shl esi,9 ;rsi=CW_USEDEFAULT push rbx push rbx push 100 push 240 push rsi push rsi sub esp,20h invoke CreateWindowExA,0,edi,edi,WS_OVERLAPPEDWINDOW or WS_VISIBLE lea edi,msg @@: invoke GetMessageA,edi,NULL,0,0 invoke DispatchMessageA,edi jmp @b WinMain endp WndProc proc hWnd:QWORD,uMsg:QWORD,wParam:QWORD,lParam:QWORD mov hWnd,rcx cmp edx,WM_DESTROY je wmDESTROY cmp edx,WM_CREATE je wmCREATE cmp edx,WM_PAINT je wmPAINT leave jmp NtdllDefWindowProc_ wmDESTROY:;уничтожить перо invoke GdipDeletePen,hPen ;уничтожить объект GDI+ invoke GdipDeleteGraphics,graphics mov ecx,gdiplusToken invoke GdiplusShutdown invoke RtlExitUserProcess,NULL ;------------------------------------------------------------ wmPAINT:;Прерывистая линия mov r9d,20 mov [rsp+28h],r9 mov qword ptr[rsp+20h],200 invoke GdipDrawLineI,graphics,hPen0,50 jmp wmBYE ;------------------------------------------------------------ wmCREATE:invoke GdipCreateFromHWND,hWnd,&graphics movd xmm1,const8 invoke GdipCreatePen1,0FFFF0000h,,UnitWorld,&hPen invoke GdipSetPenStartCap,hPen,LineCapRoundAnchor invoke GdipSetPenEndCap,hPen,LineCapArrowAnchor invoke GdipSetPenDashArray,hPen,&dash,2 wmBYE: leave ret WndProc endp ;--------------------------------------- .data ClassName db 'GDI+ рисование линий',0 gsi GdiplusStartupInput <1,0,0,0,0> const8 dd 8.0 dash dd 1.0,0.5 .data? graphics dq ? hPen dq ? gdiplusToken dd ? hDC dq ? end Если кому-то не нравится оформление программы, пожалуйста напишите почему. А теперь попробую объяснить каждую строку. Перед тем как использовать GDI+, необходимо инициализировать библиотеку gdiplus.dll для этого используется функция GdiplusStartup, 1-ый параметр -- адрес двойного слова, куда будет помещен маркер gdiplusToken, который затем следует использовать при закрытии библиотеки функцией GdiplusShutdown. Создаю в стеке структуру типа WNDCLASSEX и передаю адрес вершины стека функции RegisterClassEx. Четыре первые параметра передаю через регистры RCX, RDX, R8, R9 для функции CreateWindowEx, остальные параметры через push YY, хотя можно и через mov [rsp+XXh],YY но так нагляднее и меньше писанины Функции WinAPI не изменяют значения в регистрах RSP, RBP, RSI, RDI, RBX, R12-R15, XMM6-XMM15. Поэтому я использую регистр RBX чтобы передать нулевые значения функциям RegisterClassEx и CreateWindowEx, а регистр RDI сперва, чтобы передать функциям RegisterClassEx и CreateWindowEx адрес класса окна, а имя класса у меня совпадает с именем окна, а затем через RDI передаю адрес msg функциям GetMessage и DispatchMessage. Это позволяет использовать 1-байтную команду push RBX вместо 2-байтной push 0 и 1-байтную команду push RDI вместо 5-байтной push адрес. Для получения дескриптора экземпляра приложения не вызываю функцию GetModuleHandle, так как при создании приложения используется ключ /BASE:0x400000 и поэтому всегда hInstance=400000h, это значение помещается в RSI mov esi,IMAGE_BASE дважды использую push RSI для RegisterClassEx и CreateWindowEx, а когда для CreateWindowEx требуется CW_USEDEFAULT=80000000h сдвигаю содержимое RSI на 9 разрядов влево (400000h×29=80000000h). При получении сообщения о закрытии окна не отправляю через PostQuitMessage сообщение WM_QUIT в очередь сообщений потока, чтобы GetMessage разорвала цикл, а через RtlExitUserProcess убиваю приложение. В приложении обрабатывается три сообщения WM_DESTROY, WM_CREATE и WM_PAINT: После получения WM_CREATE создаем объект GDI+ graphics для вывода графической информации и связываем его с hWnd функцией GdipCreateFromHWND, далее функцией GdipCreatePen1 создается перо hPen красного цвета, толщиной 8 пикселей, один конец линии будет в виде окружности (GdipSetPenStartCap(hPen, LineCapRoundAnchor)), другой в виде стрелки (GdipSetPenEndCap(hPen, LineCapArrowAnchor)), сама линия будет штриховой (GdipSetPenDashArray(hPen, &dash, 2)), длина штриха 1×8=8 пикселей, расстояние между штрихами 0,5×8=4 пикселя (dash dd 1.0,0.5) После получения WM_PAINT рисуем линию GdipDrawLineI(graphics, hPen) с координатами x1=50 y1=20 x2=200 y2=20 При получении WM_DESTROY уничтожаем перо GdipDeletePen, уничтожаем объект GDI+ GdipDeleteGraphics, закрываем библиотеку gdiplus.dll функцией GdiplusShutdown, завершаем приложение RtlExitUserProcess Если пришедшее сообщение не WM_DESTROY, не WM_CREATE и не WM_PAINT, тогда произойдет не call NtdllDefWindowProc_ (DefWindowProc) с теми же параметрами, что и для WndProc с последующими leave и ret, а просто jmp NtdllDefWindowProc_ Пока разбирался с lib- и inc-файлами узнал, что ExitProcess в kernel32.dll это обертка для RtlExitUserProcess из ntdll.dll, а DefWindowProc в user32.dll обертка для NtdllDefWindowProc_ из ntdll.dll, поэтому решил обойтись без посредников... Вроде бы, ничего не упустил, а если непонятно ― спрашивайте
Mikl___, а в чём концепция этих туториалов? Это больше туториал по GDI+ или по ассемблеру? Если больше по GDI+, то ассемблер - совсем не лучший выбор, поскольку задача должна быть максимально просто донести материал именно по API. Те, кто собирается писать на ассемблере, и так знают, как раскладывать регистры и организовывать функции - им важнее концепция: какие функции в каком порядке вызывать и что передавать в аргументы, а ассемблерную реализацию они смогут написать и сами. Ассемблер сильно усложняет восприятие, поскольку в примерах вместо логики приходится следить за множеством деталей - где что лежит, в каком состоянии у нас регистры и стек: это нас сильно отвлекает от самоцели - научиться использовать этот API. Саму концепцию лучше подавать на высокоуровневых языках - на C или C++, чтобы было кратко и наглядно: вот мы вызываем это, это и это, вот получаем результат. Кроме того, когда примеры на C/C++, мы видим, откуда что берётся - работаем с хедерами-первоисточниками. На ассемблере же сразу начинается магия. Например, в примере выше сразу идут две константы: LineCapRoundAnchor и LineCapArrowAnchor. Откуда взялись их магические значения? Где искать значения других констант, если они понадобятся читателю? Непонятно. Ради интереса пробуем даже загуглить - на MSDN их нет, а гуглятся только в исходниках документации на майкрософтовском гитхабе - это явно не тот источник, с которым захочет работать читатель. А когда видим пример на плюсах с хедерами - нет проблем, мы знаем: "Ага, взято вот оттуда". В общем, стоит отделять мух от котлет: уроки по API писать на высокоуровневых языках, а уроки именно по ассемблеру (какие-нибудь трюки, готовые примеры алгоритмов и библиотечные вещи) - да, их уже на асме.
HoShiMin, во вложении файл GDIPLUS.INC в топике Введение в GDI+ я где мог расставил значения констант GDI+
Здесь речь о том, что даже если ты где-то описал константы или приложил .inc - это всё равно не первоисточник. Рано или поздно читатель захочет выйти за рамки - а значит, нужно его к этому подготовить: объяснить, где это искать в оригинале, чтобы, когда ему понадобится информация, которую не покрывают уроки, он знал не только, где её найти, но и почему нужно искать именно там. Первоисточники у нас - MSDN и сишные хедеры. Нет даже большого смысла их дублировать: ты же всё равно не сможешь портировать все функции и константы - но это и не нужно, если объяснить читателю, где их найти. Дальше он просто откроет хедер, возьмёт нужное и перенесёт к себе. И если речь о портировании си на что-то другое, то именно работа с хедерами и понимание, где там что лежит, выходит на первый план, поскольку в MSDN зачастую не пишут значения для констант и дефайнов. Вот к такому и должен быть готов читатель. Собственно, мой посыл в том, что ценность твоих уроков не в ассемблерных листингах, а в объяснении API доступным языком, а объяснять его лучше на языке оригинала. Вот читаю ту статью по введению в GDI+: всё хорошо, но мы словно начали с середины. Введение, а дальше сразу описание функций. Но до вызова функций у нас ещё целый пласт по созданию окна и оконной очереди. А этого как раз и нет. И главное - там примеры на плюсах, это очень хорошо (кстати, там у всего кода почему-то пропали отступы) - на плюсах и стоит продолжать: прям в аттач к каждой главе краткий примерчик полного приложения, как вот этот пример в первом посте. Ну и раз уж начали, ещё описания структур, энумов и констант вместо дублирования лучше сопровождать ссылками на MSDN. Типа такого: Список цветов описывает энум Color, значения которого можно посмотреть здесь [ссылка на MSDN]. Так и статьи в объёме будут многократно короче, и сразу будет понятно, откуда что взялось.
Парикмахерская. Молодая неопытная парикмахерша первый раз бреет клиента опасной бритвой. Ведет бритвой по щеке медленно и аккуратно, от усердия вся вспотела и язычок прикусила. Р-р-раз и порезала клиента: ― ой, извините! Я не хотела... ― ничего, ничего... успокойтесь, продолжайте... Еще один порез: ― ой, извините! Я не хотела... ― ничего, ничего... успокойтесь, продолжайте... Еще один порез: ― ой, извините! Я не хотела... Еще, еще и еще ... Разозленная парикмахерша несколько раз крест-накрест чиркает бритвой по лицу клиента: ― Ну, не получается у меня, не получается...
Неправда, всё получается) И вообще, это лишь моё мнение, может тут сейчас наоборот все скажут: «Не хотим си, форум про ассемблер - давай на ассемблере»
db «Не хотим си, форум про ассемблер - давай на ассемблере»,0 --- Сообщение объединено, 26 янв 2025 в 02:34 --- В полтретьего ночи проснулся, чтобы это сказать!
TrashGen, Хорошо, приступим. Общий алгоритм написания программ для GDI+ Получаем адрес PEB.mov rax,qword ptr gs:[60h] Через PEB → Ldr → InMemoryOrderModuleList, найдем ntdll.dll. mov rax,[rax+18h];Смещаемся к структуре PEB_LDR_DATA mov rax,[rax+20h];Смещаемся к полю PEB_LDR_DATA.InMemoryOrderModuleList mov rax,[rax] mov rax,[rax+20h];Базовый адрес ntdll.dll Проходимся по таблице экспорта ntdll, для поиска адресов функций LdrLoadDLL и LdrGetProcedureAddress. Используем LdrLoadDll для загрузки gdiplus.dll, далее используем LdrGetProcedureAddress для получения нужных функций: GdiplusStartup, GdiplusShutdown, GdipCreateFromHWND, GdipCreatePen1, GdipSetPenStartCap, GdipSetPenEndCap, GdipSetPenDashArray, GdipDrawLineI, GdipDeletePen, GdipDeleteGraphics. Все текстовые сообщения расшифровываем непосредственно перед использованием. Все константы получаем после длительных вычислений. Вроде бы ничего не забыл...
Цель таких уроков, все таки, по моему мнению - изучение ассемблера на примере API GDI+, у mikl цикл статей по созданию оконных приложений через асм, так там везде gdi используется, возможно он хочет там везде на gdi+ поменять в коде. Ну и + на асме много чего видно, чего нет в си, на примере какого-нибудь API приятнее учить синтаксис языка и тд, чем просто алгоритмические задачи решать условно.
Я тоже за Asm, хоть и не использую 64бит пока. Ясно, что пример проверен и работает. В файле: LineCapArrowAnchor = $14 Не логичнее ли в программе: LineCapArrowAnchor = 14h Или я не понял чего?
R81..., это очепятка... конечно же должно быть LineCapArrowAnchor = 14h Хотя, стоп. Компилятор бы LineCapArrowAnchor = $14 не пропустил. А о каком файле идет речь? В коде самого верхнего поста LineCapArrowAnchor equ 14h в файле tut_01a.zip тоже все нормально...