Кроме инструкции безусловного перехода JMP в ассемблере NASM есть ряд инструкций, которые выполняют условные переходы в зависимости от установки битов в регистре флагов EFLAGS. В частности, в этом регистре учитываются четыре бита, которые еще называют флагами состояния, - carry (флаг переноса), overflow (флаг переполнения), sign (флаг знака) и zero (флаг нуля):
CF (флаг переноса): устанавливается, если происходит беззнаковое переполнение, то есть при сумме с переносом или вычитании с заимствованием (например, при сумме чисел 0FFh и 01h). Если переполнение не происходит, то флаг не устанавливается.
OF (флаг переполнения): установливается, если происходит переполнение со знаком, а именно когда переполняется бит, следующий за старшим
знаковым битом. Например, при сумме чисел 7Fh и 01h. В двоичной системе это будет операция 0111 1111 + 0000 0001
, результатом которой формально
будет число 1000 0000
. Таким образом, произошло изменение старшего знакового бита.
SF (флаг знака): устанавливается, если старший бит результата установлен. В противном случае флаг знака сброшен (то есть флаг знака отражает состояние старшего бита результата).
ZF (флаг нуля): устанавливается, если результат вычисления дает 0. Если результат ненулевой, флаг сброшен
Эти флаги устанавливаются в результате различных операций. В частности, инструкции, которые выполняют различные математические или логические операции, типа add, sub, and, or, xor и not влияют на установку флагов, а инструкции загрузки данныех типа mov или lea не влияют.
Для проверки этих флагов и выполнения условного перехода в ассемблере есть следующие инструкции:
jc: выполняет переход к метке, если флаг переноса установлен
jnc: выполняет переход к метке, если флаг переноса НЕ установлен
jo: выполняет переход к метке, если флаг переполнения установлен
jno: выполняет переход к метке, если флаг переполнения не установлен
js: выполняет переход к метке, если флаг знака установлен
jns: выполняет переход к метке, если флаг знака не установлен
jz: выполняет переход к метке, если флаг нуля установлен
jnz: выполняет переход к метке, если флаг нуля не установлен
Все эти инструкции принимают один операнд - метку, к которой выполняется переход. Например, возьмем следующую программу для Linux:
global _start section .text _start: mov al, 255 add al, 3 ; AL = AL + 3 jc carry_set ; если флаг переноса установлен, переход к метке carry_set mov rdi, 2 ; если флаг переноса не установлен, RDI = 2 jmp exit carry_set: ; если флаг переноса установлен mov rdi, 4 ; RDI = 4 exit: ; метка exit mov rax, 60 syscall
Здесь в регистр AL загружаем число 255. Затем с помощью инструкции add
складываем значение из регистра AL с числом 3:
mov al, 255 add al, 3 ; AL = AL + 3
Если произошел перенос, то устанавливается флаг переноса CF. С помощью инструкции jc carry_set
проверяем, имеет ли место перенос. И если есть перенос, и
соответственно флаг переноса (флаг CF) установлен в 1, то выполняем переход к метке carry_set, где в регистр rdi помещается число 4:
jc carry_set ; если флаг переноса установлен, переход к метке carry_set .................... carry_set: ; если флаг переноса установлен mov rdi, 4 ; RDI = 4
Если флаг переноса НЕ установлен, то выполняем последующие инструкции - в регистр RDI помещаем число 2 и переходим к метке exit:
mov rdi, 2 ; если флаг переноса не установлен, RDI = 2 jmp exit
В данном случае поскольку регистр AL - 8 разрядный, а максимальное 8-разрядное число - 255, то при сложении с числом 3 результат будет равен
258
или 0b1_0000_0010
, который не помещается в один 8-разрядный регистр AL. Соответственно будет выполняться перенос и будет происходить переход к
метке carry_set, а в регистре RDI окажется число 4.
Аналогичный пример для Windows:
global _start section .text _start: mov al, 255 add al, 3 ; AL = AL + 3 jc carry_set ; если флаг переноса установлен, переход к метке carry_set mov rax, 2 ; если флаг переноса не установлен, RAX = 2 jmp exit carry_set: ; если флаг переноса установлен mov rax, 4 ; RAX = 4 exit: ; метка exit ret
Подобным образом можно работать с другими инструкциями условного перехода. Например:
global _start section .text _start: mov rcx, 5 mov rdx, 5 sub rdx, rcx ; RDX = RDX - RCX jz zero_set ; если флаг нуля установлен, переход к метке zero_set mov rdi, 2 ; если флаг нуля не установлен, RDI = 2 jmp exit zero_set: ; если флаг нуля установлен mov rdi, 4 ; RDI = 4 exit: mov rax, 60 syscall
Здесь помещаем в регистры RDX и RCX некоторые числа и вычитаем из значения регистра RDX число регистра RCX. Если эти числа равны, то результатом будет число ноль, и будет усоановлен флаг нуля ZF. То есть флаг нуля позволяет проверить два числа на равенство при применении вычитания. И в данном случае мы проверяем установку флага нуля, и если он установлен, переходим к метке zero_set.
Используя подобные инструкции, легко создать циклические конструкции:
global _start section .text _start: mov rcx, 5 mov rdi, 0 loop: add rdi, 2 ; RDI = RDI + 2 dec rcx ; RCX = RCX - 1 jnz loop ; если флаг нуля НЕ установлен, переход обратно к метке loop mov rax, 60 syscall
Здесь регистр выступает в роли счетчика. После метки loop фактически идет цикл. В цикле увеличиваем значение регистра RDI на 2. И затем уменьшаем значение регистра RCX на 1.
Далее с помощью инструкции jnz loop
проверяем флаг нуля. Если флаг нуля НЕ установлен (то есть если в RCX НЕ нулевое значение), то переходим обратно к метке loop и повторяем действия.
Если вдруг после вычитания в регистре RCX оказался ноль, то выходим из этого цикла и переходим к следующей инструкции - mov rax, 60
Ассемблер предоставляет ряд инструкций для управления флагами состояния. Отмечу основные:
clc: сбрасывает в 0 флаг переноса (CF)
setc: устанавливает флаг переноса (CF)
lahf: копирует флаги состояния из регистра eflags в регистр ah
sahf: сохраняет флаги состояния из регистра ah в регистр eflags
Инструкции lahf и sahf применяют следующий порядок битов для флагов, начиная с младшего бита:
Флаг переноса (CF)
Всегда равен 1
Флаг паритетности (PF)
Всегда равен 0
Дополнительный флаг переноса (AF)
Всегда равен 0
Флаг нуля (ZF)
Флаг знака (SF)
Биты 1, 3, и 5 не используются.