Skip to content

Лекция 3

Inspirate789 edited this page May 15, 2022 · 8 revisions

CMOVcc - условная пересылка данных

CMOVcc <приёмник>, <источник> - пересылает данные только при выполнении условий, аналогичных командам условного перехода (переход произойдёт только если стоят нужные флаги).

Условия аналогичны Jcc

XCHG - обмен операндов между собой

XCHG <операнд1>, <операнд2>

Выполняется над двумя регистрами либо над регистром и переменной.

нельзя обменивать значения двух переменных, так как на шину адреса нельзя выставить 2 адреса (она одна и работает только в одну сторону - либо читаем, либо пишем)

XLAT/XLATB - трансляция в соответствии с таблицей

XLAT [адрес] (принято писать адрес, но использоваться будет только сегментная часть, которая может собой заменять DS)

XLATB

  • Помещает в AL байт из таблицы (массив, который не превышает 256 ячеек памяти) по адресу DS:BX со смещением относительно начала таблицы, равным AL.
  • Адрес, указанный в исходном коде, не обрабатывается компилятором и служит в качестве комментария.
  • Если в адресе явно указан сегментный регистр, он будет использоваться вместо DS.

Использование:

  • Транслитерация (перевод английских символов в русские один к одному)
  • Перевод шестнадцатеричных чисел в символы

По сути это аппаратная усечённая поддержка словарей.

LEA - вычисление эффективного адреса (Load Effective Address)

LEA <приёмник>, <источник>

  • Вычисляет эффективный адрес источника и помещает его в приёмник (в данном случае адрес === смещение).
  • Позволяет вычислить адрес, описанный сложным методом адресации. Поддерживает на аппаратном уровне (на уровне процессора) сложные формулы с несколькими операндами: сложение с константой и сложение/умножение каких-то регистров. Работает быстрее арифметических команд и не меняет флаги.
  • Иногда используется для быстрых арифметических вычислений:
    lea bx, [bx+bx*4]
    lea bx, [ax+12]
  • Эти вычисления занимают меньше памяти, чем соответствующие MOV и ADD, и не изменяют флаги.

Использование:

  • Вычисление адреса "на лету"
  • Быстрая арифметика (не нужно писать несколько арифметических команд)

Двоичная арифметика - ADD, ADC, SUB, SBB.

ADC, SBB нужны для работы с большими числами, не помещающимися в регистры.

  • ADD, SUB не делают различий между знаковыми и беззнаковыми числами.
  • ADC <приёмник>, <источник> - сложение с переносом. Складывает приёмник, источник и флаг CF.
  • SBB <приёмник>, <источник> - вычитание с займом. Вычитает из приёмника источник и дополнительно - флаг CF.

Пример: сложение и вычитание 32-разрядных чисел: два числа, разбитые на старшие и младшие половинки, лежат в парах регистров DX и AX, BX и CX.

add ax, cx ; Складываем младшие половинки (могло произойти переполнение с установкой флага CF)
adc dx, bx ; Складываем старшие половинки с учётом флага CF, не прописывая руками никаких условных операторов.
sub ax, cx ; Вычитаем младшие половинки (разность может быть отрицательной, тогда установится флаг CF)
sbb dx, bx ; Вычитаем старшие половинки с учётом флага CF, не прописывая руками никаких условных операторов.

Эти команды могут выставлять все арифметические флаги.

Арифметические флаги - CF, OF, SF, ZF, AF, PF.

Команды умножения - IMUL, MUL, IDIV, DIV.

Умножение чисел со знаком:

  • IMUL <источник>
  • IMUL <приёмник>, <источник> - не из нотации процессора 8086, а из последующих
  • IMUL <приёмник>, <источник1>, <источник2> - не из нотации процессора 8086, а из последующих

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

  • IDIV <источник>

Результат округляется в сторону нуля, знак остатка совпадает со знаком делимого.

INC, DEC - инкремент, декремент.

INC <приёмник>
DEC <приёмник>
  • Увеличивает/уменьшает приёмник на 1.
  • В отличие от ADD, не изменяет CF.
  • OF, SF, ZF, AF, PF устанавливаются в соответствии с результатом.

NEG - команда изменения знака

NEG <приёмник>
  • Переводит число в дополнительный код и прибавляет к нему 1, чтобы получить число с противоположным знаком.

Десятичная арифметика (ДВОИЧНО-ДЕСЯТИЧНАЯ) - DAA, DAS, AAA, AAS, AAM, AAD.

Двоично-десятичные числа используются в аппаратуре, которая должна на дисплей выводить десятичные числа, не занимаясь дополнительными вычислениями (переводом). Пример: часы с лампочками на 1 этаже в бауманке.

  • Неупакованное двоично-десятичное число - байт от 00h до 09h.
  • Упакованное двоично-десятичное число - байт от 00h до 99h (цифры A..F не задействуются).
  • При выполнении арифметических операций необходима коррекция: 19h + 1 = 1Ah => 20h

DAA, DAS - Коррекция упакованных двоично-десятичных чисел после сложения или вычитания.

AAA, AAS - Коррекция неупакованных двоично-десятичных чисел после сложения или вычитания.

AAM, AAD - Коррекция неупакованных двоично-десятичных чисел после умножения или деления.

inc al
daa

Логические команды - AND, OR, XOR, NOT, TEST.

См. первую лекцию

XOR используется для быстрого зануления!!!!! (так как a XOR a => 0)

Логический, арифметический, циклический сдвиг - SAR, SAL, SHR, SHL, ROR, ROL, RCR, RCL.

Арифметический сдвиг (число сдвигается с учётом знака, т.е. знак сохраняется: -8>>2 = -4): SAR (Shift Arithmetic Right), SAL (Shift Arithmetic Left).

Логический сдвиг (знак не сохраняется, т.е. знаковый бит сдвигается так же, как и все остальные): SHR (SHift Right), SHL (SHift Left).

Циклический сдвиг: ROR (Rotate Operand Right), ROL (Rotate Operand Left).

Циклический сдвиг через перенос: RCR (Rotate through Carry Right), RCL (Rotate through Carry Left).

  • SAL тождественна SHL.
  • SHR зануляет старший бит (знак), SAR - сохраняет.
  • ROR, ROL - циклический сдвиг вправо/влево.
  • RCR, RCL - циклический сдвиг через CF (в операции участвует не 16 бит, а 17 - при сдвиге вправо младший бит попадёт в CF, а CF - в старший бит, при сдвиге влево всё произойдёт наоборот).

Операции над битами и байтами - BT, BTR, BTS, BTC, BSF, BSR, SRTcc.

База - адрес битовой строки.

Смещение - номер бита, который следует считать.

OFFTOP:
Пример битовой строки - маска подсети в IP-адресе.
Также маски используются для обнуления, установления значений отдельных битов и т.д. ...
Регистр флагов также можно рассматривать как битовую строку.

Команды:

  • BTS <база>, <смещение> - считывает во флаг CF значение из битовой строки.
  • BTS <база>, <смещение> - установить бит в 1.
  • BTR <база>, <смещение> - сбросить бит в 0.
  • BTC <база>, <смещение> - инвертировать бит.
  • BSF <приёмник>, <источник> - прямой поиск бита (от младшего разряда к старшему, т.е. справа налево).
  • BSR <приёмник>, <источник> - обратный поиск бита (от старшего разряда к младшему, т.е. слева направо).
  • SETcc <приёмник> - выставляет приёмник (1 байт) в 1 или 0 в зависимости от условия, аналогично Jcc.

Организация циклов - LOOP, LOOPE/LOOPZ, LOOPNE/LOOPNZ

  • LOOP <метка> - уменьшает CX и выполняет "короткий" переход на метку, если CX не равен нулю.
  • LOOPE/LOOPZ <метка> - цикл "пока равно"/"пока ноль"
  • LOOPNE/LOOPNZ <метка> - цикл "пока не равно"/"пока не ноль"

Декрементируют CX и выполняют переход на метку, если CX не ноль и если выполняется условие (ZF).

Строковые операции: копирование, сравнение, сканирование, чтение, запись.

Есть 2 основных способа хранения текстовых строк в памяти компьютера:

  • Сишный: строка неограниченной длины, завершающаяся нулевым байтом (т.е. не может содержать в себе нулевой байт)
  • Паскалевский: в первых 1 или 2 байтах хранится длина строки, а затем идут сами символы. Тут мы заранее знаем длину строк (не нужно вычислять длину за O(n), как в Си это делает strlen), но эта длина ограничена.

Строка-источник - DS:SI, строка-приёмник - ES:DI.

За один раз обрабатывается один байт (слово).

  • MOVS/MOVSB/MOVSW <приёмник>, <источник> - копирование (На самом деле эта команда не имеет параметров. Она всегда пишет данные из DS:SI в ES:DI)
  • CMPS/CMPSB/CMPSW <приёмник>, <источник> - сравнение и выставление флага по его результатам
  • SCAS/SCASB/SCASW <источник> - сканирование источника (всегда это DS:SI), сравнение с AL/AX и выставление флага по его результатам
  • LODS/LODSB/LODSW <источник> - чтение (в AL/AX)
  • STOS/STOSB/STOSW <приёмник> - запись (из AL/AX)

Каждая такая команда делает сразу несколько действий (работают за несколько тактов процессора; на аппаратном уровне разбиты на атомарные команды):

  1. Читает из памяти по адресу источника (например DS:SI; SI - Source Index) 1, 2 или 4 байта (в зависимости от команды)
  2. Увеличивает индексы SI и DI на 1, 2 или 4
  3. Если команда связана с записью, то произойдёт запись 1, 2 или 4 байт по адресу приёмника (ES:DI; DI - Destination Index)

Среди перечисленных команд есть те, которые работают и с источником, и с приёмником, и те, которые работают только с источником или только с приёмником.

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

Префиксы: REP/REPE/REPZ/REPNE/REPNZ

Применение: пишем префикс перед командой (через пробел), тогда команда будет выполняться в цикле со счётчиком CX, как команда loop.

Префиксы REPE/REPZ/REPNE/REPNZ дополнительно будут контролировать состояние флага ZF (т.е., например, REPZ будет выполнять цикл до тех пор, пока CX не равен 0 и ZF равен 0; остальное по аналогии).

С помощью команды SCASB и префиксов можно производить поиск конца строки (положить в CX заведомо очень большое положительное число, которое точно больше длины нашей строки и запустить долгий цикл поиска конца строки; в результате в CX будет лежать разница между исходным значением CX и длиной нашей строки).

Команды управления флагами.

  • STC/CLC/CMC - установить/сбросить/инвертировать CF
  • STD/CLD - установить/сбросить DF (Direction Flag); Если он равен нулю (по умолчанию), то обработка строк будет идти слева направо, т.е. по возрастанию адресов памяти, иначе наоборот
  • LAHF - загрузка флагов состояния в AH
  • SAHF - установка флагов состояния из AH
  • CLI/STI - запрет/разрешение прерываний (т.е. сброса/установки IF - Interruption Flag). Разумеется речь идёт не про программные, а про аппаратные прерывания.

Зачем запрещать прерывания:

(ведь на обычные программы они вообще-то не влияют)

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

Поэтому системные программисты иногда вынуждены запрещать обработку прерываний.

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

Команды загрузки сегментных регистров (напрямую).

  • LDS <приёмник>, <источник> - загрузить адрес, используя DS
  • LES <приёмник>, <источник> - загрузить адрес, используя ES
  • LFS <приёмник>, <источник> - загрузить адрес, используя FS
  • LGS <приёмник>, <источник> - загрузить адрес, используя GS
  • LSS <приёмник>, <источник> - загрузить адрес, используя SS

Приёмник - регистр, источник - переменная

Регистр CS, так же как и IP, нельзя менять напрямую.

Регистры. Стек.

SP (Stack Pointer - указатель на вершину стека), BP (Base Pointer - вспомогательный регистр, используемый программистами и компиляторами для составления подпрограмм)

SP меняется автоматически, но его можно также менять и напрямую.

Стек.

  • LIFO/FILO (last in, first out) - последним пришёл, первым ушёл
  • Сегмент стека - область памяти программы, используемая её подпрограммами, а также (вынужденно) обработчиками прерываний
  • SP - указатель на вершину стека
  • В x86 стек "растёт вниз", в сторону уменьшения адресов (от старших адресов к младшим) (от конца сегмента к началу). В таком случае удобно определять переполнение стека, т.е. нужно просто отследить момент, когда SP стал равен нулю. Если бы этой механики не было, то приходилось бы где-то хранить размер стека.
  • При запуске программы SP указывает на конец сегмента.

Команды непосредственной работы со стеком.

Каждая такая команда делает сразу несколько действий (работают за несколько тактов процессора; на аппаратном уровне разбиты на атомарные команды):

PUSH:

  1. Уменьшает указатель вершины стека (регистр SP) на размер источника (того, что мы кладём в стек)
  2. Записывает значение из источника по адресу SS:SP

POP:

  1. Считывает значение из вершины стека (с адреса SS:SP) и записывает его в приёмник
  2. Увеличивает указатель вершины стека (регистр SP) на размер приёмника (того, что мы достаём из стека)

Команды:

  • PUSH <источник> - поместить данные в стек. Уменьшает SP на размер источника и записывает значение по адресу SS:SP.
  • POP <приёмник> - считать данные из стека. Считывает значение с адреса SS:SP и увеличивает SP.
  • PUSHA - поместить в стек регистры AX, CX, DX, BX, SP, BP, SI, DI.
  • POPA - загрузить регистры из стека (SP игнорируется)
  • PUSHF - поместить в стек содержимое регистра флагов
  • POPF - загрузить регистр флагов из стека

CALL - вызов процедуры, RET - возврат из процедуры

CALL <операнд>

  • Она похожа на PUSH (но немного сложнее): сохраняет адрес следующей за ней команды в стеке (той, которая физически за ней стоит в памяти, а не та, которая вызывается при переходе в подпрограмму), уменьшает SP (в случае ближнего перехода на 2 байта, в случае дальнего - на 4 байта) и записывает по этому адресу значение IP либо CS:IP, в зависимости от размера аргумента. Далее управление программы передаётся на значение операнда (в регистр IP записывается смещение подпрограммы, а в случае дальнего перехода оно заносится ещё и в CS:IP; насчёт дальнего перехода не уверен, там не очень чёткая формулировка была)

Адрес возврата записывается именно в стек, а не в регистры, так как если бы мы записывали его в регистры, то мы бы не смогли сделать длинную рекурсию (регистров бы не хватило).

  • Передаёт управление на значение аргумента.

RET/RETN/RETF <число>

Делает действия, обратные команде CALL:

  • Загружает из стека адрес возврата в регистр IP или, в случае дальнего перехода, в CS:IP, у
  • Увеличивает SP (на 2/4 байта в случае ближнего/дальнего перехода)

RETN/RETF - ближний/дальний (NEAR/FAR) возврат

Если указан операнд, его значение будет дополнительно прибавлено к SP для очистки стека от параметров.

Параметры в подпрограмму передаются через стек.

Регистр BP (Base Pointer)

Использование:

  • Сохранение "начального" адреса SP, т.е. в самом начале подпрограммы программист сам должен написать MOV BP, SP, чтобы потом тоже можно было что-то писать в стек, а исходное значение SP сохранилось бы для нас.
  • Адресация параметров (то, что положила в стек вызывающая подпрограмма)
  • Адресация локальных переменных

Глобальные переменные хранятся в сегменте данных. Локальные переменные тоже можно было бы хранить там, но тогда при каждом рекурсивном вызове они бы затирались, поэтому локальные переменные хранятся в стеке.

То есть, перед вызовом подпрограммы вызывающая программа кладёт в стек параметры, затем выполняет команду CALL, подпрограмма сама в своём начале сохраняет текущее значение регистра SP в регистре BP и дополнительно уменьшает SP на размер области памяти, необходимый для хранения локальных переменных.

Пример вызова подпрограммы №1 (без параметров и локальных переменных):

Example_1

Здесь совершается ближний переход, т.е. перемещение в пределах одного сегмента (SP уменьшается на 2)

Один из самых распространённых способов возврата значения из подпрограммы, если это значение числовое, - через регистр AX.

Пример вызова подпрограммы №2 (с передачей параметров):

Example_2

Вместо POP DX (перемещения параметра из стека в неиспользуемый регистр) можно было бы просто увеличить SP на 2. Результат один и тот же - очищение стека от локального параметра.

Пример вызова подпрограммы №3:

Example_3

RET 2, а не просто RET, выполняем для того, чтобы очистить стек ещё и от параметров.

Фрейм (Frame) вызова подпрограммы (flashback с кадром стека в лекциях по Си) - область стека от параметров до локальных переменных (немного вольное определение по-моему).

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

Соглашения о вызовах - договорённости о том, какая сторона будет очищать стек и т.д..

Есть много соглашений: ...

Сишные (см. лекции по Си):

  • Стек очищает вызывающая сторона
  • Числа возвращаются через регистр AX
  • Большие числа или строки будут возвращаться через указатель

Использование стека подпрограммами

Стековый кадр (фрейм) — механизм передачи аргументов и выделения временной памяти с использованием аппаратного стека. Содержит информацию о состоянии подпрограммы.

Включает в себя:

  • Параметры
  • Адрес возврата (обязательно)
  • Локальные переменные
Clone this wiki locally