Програмирование на 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.

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

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