Какой флаг будет проверяться при выполнении инструкции условного перехода je

Оглавление


                           Условные переходы
-----------------------------------------------------------------

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

     Инструкция условного  перехода может  осуществлять  или  нет
переход на целевую (указанную в ней) метку, в зависимости от сос-
тояния регистра флагов. Рассмотрим следующий пример:

             .
             .
             .
             mov   ah,1           ; функция DOS ввода с клавиату-
                                  ; ры
             int   21h            ; получить следующую нажатую
                                  ; клавишу
             cmp   al,'A'         ; была нажата буква "A"?
             je    AWasTyped      ; да, обработать ее
             mov   [TampByte], al ; нет, сохранить символ
             .
             .
             .
    AWasTyped:
             push   ax            ; сохранить символ в стеке
             .
             .
             .

     Сначала в данной программе с  помощью  функции  операционной
системы DOS  воспринимается нажатая клавиша.  Затем для сравнения
введенного символа с символом A используется инструкция  CMP. Эта
инструкция аналогична инструкции SUB,  только ее выполнение ни на
что не влияет,  поскольку назначение данной инструкции состоит  в
том,  чтобы можно было сравнить два операнда, установив флаги так
же,  как это делается в инструкции SUB. Поэтому в предыдущем при-
мере  флаг нуля устанавливается в значение 1 только в том случае,
если регистр AL содержит символ A.

     Теперь мы подошли к основному моменту. Инструкция JE  предс-
тавляет  инструкцию условного перехода, которая. осуществляет пе-
редачу управления только в том случае, если флаг нуля равен 1.  В
противном  случае выполняется инструкция, непосредственно следую-
щая за инструкцией JE (в данном случае -  инструкция  MOV).  Флаг
нуля  в  данном  примере будет установлен только в случае нажатия
клавиши A, и только в этом случае процессор 8086 перейдет  к  вы-
полнению инструкции с меткой AWasTyped, то есть инструкции PUSH.

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

     Перечень инструкций  условных переходов приводится в Таблице
5.2.

            Инструкции условных переходов             Таблица 5.2
-----------------------------------------------------------------
Название             Значение                   Проверяемые флаги
-----------------------------------------------------------------
JB/JNAE   Перейти, если меньше / перейти, если     CF = 1
          не больше или равно

JAE/JNB   Перейти, если больше или равно / пе-     CF = 0
          рейти, если не меньше

JBE/JNA   Перейти, если меньше или равно / пе-  CF = 1 или ZF = 1
          рейти, если не больше

JA/JNBE   Перейти, если больше / перейти, если    CF = 0 и ZF = 0
          не меньше или равно

JE/JZ     Перейти, если равно                       ZF = 1

JNE/JNZ   Перейти, если не равно                    ZF = 0

JL/JNGE   Перейти, если меньше чем / перейти,       SF = OF
          если не больше чем или равно

JGE/JNL   Перейти, если больше чем или равно /      SF = OF
          перейти, если не меньше чем

JLE/JNLE  Перейти, если меньше чем или равно / ZF = 1 или SF = OF
          перейти, если не больше, чем

JG/JNLE   Перейти, если больше чем / перейти,  ZF = 0 или SF = OF
          если не меньше чем или равно

JP/JPE    Перейти по четности                       PF = 1

JNP/JPO   Перейти по нечетности                     PF = 0

JS        Перейти по знаку                          SF = 1

JNS       Перейти, если знак не установлен          SF = 0

JC        Перейти при наличии переноса              CF = 1

JNC       Перейти при отсутствии переноса           CF = 0

JO        Перейти по переполнению                   OF = 1

JNO       Перейти при отсутствии переполнения       OF = 0
-----------------------------------------------------------------

      CF - флаг переноса,  SF - флаг знака,  OF - флаг переполне-
ния, ZF - флаг нуля, PF - флаг четности

     Более подробная информация об инструкциях-синонимах и  общие
сведения  об инструкциях перехода содержатся в Главе 6. Там также
подробно рассказывается о способах, с помощью которых  инструкции
процессора 8086 могут изменять регистр флагов.

     Несмотря на свою  гибкость,  инструкции  условного  перехода
имеют также серьезные ограничения, поскольку переходы в них всег-
да короткие.  Другими словами целевая метка, указанная в инструк-
ции условного перехода, должна отстоять от инструкции перехода не
более, чем на 128 байт.  Например,  Турбо Ассемблер не может  ас-
семблировать:

              .
              .
              .
 JumpTarget:
              .
              .
              .
              DB   1000 DUP (?)
              .
              .
              .
              dec   ax
              jnz   JumpTarget
              .
              .
              .

так как метка JumpTarget отстоит от инструкции JNZ более  чем  на
1000 байт. В данном случае нужно сделать следующее:

              .
              .
              .
   JumpTarget:
              .
              .
              .
              DB   1000 DUP (?)
              .
              .
              .
              dec   ax
              jnz   SkipJump
              jmp   JumpTarget
   SkipJump:
              .
              .
              .

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



                                 Циклы
-----------------------------------------------------------------

     Одним из видов конструкций в программе, которые можно  пост-
роить  с  помощью  условных переходов, являются циклы. Цикл - это
просто-напросто блок кода, завершающийся условным переходом, бла-
годаря  чему данных блок может выполняться повторно до достижения
условия завершения. Возможно, вам уже знакомы  такие  конструкции
циклов,  как  for  и while в языке Си, while и repeat в Паскале и
FOR в Бейсике.

     Для чего используются циклы? Они служат для работы с  масси-
вами, проверки состояния портов ввода-вывода до получения опреде-
ленного состояния, очистки блоков памяти, чтения строк с  клавиа-
туры  и  вывода их на экран и т.д. Циклы - это основное средство,
которое используется для выполнения повторяющихся действий.  Поэ-
тому  используются они довольно часто, настолько часто, что в на-
боре инструкций процессора 8086 предусмотрено фактически несколь-
ко инструкций циклов: LOOP, LOOPNE, LOOPE и JCXZ.

     Давайте рассмотрим сначала инструкцию LOOP. Предположим,  мы
хотим  вывести 17 символов текстовой строки TestString. Это можно
сделать следующим образом:

           .
           .
           .
           .DATA
 TestString        DB   'Это проверка! ...'
           .
           .
           .
           .CODE
           .
           .
           .
           mov   cx,17
           mov   bx,OFFSET TestString
 PrintStringLoop:
           mov   dl,[bx]              ; получить следующий
                                      ; символ
           inc   bx                   ; ссылка на следующий
                                      ; символ
           mov   ah,2                 ; функция DOS вывода на
                                      ; экран
           int   21h                  ; вызвать DOS для вывода
                                      ; символа
           dec   cx                   ; уменьшить счетчик длины
                                      ; строки
           jnz   PrintStringLoop      ; обработать следующий
                                      ; символ, если он имеется
           .
           .
           .

     Есть, однако, лучший способ. Возможно, вы помните, что ранее
мы  уже упоминали о том, что регистр CX весьма полезно бывает ис-
пользовать для организации циклов. Инструкция:

           loop   PrintStringLoop

делает то же, что и инструкции:

           dec   cx
           jnz   PrintStringLoop

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

     Как же строятся циклы с более сложным  условием  завершения,
чем  обратный отсчет значения счетчика? Для таких случаев предус-
мотрены инструкции LOOPE и LOOPNE.

     Инструкция LOOPE работает также, как инструкция LOOP, только
цикл  при ее выполнении будет завершаться (то есть перестанут вы-
полняться переходы),  если регистр CX примет значение 0 или  флаг
нуля будет установлен в значение 1 (нужно помнить о том, что флаг
нуля устанавливается в значение 1, если результат последней ариф-
метической операции был нулевым или два операнда в последней опе-
рации сравнения не совпадали).  Аналогично, инструкция LOOPNE за-
вершает  выполнение цикла,  если регистр CX принял значение 0 или
флаг нуля сброшен (имеет нулевое значение).

     Предположим, вы хотите повторять цикл, сохраняя коды нажатых
клавиш, пока не будет нажата клавиша ENTER или не будет накоплено
128 символов. Для выполнения такой работы  можно  написать  такую
программу (где используется инструкция LOOPNE):

          .
          .
          .
          .DATA
  KeyBuffer      DB   128 DUP (?)
          .
          .
          .
          .CODE
          .
          .
          .
          mov   cx,128
          mov   bx,OFFSET KeyBuffer
 KeyLoop:
          mov   ah,1              ; функция DOS ввода с
                                  ; клавиатуры
          int   21h               ; считать следующую
                                  ; клавишу
          mov   [bx],al           ; сохранить ее
          inc   bx                ; установить указатель
                                  ; для следующей клавиши
          cmp   al,0dh            ; это клавиша ENTER?
          loopne KeyLoop          ; если нет, то получить
                                  ; следующую клавишу, пока
                                  ; мы не достигнем максимально-
                                  ; го числа клавиш
          .
          .
          .

     Инструкция  LOOPE  известна  также,  как  инструкция  LOOPZ,
инструкция  LOOPNE  - как инструкция LOOPNZ, также как инструкции
JE эквивалентна инструкция JZ (это инструкции-синонимы).

     Имеется еще одна  инструкция  цикла.  Это  инструкция  JCXZ.
Инструкция  JCXZ  осуществляет  переход только в том случае, если
значение регистра CX равно 0. Это дает удобный  способ  проверять
регистр  CX  перед началом цикла. Например, в следующем фрагменте
программы, при обращении к которому регистр BX указывает на  блок
байт, которые  требуется  обнулить,  инструкция JCXZ используется
для пропуска тела цикла в том случае,  если регистр CX имеет зна-
чение 0:

         .
         .
         .
         jcxz   SkipLoop         ; если CX имеет значение 0, то
                                 ; ничего делать не надо
 ClearLoop:
         mov   BYTE PTR [si],0   ; установить следующий байт в
                                 ; значение 0
         inc   si                ; ссылка на следующий очищаемый
                                 ; байт
 SkipLoop:
         .
         .
         .

     Почему желательно пропустить выполнение цикла, если значение
регистра  CX  равно  0? Потому что в противном случае значение CX
будет уменьшено до величины 0FFFFh и инструкция  LOOP  осуществит
переход  на  указанную  метку. После этого цикл будет выполняться
65535 раз. Вы же хотели, чтобы значение регистра  CX,  равное  0,
указывало, что требуется обнулить 0 байт,  а не 65536. Инструкция
JCXZ позволяет вам в этом случае быстро  и  эффективно  выполнить
нужную проверку.

     Относительно инструкций циклов можно сделать пару интересных
замечаний. Во-первых, нужно помнить о том, что инструкции циклов,
как и инструкции  условных  переходов,  могут  выполнять  переход
только  на  метку,  отстоящую от инструкции цикла не более чем на
128 байт в ту или другую сторону.  Циклы,  превышающие 128  байт,
требуют  использования  условных  переходов с помощью безусловных
переходов (этот метод описан в предыдущем разделе "Условные пере-
ходы"). Во-вторых, важно понимать, что ни одна из инструкций цик-
лов не влияет на состояние флагов.  Это означает, что инструкция:

            loop   LoopTop

не эквивалентна в точности инструкциям:

            dec   cx
            jnz   LoopTop

поскольку инструкция DEC изменяет флаги переполнения, знака,  ну-
ля,  дополнительного  переноса  и  четности, а инструкция LOOP на
флаги не влияет. Кроме того, использование инструкции DEC не  эк-
вивалентно варианту:

            sub   cx,1
            jnz   LoopTop

поскольку инструкция SUB влияет на флаг  переноса,  а  инструкция
DEC -  нет.  Различия невелики,  но при программировании на языке
Ассемблера важно понимать,  какие именно флаги  устанавливают  те
или иные инструкции.




Оглавление

Команды условных переходов

Команды условных
переходов передают управление в
зависимости от значения некоторого
условия. Они записываются в формате:

Jcond Labl,

где cond 
формальное условие перехода,и передают
управление по метке Labl в случае истинности
заданного условия. В противном случае
выполняется очередная команда программы.

Все условные
переходы являются короткими (тип SHORT).
Это обусловлено тем, что в машинном коде
команд условных переходов лишь один
байт выделен для хранения дистанции
disp8 до метки, который и используется для
вычисления адреса перехода
(IP)=(IP)+disp8(Labl). Отсюда любая команда
условного перехода передает управление
в пределах от (128)
до (+127) байтов.

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

Команды условных
переходов могут передавать управление
в зависимости от различных условий. Все
они могут быть разделены на следующие
подгруппы:

1) команды перехода
по состоянию арифметических флагов;

2) команды перехода
по соотношению между числами;

3) команды перехода
по состоянию регистра CX.

Команды перехода по состоянию арифметических флагов

Команды этой
подгруппы используют в качестве условия
перехода состояние арифметических
флагов. При этом проверяемое условие
перехода cond может принимать следующие
фактические значения:

Z/E 
нуль/равно (ZF=1), NZ/NE 
не нуль/не равно (ZF=0),

C 
наличие переноса (CF=1), NC 
отсутствие переноса (CF=0),

S 
отрицательный результат (SF=1), NS 
положительный результат (SF=0),

P/PE 
четный паритет (PF=1), NP/PO 
нечетный паритет (PF=0),

O 
наличие переполнения (OF=1), NO 
отсутствие переполнения (OF=0).

Здесь через
дробную черту приведены альтернативные
названия одного и того же условия.

Таким образом,
команды условного перехода по состоянию
арифметических флагов в зависимости
от проверяемого условия могут записываться
в следующем виде: JZ/JE, JNZ/JNE; JC, JNC; JS, JNS;
JP/JPE, JNP/JPO; JO, JNO.

Эти команды могут
использоваться после любой команды,
формирующей соответствующий флаг.

Пример 3.93:

DEC CL

JNZ Begin ; Переход,
если (CL) не нуль

. . . . . . . . .

ADD AX, Word ; Сложение
знаковых чисел

JO Overflow ; Переход,
если переполнение

Команды перехода по соотношению между числами

Команды этой
подгруппы используются только после
команды сравнения CMP. В результате
сравнения формируются флаги, позволяющие
проверить все соотношения между числами.
Однако, рассматриваемые команды реализуют
операторы отношений <, >, < =, > =.

Для анализа
результатов сравнения беззнаковых и
знаковых чисел служат различные команды
условного перехода. Это обусловлено
тем, что использование дополнительного
кода для представления знаковых чисел
вносит существенное различие в
интерпретацию результатов сравнения
чисел без знака и со знаком.Например,
код FFh=11111111b как число без знака имеет
значение 255>0, а как число со знаком 
(1)<0.

Для устранения
этой путаницы при сравнении беззнаковых
чисел используются термины «ниже»
(Below) »выше»
(Above), а при сравнении чисел со знаком 
«меньше» (Less) 
«больше» (Greater), выбор которых
поясняется рис.3.6.

Рис. 3.6. Числовые
оси для представления чисел

В соответствии
с этим формальное условие cond команд
условных переходов по соотношению между
беззнаковыми числами может принимать
следующие фактические значения:

B/NAE 
ниже/не выше и не равно, NB/AE 
не ниже/выше или равно, BE/NA 
ниже или равно/не выше, NBE/A 
не ниже и не равно /выше.

Отсюда команды
условных переходов по соотношению между
беззнаковыми числами имеют вид: JB/JNAE,
JNB/JAE, JBE/JNA, JNBE/JA.

Аналогично условие
cond команд условных переходов по
соотношению между знаковыми числами
может принимать значения:

L/NGE 
меньше/не больше и не равно, NL/GE 
не меньше/больше или равно,

LE/NG 
меньше или равно/не больше, NLE/G 
не меньше и не равно/

/больше.

Отсюда команды
условных переходов по соотношению между
знаковыми числами имеют вид: JL/JNGE,
JNL/JGE, JLE/JNG, JNLE/JG.

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

Таблица 3.3

Выбор команд
условных переходов в сочетании с командой
CMP

Условие
перехода

Следующая
за CMP команда

для
чисел без знака

для
чисел со знаком

Приёмник
> источник

JA/JNBE

JG/JNLE

Приёмник
= источник

JE

JE

Приёмник
< > источник

JNE

JNE

Приёмник
< источник

JB/JNAE

JL/JNGE

Приёмник
< = источник

JBE/JNA

JLE/JNG

Приёмник
> = источник

JAE/JNB

JGE/JNL

Для анализа
операндов на равенство, то есть для
реализации операторов отношений = и <
>, необходимо использовать команды JE
и JNE из первой подгруппы. В этом случае
нет различий в сравнении знаковых и
беззнаковых чисел.

Пример 3.94:

Реализовать
ветвление программы в зависимости от
соотношения между знаковым числом
из регистра AL и числом 100.

CMP AL, 100 ;
Сравнение (AL) с числом 100

JGE GE100 ; Переход,
если (AL)>=100

. . . . . . . . ;
Команды для выполнения при (AL)<100

GE100: JG
G100 ; Переход, если (AL)>100

. . . . . . .
. ; Команды для выполнения при (AL)=100

G100: .
. . . . . . . ; Команды для выполнения при
(AL)>100

Соседние файлы в папке Микропроцессорные системы (книга Комаров)

  • #

    08.03.201578.74 Кб53KUDRIASH.TTF

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

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

Безусловные переходы

Безусловный переход — это переход, который выполняется всегда. Безусловный переход осуществляется с помощью команды JMP. У этой команды один операнд, который может быть непосредственным адресом (меткой), регистром или ячейкой памяти, содержащей адрес. Существуют также «дальние» переходы — между сегментами, однако здесь мы их рассматривать не будем. Примеры безусловных переходов:

    jmp metka    ;Переход на метку
    jmp bx       ;Переход по адресу в BX
    jmp word[bx] ;Переход по адресу, содержащемуся в памяти по адресу в BX

Условные переходы

Условный переход осуществляется, если выполняется определённое условие, заданное флагами процессора (кроме одной команды, которая проверяет CX на равенство нулю). Как вы помните, состояние флагов изменяется после выполнения арифметических, логических и некоторых других команд. Если условие не выполняется, то управление переходит к следующей команде.

Существует много команд для различных условных переходов. Также для некоторых команд есть синонимы (например, JZ и JE — это одно и то же). Для наглядности все команды условных переходов приведены в таблице:

Команда Переход, если Условие перехода
JZ/JE нуль или равно ZF=1
JNZ/JNE не нуль или не равно ZF=0
JC/JNAE/JB есть переполнение/не выше и не равно/ниже CF=1
JNC/JAE/JNB нет переполнения/выше или равно/не ниже CF=0
JP число единичных бит чётное PF=1
JNP число единичных бит нечётное PF=0
JS знак равен 1 SF=1
JNS знак равен 0 SF=0
JO есть переполнение OF=1
JNO нет переполнения OF=0
JA/JNBE выше/не ниже и не равно CF=0 и ZF=0
JNA/JBE не выше/ниже или равно CF=1 или ZF=1
JG/JNLE больше/не меньше и не равно ZF=0 и SF=OF
JGE/JNL больше или равно/не меньше SF=OF
JL/JNGE меньше/не больше и не равно SF≠OF
JLE/JNG меньше или равно/не больше ZF=1 или SF≠OF
JCXZ содержимое CX равно нулю CX=0

У всех этих команд один операнд — имя метки для перехода. Обратите внимание, что некоторые команды применяются для беззнаковых чисел, а другие — для чисел со знаком. Сравнения «выше» и «ниже» относятся к беззнаковым числам, а «больше» и «меньше» — к числам со знаком. Для беззнаковых чисел признаком переполнения будет флаг CF, а соответствующими командами перехода JC и JNC. Для чисел со знаком о переполнении можно судить по состоянию флага OF, поэтому им соответствуют команды перехода JO и JNO. Команды переходов не изменяют значения флагов.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use16                   ;Генерировать 16-битный код
org 100h                ;Программа начинается с адреса 100h
 
    mov al,[x]          ;AL = x
    add al,[y]          ;AL = x + y
    jo error            ;Переход, если переполнение
    mov ah,09h          ;
    mov dx,ok_msg       ; > Вывод строки 'OK'
    int 21h             ;/
exit:
    mov ah,09h          ;
    mov dx,pak          ; > Вывод строки 'Press any key...'
    int 21h             ;/
 
    mov ah,08h          ;
    int 21h             ;/ Ввод символа
 
    mov ax,4C00h        ;
    int 21h             ;/ Завершение программы
error:
    mov ah,09h          ;
    mov dx,err_msg      ; > Вывод сообщения об ошибке
    int 21h             ;/
    jmp exit            ;Переход на метку exit
;----------------------------------------------------------
x         db -89
y         db -55
err_msg   db 'Error: overflow detected.',13,10,'$'
ok_msg    db 'OK',13,10,'$'
pak       db 'Press any key...$'

Команды CMP и TEST

Часто для формирования условий переходов используются команды CMP и TEST. Команда CMP предназначена для сравнения чисел. Она выполняется аналогично команде SUB: из первого операнда вычитается второй, но результат не записывается на место первого операнда, изменяются только значения флагов. Например:

    cmp al,5     ;Сравнение AL и 5
    jl c1        ;Переход, если AL < 5 (числа со знаком)
    cmp al,5     ;Сравнение AL и 5
    jb c1        ;Переход, если AL < 5 (числа без знака)

Команда TEST работает аналогично команде AND, но также результат не сохраняется, изменяются только флаги. С помощью этой команды можно проверить состояние различных битов операнда. Например:

    test bl,00000100b ;Проверить состояние 2-го бита BL
    jz c2             ;Переход, если 2-й бит равен 0

Пример программы

Простая программка, которая выводит меню и предлагает пользователю сделать выбор. Для ввода символа используется функция DOS 01h (при вводе символ отображается на экране). В зависимости от введённого символа осуществляется переход на нужный кусок кода. Для разнообразия, я поместил данные в начале программы, а не в конце (кстати, обычно так и делают). Чтобы данные не выполнились как код, перед ними стоит команда безусловного перехода.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
use16                   ;Генерировать 16-битный код
org 100h                ;Программа начинается с адреса 100h
    jmp start           ;Безусловный переход на метку start
;----------------------------------------------------------
menu    db '1 - Print hello',13,10
        db '2 - Print go away',13,10
        db '0 - Exit',13,10,'$'
select  db 13,10,'Select>$'
hello   db 13,10,'Hello!',13,10,13,10,'$'
go_away db 13,10,'Go away!',13,10,13,10,'$'
;----------------------------------------------------------
start:
    mov ah,09h          ;
    mov dx,menu         ; > Вывод меню
    int 21h             ;/
 
select_loop:
    mov ah,09h          ;
    mov dx,select       ; > Вывод строки 'Select>'
    int 21h             ;/
 
    mov ah,01h          ;Функция DOS 01h - ввод символа
    int 21h             ;Введённый символ помещается в AL
 
    cmp al,'1'          ;Сравнение введённого символа с '1'
    je c1               ;Переход, если равно
    cmp al,'2'          ;Сравнение введённого символа с '2'
    je c2               ;Переход, если равно
    cmp al,'0'          ;Сравнение введённого символа с '0'
    je exit             ;Переход, если равно
    jmp select_loop     ;Безусловный переход
c1:
    mov ah,09h          ;
    mov dx,hello        ; > Вывод строки 'Hello'
    int 21h             ;/
    jmp start           ;Безусловный переход
c2:
    mov ah,09h          ;
    mov dx,go_away      ; > Вывод строки 'Go away'
    int 21h             ;/
    jmp start           ;Безусловный переход
exit:
    mov ax,4C00h        ;
    int 21h             ;/ Завершение программы

Скриншот работы программы:

Упражнение

Упражнение простое. Напишите программу для сравнения двух переменных со знаком a и b. В зависимости от результатов сравнения выведите «a < b», «a > b» или «a = b». Проверьте работу программы в отладчике. Результаты можете выкладывать в комментариях.

Следующая часть »

Логика и организация программы

На прошлом уроке для того, чтобы сделать задержку и считать символ с клавиатуры, мы были вынуждены использовать цикл. Сегодня мы более подробно познакомимся с непоследовательным выполнением команд. Большинство программ содержат ряд циклов, в которыхнесколько команд повторяются до достижения определенного
требования, и различные проверки, определяюшие, какие из
нескольких действий следует выполнять. Обычным требованием
является проверка — должна ли программа завершить
выполнение. Эти требования включают передачу управления по адресу
команды, которая не находится непосредственно за выполняемой
в текущий момент командой. Передача управления может осуществляться вперед для выполнения новой группы команд или назад
для повторения уже выполненных команд.
Некоторые команды могут передавать управление, изменяя
нормальную последовательность шагов непосредственной
модификацией значения смещения в командном указателе. Ниже
приведены четыре способа передачи управления (все будут
рассмотрены в этой главе):

        Безусловный переход:  JMP
        Цикл:                 LOOP
        Условный переход:     Jnnn (больше,меньше,равно)
        Вызов процедуры:      CALL

Заметим, что имеется три типа адресов: SHORT, NEAR и FAR.
Адресация SHORT используется при циклах, условных пеpеходах
и некоторых безусловных переходах. Адресация NEAR и FAR
используется для вызовов процедур (CALL) и безусловных
переходов, которые не квалифицируются , как SHORT. Все три
типа передачи управления воздействуют на содержимое регистра
IP; тип FAR также изменяет регистр CS.

Команда JMP

Одной из команд, обычно используемых для передачи управле
ния является команда JMP. Эта команда выполняет безусловный
переход, т.е. обеспечивает передачу управления при любых
обстоятельствах. Команда JMP для перехода в пределах -128 до +127 байт
имеет тип SHORT. Ассемблер генерирует в этом случае
однобайтовый операнд в пределах от 00 до FF. Команда JMP,
превосходящая эти пределы, получает тип FAR, для которого
генерируется другой машинный код и двухбайтовый операнд.
Ассемблер в первом просмотре исходной программы определяет
длину каждой команды. Однако, команда JMP может быть длиной
два или три байта. Если к моменту просмотра команды JMP
ассемблер уже вычислил значение опеpанда (при переходе
назад):

             A50:
                  ...
                  JMP  A50

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

                  JMP  A90
                  ...
             A90:

то он не знает тип перехода NEAR или FAR, и автоматически
генерирует 3-х байтовую команду. Для того, чтобы указать
ассемблеру на необходимость генерации двухбайтовой команды,
следует использовать оператор SHORT:

                  JMP  SHORT A90
                  ...
             A90:

В качестве полезного упражнения, введите программу,
проассемблируйте ее, скомпануйте и переведите в COM-формат.
Определение данных не требуется, поскольку непосредственные
операнды генерируют все необходимые данные. Используйте
отладчик DEBUG для пошагового выполнения COM-модуля и
просмотрите несколько повторений цикла. Когда регистр AX
будет содержать 08, BX и CX увеличатся до шест. 24 (дес.
36) и шест. 80 (дес. 128), соответственно. Для выхода из
отладчика используйте команду Q.

команда LOOP

Команда JMP в примере на рис. 7.1 реализует бесконечный
цикл. Но более вероятно подпрограмма должна выполнять
определенное число циклов. Команда LOOP, которая служит для
этой цели, использует начальное значение в регистре CX. В
каждом цикле команда LOOP автоматически уменьшает содержимое
регистра CX на 1. Пока значение в CX не равно нулю,
управление передается по адресу, указанному в операнде, и
если в CX будет 0, управление переходит на слудующую после
LOOP команду.
Аналогично команде JMP, операнд команды LOOP определяет
расстояние от конца команды LOOP до адреса метки A20, кото
рое прибавляется к содержимому командного указателя. Для
команды LOOP это расстояние должно быть в пределах от -128
до +127 байт. Если операнд превышает эти границы, то ассемб
лер выдаст сообщение «Relative jump out of range» (превышены
границы перехода).
Дополнительно существует две разновидности команды LOOP —
это LOOPE (или LOOPZ) и LOOPNE (или LOOPNZ). Обе команды
также уменьшают значение регистра CX на 1. Команда LOOPE
передает управление по адресу операнда, если регистр CX
имеет ненулевое значение и флаг нуля установлен (ZF=1).
Команда LOOPNE передает управление по адресу операнда, если
регистр CX имеет ненулевое значение и флаг нуля сброшен
(ZF=0).

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

Следующий материал данной главы требует более детального
ознакомления с флаговым регистром. Этот pегистр содержит 16
бит флагов, которые управляются различными командами для
индикации состояния операции. Во всех случаях флаги сохраня
ют свое значение до тех пор, пока другая команда не изменит
его. Флаговый регистр содержит следующие девять используемых
бит (звездочками отмечены неиспользуемые биты):

   Номер бита:    15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
   Флаг:           *  *  *  *  O  D I T S Z * A * P * C

Рассмотрим эти флаги в последовательности справа налево.

CF (Carry Flag)

флаг переноса. Содержит значение
«переносов» (0 или 1) из старшего разряда при арифметичес
ких операциях и некоторых операциях сдвига и циклического
сдвига (см. гл.12).

PF (Parity Flag)

флаг четности. Проверяет младшие
восемь бит pезультатов операций над данными. Нечетное число
бит приводит к установке этого флага в 0, а четное — в 1. Не
следует путать флаг четности с битом контроля на четность.

AF (Auxiliary Carry Flag)

дополнительный флаг переноса.
Устанавливается в 1, если арифметическая операция приводит
к переносу четвертого бита справа (бит номер 3) в регистро
вой однобайтовой команде. Данный флаг имеет отношение к
арифметическим операциям над символами кода ASCII и к
десятичным упакованным полям.

ZF (Zero Flag)

флаг нуля. Устанавливается в качестве
результата aрифметических команд и команд сравнения. Как
это ни странно, ненулевой результат приводит к установке
нулевого значения этого флага, а нулевой — к установке
единичного значения. Кажущееся несоответствие является,
однако, логически правильным, так как 0 обозначает «нет»
(т.е. результат не равен нулю), а единица обозначаeт «да»
(т.е. результат равен нулю). Команды условного перехода JE и
JZ проверяют этот флаг.

SF (Sign Flag)

знаковый флаг. Устанавливается в соответ
ствии со знаком результата (старшего бита) после арифмети
ческих опеpаций: положительный результат устанавливает 0, а
отрицательный — 1. Команды условного перехода JG и JL
проверяют этот флаг.

TF (Trap Flag)

флаг пошагового выполнения. Этот флаг
вам уже приходилось устанавливать, когда использовалась ко
манда Т в отладчике DEBUG. Если этот флаг установлен в еди
ничное cостояние, то процессор переходит в режим пошагового
выполнения команд, т.е. в каждый момент выполняется одна
команда под пользовательским управлением.

IF (Interrupt Flag)

флаг прерывания. При нулевом состоя
нии этого флага прерывания запрещены, при единичном —
разрешены.

DF (DIrection Flag)

флаг направления. Используется в
строковых операциях для определения направления передачи
данных. При нулевом состоянии команда увеличивает содержимое
регистров SI и DI, вызывая передачу данных слева направо,
при нулевом — уменьшает содержимое этих регистров, вызывая
передачу данных справа налево.

OF (Overflow Flag)

флаг переполнения. Фиксирует арифме
тическое переполнение, т.е. перенос в/из старшего (знаково
го) бита при знаковых арифметических операциях.
В качестве примера: команда CMP сравнивает два операнда
и воздействуте на флаги AF, CF, OF, PF, SF, ZF. Однако, нет
необходимости проверять все эти флаги по отдельности. В сле-
дующем примере проверяется содержит ли регистр BX нулевое
значение:

             CMP  BX,00          ;Сравнение BX с нулем
             JZ   B50            ;Переход на B50 если нуль
             .    (действия при ненуле)
             .
   B50:      ...                 ;Точка перехода при BX=0

Если BX содержит нулевое значение, команда CMP устанавливает
флаг нуля ZF в единичное состояние, и возможно изменяет (или
нет) другие флаги. Команда JZ (перехлд если нуль) проверяет
только флаг ZF. При единичном значении ZF, обозначающее
нулевой признак, команда передает управление на адрес,
указанный в ее операнде, т.е. на метку B50.
В предыдущих примерах было показано, что команда LOOP
уменьшает на единицу содержимое регистра CX и проверяет его:
если не ноль, то управление передается по адресу, указанному
в операнде. Таким образом, передача управления зависит от
конкретного состояния. Ассемблер поддерживает большое
количество команд условного перехода, которые осуществляют
передачу управления в зависимости от состояний флагового
регистра. Например, при сравнении содержимого двух полей
последующий переход зависит от значения флага.
Команду LOOP в программе на рис.7.2 можно заменить на две
команды: одна уменьшает содержимое регистра CX, а другая
выполняет условный переход:

 Использование LOOP    Использование условного перехода
        LOOP A20                 DEC  CX
                                 JNZ  A20

Команды DEC и JNZ действуют аналогично команде LOOP:
уменьшают содержимое регистра CX на 1 и выполняет переход на
метку A20, если в CX не ноль. Команда DEC кроме того
устанавливает флаг нуля во флаговом регистре в состояние 0
или 1. Команда JNZ затем проверяет эту установку. В рассмот
ренном примере команда LOOP хотя и имеет огпаниченное исполь
зование, но более эффективна, чем две команды: DEC и JNZ.
Аналогично командам JMP и LOOP операнд в команде JNZ
cодержит значение расстояния между концом команды JNZ и
адресом A20, которое прибавляется к командному указателю.
Это расстояние должно быть в пределах от -128 до +127 байт.
В случае перехода за эти границы ассемблер выдаст сообщение
«Relative jump out of range» (превышены относительные грани
цы перехода).

Знаковые и беззнаковые данные

Рассматривая назначение команд условного перехода следует
пояснить характер их использования. Типы данных, над которы
ми выполняются арифметические операции и операции сравнения
определяют какими командами пользоваться: беззнаковыми или
знаковыми. Беззнаковые данные используют все биты как биты
данных; характерным примером являются символьные строки:
имена, адреса и натуральные числа. В знаковых данных самый
левый бит представляет собой знак, причем если его значение
равно нулю, то число положительное, и если единице, то
отрицательное. Многие числовые значения могут быть как
положительными так и отрицательными.
В качестве примера предположим, что регистр AX содержит
11000110, а BX — 00010110. Команда

                       CMP  AX,BX

сравнивает содержимое регистров AX и BX. Если данные
беззнаковые, то значение в AX больше, а если знаковые — то
меньше.

Условные переходы

Переходы для беззнаковых данных

Мнемоника      Описание                 Проверяемые флаги
JE/JZ     Переход, если равно/нуль                ZF
JNE/JNZ   Переход, если не равно/не нуль          ZF
JA/JNBE   Переход, если выше/не ниже или равно    ZF,CF
JAE/JNB   Переход, если выше или равно/не ниже    CF
JB/JNAE   Переход, если ниже/не выше или равно    CF
JBE/JNA   Переход, если ниже или равно/не выше    CF,AF

Любую проверку можно кодировать одним из двух мнемоничес
ких кодов. Например, JB и JNAE генерирует один и тот же
объектный код, хотя положительную проверку JB легче понять,
чем отрицательную JNAE.

Переходы для знаковых данных

Мнемоника      Описание                 Проверяемые флаги
JE/JZ     Переход, если равно/нуль                  ZF
JNE/JNZ   Переход, если не равно/не нуль            ZF
JG/JNLE   Переход, если больше/не меньше или равно  ZF,SF,OF
JGE/JNL   Переход, если больше или равно/не меньше     SF,OF
JL/JNGE   Переход, если меньше/не больше или равно     SF,OF
JLE/JNG   Переход, если меньше или равно/не больше  ZF,SF,OF

Команды перехода для условия равно или ноль (JE/JZ) и не
равно или не ноль (JNE/JNZ) присутствуют в обоих списках
для беззнаковых и знаковых данных. Состояние равно/нуль
происходит вне зависимости от наличия знака.

Специальные арифметические проверки

 Мнемоника      Описание                 Проверяемые флаги
JS     Переход, если есть знак (отрицательно)       SF
JNS    Переход, если нет знака(положительно)        SF
JC     Переход, если есть перенос (аналогично JB)   CF
JNC    Переход, если нет переноса                   CF
JO     Переход, если есть переполнение              OF
JNO    Переход, если нет переполнения               OF
JP/JPE Переход, если паритет четный                 PF
JNP/JP Переход, если паритет нечетный               PF

Еще одна команда условного перехода проверяет равно ли
содержимое регистра CX нулю. Эта команда необязательно
должна pасполагаться непосредственно за командой арифметики
или сравнения. Одним из мест для команды JCXZ может быть
начало цикла, где она проверяет содержит ли регистр CX
ненулевое значение.
Не спешите пока заучивать эти команды наизусть. Запомните
только, что для беззнаковых данных есть переходы по состоя
ниям равно, выше или ниже, а для беззнаковых — равно,
больше или меньше. Переходы по проверкам флагов переноса,
переполнения и паритета имеют особое назначение. Ассемблер
транслирует мнемонические коды в объектный код независимо
от того, какую из двух команд вы применили. Однако, команды
JAE и JGE являясь явно одинаковыми, проверяют различные
флаги.

Время на прочтение
8 мин

Количество просмотров 17K

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

Стоит указать цель этих статей, чтобы не было недопонимания. Я не буду разбирать каждый компилятор Си в отдельности. Это долго и нудно. Вместо этого, я хочу увлечь читателей разбором интересных интерпретаций Си-кода, чтобы люди осознавали, как их код может меняться и исполняться процессором. А так же развеять некоторые мифы, которые ходят среди начинающих программистов. Например, есть, правда, те, кто считает, что если складывать числа в цикле, то это будет быстрее, чем просто умножить одно число на другое. Статья не разбирает конкретно gcc с -m32 -O0, некоторые не совсем поняли идею. Если будет реальный смысл, то я поменяю и компилятор, и ключи.

Т. е. что я хочу сказать? Рассмотрим два старых примера:

int main(void) 
{ 
	register int a = 1; //записываем в регистровую переменную 1 
	return a; //возвращаем значение из регистровой переменной 
} 

и

int a = 1; 
int b = a * 2; 

Действительно, clang в первом случае определяет переменную в стек, но насколько это интересно или существенно для нас? Те, кто знаком со спецификатором register, читали/знают, что это, всего лишь, рекомендация. Поэтому компилятор может просто проигнорировать спецификатор. К тому же, цель примера была познакомить читателя с регистрами, взяв простейший пример. Подошел идеально для этого gcc. Второй пример еще проще, в нем clang сразу же делает сдвиг, и сдается уже при умножении на 3, выдавая imul. Честно, не очень понимаю, что в данном примере любопытного, поэтому также привел код для gcc, который извращается до числа 22. Мы все и так знаем, что в стандарте языка не прописано, как реализовывать ту или иную вещь. И разработчики компилятора вольны делать свои реализации, лишь бы они не нарушали стандарт. Поэтому и имеем разную интерпретацию кода в зависимости от компилятора. Но, простите меня, разбирать каждую? В чем практичность данного материала? Заморочить всем голову? Как правильно было замечено, если интересует конкретный компилятор, то можно просто посидеть с отладчиком. И это уже не будет так страшно для тех, кто читает эти статьи.

Итак, продолжим.

Простейшее условие

Для начала сравним переменную и число:

int a = 0;
if (a < 5)
{
	return 1;
}
return 0;

АСМ (gcc 7.2):

	mov 		DWORD PTR [ebp-4], 0
	cmp 		DWORD PTR [ebp-4], 4
	jg 		.L2
	mov 		eax, 1
	jmp 		.L3
.L2:
	mov 		eax, 0
.L3:
	leave
	ret

В первой строке, компилятор добавляет в стек значение переменной «а». Во второй у нас новая инструкция cmp. Не трудно догадаться, что эта инструкция сравнивает два значения. В нашем случае: значение из стека и 4.

Но как она работает? Просто отнимает от первого операнда второй. Похожим образом работает и инструкция sub, но в случае с cmp результат не сохраняется. Однако флаги в регистре EFLAGS/RFLAGS устанавливаются в соответствии с этим результатом. Не вдаваясь в подробности, мы можем узнать был ли положительный результат, отрицательный или ноль. Как раз следующая команда условного перехода jg срабатывает в том случае, если результат был положительным (jump if greater).

Если вы это осмыслили, то мог возникнуть справедливый вопрос: а почему больше, если знак был меньше? Действительно, написали мы если а < 5, но это превратилось в делай что-то, если а > 4. Но логика программы не нарушилась. Ведь, если a > 4, то происходит return 0. Тут может возникнуть еще один справедливый вопрос, а если написать условие: if (a > 4) return 0, как изменится сам код?

	mov 		DWORD PTR [ebp-4], 0
	cmp 		DWORD PTR [ebp-4], 4
	jle 		.L2
	mov 		eax, 0
	jmp 		.L3
.L2:
	mov 		eax, 1
.L3:
	leave
	ret

И мы снова получаем разворот условия: jle, как вы догадались, меньше либо равно (jump if less or equal)

Тут все дело в том, что return завершает программу, поэтому нужно еще выполнить две последние инструкции, именно поэтому строчка jmp .L3 не меняется в обоих примерах. Это инструкция безусловного перехода. В нашем случае: она пропускает строчку следующую за условием, где в регистр eax должно записаться совсем другое число.

Т. е. компилятор проверяет обратное условие, чтобы при оригинальном false отослать по коду вниз, но если оригинальное условие – true, то выполняется код, который идет непосредственно за cmp и условным переходом. Давайте для наглядности отметим цифрами ветки условия:

int a = 0;
if (a > 5)
{
	//#0
	return 1;
}
//#1
return 0;

	mov 		DWORD PTR [ebp-4], 0
	cmp 		DWORD PTR [ebp-4], 4
	jle 		.L2
	;#0
	mov 		eax, 0
	jmp 		.L3
.L2:
	mov 		eax, 1
	;#1
.L3:
	leave
	ret

Как видите, структура программы не нарушается, но если мы заменим условный переход, то:

	mov 		DWORD PTR [ebp-4], 0
	cmp 		DWORD PTR [ebp-4], 5
	jg 		.L2
	;#1
	mov 		eax, 1
	jmp 		.L3
.L2:
	mov 		eax, 0
	;#0
.L3:
	leave
	ret

Т. е. внутренняя часть условия падает вниз программы, что не очень хорошо: допустим, после условия (в секции #1) есть еще куча строчек, тогда, чтобы увидеть секцию #0, мы будем крутить листинг очень далеко вниз. (Забыл уточнить, что затем надо будет еще и вернуться для продолжения выполнения кода после if. Т. е. еще одна метка и еще один переход.)

unsigned

Мы только что рассмотрели сравнение чисел, имеющих знак (signed), а что если мы будем сравнивать беззнаковые числа (unsigned)?

unsigned int a = 0;
if (a > 5)
{
     return 1;
} 
return 0;

	mov 		DWORD PTR [ebp-4], 0
	cmp 		DWORD PTR [ebp-4], 5
	jbe 		.L2
	mov 		eax, 1
	jmp 		.L3
.L2:
	mov 		eax, 0
.L3:
	leave
	ret

Ничего не изменилось, кроме инструкции условного перехода: вместо jle теперь jbe (jump below or equal). Зачем две различные инструкции для сравнения знаковых и беззнаковых чисел?

int a = 0 – 1; //-1
unsigned int a = 0 – 1; //4294967295

Хотя по факту, в памяти, все равно, будет 4294967295. Это всего лишь способ отображения, вы можете написать в Си:

unsigned int a = 0 - 1;
printf("%i", a); //-1

Но при инструкции cmp устанавливается не один флаг, а несколько. Инструкция jbe проверяет флаг переполнения при вычитании, а jle проверяет флаг, который равен значению старшего бита результата (т. е. при отрицательном результате там 1). В реальности все чуточку сложнее: JBE (CF=1 or ZF=1), JLE (ZF=1 or SF<>OF), но нам можно на этом не зацикливаться. Перейдем к более интересным вещам:

unsigned int a = 0;
if (a < 0)
{
     return 1;
} 
return 0;

будет преобразовано в:

	mov 		DWORD PTR [ebp-4], 0
	mov 		eax, 0
	leave
	ret

Здорово, правда? По логике нашего кода переменная «a» не будет никогда меньше нуля, поэтому условие можно просто выкинуть.

А что с этим:

unsigned int a = 0;
if (a > 0)
{
     return 1;
} 
return 0;

АСМ:

	mov 		DWORD PTR [ebp-4], 0
	cmp 		DWORD PTR [ebp-4], 0
	je 		.L2	
	mov 		eax, 1
	jmp 		.L3
.L2:
	mov 		eax, 0
.L3:
	leave
	ret

Инструкция je осуществляет переход, если результат сравнения равен нулю (jump if equal).

”<” быстрее, чем ”<=”? Или чем ”< || =”?

Мы уже рассмотрели несколько инструкций условного перехода: jle, jbe, jg и je. Таких инструкций немногим больше для всех случаев, также есть и обратные: например jne – не нуль или не равно или jnbe – не ниже и не равно. Т. е. для любого сравнения чисел мы получим две инструкции cmp (или test) и jcc (условный переход). Таким образом можно сделать вывод, что, например, нет разницы в количестве инструкций для < и <=.

А вот для

if (a < 0 || a == 0)

разница будет, но только при -O0.

Давайте взглянем на следующую программу:

#include <stdio.h>

int main() 
{ 
	int a = 0;
	scanf("%d", &a);
	if (a < 0 || a == 0)
	{
	     return 10;
	} 
	return 20;
} 

На этот раз я использую clang 5.0.0 -O3 -m32, так как генерируется меньше asm кода, и по этому примеру будет легче объяснить, что происходит:

	sub 		esp, 12
	mov 		dword ptr [esp + 8], 0
	;scanf
	sub 		esp, 8
	lea 		eax, [esp + 16]
	push 		eax
	push 		.L.str
	call 		scanf
	add 		esp, 16
	;end scanf
	
	;интересующий нас код
	cmp 		dword ptr [esp + 8], 0		;#1
	mov 		ecx, 10				;#2
	mov 		eax, 20				;#3
	cmovle 	eax, ecx				;#4
	
	;эпилог функции
	add 		esp, 12
	ret
.L.str:
	.asciz "%d"

#1: сравнение переменной «а» с нулем
#2: в регистре ecx теперь 10
#3: в регистре eax теперь 20
#4: cmovle аналогичен jle, только он перемещает значение при условии. Таким образом, если a<=0, то в eax попадает значение из ecx (10), иначе, просто, останется 20.

Вы уже понимаете, что если в Си-коде заменить на a<=0, то ничего не изменится, но можете проверить, если есть желание.

Когда условия перестают быть условиями

Представьте ситуацию: в вашем коде есть условие, но при отладке вы не можете найти условные инструкции. Интересно?

Взглянем на следующий код:

int x = 10;
scanf("%d", &x);
if (x < 0)
{
	return 3;
}
return 2;

Вы могли бы ожидать cmp и метки, могли ожидать даже более нетривиальные вещи, как setx, но получили следующее (clang 5.0.0 -O3 -m32):

	mov 		eax, dword ptr [esp + 8] 
	shr 		eax, 31
	or 		eax, 2
	add 		esp, 12
	ret

Ну, и что это? Давайте разберемся. С первой строкой все понятно: в eax перенесли значение переменной x.

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

Далее идет побитовая операция «или». Т. е. у нас получается в результате или 10, или 11 (в двоичной системе счисления). На этом все, следующие строки относятся к эпилогу функции.

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

По этой же логике, но немного не так действует, например, gcc 4.8.5:

	sar 		eax, 31
	not 		eax
	add 		eax, 3

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

[1000] shr [0100] shr [0010] shr [0001]

[1000] sar [1100] sar [1110] sar [1111]

Т. е. если у нас будут все единицы, то число было отрицательным, мы инвертируем все биты, получаем нуль, добавляем 3, ровно то, что мы хотели. А если число положительное, то будут все нули, после инвертирования станут единицами. По факту — это -1, добавив к нему 3, получим 2.

MSVC -O2 при этом поступает более ожидаемо:

	cmp		DWORD PTR _x$[ebp], eax		; x < 0
	setl		al				; less ? mov al, 1
	add		eax, 2				; eax + 2

Очень кратко: младший байт регистра eax выставляется в единицу при условии, что результат сравнения оказался отрицательным, затем добавляется 2. Это классика. Компиляторы очень любят данный прием, надеюсь, мы его еще повстречаем.

Оператор else

Я думаю, все понимают, что для else не проверяется обратное условие. В коде просто появляется еще одна метка, вот и все особенности. Давайте убедимся в этом и рассмотрим следующий код:

int x = 10;
int c = 0;
if (x < 4)
{
	c = 3;
}
else
{
	c = 2;
}
return c;

АСМ:

	mov 		DWORD PTR [ebp-8], 10
	mov 		DWORD PTR [ebp-4], 0
	cmp 		DWORD PTR [ebp-8], 3
	jg 		.L2
	mov 		DWORD PTR [ebp-4], 3
	jmp 		.L3
.L2:
	mov 		DWORD PTR [ebp-4], 2
.L3:
	mov 		eax, DWORD PTR [ebp-4]

Как видите, отличие только в том, что после выполнения кода внутри if, мы перескакиваем через внутренности блока else (метка .L2). Надеюсь, подробный анализ не нужен, вроде бы, все очевидно.

Логические операции в if

Давайте посмотрим на немного нестандартный пример:

int main(void) 
{
	int a = -1;
	if (a++ < 0 || a++ > 5)
	{
		a++;
	}
	else
	{
		a+=2;
	}	
	return 0;
}

Сначала попробуйте дать ответ: какое значение будет в переменной «a»? Если для вас это было не сложно, то вы уже примерно представляете, как будет выглядеть asm код.

	mov 		DWORD PTR [ebp-4], -1
	mov 		eax, DWORD PTR [ebp-4] 	;#1
	lea 		edx, [eax+1]		;#1	
	mov 		DWORD PTR [ebp-4], edx	;#1
	test 		eax, eax		;#1
	js 		.L2			;#1
	mov 		eax, DWORD PTR [ebp-4]	;#2
	lea 		edx, [eax+1]		;#2
	mov 		DWORD PTR [ebp-4], edx	;#2
	cmp 		eax, 5			;#2
	jle 		.L3			;#2
.L2:
	mov 		eax, 1			;#3
	jmp 		.L4			;#3
.L3:
	mov 		eax, 0			;#4
.L4:
	test 		al, al			;#5
	je 		.L5			;#5
	add 		DWORD PTR [ebp-4], 1	;#6
	jmp 		.L6			;#6
.L5:
	add 		DWORD PTR [ebp-4], 2	;#7
.L6:
	mov 		eax, 0			;#8

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

#1: a++ < 0. При этом в eax записывается значение до инкремента, именно его и нужно сравнить с нулем. Инструкция test работает по принципу and, но не изменяет сами операнды, только флаги. В нашем случае после test мы проверяем знак числа, если он равен 1, то осуществляем переход. Также в стек возвращается значение, увеличенное на единицу. Увеличение делается инструкцией lea edx, [eax + 1]. Инструкция lea используется для загрузки эффективного адреса. В нашем случае, она заменяет сразу две инструкции: mov edx, eax и add edx, 1.
#2: a++ > 5. Фактически, происходит тоже самое, только переход на метку .L3 в случае, если a <= 5. Т. е. на метку .L2 мы попадаем, если выполняется первое условие или второе. При этом обратите внимание, что второе условие не будет вычисляться, если выполнилось первое. Но вы это и так должны были знать.
#3: В регистре eax оказывается 1
#4: В регистре eax оказывается 0
#5: Проверяем младший байт регистра eax на нуль, если равен, то переходим на метку .L5
#6: Иначе добавляем в переменную «a» 1. Переходим в конец программы.
#7: Если младший байт в eax был равен нулю, то добавляем переменной «а» 2

Т. е. #3 отвечает за то, что оба условия выполнены и ставится «флаг», затем осуществляется проверка в #5, если «флаг» стоит, то добавили 1, иначе 2.

Стоит также отметить, что при if (a < 0 && a < -5), если первое условие не выполняется, то второе также не будет вычисляться.

Заключение

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

К сожалению, статья вышла настолько длинной, что не удалось рассмотреть оператор switch, поэтому, если хотите, можно это сделать в следующей статье. Заодно, можно рассмотреть и оптимизированные программы с else.

Предыдущая статья
Следующая статья

Понравилась статья? Поделить с друзьями:
  • Какой текст отражен в инструкции рядом с окнами аварийного выхода
  • Какой срок действия инструкции по охране труда утвержденной работодателем
  • Какой символ должен стоять в конце каждой инструкции php
  • Какой раздел не содержит должностная инструкция
  • Какой раздел инструкции следует включить перечень вредных и опасных производственных факторов