Архив записей по месяцу: Февраль 2017

О разновидностях языков программирования

Есть много разных классификаций языков программирования. Появилась она и у меня.

Шахтёрские. У этих языков нет громкой миссии «Спасти Мир и Челочество (или Мир от Человечества)», они не претендуют на то, чтобы быть самыми модными, и уже забыли о том, чтобы быть «дружелюбными к новичку», они не обязательно красивы, элеганты или изящны. У них только одно назначение – решать задачи и проблемы в своей области, и служить средством добычи зарплаты для их практикующих. Пример – PHP и Java.

Скачать программы для развития.

Дзенские. Практикующие эти языки преследуют своей целью просветление. Освобождение от шаблонов и оков проявленного мира программирования, соцерзание истинной природы бытия и осознание своей природы будды. Примеры – C, Ассемблер и Forth.

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

Как и в случае с обычными религиями на одного искреннего мессию приходится толпа шарлатанов. Распознать их легко: неотъемлимым свойством языка, где есть святость, является гомоиконность. Святой язык — это гомоиконный язык. Если же вам будет проповедовать о блаженстве гетероиконных языков — гоните еретиков прочь! Когда Бог говорил с людьми, он говорил на Лиспе со вставками ассемблера.

Шаманские. Если предыдущие языки претендуют на связь с Верхним миром, то создатели этих заключают договор с сущностями из Нижнего. Именно этим объясняется то, что C++ способен решить практически любую задачу, но никто не понимает, каким образом это происходит.

Головоломки-ловушки. Вы помните цикл фильмов «Восставшие из ада»? В них главные герои находят коробочку-головоломку, и когда они находят способ, как её открыть, за ними приходят инфернальные сущности и утаскивают в глубины ада. Некоторые считают, что Хаскелл – это что-то вроде такое коробочки. Ведь неслучайно, что его разрабатывают в Microsoft Research.

Хипстерские. Представьте себе язык, в который без особенной системы понатыкали «модных» фич и раскрутили так, что множество хипстеров абсолютно уверены, что он скоро поработит весь мир. «Я тоже на нём программирую», – говорит хипстер, – «ведь в нём есть pattern matching!» Задачей такого языка является создавать «движуху», «тренды», дать возможность почувствовать, что находишься «на краю прогресса», безопасно попробовать функциональное программирование и так далее.

Список категорий, конечно, неполон. Можно вспомнить, например, элегантные (Objective-C) и мифологические (Cobol) языки, но близится вечер и пришло время завершить эту воскресную постиллу.

Программы для развития.

WebAssembly, или заговор четырёх

Пролог

Вестерновый салун где-то на восточно-европейских просторах. Несколько олдфагов и миддлфагов играют в техасский холдем опкодами x86-процессора. Пара ньюфагов пытаются скомпилировать OneHalf под iOS. Неожиданно дверь распахивается, входит самурай с 64-х битной катаной на поясе и кладёт на стол постер «Разыскивается».

«WASM Wanted!» — гласит большая надпись.

— Мы снова в деле, котаны, — говорит самурай.

Великолепная четвёрка

Долгое, очень долгое время практически единственным языком программирования для страниц, загружающихся в браузере, был Javascript. Здесь можно вспомнить о Java-апплетах и Flash-виджетах, используя которые бесстрашные программисты пытались превзойти всевозможные ограничения, но все эти технологии имели ограниченное и не всегда удобное применение. Основным способом оживить свою страницу — заставить идти снег или бегать котика из одного угла в другой — оставался и остаётся Javascript. Для него было написано множество библиотек и фреймворков, но всё же в различных кругах росло недовольство.

— Желаем программировать на нормальном языке! — роптали одни.
— Почему всё такое медленное? — негодовали другие.
— Хотим компилировать! — восклицали третьи.

Наконец, их мольбы были услышаны. Google, Microsoft, Mozilla и группа инженеров, стоящая за Webkit (Apple, как обычно, делает загадочный вид), объединились, чтобы создать стандарт, который откроет новую эпоху в области web-программирования.

Они решили назвать его WebAssembly, а сокращённо — попросту wasm.

Что это такое

Проблемы, которые были озвучены — сложность программирования на других языках и низкая скорость выполнения, производители браузеров пытались решить по отдельности. Mozilla разрабатывала asm.js — подмножество JavaScript, которое, с одной стороны, исполняется обычным браузером, а с другой, браузер, поддерживающий этот стандарт (Firefox), способен выполнить его гораздо быстрее, чем обычный JavaScript. Также asm.js задумывался как решение проблемы с программированием на других языках — чтобы он мог служить целью компиляции других языков. Google предложил своё решение — Native Client, в котором бы исполнялся запесоченный для безопасности скомпилированный код C и C++. Однако ни то, ни другое решение не смогло завладеть умами людей и разработчиков.

WebAssembly — это новая попытка разработать новый стандарт, который решит старые проблемы и будет принят всеми основными участниками. Объединились производители основных браузеров, поэтому есть надежда, что в этот раз она окажется успешной.

Что из себя представляет WebAssembly, главный репозиторий которого находится здесь? Это не совсем привычный нам байткод. WebAssembly имеет формат AST — абстрактного синтасического дерева, которое может быть представлено как в бинарном виде — основном для распространения, так и в текстовом, соответствующим двоичному.

Вот пример WebAssembly в человекочитаемой форме.


(module
  (func $return_i32 (result i32)
    (unreachable))
  (func $return_f64 (result f64)
    (unreachable))

  (func $if (param i32) (result f32)
   (if_else (get_local 0) (unreachable) (f32.const 0)))

  (func $block
   (block (i32.const 1) (unreachable) (i32.const 2)))

  (func $return_i64 (result i64)
   (return (i64.const 1))
   (unreachable))

  (func $call (result f64)
   (call $return_i32)
   (unreachable))

  (export "return_i32" $return_i32)
  (export "return_f64" $return_f64)
  (export "if" $if)
  (export "block" $block)
  (export "return_i64" $return_i64)
  (export "call" $call)
)

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

План захвата мира

В первую очередь, WebAssembly рассчитан на web-платформу. Вот как определяется эта цель:

Проектируется, чтобы запускаться внутри и хорошо интегрироваться с существующей веб-платформой:

    • сохранять безверсионную, протестированную и обратносовместимую историю Веб;
    • запускаться в той же семантической вселенной, что и JavaScript;
    • позволять синхронизированные вызовы из и в JavaScript;
    • соблюдать такие же политики безопасности;
    • иметь доступ к функциональности браузера через такие же Web API, которые доступны в JavaScript;
    • определить человекоредактируемый форма, конвертируемый в и из двоичного для поддержки функциональности просмотра исходников.

Однако в долгосрочной перспективе под властью wasm должны оказаться и другие платформы: сервера в датацентрах, IoT-устройства, мобильные и десктопные приложения и т. д., и т. п.

Для программистов самое интересное – это то, для генерации приложений в WebAssembly можно будет использовать не JavaScript, а другие языки. Первым, для кого делается поддержка, является C++. Ветераны указатели и шаблонов снова впереди!

Эпилог

Будущее покажет, перерастёт ли это попытка в лавину. Если да – веб-разработка (прямая и обратная) стоит на пороге больших перемен и становится ближе к традиционной.

Программы для развития.

Програмирование на fasm под Win64 часть 1 «Ассемблер, отладчик, IDE»

Введение

Я начинаю цикл статей по ассемблеру fasm. Возможно у вас есть вопрос: “Зачем в 21 веке нужен ассемблер?”. Я бы ответил так: Конечно, знание ассемблера не обязятельно, но оно способствует пониманию, во что превращается ваш код, как он работает, это позволяет почувствовать силу. Ну и в конце концов: Писать на ассемблере просто приятно (ну по крайней мере небольшие приложения).
Так что надеюсь, что мои статьи будут вам полезны.

Где взять fasm?

Собственно тут: http://flatassembler.net/download.php
На этой странице Томаш Грыштар(создатель fasm-а) выкладывает последнюю версию ассемблера. Там есть версии для DOS, Linux, Unix и Windows, нам нужна для Windows. В скачанном архиве находятся следующие компоненты:

  • fasm.exe – собственно сам ассемблер
  • fasmw.exe – IDE (среда разработки)
  • fasm.pdf – документация
  • папка source – исходники fasm-а (написан на самом себе)
  • папка include – папка с заголовками, импортами, и т.д.
  • папка examples – примеры программ на fasm-е
    Содержимое fasm.pdf дублирует 2 раздела документации “flat assembler 1.71 Programmer’s Manual” и “Windows programming” отсюда: http://flatassembler.net/docs.php

IDE (среда разработки)

Перед тем, как писать программы нужно определиться, в чём их писать. Для fasm-а существуют разные IDE, например: fasmw.exe(находится в архиве с fasm-ом), RadAsm, WinAsm Studio, Fresh, … Выберайте, какая вам больше по вкусу. Сразу скажу, что IDE из поставки fasm-а обладает минимальным количеством фичей, так что я бы рекомендовал использовать альтернативную IDE. Я, например, использую RadAsm 3.0, его можно взять здесь: https://fbedit.svn.sourceforge.net/svnroot/fbedit/RadASM30/Release/RadASM.zip К статье приложен файл Fasm.ini, там выбрана чёрная тема, добавлены x64 регистры и добавлена подсветка для большего числа инструкций. Можете поставить его вместо Fasm.ini поумолчанию, только исправьте в нём пути к папке с fasm-ом в 6 и 7 строках.

Отладчик

Писать программы это — хорошо, но нужно находить и исправлять баги, для этого нужен отладчик. Существуют разные отладчики способные отлаживать 64-битный код например: WinDbg, fdbg, x64dbg. Я использую WinDbg.

Далее я скажу пару слов о WinDbg. Скачать WinDbg можно с сайта Microsoft. При первом открытии WinDbg производит ужасающее впечатление, но не пугайтесь, нужно просто правильно настроить workspace. Добавлять разные окна можно с помощью меню “View”: от пункта “Command”(то, что показано по умолчанию) до “Processes and Threads”. Чтобы менять цвета оформления в том же меню “View” щёлкните по пункту “Options…”. Кроме окна с командами, которое есть по умолчанию нам понадобятся следующие окна:

  1. “Disassembly” – окно дизассемблера, чтобы видеть код.
  2. “Registers” – регистры, о них в следующей статье.
  3. “Memory” – для просмотра участков памяти.
  4. И ещё один “Memory” – для стека, поставьте в нём поле “Virtual” в значение rsp, а поле “Display format” в “Pointer and symbol”, о стеке я расскажу позже.

В принципе можете не заморачиваться и взять мой workspace, приложенный к статье “ссылка”, в WinDbg “File”->“OpenWorkspace in File…”

Это — всё, что я хотел рассказать в первой части.

Программы для развития.

Програмирование на fasm под Win64 часть 2 «Системы счисления, память, регистры»

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

Системы счисления

Прежде всего я хотел бы остановиться на способах записи целых чисел. Начнём с примера: запись 32167 означает следующее: 32167=3*10^4 + 2*10^3 + 1*10^2 + 6*10^1 + 7*10^0 \quad (1)

То есть любое число можно представить в виде суммы:
\sum\limits_{k=0}^N a_k*10^k

Где a k – цифра от 0 до 9, N – количество цифр в записи числа. Такая запись числа называется записью в десятичной системе счисления. В принципе, в вышеупомянутой сумме 10 можно заменить на любое другое натуральное число, это число будет называться основанием системы счисления. Например в восьмеричной системе счисления число представляется в виде суммы:
\sum\limits_{k=0}^N a_k*8^k

В данном случае требуется не 10 цифр, а 8, от 0 до 7. Обычно, если требуется указать систему счисления, её основание записывается как нижний индекс справа от числа(основание записывается в десятичной системе счисления), например:
76647_{8}=7*8^4 + 6*8^3 + 6*8^2 + 4*8^1 + 7*8^0 \quad (2)

в свою очередь:
76647_{8}=32167_{10}

то есть сумма (1) равна суме (2).
В системе счисления с основанием n существует n цифр, например в десятичной – десять, в восьмеричной – восемь. Если же нужно использовать систему счисления с основанием большим десяти, то придётся добавлять новые цифры, например в шестнадцатеричной системе счисления 16 цифр: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. Цифры A-F, равны 10-15 соответственно. Чтобы лучше понять рассмотрим несколько примеров:
A_{16}=10_{10} \\ F_{16}=15_{10} \\ A3_{16}=10*16^1 + 3*16^0=163_{10} \\ F2E_{16}=15*16^2 + 2*16^1 + 14*16^0=3886_{10}

Подробнее почитать о системах счисления можно в википедии

Что касается fasm-а, то в нём числа можно записывать в 4 разных системах счисления: в двоичной, в восьмеричной, в десятичной и в шестнадцатеричной. Чтобы записать число в десятичной системе счисления можно либо не указывать систему счисления вообще, либо дописать к числу справа букву “d”, например: 32167 или 32167d. Чтобы записать в восьмеричной нужно дописать к числу справа букву “o”, например: 32167o равно числу 13431. Чтобы записать в двоичной нужно дописать справа букву “b”, например: 100110b равно числу 38. Записать число в шестнадцатеричной системе счисления можно несколькими способами: можно дописать слева последовательность “0x”, можно дописать слева “$” и можно дописать справа букву “h”, но в последнем случае число должно начинаться с одной из десятичных цифр. Пример: 0xFE=$FE=0FEh=254, запись FEh приведёт к ошибке.

Компьютер работает с числами в двоичной системе счисления. Один двоичный знак называется битом, соответственно число состоящее из n двоичных знаков называется n-битным (или n-разрядным). Например число 11010010b – 8-ми битное. Группа из 8 бит называется байтом(byte), из 2-х байт – словом(word), из 4-х байт – двойным словом(double word), из 8-и байт – счетверённым словом(quad word), и т.д.

Организация памяти

Каждая программа (её код и данные) располагается в оперативной памяти. Оперативная память состоит из ячеек размером в 1 байт. Для того чтобы обращаться к этим ячейкам, каждой из них сопоставлено некоторое число, называемое адресом (в нашем случае — 64-битное) (см. рис. 1).
Адреса принято записывать в 16-ричной системе счисления. Множество адресов вместе с сопоставленными им ячейками памяти называется адресным пространством. В адресном пространстве не каждому адресу сопоставлена какая-то ячейка памяти. Существует физическое адресное пространство, в нём каждой ячейке оперативной памяти сопоставлен единственный адрес. В целях безопасности сделано так, что у каждого процесса адресное пространство своё, т.е. процессу доступна не вся оперативная память компьютера, а только некоторая её часть. Адресное пространство процесса называется виртуальным. Одной и той же ячейке памяти в разных адресных пространствах могут быть сопоставлены разные адреса.
У виртуальной памяти есть атрибуты доступа: запись, чтение, исполнение. Например, если память не доступна для записи, то попытка записать в неё приведёт к ошибке, если она не доступна для исполнения, то попытка исполнения кода, находящегося там приведёт к ошибке, и т.д.
Ещё хотелось бы добавить, что когда говорят “указатель на <что-то>”, то имеется ввиду виртуальный адрес, с которого начинается <что-то>.

Общий вид исходника

Начну с того, что коснусь структуры исполняемого файла.
Исполняемый файл Windows состоит из нескольких частей:
1)Заголовок – обязателен
2)Секции, их может быть любое количество
У каждой секции есть имя (состоит из 8 ANSI символов) и атрибуты, в них входят атрибуты доступа к памяти.

Теперь рассмотрим HelloWorld простейшую программу на fasm-е:

format PE64 console;формат файла, подсистема
entry start;точка входа

include 'win64w.inc';заголовки

section '.code' readable executable code;секция кода

proc start;процедура

	nop
	nop
	nop

	ret
endp ;конец процедуры

section '.data' readable writeable data; секция данных
dd ?;данные

Первой строчкой указывается формат файла(в нашем случае PE64 – это 64-битный исполняемый файл Windows PE-Portable Executable) и подсистема console(это значит что при запуске нашей программы у неё будет создана консоль).
Вторая строчка entry start указывает точку входа, это — место с которого начинает исполняться код нашей программы.
include 'win64w.inc' – подключение некоторых заголовочных файлов.
Слово section обозначает начало новой секции, после него идёт имя в кавычках, а затем атрибуты.
Секцию кода мы пометили как доступную для чтения и исполнения, но не для записи, атрибут “code” указывает на то, что в данной секции находится код, он не обязателен, без него всё будет нормально работать.
Секция данных доступна для чтения и записи, атрибут “data” указывает на то, что в данной секции находятся данные, он, опять же, не обязателен. В принципе можно создавать любое количество секций(даже с одинаковыми именами) и класть код и данные в любые из них, главное, чтобы то, что предназначено для исполнения находилось в исполняемой памяти, а то, что для записи находилось в памяти доступной для записи.

В секции данных находится загадочная строка dd ?, сейчас расскажу, что она означает.

Объявление данных

Для объявления данных существует ряд директив, изображённых в следующей таблице:

Размер (в байтах) Объявление данных
1 db,file
2 dw,du
4 dd
6 dp,df
8 dq
10 dt

Чтобы объявить данные размером в байт нужно написать db <значение>, например для объявления байта со значением 0xAB служит строчка db 0xAB, остальные директивы предназначены для объявления данных большего размера. Если вместо значения после директивы стоит “?”, это означает, что вам безразлично, какое там будет значение. Следует так-же отметить, что директива dt не предполагает указания целых чисел, а предполагает числа с плавающей точкой. С помощью этих директив можно объявить несколько блоков данных подряд, например: db 12o,10,? объявляет 3 байта подряд: сначала со значением 12(в восьмеричной системе счисления), далее 10, далее – безразлично. Строка dd 32167,12объявляет 2 двойных слова со значениями 32167 и 12 соответственно. Чтобы заполнить участок памяти одним значением используется слово “dup”, например строка dd 12 dup (10) объявляет 12 двойных слов со значением 10, строка dd 5 dup (3,6) объявляет 5 пар двойных слов со значениями 3 и 6 соответственно.
Что касается директив db и du, они в качестве значения могут принимать строки (в кавычках), например: строка db 'Hello world!',0 объявляет строку в формате ANSI с нулём на конце, а du 'Hello world!',0 объявляет такую-же строку только в формате юникод (UTF-16).
Директива file принимает в качестве значения имя файла (file <имя файла>), после компиляции в этом месте программы будет находиться содержимое файла <имя файла>. Чтобы в коде можно было обращаться к определённым данным нужно дать им какое-нибудь имя. Данные с именем мы будем называть переменными. Имя может состоять из латинских букв, цифр и знака подчёркивания (хотя буквы могут быть не только латинские, но и любые другие, но я настоятельно не рекомендую их использовать). Имя пишется перед директивой объявления данных, например так: qwe dd 32167, в данном случае мы объявили переменную с именем “qwe”, размером 4 байта, у которой начальное значение равно 32167.

Регистры

Перед тем, как подходить к писанию кода необходимо обсудить регистры. Всего есть 16 регистров общего назначения (вообще регистров больше, но общего назначения – 16). Имена 8-и из них: rax,rcx,rdx,rbx,rsp,rbp,rsi,rdi, все остальные называются r8-r15, нижние 32 бита регистров называются: eax,ecx,edx,ebx,esp,ebp,esi,edi, остальные r8d-r15d. Нижние 16 бит называются ax,cx,dx,bx,sp,bp,si,di, остальные r8w-r15w. Нижние 8 бит называются al,cl,dl,bl,spl,bpl,sil,dil, остальные r8b-r15b. Так же можно обращаться верхним 8-и битам нижних 16 бит первых 4-х регистров по именам: ah,ch,dh,bh. Так же есть регстр – указатель инструкций rip, и регистр флагов rflags. rip всегда указывает на следующую инструкцию (которая идёт после той, которая выполняется), регистры изображены на рис. 2

Собственно программа

Если вы скомпилируете программу приведённую выше, то обнаружите при запуске, что она ничего не делает, потому что она действительно ничего не делает! Выше я говорил про точку входа, она начиналась со строки proc start, и закончилась на endp, что представляют собой эти макросы (proc и endp), я пока говорить не буду, скажу лишь что объявив таким образом процедуру к ней можно обращаться из любого места программы. Внутри процедуры “start” трижды написана команда “nop”, так вот:
nop — команда, которая ничего не делает.
Далее я хотел бы ввести ещё одну команду:
int3 (без пробела) — команда точки останова. Если программа находится под отладчиком, то как только исполнение доходит до этой инструкции программа останавливается, что будит видно в отладчике. Если же программа не находится под отладчиком, то выполнение этой инструкции немедленно приводит к ошибке.
Попробуйте добавить int3 перед 3-мя nop-ами в приведённой в статье программе, скомпилируйте и откройте отладчиком, вы увидите что-то вроде того, что изображено на рис. 3.
Это — точка останова внутри системного кода. Продолжите выполнение программы (в WinDbg — F5), после чего вы увидите в окне дизассемблера что-то вроде этого:

push rbp
mov rbp,rsp
int3
nop
nop
nop
leave
ret

“push rbp\mov rbp,rsp” и “leave\ret” — это пролог и эпилог процедуры соответственно, позже я объясню, что это и зачем.
Нажмите несколько раз “step over” (в WinDbg – F10) или “step into” (в WinDbg – F8) и вы увидите как отладчик переходит от одной инструкции к другой.

Программы для развития.

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

Програмирование на fasm под Win64 часть 3 «Окна WinDbg, метки, основные команды»

В данной статье я расскажу об окнах в WinDbg, и рассмотрю некоторые машинные инструкции.

Окна в WinDbg

Для начала в одном из окон “memory” в поле “Virtual” напишите “@rsp” (без кавычек), и там же в поле “Display format” поставьте значение “Pointer and Symbol”, это — окно стека, что это я расскажу позже. В общем у меня окно WinDbg выглядит как на рис.1.
В окне “disassembly” вы видите 3 колонки: адрес, машинный код и мнемоника. Адрес — это адрес начала инструкции. Обратите внимание: Адреса в WinDbg (как и в других отладчиках) записываются в шестнадцатеричной системе счисления. Далее идёт колонка в которой показан машинный код инструкции (тоже в шестнадцатеричной системе счисления). И последняя колонка — мнемоника, т.е. запись инструкции на языке ассемблера.

В окне “Registers” 2 колонки: левая — названия регистров, правая — их значения. Обратите внимание: значения регистров представлены либо как шестнадцатеричные целые, либо как десятичные числа с плавающей точкой (вещественные).

В окне “memory”, где я сказал указать в поле “Virtual” значение “@rsp”, есть 2 колонки: левая — адрес, правая — значение типа qword по данному адресу.

В оставшемся окне “memory” — 3 колонки: адрес, значения байтов (шестнадцатеричные) по данному адресу и символы ANSI, соответствующие данным значениям байтов (ANSI — как раз однобайтовая кодировка). Можете попереключать поле “display format” и посмотреть, разные форматы отображения данных. Поле “Virtual” показывает, с какого адреса начать отображение памяти, адрес может включать и регистры, числа в этом поле считаются шестнадцатеричными.

Метки

Надо было сказать в предыдущих статьях: точка с запятой — это знак комментария, всё что есть в строке после неё ассемблер проигнорирует, многострочных комментариев нет.

Метка — это символьное имя (идентификатор) позволяющий обращаться к данной области программы.
Чтобы поставить метку в коде нужно написать её имя и поставить двоеточие, например:

proc start

	nop
        label1:;<--Метка
        nop
	ret
endp

В прошлой статье я показывал, как можно объявлять переменные, например: Variable dd ? Так вот, имя переменной — тоже метка, оно отличается от метки поставленной с помощью двоеточия тем, что оно предполагает, что данные, на которые она указывает будут интерпретированы как данные соответствующего размера. Так же имя метки может начинаться с точки, такая метка называется локальной. При ассемблировании к имени локальной метки добавляется слева имя ближайшей сверху нелокальной метки, например:

proc start
;код 1
Lbl:
;код 2
.locLbl:
;код 3
ret
endp

В данном примере полное имя метки .locLbl — “Lbl.locLbl”, любая ссылка из кода 2 и 3 на метку по имени “.locLbl”, на самом деле ссылается на “Lbl.locLbl”. Если же попытаться сослаться на эту метку из кода 1 по имени “.locLbl”, fasm будет искать метку с полным именем “start.locLbl”, не найдёт её и сообщит об ошибке. Чтобы сослаться на метку “.locLbl” из кода 1 нужно писать полное имя метки: “Lbl.locLbl”.

Основные команды

Теперь можно приступать к программированию. В прошлой статье я говорил об инструкции int3, используйте её чтобы остановить отладчик в том месте, где вам нужно. Общий вид программы будет примерно такой:

format PE64 console ;формат - PE 64 бита файла, подсистема
entry start ;точка входа (место, с которого начинается выполнение кода программы)

include 'win64w.inc';заголовки

section '.code' readable executable code;секция кода

proc start ;начало процедуры

	int3
	;<--Здесь будет располагаться ваш код.
	ret
endp ;конец процедуры

section '.data' readable writeable data; секция данных
dd ?;данные

Первая инструкция, которую я бы хотел обсудить называется mov. У этой инструкции два явных операнда (операнд — это то, что подаётся на вход инструкции, явный операнд — это операнд, который вы указываете в коде). В качестве операндов у этой инструкции может выступать константа, память или регистр. Записывается как mov <op1>,<op2>, эта команда копирует данные из <op2> в <op1>,сразу замечу, что <op1> не может быть константой (не только для “mov”) и вообще большинство (не все) инструкций с двумя явными операндами кладут результат в первый операнд, рассмотрим пример:
mov rax,0x32167
данная инструкция записывает число 0×32167 в регистр rax, посмотрите в отладчике, как при исполнении этой инструкции значение в регистре rax меняется на 0×32167. Можно вместо rax поставить любой регистр, главное, чтобы количество двоичных разрядов константы справа было меньше либо равно количеству разрядов в регистре, например конструкция mov al,0x1FF приведёт к ошибке, т.к. в числе 0×1FF больше восьми бит. Стоит также обратить внимание на то, что запись в регистр размера одного байта или слова не влияет на остальную часть регистра, но если вы пишете в регистр размером в 32 бита, то верхняя часть соответствующего 64-битного регистра обнуляется. Поэкспериментируйте, а потом двигайтесь дальше.

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

Следующие правила записи адреса верны для всех инструкций, которые принимают память в качестве одного из операндов.
Когда ссылаются на память, адрес заключают в квадратные скобки: []. Адрес может выглядеть следующим образом:

    1. [reg1+reg2*2 n +offs32]
    2. [rip+offs32]
    3. [offs64]

 

В первом случае reg1 и reg2 — регистры общего назначения, n может принимать значения от 0 до 3, offs32 — 32-битное число со знаком. Во втором случае offs32 — тоже 32-битное со знаком, прибавляется к регистру rip, замечу, что если вы сошлётесь на глобальную переменную в своём коде по имени, то fasm закодирует адрес именно таким способом. Третий случай есть не для всех инструкций и подразумевает ссылку на память по непосредственному адресу, так можно закодировать например инструкции:
mov al,[mem]
mov ax,[mem]
mov eax,[mem]
mov rax,[mem]
mov [mem],al
mov [mem],ax
mov [mem],eax
mov [mem],rax
Правда почему-то fasm отказывается ассемблировать эти инструкции с 3-м типом адреса, в документации они есть. И последнее на счёт адреса, у каждой инструкции может быть не более одного явного операнда, ссылающегося на память.
Рассмотрим простой пример:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start

	int3
	mov eax,[Variable2];Запись значения Variable2 в регистр eax
	mov [Variable1],eax;запись значения регистра eax в переменную Variable1

	ret
endp

section '.data' readable writeable data
Variable1 dd ?
Variable2 dd 210h

В этом примере значение Variable2 копируется в Variable1. Чтобы посмотреть в отладчике значения Variable1 и Variable2 скопируйте адрес из инструкции mov в дизассемблере в поле “Virtual” окна “memory” и поставьте “display format” в “long hex”.
Далее я бы хотел рассмотреть пять инструкций общего назначения, которые так же имеют 2 явных операнда, все они записывают результат в <op1>:
add <op1>,<op2> — складывает <op1> и <op2>, если результат не вмещается в <op1>, тогда старшие биты отбрасываются
and <op1>,<op2> — применяет к каждому биту операцию логическое “и”
or <op1>,<op2> — применяет к каждому биту операцию логическое “или”
sub <op1>,<op2> — вычетает из <op1> <op2>, отрицательный результат записывается в дополнительном коде
xor <op1>,<op2> — применяет к каждому биту логическое “исключающее или”
Кроме них так же полезны инструкции:
xchg <op1>,<op2> — меняет местами значения <op1> и <op2>
lea reg,[memory] — считает адрес [memory] и записывает его значение в регистр reg, обратите внимание, что данная команда не выполняет реального обращения к памяти, а просто считает адрес
not <op1> — применяет логическое “не” к <op1>
Ну, и последняя инструкция о которой я расскажу в этой статье — это nop <op1> операндом этой инструкции может быть либо регистр, либо память, она ничего не делает и в случае если операнд — память обращения к данной памяти не происходит.
Попробуйте использовать эти инструкции и посмотрите в отладчике, к каким результатам приводит их исполнение.
На этом пока всё.,

Программы для развития.

Програмирование на fasm под Win64 часть 4 «флаги, прыжки, условия»

В данной статье мы рассмотрим некоторые биты регистра флагов, условные прыжки и ещё некоторые условные инструкции.

Регистр флагов

В предыдущих статьях я говорил о регистре флагов “rflags”, он 64-х битный. Регистром флагов он называется, потому что содержит флаги. Флаг — это число длиной в 1 бит, если значение флага равно 1, то говорят, что этот флаг установлен, если — 0, то сброшен. Часть битов регистра флагов зарезервирована (т.е. не используется), часть битов относится к системным флагам, их мы рассматривать не будем. В регистр флагов можно непосредственно записывать значение (правда системные флаги нам менять не дадут), так же они устанавливаются или сбрасываются при выполнении некоторых инструкций.Доступные нам флаги показаны на рис. 1, рассмотрим некоторые из них:

Carry Flag (флаг переноса)
При сложении 2-х целых чисел результат может не вместиться в операнд-приемник (операнд, в который происходит запись результата), причём не хватать будет 1-го бита, так вот, если требуется дополнительный бит для хранения результата, тогда этот флаг будет установлен, в противном случае — сброшен. Для вычитания: при вычитании из меньшего большего установлен (без знака), иначе — сброшен.
Используется в без знаковой арифметике.
Операции and, or, xor всегда сбрасывают этот флаг.

Zero Flag (флаг нуля)
Установлен, если результат операции равен нулю, сброшен в противном случае.

Sign Flag (флаг знака)
Равен значению старшего значащего бита результата (знакового бита). Используется в арифметике со знаком.

Overflow Flag (флаг переполнения)
Устанавливается, если старший (знаковый) бит результата отличается от значения этого бита для обоих исходных чисел, сбрасывается в противном случае.

Из приведённых в предыдущей статье инструкций на флаги влияют: add, and, or, sub, xor. Инструкции mov, xchg, lea не влияют на флаги. Я приведу ещё несколько инструкций влияющих на флаги:
inc <op1> — увеличивает op1 на единицу, операндом может быть как регистр, так и память.
dec <op1> — уменьшает на единицу op1, аналогично inc.
cmp <op1>,<op2> — сравнивает op1 и op2, по факту просто вычитает из op1 op2, выставляет флаги, а результат вычитания никуда не записывает.
test <op1>,<op2> — аналог and, но не записывает никуда результат операции.

У вас наверно возникает разумный вопрос: “А зачем эти флаги нужны?”, ответ таков: “Для выполнения условных инструкций.” Условные инструкции — это инструкции результат выполнения которых зависит от некоторых флагов в регистре флагов.

Прыжки

Первыми условными инструкциями которые мы здесь рассмотрим будут условные прыжки. Сначала рассмотрим безусловный прыжок: jmp <метка>, в коде выглядит примерно так:

;код
label:;метка
;код
jmp label
;код

после выполнения данной инструкции продолжает исполняться не тот код, который идёт после jmp, а тот, который идёт после метки. Условные прыжки выполняются только если выполнено определённое условие, в противном случае выполняется код идущий сразу после инструкции прыжка. Записываются они следующим образом: j<условие> <метка>здесь я приведу некоторые из них:
jz или je — выполняются при ZF=1
jne или jnz — выполняются при ZF=0
ja — выполняется, если CF=0 и ZF=0
jb или jc — выполняются, если CF=1
jae или jnb — выполняются, если CF=0
jbe — если CF=1 или ZF=1
jl — SF<>OF
jnl — SF=OF
jle — ZF=1 или SF<>OF
jg — ZF=0 и SF=OF

Чтобы лучше понять их значения рассмотрим несколько примеров.

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start

	int3
	mov eax,3;помещаем в регистр eax число 3
	mov edx,4;в edx - 4
	cmp eax,edx;сравниваем
	je .equal;прыгнуть, если равны


		nop
		nop
		nop


	.equal:

	ret
endp

В данном случае прыжок не будет выполнен, т.к. при вычитании 4 из 3 нуля не будет, а соответственно ZF=0, в общем по-этому инструкция и называется je, т.е. jump equal, прыгнуть, если равно. Если в этом примере вместо je поставить jb или jl, то прыжок будет осуществлён, потому что эти мнемоники означают прыжок, если меньше. jb отличается от jl только тем, что jb подразумевает сравнение без знаковых чисел, а jl — знаковых. В связи с этим ещё один пример:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start

	int3
	mov eax,-3;помещаем в регистр eax число -3
	mov edx,4;в edx 4
	cmp eax,edx;сравниваем
	jl .equal;прыгнуть, если меньше


		nop
		nop
		nop


	.equal:

	ret
endp

В данном случае прыжок будет выполнен, т.к. -3 меньше, чем 4, но если вы поставите там jb, то -3 надо интерпретировать как 0xFFFFFFFD, что больше 4-х, и поэтому прыжок не будет выполнен. Для прыжков приведённых выше:
jz или je — прыгнуть если равно
jne или jnz — прыгнуть если не равно
ja — прыгнуть если больше (без знака)
jb или jc — прыгнуть если меньше (без знака)
jae или jnb — прыгнуть если больше либо равно (без знака)
jbe — прыгнуть если меньше либо равно (без знака)
jl — прыгнуть если меньше (со знаком)
jnl — прыгнуть если не меньше (со знаком)
jle — прыгнуть если меньше либо равно (со знаком)
jg — прыгнуть если больше (со знаком)

С помощью прыжков можно написать аналог оператора “if” в языках высокого уровня:

	cmp eax,edx
	je .lbl1


		;код 1


	jmp .EndCond
	.lbl1:


		;код 2


	.EndCond:

В данном примере если eax=edx выполнится код 2, если нет, то код 1.

Так же с помощью прыжков можно организовать цикл:

mov eax,10
	.loop:


		;код


	dec eax
	cmp eax,0
	jne .loop

Здесь код будет выполняться пока eax не равно нулю, инструкция dec eax уменьшает eax на единицу, поэтому код в цикле выполнится 10 раз, замечу, что инструкцию cmp eax,0 можно убрать т.к. инструкция dec eax выставит флаг нуля, когда eax станет равно нулю.

Для иллюстрации использования прыжков рассмотрим пример:
объявим в секции данных массив: Array dd 12,32,1,0,17
сейчас я приведу программу, которая ищет в этом массиве максимальное число (без знака) и оставляет результат в регистре eax, было бы не плохо, если бы вы сначала попытались написать программу самостоятельно, а потом смотрели дальше.

Программа:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start

	int3
	mov ecx,5;количество итераций цикла - 5
	xor eax,eax;положим в eax ноль
	lea rdx,[Array];rdx - указатель (адрес) на массив
	.loop:


		cmp [rdx],eax;сравниваем очередное значение из массива со значением в eax
		jbe .NotGreater;если значение из массива меньше либо
		;равно значению в eax делаем прыжок


			mov eax,[rdx];если всё-таки больше,
			;записываем в eax большее значение


		.NotGreater:


	add rdx,4;увеличиваем rdx, чтобы он указывал на следующий элемент массива
	dec ecx
	jnz .loop;если не прошло 5-и итераций возвращаемся к началу цикла

	;здесь в eax будет значение 32

	ret
endp

section '.data' readable writeable data
Array dd 12,32,1,0,17

Для того, чтобы использовать числа со знаком нужно вместо jbe .NotGreater поставить jle .NotGreater и вначале вместо нуля положить в eax -2147483648 (наименьший dword со знаком).

Ещё один пример, который я хотел рассмотреть, это — сортировка пузырьком, здесь я приведу пример программы сортирующей по возрастанию массив Arr dd 0x45,0x65,0x85,0x12,0x432,0x65,0x21,0x09,0x860,0x52,0x46, но было бы не плохо, если бы вы попытались написать программу самостоятельно, а потом смотрели бы мою реализацию. Алгоритм такой:

    1. присваиваем переменной sorted значение истина (один)
    2. для каждого I от нуля до (длины массива-2) делаем шаг 3
    3. если I-ый элемент массива больше (I+1)-го, то меняем эти элементы местами и присваиваем переменной sorted значение ложь (ноль)
    4. если sorted=ложь, то возвращаемся к пункту 1, если истина, то заканчиваем выполнение цикла.

 

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start

	int3

	.GreatLoop:


		mov al,1;al=истина
		mov ecx,10;в массиве 11 элементов, поэтому цикл должен повториться 10 раз
		lea rdx,[Arr];rdx=указатель(адрес) массива
		.loop:


			mov r8d,[rdx]
			cmp [rdx+4],r8d;сравниваем два соседних элемента
			jae .NotGreater;если следующий больше либо равен предыдущему,
							;тогда ничего не делаем

				;если меньше, тогда
				mov al,0;al=ложь
				mov r9d,[rdx+4];меняем местами соседние элементы
				mov [rdx],r9d
				mov [rdx+4],r8d


			.NotGreater:


		add rdx,4;rdx указывает на следующий элемент
		dec ecx
		jnz .loop


	test al,al;эквивалентно cmp al,0
	je .GreatLoop;если al=ложь, тогда продолжаем работу

	ret
endp

section '.data' readable writeable data
Arr dd 0x45,0x65,0x85,0x12,0x432,0x65,0x21,0x09,0x860,0x52,0x46

На этом рассмотрение прыжков я хочу закончить.

Команды set<условие> и cmov<условие>

Команда set<cond> принимает в качестве единственного операнда либо однобайтовый регистр, либо один байт в памяти. Если условие выполнено, она в этот байт записывает значение 1, если — нет, то 0. Например инструкция sete al запишет в регистр al значение 1, если установлен ZF, и 0, если не установлен.

Инструкция cmov<cond> имеет 2 операнда, первый приемник — только 16, 32 или 64 разрядный регистр, второй либо регистр, либо память такого же размера. Команда записывает в первый операнд значение второго, если условие выполнено, в противном случае – ничего не делает. Например инструкция cmovge eax,[variable] запишет значение [variable] в регистр eax, если флаг SF=OF, иначе не сделает ничего.

Попробуйте заменить некоторые прыжки в предыдущих двух примерах на только что рассмотренные 2 инструкции, я приведу алгоритм сортировки:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start

	int3

	.GreatLoop:


		mov al,1;al=истина
		mov ecx,10;в массиве 11 элементов, поэтому цикл должен повториться 10 раз
		lea rdx,[Arr];rdx=указатель(адрес) массива
		.loop:


			mov r8d,[rdx]
			mov r9d,[rdx+4]
			cmp r9d,r8d;сравниваем два соседних элемента

			cmovb r8d,[rdx+4];если следующий меньше предыдущего,
			cmovb r9d,[rdx];тогда меняем местами значения

			setnb ah;обратите внимание, что здесь стоит не setnb al,
			and al,ah;а эта связка, это нужно для того чтобы если 
					;al=0, тогда оно не сможет принять значение 1

			mov [rdx],r8d
			mov [rdx+4],r9d


		add rdx,4;rdx указывает на следующий элемент
		dec ecx
		jnz .loop


	test al,al;эквивалентно cmp al,0
	je .GreatLoop;если al=ложь, тогда продолжаем работу

	ret
endp

section '.data' readable writeable data
Arr dd 0x45,0x65,0x85,0x12,0x432,0x65,0x21,0x09,0x860,0x52,0x46

команды sbb и adc

Данные команды предназначены для того, чтобы работать с целыми числами больше 64 бит. Команда adc действует аналогично add, но помимо этого прибавляет к результату значение флага CF. sbb аналогична sub, но вычетает из результата значение CF. Например для вычитания 128 битных чисел можно написать:

mov rax,[Var1]
sub [Var2],rax
mov rax,[Var1+8]
sbb [Var2+8],rax
...
Var1 dq 1,2
Var2 dq 2,2

Для сложения вместо sub и sbb — add и adc.

На этом я хотел бы закончить статью.

Программы для развития.

Програмирование на fasm под Win64 часть 5 «стек, процедуры, соглашения о вызове»

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

Стек

Стек — область памяти, в него можно добавлять данные и извлекать их, добавление и извлечение организовано по правилу “последним вошёл, первым вышел”.
Приведу пример: если добавить в стек подряд числа 1, 2, 3, то извлекаются они в порядке: 3, 2, 1. Для помещения в стек значения служит команда push <op>,
в качестве операнда может выступать регистр общего назначения или память, причём размер должен быть либо 2 байта, либо 8 байт. Вытащить из стека значение
можно командой pop <op>, в качестве операнда тут опять же может выступать либо регистр, либо память. Для примера напишем программу, которая перевернёт массив
задом на перёд, вообще-то для этого действия стек не нужен и более оптимально было-бы обойтись без стека, но в качестве демонстрации — подойдёт.

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start

	int3

	mov ecx,5;ecx - счётчик
	lea rax,[Arr];rax указывает на исходный массив

	.PushArr:;цикл из 5-и итераций


		push qword [rax];помещаем очередной элемент массива в стек


	add rax,8;передвигаем указатель на следующий элемент массива
	dec ecx;вычитаем единицу из счётчика
	jnz .PushArr;если не весь массив обработан, возвращаемся к следующей итерации

	mov ecx,5;ecx - счётчик
	lea rax,[ArrDst];rax указывает на конечный массив

	.PopArr:;цикл из 5-и итераций


		pop qword [rax];достаём из стека элемент и записываем его в конечный массив


	add rax,8;передвигаем указатель на следующий элемент массива
	dec ecx;вычитаем единицу из счётчика
	jnz .PopArr;если не весь массив обработан, возвращаемся к следующей итерации

	ret
endp

section '.data' readable writeable data
Arr dq 0x32167,0x456ab,0x76bd,0xa657f,0x213fe;исходный массив
ArrDst dq 5 dup (?);конечный массив

Как я говорил в одной из предидущих статей, можно писать и читать регистр флагов, делается это инструкциями pushf и popf, без явных операндов.
pushf — добавляет значение регистра флагов в стек, popf достаёт значение со стека и записывает в регистр флагов, соответственно, писать в регистр
флагов можно так:

push SomeValue
popf

Из написанного выше понятно, что можно класть значения в стек и вынимать их, а можно ли обратиться не к верхнему значению в стеке? Можно. На вершину стека
указывает регистр rsp, когда вы добавляете значение в стек, значение этого регистра уменьшается, и туда, куда указывает rsp записывается значение,
когда вы извлекаете значение из стека, извлекается значение, на которое указывает rsp, а потом значение rsp увеличивается. Соответственно, чтобы обратиться
к произвольному значению из стека надо написать что-то в духе: mov rax,[rsp+8] — здесь в регистр rax записывается значение из стека.

Процедуры

Процедура (или функция) — это подпрограмма, к которой можно обращаться из разных мест вашей программы, она представляет собой такой же машинный код,
как и остальная программа, в её начале ставится метка. Для того, чтобы передать управление процедуре используется команда call <op>, где в качестве операнда
выступает либо адрес (метка), либо регистр (указвыающий на метку), либо память (в которой распологается адрес метки).
Эта команда передаёт управление по указанному адресу как и jmp, но кроме этого она ещё кладёт в стек адрес возврата,
т.е. адрес следующей за call команды.
Заканчивается процедура инструкцией retn или retn imm16, где imm16 обозначает 2-х байтовое целое непосредственное значение
(т.е. не регистр и не память, а значение закодированное в инструкции). Эта инструкция достаёт из стека адрес возврата и передаёт на него управление,
imm16 — это количество байт, которое будет вытащено из стека помимо адреса возврата, т.е. просто увеличится значение регистра rsp.
Кроме инструкции retn также есть инструкция retf, но мы не будем её рассматривать.
Также, как возможно вам известно из языков высокого уровня, у каждой процедуры есть локальные переменные, к которым можно обращаться только из этой процедуры.
Локальные переменные создаются в стеке, т.е. значение регистра rsp уменьшается и в получившимся пространстве появляется свободное место для хранения
переменных, перед выходом из процедуры (перед командой retn) регистр rsp должен быть возвращён в исходное состояние. Рассмотрим пример:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

Procedure:;начало процедуры
sub rsp,16;выделяем 16 байт для локальных переменных

mov [rsp],rax;записываем значение rax в локальную переменную
mov [rsp+8],rdx;rdx - в другую

mov rcx,[rsp];читаем значение одной из переменных в rcx
xor rcx,[rsp+8];ксорим rcx со значением другой локальной переменной

add rsp,16;возвращаем rsp на место
retn;выходим из процедуры

proc start

	int3
	call Procedure;вызываем процедуру

	ret
endp

Сразу скажу, что обращаться к стеку можно только по адресам большим текущего значения rsp, так что не рекомендуется обращаться по адресам [rsp-xx]
Удобно для обращения к локальным переменным использоватьвторой другой регистр: rbp. Это будет выглядить примерно так:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

Procedure:;начало процедуры
push rbp;сохраняем значение rbp
mov rbp,rsp;rbp указывает на стек
sub rsp,16;выделяем 16 байт для локальных переменных

mov [rbp-16],rax;записываем значение rax в локальную переменную
mov [rbp-8],rdx;rdx - в другую

mov rcx,[rbp-16];читаем значение одной из переменных в rcx
xor rcx,[rbp-8];ксорим rcx со значением другой локальной переменной

mov rsp,rbp;возвращаем rsp на место
pop rbp;восстанавливаем значение rbp
retn;выходим из процедуры

proc start

	int3
	call Procedure;вызываем процедуру

	ret
endp

Вместо связки команд mov rsp,rbp / pop rbp можно использовать команду leave, без явных операндов.
Обращаться к переменным по адресам [rbp-xx] не очень удобно, хотелось бы обращаться к ним по именам. Для этого нам нужны несколько макросов
для использования которых мы подключаем win64a.inc .
Макросы следующие:
proc
endp
ret
locals
endl

Макрос proc объявляет начало процедуры, имя после этого макроса будет именем метки начала процедуры. Например если вы напишете proc SomeProc,
то обращаться к ней следует так: call SomeProc. Макрос endp объявляет конец процедуры, для каждого объявления процедуры обязательно должен присутствовать
endp. ret означает возврат из процедуры, т.е. там, где в процедуре вы поставите ret fasm поставит что-то вроде mov rsp,rbp / pop rbp / retn.
Обратите внимание, что endp и ret — разные вещи: первый указывает конец процедуры, а второй — только возврат, их может быть несколько, рассмотрим
пример:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc SomeProc;Объявляем процедуру

	cmp [SomeData],1000;сравниваем SomeData с 1000
	ja .Lbl1;если SomeData больше 1000 - прыгаем на Lbl1

		add [SomeData],200;если SomeData меньше либо равно 100, то прибавляем к ниму 200

	ret;возврат из процедуры

	.Lbl1:

		sub [SomeData],200;если SomeData больше 1000, то вычитаем из него 200

	ret;возврат из процедуры

endp;процедура закончена

proc start

	int3
	mov [SomeData],32167
	call SomeProc;вызываем процедуру

	nop

	ret
endp

section '.data' readable writeable
SomeData dd ?

В процедуре из этого примера два возврата, и после каждого исполнение процедуры заканчивается и управление передаётся в процедуру “start” на команду nop.
Что касается макросов locals и endl, то они нужны для объявления локальных переменных. Делается это следующим образом: в начале процедуры вызывается
макрос locals, после него объявляются переменные по тем же правилам, что и глобальные переменные, и после них вызывается макрос endl, приведу пример:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc Procedure;начало процедуры
locals
Var1 dq ?
Var2 dq ?
endl

	mov [Var1],rax;записываем значение rax в локальную переменную
	mov [Var2],rdx;rdx - в другую

	mov rcx,[Var1];читаем значение одной из переменных в rcx
	xor rcx,[Var2];ксорим rcx со значением другой локальной переменной

ret;возврат из процедуры
endp;процедура закончена

proc start

	int3
	call Procedure;вызываем процедуру

	ret
endp

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

Соглашения о вызове

К этому моменту мы научились объявлять процедуры и их локальные переменные с помощью макросов, но помимо этого процедуры могут принемать параметры и возвращать
значения. Для этого существуют соглашения о вызове. Соглашение о вызове это — способ передачи параметров в функцию и возврата из неё знпчений. В принципе,
на ассемблере, вы можете изобрести какие угодно соглашения о вызове, и их использовать, но для того, чтобы вызывать процедуры из чужого кода нужно заранее
договориться о способе передачи параметров и возврате значений, для этого существуют стандартные соглашения о вызове. Если погуглить, то можно выяснить,
что в 32-битном режиме есть ряд соглашений о вызове: stdcall, fastcall, cdecl, и т.д. В 64-битном режиме же принято одно соглашение о вызове и заключается
оно в следующем:

    1. Первые 4 параметра передаются через регистры, если это — указатели или целые числа то передаются они в следующем порядке:
      1-rcx, 2-rdx, 3-r8, 4-r9. Если это — числа с плавающей точкой, то они передаются через регистры xmm0-xmm3 (о них я расскажу в последующих статьях).
      При этом в стеке вызывающая функция выделяет место для хранения этих параметров.
    2. Следующие параметры передаются через стек.
    3. После выполнения процедуры стек очищает от переметров вызывающая функция.
    4. Возвращаемое значение функция записывает в rax.
    5. После вызова функции должны остаться неизменными регистры: r12-r15, rdi, rsi, rbx, rbp, остальные регистры общего назначения могут после вызова измениться.

Приведу пример вызова функции SomeFunc с 6 параметрами:

...
sub rsp,6*8;выделяем место в стеке для 6-и параметров

mov rcx,Val1;первый параметр
mov rdx,Val2;второй параметр
mov r8,Val3;третий параметр
mov r9,Val4;четвёртый параметр
mov [rsp+4*8],Val5;пятый - уже через стек
mov [rsp+5*8],Val6;шестой - тоже
call SomeFunc;вызываем функцию
;в регистре rax - возвращённое значение

add rsp,6*8;освобождаем стек от параметров
...

В ринципе так мы больше писать не будем, потому что существуют макросы fastcall и invoke, отличаются они тем, что если в fastcall стоит инструкция
call Func, то в invoke стоит call [Func]. Только что приведённый код можно переписать в следующем виде:

...
fastcall SomeFunc,Val1,Val2,Val3,Val4,Val5,Val6
...

Для того, чтобы внутри процедуры обращаться к параметрам по именам нужно перечислить их в макросе proc после имени процедуры, например:

...
proc SomeProc,Param1,Param2,Param3,Param4,Param5,Param6
...

К параметрам можно обращаться также, как к локальным переменным – по именам.ВАЖНО: В НАЧАЛЕ ИСПОЛНЕНИЯПРОЦЕДУРЫ В ПЕРВЫХ 4-Х ПАРАМЕТРАХ ЛЕЖИТ МУСОР,
ОБРАЩАТЬСЯ К НИМ СЛЕДУЕТ ЧЕРЕЗ РЕГИСТРЫ! Приведу пример объявления процедуры с 2-мя параметрами, в следующем коде процедура “Reverse” переворачивает
массив байтов заданной длины.

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc Reverse,Arr,ArrLen

	;rcx-Arr - указатель на массив
	;rdx-ArrLen - длина массива
	;обратите внимание, что к параметрам идёт обращение через регистры,
	;т.к. по адресам Arr и ArrLen лежит мусор, но если нужно сохранить
	;начения этих переменных можно записать их туда:
	;mov [Arr],rcx
	;mov [ArrLen],rdx

	lea rax,[rcx+rdx-1];rax - указатель на массив

	shr rdx,1;дулим rdx на 2
	jz .exit; если в массиве меньше двух элементов - на выход


		.loop:


			mov r8b,[rcx];меняем местами значение по адресу [rcx]
			xchg r8b,[rax]; и [rax]
			mov [rcx],r8b

		inc rcx;увеличеавем rcx
		dec rax;уменьшаем rax
		dec rdx;уменьшаем счётчик
		jnz .loop


	.exit:
	ret
endp

proc start
	locals
	Arr1 db 3,2,1,6,7
	Arr2 db 9,8,7,6,5,4,3,2,1,0
	endl

	int3
	lea rcx,[Arr1]
	fastcall Reverse,rcx,5;вызываем функцию для первого массива

	lea rcx,[Arr2]
	fastcall Reverse,rcx,10;вызываем функцию для второго массива

	ret
endp

Скажу ещё пару слов о выделении памяти в стеке и передаче параметров.
Если вы взгланите на код процедуры start (см. Рис.1), то увидите, что для вызова процедур постоянно выделяется и освобождается память в стеке
(выделено красным), чтобы это оптимизировать нужно вызывать макросы frame и endf в начале и в конце процедуры соответственно, при их использовани
память выделяется в начале процедуры и освобождается в конце, пример:

proc start
	locals
	Arr1 db 3,2,1,6,7
	Arr2 db 9,8,7,6,5,4,3,2,1,0
	endl
	frame

	int3
	lea rcx,[Arr1]
	fastcall Reverse,rcx,5;вызываем функцию для первого массива

	lea rcx,[Arr2]
	fastcall Reverse,rcx,10;вызываем функцию для второго массива

	endf
	ret
endp

Из всего выше описанного очевидно, что использование регистра rbp не обязательно, и это можно исправить, достаточно в программе вставить следующие строки:

  prologue@proc equ static_rsp_prologue
  epilogue@proc equ static_rsp_epilogue
  close@proc equ static_rsp_close

Они заставят использовать вместо rbp только rsp, и rbp тогда можно использовать в своих целях. ОБРАТИТЕ ВНИМАНИЕ:

    1. ИСПОЛЬЗОВАТЬ ТЛЬКО RSP БУДУТ ПРОЦЕДУРЫ СТОЯЩИЕ ПОСЛЕ ЭТИХ СТРОК
    2. В ЭТОМ РЕЖИМЕ НЕ НУЖНО ИСПОЛЬЗОВАТЬ frame и endf

Чтобы Вернуть обратно rbp нужно вставить строки:

  prologue@proc equ prologuedef
  epilogue@proc equ epiloguedef
  close@proc equ

На этом — всё.

Комментарии

l_inc05.04.2016 02:46

Продолжаем традицию… 🙂

push <op>, в качестве операнда может выступать регистр общего назначения или память

Или числовая константа.

Сразу скажу, что обращаться к стеку можно только по адресам большим текущего значения rsp, так что не рекомендуется обращаться по адресам [rsp-xx]

Вообще, “нельзя” и “не рекомендуется” — немного разные вещи. По адресам меньше верхушки стека вполне можно обращаться, но не стоит к этому привыкать. Хотя мне неизвестно ни одной веской причины, по которой в пользовательском режиме это делать плохо.

ret означает возврат из процедуры, т.е. там, где в процедуре вы поставите ret fasm поставит что-то вроде mov rsp,rbp / pop rbp / retn .

Зачем гадать, что поставит fasm, если это элементарно проверить. Макрос ret по умолчанию (если не заменять epilogue@proc ) раскрывается в leave + retn .

в начале процедуры вызывается макрос locals, после него объявляются переменные по тем же правилам, что и глобальные переменные

Не совсем по тем же. Нельзя объявить безымянную инициализированную переменную. Впрочем, это можно отнести скорее к проблемам макросов, чем к документированному поведению.

Соглашение о вызове это — способ передачи параметров в функцию и возврата из неё знпчений.

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

После вызова функции должны остаться неизменными регистры: r12-r15, rdi, rsi, rbx, rbp, остальные регистры общего назначения могут после вызова измениться.

rsp ещё не меняется.

для вызова процедур постоянно выделяется и освобождается память в стеке (выделено красным), чтобы это оптимизировать нужно вызывать макросы frame и endf в начале и в конце процедуры соответственно

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

P.S. Такая туча опечаток… Понравилось: “Дулим rdx на 2”.

qwe8013 l_inc05.04.2016 17:59

>>В частности, выравнивание стэка (очень важно, но упомянуто не было)
Вот дебил, забыл написать.

>Продолжаем традицию… 🙂
Действительно, хорошая традиция, стоило бы мне допускать меньше неточностей, всё ещё надеюсь, что мои статьи кому-нибудь помогут.

Программы для развития.

As

Прикреплённые файлы к статье

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

Начало

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

Что такое DLL?

DLL – Dynamic Link Library (Динамически подключаемая библиотека). Она представляет собой исполняемый файл (PE). С точки зрения структуры заголовков исполняемого
файла DLL почти не отличается от EXE. Впрочем, в данной статье я не буду описывать заголовки PE, а ограничусь лишь упоминанием таблицы импорта. Сразу скажу, что
у DLL есть точка входа, в DLL хранят ресурсы и код, таким образом программа, загружая к себе в
адресное пространство DLL, получает доступ ко всему что в ней хранится. Сейчас нам важен код, и далее мы поговорим о нем. Для того чтобы получить доступ к коду, нужно узнать, где именно
в памяти хранится нужный нам код. Для этого в DLL существует таблица экспорта, она содержит информацию о соответствии между определёнными смещениями в DLL и
их именами или номерами. Имена хранятся в формате ANSI. В принципе, по смещению заданному именем (или номером) может находиться как код функции, так и данные,
но обычно там – код.

API

API – application programming interface (интерфейс программирования приложений). В API входят системные функции, которые вызывает программа при необходимости.
Все эти функции хранятся в системных DLL-библиотеках и указаны там в таблицах экспорта. Сразу оговорюсь, что при вызове этих функций значение регистра rsp
должно быть кратно 16, если вы вызываете функции не используя стандартные макросы это необходимо учитывать, со стандартными макросами об этом можно не
беспокоиться, если конечно вы не “дёргаете” самостоятельно rsp.

Импорт

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

Статический импорт

Статический импорт осуществляется с помощью таблицы импорта, в которой записаны имена библиотек и имена функций, импортируемых из них. Начинается таблица импорта
в коде с 2-х слов: data import, а заканчивается end data. Внутри таблицы импорта нужно написать, какие библиотеки нам нужны, и какие функции мы будем
импортировать. Делается это примерно так (слеш в конце строки означает, что эту и следующую строку надо интерпретировать как одну):

data import
library kernel32,'kernel32.dll',user32,'user32.dll'
import kernel32,\
		CreateFileA,'CreateFileA',\
		CloseHandle,'CloseHandle'

import user32,\
		MessageBoxA,'MessageBoxA'
end data

В данном примере мы сначала объявляем, какие библиотеки нам нужны и каждому имени библиотеки ставим в соответствие идентификатор (идентификатор – это набор символов,
с помощью которого можно обращаться к соответствующим объектам, например: имя переменной – идентификатор, имя метки – тоже, и т.д.): kernel32 – для kernel32.dll,
user32 – для user32.dll. Потом с помощью макроса import мы перечисляем функции, которые хотим импортировать из данной библиотеки. Первый аргумент этого макроса —
идентификатор из library, а дальше идут пары: имя функции для использования из кода, и имя функции в кавычках, как оно записано в таблице экспорта. Для примера
рассмотрим программу, которая выводит на консоль долгожданную строку “Hello world!”:

format PE64 console; формат -- 64 бита, подсистема -- console
entry start;точка входа -- start

include 'win64a.inc';заголовки с нужными макросами

section '.code' readable executable code;секция кода

proc start;точка входа
	locals;локальные переменные
	temp dd ?;переменная, в неё будет записано количество символов при вводе и выводе
	endl
	frame

	invoke GetStdHandle,STD_OUTPUT_HANDLE;получение описателя вывода
	lea rdx,[Text]
	lea r9,[temp]
	invoke WriteConsoleA,rax,rdx,14,r9,0;вывод на консоль

	invoke GetStdHandle,STD_INPUT_HANDLE;получение описателя ввода
	lea rdx,[temp]
	lea r9,[temp]
	invoke ReadConsoleA,rax,rdx,1,r9,0;читаем с консоли до нажатия Enter

	invoke ExitProcess,0;завершаем процесс
	endf
	ret;возврат из процедуры
endp

section '.data' readable writeable data;секция данных
data import;начало таблицы импорта
library kernel32,'kernel32.dll';импортируем только из kernel32.dll
import kernel32,\;функции:
	   GetStdHandle,'GetStdHandle',\
	   WriteConsoleA,'WriteConsoleA',\
	   ReadConsoleA,'ReadConsoleA',\
	   ExitProcess,'ExitProcess'
end data
Text db 'Hello world!',13,10;текст для вывода

Сразу скажу, что без строки include 'win64a.inc' нельзя использовать макросы, которые тут применяются. Здесь мы объявляем одну локальную переменную temp
для записи туда ненужных для нас данных. Дальше мы вызываем функцию GetStdHandle и первым (и единственным) параметром к ней идёт STD_OUTPUT_HANDLE.
STD_OUTPUT_HANDLE – это константа. Константы объявляются так: имя константы=значение, в данном случае: STD_OUTPUT_HANDLE = -11, пока что я не буду на этом
заострять внимание. Сама же функция GetStdHandle возвращает (в rax) хендлы ввода, вывода или ошибок. Хендл (или описатель) — это некоторое “магическое число” нужное
для доступа к некоторым системным ресурсам. В этой программе мы один раз получаем хендл вывода, и в другой раз — хендл ввода, эти хендлы можно спользовать сколько
угодно раз в программе. После получения описателя вывода мы сразу передаём его в функцию WriteConsoleA
Она выводит на консоль текст и имеет параметры:

    1. Хендл вывода
    2. Указатель на строку в формате ANSI
    3. Количество символов для вывода
    4. Указатель на переменную типа dword, в которую будет записано число выведенных символов
    5. должен быть 0

Заметьте, здесь я положил в rdx значение второго параметра, поэтому макрос invoke не добавит дополнительных обращений к rdx. Важно: если вы используете для передачи
параметров в функцию регистры rcx,rdx,r8,r9, тогда вы должны передавать rcx как первый параметр, rdx – второй, r8 – третий, r9 – четвёртый. Никогда не делайте так:

lea rcx,[SomeData]
invoke SomeFunc,FirstParam,rcx,...

Это может привести к ошибкам.
Далее получаем хендл вывода и передаём в функцию ReadConsoleA. Она читает введённую
строку из консоли, у неё аналогичные параметры. Только первый параметр — хендл ввода, а второй — указатель на область памяти, в которую будут записаны прочитанные с консоли символы. Как вы могли заметить,
я передал в качестве указателя на строку адрес той же переменной temp и передал в качестве длины входящей строки 1 символ. Я это сделал, т.к. ни количество введённых
символов, ни введённая строка нас в данном случае не интересуют. В конце вызывается функция ExitProcess, она завершает процесс c кодом завершения, передаваемым в
качестве параметра, поэтому вызов ret в конце процедуры не обязателен.

И тут нужно сказать про директиву include… Она… Как бы это… Ну, вон там вот… В общем, она вставляет в исходник содержимое файла, имя которого следует после неё.
Например строка include 'win64a.inc' вставляет в исходник содержимое файла “win64a.inc”, он входит в стандартную поставку fasm-а в папке “INCLUDE”.
Если вы его откроете, то увидите ещё кучу разных include-ов.

Вообще-то говоря, можно было бы использовать и юникодную версию функций, просто вместо A в конце имени функции нужно ставить W, и Text объявить не как db, а как du, да и
у ReadConsoleW последний параметр не обязан быть нулём.

Очевидно, что постоянно прописывать все импортируемые функции не удобно, поэтому в стандартной поставке fasm-а есть файлы в INCLUDE\API, которые облегчат нам жизнь.
Просто вместо макроса import вставляете include 'api\kernel32.inc', аналогично и со всеми библиотеками для которых есть инклуды в этой папке:

data import;начало таблицы импорта
library kernel32,'kernel32.dll'
include 'api\kernel32.inc'
end data

Помимо макроса import в этих инклудах используется макрос api. Этот макрос предназначен для функций, у которых есть 2 варианта: ANSI и Unicode. У ANSI функций
в конце названия стоит буква A, у Юникод — W. Так вот, макрос api позволяет пользоваться функцией не указывая A или W. Работает это так: если в начале файла стоит
include 'win64a.inc', то все обращения к ReadConsole (например) будут заменены на ReadConsoleA, а если стоит include 'win64w.inc', то ReadConsoleW.
Пример той же программы, только – Юникод:

format PE64 console
entry start

include 'win64w.inc'

section '.code' readable executable code

proc start
	locals
	temp dd ?
	endl
	frame

	invoke GetStdHandle,STD_OUTPUT_HANDLE
	lea rdx,[Text]
	lea r9,[temp]
	invoke WriteConsole,rax,rdx,14,r9,0

	invoke GetStdHandle,STD_INPUT_HANDLE
	lea rdx,[temp]
	lea r9,[temp]
	invoke ReadConsole,rax,rdx,1,r9,0

	invoke ExitProcess,0
	endf
	ret
endp

section '.data' readable writeable data
data import;начало таблицы импорта
library kernel32,'kernel32.dll'
include 'api\kernel32.inc'
end data
Text du 'Hello world!',13,10

У меня есть программа для генерации import-инклудников. Её вместе с моими инклудами я прикреплю к статье, правда после создания инклудника иногда возникают 2 проблемы:

    1. В разных инклудах совпадают функции, соответственно, где-то нужно их удалить или закомментировать.
    2. Имена функций совпадают со словами, используемыми fasm-ом, надо менять имя функции !!НЕ ИМЯ В КАВЫЧКАХ, А ИМЯ ИДЕНТИФИКАТОРА!!

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

Динамический импорт

Динамический импорт осуществляется в явном виде программой. Для начала нужно либо загрузить нужную библиотеку, либо получить указатель на уже загруженную. Чтобы получить указатель
на уже загруженную библиотеку надо использовать функцию GetModuleHandle (A/W) с единственным параметром — указателем на имя библиотеки,
она вернёт адрес, где расположена библиотека, либо же 0 в случае ошибки. В WinApi (не в Native API) в качестве строк используются строки с нулем на конце, т.е. последний
символ должен быть нулевым. Чтобы загрузить библиотеку надо использовать функцию LoadLibrary (A/W), у которой тоже 1 параметр:
имя библиотеки, эта функция возвращает либо указатель на загруженную библиотеку, либо 0 в случае ошибки. На каждый вызов LoadLibrary в вашем коде должен присутствовать
вызов FreeLibrary, освобождающий память, в которой хранится библиотека. Ну и наконец, чтобы найти нужную функцию, используется функция GetProcAddress с 2-мя параметрами: первый — адрес загрузки библиотеки, второй — указатель на строку в
формате ANSI с именем функции. Обратите внимание, что нет функций GetProcAddress(A/W), есть только одна, и она принимает строку в ANSI.
Пример:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start
	locals
	temp dd ?
	endl
	frame

	lea rcx,[Msvcrt_name]
	invoke LoadLibrary,rcx;загружаем библиотеку msvcrt.dll
	mov [hMsvcrt],rax

	lea rdx,[printf_name]
	invoke GetProcAddress,[hMsvcrt],rdx;получаем адрес функции printf
	mov [printf],rax

	lea rcx,[K32_name]
	invoke GetModuleHandle,rcx;получаем адрес kernel32.dll

	lea rdx,[WriteConsole_name]
	invoke GetProcAddress,rax,rdx;получаем адрес функции WriteConsole
	mov [WriteConsole],rax

	;Дальше всё то же самое, как и в предыдущем примере, за исключением вызова printf

	invoke GetStdHandle,STD_OUTPUT_HANDLE
	lea rdx,[Text]
	lea r9,[temp]
	invoke WriteConsole,rax,rdx,19,r9,0

	lea rcx,[Text1]
	invoke printf,rcx;вызов printf

	invoke GetStdHandle,STD_INPUT_HANDLE
	lea rdx,[temp]
	lea r9,[temp]
	invoke ReadConsole,rax,rdx,1,r9,0

	invoke FreeLibrary,[hMsvcrt];освобождаем память

	invoke ExitProcess,0
	endf
	ret
endp

section '.data' readable writeable data
data import;начало таблицы импорта
library kernel32,'kernel32.dll'
import kernel32,\
	   GetModuleHandle,'GetModuleHandleA',\
	   LoadLibrary,'LoadLibraryA',\
	   FreeLibrary,'FreeLibrary',\
	   GetProcAddress,'GetProcAddress',\
	   GetStdHandle,'GetStdHandle',\
	   ReadConsole,'ReadConsoleA',\
	   ExitProcess,'ExitProcess'
end data
Text db 'WriteConsole used',13,10
Text1 db 'printf used',13,10,0

printf_name db 'printf',0
WriteConsole_name db 'WriteConsoleA',0
Msvcrt_name db 'msvcrt.dll',0
K32_name db 'kernel32.dll'

hMsvcrt dq ?;адрес загрузки msvcrt.dll
printf dq ?;адрес printf
WriteConsole dq ?;адрес WriteConsole

Скажу так-же, что если загрузить одну и ту же библиотеку n раз, тогда не будет n копий этой библиотеки, а будет одна, но освободится она только после n вызовов FreeLibrary.

На этом, как говорится, “полномочия этой статьи — всё”.

Комментарии

l_inc18.04.2016 02:20

Надо сказать, в этой статье серьёзных технических недочётов или упущений я не заметил. Есть, правда, педагогические, стилистические и грамматические, а из технического несколько мелочей. Но кое-какие комментарии, я думаю, не помешают, раз автор (исходя из реакции на предыдущие комментарии) не против. 🙂

DLL – Dynamic Link Library (Динамически подключаемая библиотека).

Вообще, более точный перевод “динамически связываемая библиотека” или, чтобы инициировать процесс возникновения ассоциативных связей, “библиотека динамической компоновки”, но “подключаемая” тоже вполне соответствует смыслу.

программа, загружая к себе в адресное пространство DLL, получает доступ ко всему что в ней хранится

Стоит заметить, что и код dll тоже получает доступ ко всему, что находится в программе. Причём, возможность воспользоваться этим доступом у dll есть сразу в момент попытки её загрузить, что часто используется во вредоносах.

Имена хранятся в формате ANSI

Вообще ANSI – это организация. Хотя исторически эта аббревиатура неудачно используется для ссылки на программу, WinAPI-функцию или даже на строку, требующую применения символьных кодовых страниц, по-хорошему правильно всё-таки сказать “в кодировке ASCII”.

section '.data' readable writeable data;секция данных

Все данные в секции используются только для чтения. Вполне можно убрать у неё writeable и традиционно переименовать в '.rdata' .

Заметьте, здесь я положил в rdx значение второго параметра, поэтому макрос invoke не добавит дополнительных обращений к rdx.

Это, конечно, неплохо, но этот макрос поддерживает спецификатор addr . Поэтому следующий код сделал бы то же самое:

invoke WriteConsoleA,rax,addr Text,14,addr temp,0

И, кстати, неплохо бы не привыкать использовать числовые константы. Вместо 14 лучше объявить автоматически вычисляемую sizeof.Text или ещё лучше countof.Text или lengthof.Text , т.к. sizeof всё же обычно относится к размеру в байтах, а не к длине в символах.

Важно: если вы используете для передачи параметров в функцию регистры rcx,rdx,r8,r9, тогда вы должны передавать rcx как первый параметр, rdx – второй, r8 – третий, r9 – четвёртый. Никогда не делайте так … Это может привести к ошибкам.

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

поэтому вызов ret в конце процедуры не обязателен.

Да вообще-то и точку входа-то оформлять как процедуру в связи с этим не стоит, т.к. не нужны ни пролог, ни эпилог, ни сохранение регистров. Правда, как без процедуры правильно оформить точку входа — без подробного разбора совсем не очевидно.

Просто вместо макроса import вставляете include 'api\kernel32.inc', аналогично и со всеми библиотеками для которых есть инклуды в этой папке

Тут важно обращать внимание на совпадение идентификатора, указываемого в library, с идентификатором внутри этих файлов. Т.к. при явном использовании макроса import этот идентификатор можно выбирать произвольно.

Пример той же программы, только – Юникод

Ну раз уж в этом примере используются универсальные имена функций, а не с суффиксом W, то хорошо бы и вместо du использовать TCHAR, чтобы при обратной замене на win64a.inc программа работала адекватно. И, возможно, стоило также упомянуть про подключение соответствующего файла кодировки (например, include 'encoding\utf8.inc' ), иначе юникод будет правильным только для младшей половины ASCII-таблицы.

надо использовать функцию GetModuleHandle (A/W) с единственным параметром — указателем на имя библиотеки, она вернёт адрес, где расположена библиотека, либо же 0 в случае ошибки

В общем случае как GetModuleHandle, так и LoadLibrary возвращают не адрес, а хэндл, о чём и говорит документация. Этот хэндл отличается от адреса загрузки библиотеки специальным значением пары-тройки младших бит, которые выставляются в 1 для нестандартно подгруженных библиотек (например, с флагом LOAD_LIBRARY_AS_IMAGE_RESOURCE ).

На каждый вызов LoadLibrary в вашем коде должен присутствовать вызов FreeLibrary

На каждый успешный вызов LoadLibrary . Возможно это и очевидно, но стоит уточнить, что если DllMain библиотеки вернёт ошибку, то библиотека будет выгружена автоматически без FreeLibrary .

Яндекс.Метрика