Оглавление
- Умножение двоичных чисел
- Правила деления в Assembler
- Подытожим…
- Условные переходы
- Правила умножения в Assembler
- Правила деления в Assembler
- Рассмотрим команды ассемблера на практическом примере.
- Умножение в Assembler
- Непрямая адресация памяти
- 2.5 Константы
- Системные вызовы Linux
- Деление в Assembler
- Режимы адресации
- Правила деления в Assembler
- Адресация памяти
- Быть или не быть?
- Битовые операции
- 3.2.5 TIMES: Повторение инструкций или данных
- 3.3 Эффективные адреса
- Обзор группы арифметических команд и данных
Умножение двоичных чисел
В отличие от сложения и вычитания операция умножения реализуется двумя типами команд – учитывающими и не учитывающими знаки операндов.
Умножение чисел размером 1 байт без учета знака
--------------------------------------------------------------------- :mul_unsign.asm – программа умножения чисел размером 1 байт без учета знака. ;Вход: multiplier], и multiplied – множители размером 1 байт. ;Выход: product – значение произведения. --------------------------------------------------------------------- .data :значения в multiplier], и multiplied нужно внести product label word productj label byte multiplied db?:множитель 1 (младшая часть произведения) product_h db 0;старшая часть произведения multiplied db?;множитель 2 .code mul_unsign proc mov al.multiplierl mul multiplier2:оценить результат: jnc по_саrrу;нет переполнения – на no_carry обрабатываем ситуацию переполнения mov product_h.ah:старшая часть результата no_carry: mov product_l.al;младшая часть результата ret mul_unsign endp main: call mul_unsign end main
Здесь все достаточно просто и реализуется средствами самого процессора. Проблема состоит лишь в правильном определении размера результата. Произведение чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX.
Умножение чисел размером N и М байт без учета знака
Для умножения чисел размером N и М байт, существует несколько стандартных алгоритмов, описанных в литературе. В этом разделе мы рассмотрим только один из них. В его основе лежит алгоритм умножения неотрицательных целых чисел, предложенный Кнутом.
Умножение N-байтного числа на число размером М байт
ПРОГРАММА mul_unsign_NM --------------------------------------------------------------------- //mul_unsign_NM – программа на псевдоязыке умножения N-байтного числа //на число размером М байт //(порядок – старший байт по младшему адресу (не Intel)) //Вход: U и V – множители размерностью N и М байт соответственно : b=256 – размерность машинного слова. //Выход: W – произведение размерностью N+M байт. --------------------------------------------------------------------- ПЕРЕМЕННЫЕ INT_BYTE u; v; w: k=0: INT_WORD b=256: temp_word НАЧ_ПРОГ ДЛЯ j: = M-l ДО 0 //J изменяется в диапазоне М-1..0 НАЧ_БЛОК_1 //проверка на равенство нулю очередного элемента множителя (не обязательно) ЕСЛИ v==0 TO ПЕРЕЙТИ_НА тб k: = 0: i: = n-l ll\ изменяется в диапазоне N-1..0 ДЛЯ 1: = N-1 ДО О НАЧ_БЛ0К_2 //перемножаем очередные элементы множителей temp_word: = u*v+w+k w: = temp_word MOD b //остаток от деления temp_word\b › w k: = temp_word\b //целая часть частного temp_word\b > k К0Н_БЛ0К_2 w: = k шб: КОН БЛОК_1 КОН_ПРОГ :inul_unsign_NM.asm – программа на ассемблере умножения N-байтного числа на число :размером М байт (порядок – старший байт по младшему адресу (не Intel)). .data:значения в U и V нужно внести U db?;U-un.i".UiU() – множитель_1 размерностью N байт 1-S-U:i=N V db?; V"Vm.i_ViV(| – множитель_2 размерностью М байт j=$-V:j=M len_product=$-U ;w – результат умножения, длина N+M W db len_product dup (0);1en_product=N+M k db 0:перенос 0 < k < 255 b dw lOOh: размер машинного слова .code
Правила деления в Assembler
Почти аналогично реализуется и деление, вот примеры:
- Если аргументом команды div является 1-байтовый регистр (например ), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah.
- Если аргументом является регистр из 2 байт(например), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx.
- Если же аргументом является регистр из 4 байт(например), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx.
Подытожим…
Итак, приведу неполный перечень того, в каких случаях используется ассемблер.
-
Создание загрузчиков, прошивок устройств (комплектующих ПК, встраиваемых систем), элементов ядра ОС.
-
Низкоуровневая работа с железом, в т.ч. с процессором, памятью.
-
Внедрение кода в процессы (injection), как с вредоносной целью, так и с целью защиты или добавления функционала. Системный софт.
-
Блоки распаковки, защиты кода и прочего функционала (с целью изменения поведения программы, добавления новых функций, взлома лицензий), встраиваемые в исполняемые файлы (см. UPX, ASProtect и пр).
-
Оптимизация кода по скорости, в т.ч. векторизация (SSE, AVX, FMA), математические вычисления, обработка мультимедиа, копирование памяти.
-
Оптимизация кода по размеру, где нужно контролировать каждый байт. Например, в демосцене.
-
Вставки в языки высокого уровня, которые не позволяют выполнять необходимую задачу, либо позволяют делать это неоптимальным образом.
-
При создании компиляторов и трансляторов исходного кода с какого-либо языка на язык ассемблера (например, многие компиляторы C/C++ позволяют выполнять такую трансляцию). При создании отладчиков, дизассемблеров.
-
Собственно, отладка, дизассемблирование, исследование программ (reverse engineering).
-
Создание файлов данных с помощью макросов и директив генерации данных.
-
Вы не поверите, но ассемблер можно использовать и для написания обычного прикладного ПО (консольного или с графическим интерфейсом – GUI), игр, драйверов и библиотек 🙂
Условные переходы
Все команды этой группы выполняют переход (PC ← PC + A + 1) при разных условиях.
Мнемоника | Описание | Условие | Флаги |
---|---|---|---|
BRBC s, A | Переход если флаг S сброшен | Если SREG(S) = 0 | — |
BRBS s, A | Переход если флаг S установлен | Если SREG(S) = 1 | — |
BRCS A | Переход по переносу | Если C = 1 | — |
BRCC A | Переход если нет переноса | Если C = 0 | — |
BREQ A | Переход если равно | Если Z = 1 | — |
BRNE A | Переход если не равно | Если Z = 0 | — |
BRSH A | Переход если больше или равно | Если C = 0 | — |
BRLO A | Переход если меньше | Если C = 1 | — |
BRMI A | Переход если отрицательное значение | Если N = 1 | — |
BRPL A | Переход если положительное значение | Если N = 0 | — |
BRGE A | Переход если больше или равно (со знаком) | Если (N и V) = 0 | — |
BRLT A | Переход если меньше (со знаком) | Если (N или V) = 1 | — |
BRHS A | Переход по половинному переносу | Если H = 1 | — |
BRHC A | Переход если нет половинного переноса | Если H = 0 | — |
BRTS A | Переход если флаг T установлен | Если T = 1 | — |
BRTC A | Переход если флаг T сброшен | Если T = 0 | — |
BRVS A | Переход по переполнению дополнительного кода | Если V = 1 | — |
BRVC A | Переход если нет переполнения дополнительного кода | Если V = 0 | — |
BRID A | Переход если прерывания запрещены | Если I = 0 | — |
BRIE A | Переход если прерывания разрешены | Если I = 1 | — |
SBRC Rd, K | Пропустить следующую команду если бит в регистре очищен | Если Rd = 0 | — |
SBRS Rd, K | Пропустить следующую команду если бит в регистре установлен | Если Rd = 1 | — |
SBIC A, b | Пропустить еследующую команду если бит в регистре ввода/вывода очищен | Если I/O(A, b) = 0 | — |
SBIS A, b | Пропустить следующую команду если бит в регистре ввода/вывода установлен | Если I/O(A, b) = 1 | — |
Правила умножения в Assembler
Итак, как мы уже сказали, при умножении и делении в Assembler есть некоторые тонкости, о которых дальше и пойдет речь. Тонкости эти состоят в том, что от того, какой размерности регистр мы делим или умножаем многое зависит. Вот примеры:
- Если аргументом команды mul является 1-байтовый регистр (например ), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.
- Если аргументом является регистр из 2 байт(например), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.
- Если аргументом является регистр из 4 байт(например), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.
Правила деления в Assembler
Почти аналогично реализуется и деление, вот примеры:
- Если аргументом команды div является 1-байтовый регистр (например div bl ), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah. ax/bl = al, ah
- Если аргументом является регистр из 2 байт(например div bx ), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx. (dx,ax)/bx = ax, dx
- Если же аргументом является регистр из 4 байт(например div ebx ), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx. (edx,eax)/ebx = eax, edx
Рассмотрим команды ассемблера на практическом примере.
С использованием среды разработки TASMED или любого текстового редактора набираем код. Программа, задаст вопрос на английском языке о половой принадлежности (имеется ввиду ваш биологический пол при рождении). Если вы нажмете m (Man), будет выведено приветствие с мужчиной, если w (Woman), то с женщиной, после этого программа прекратит работу. Если будет нажата любая другая клавиша, то программа предположит, что имеет дело с гоблином, не поверит и будет задавать вам вопросы о половой принадлежности, пока вы не ответите верно.
;goblin.asm
.model tiny ; for СОМ
.code ; code segment start
org 100h ; offset in memory = 100h (for COM)
start: main proc
begin:
mov ah,09h
mov dx,offset prompt
int 21h
inpt:
mov ah,01h
int 21h
cmp al,’m’
je mode_man
cmp al,’w’
je mode_woman
call goblin
jmp begin
mode_man:
mov addrs,offset man; указатель на процедуру в addrs
jmp cont
mode_woman:
mov addrs,offset woman; указатель на процедуру в addrs
cont:
call word ptr addrs; косвенный вызов процедуры
mov ax,4c00h
int 21h
main endp
man proc
mov ah,09h
mov dx,offset mes_man
int 21h
ret
man endp
woman proc
mov ah,09h
mov dx,offset mes_womn
int 21h
ret
woman endp
goblin proc
mov ah,09h
mov dx,offset mes_gobl
int 21h
ret
goblin endp
;DATA
addrs dw 0;for procedure adress
prompt db ‘Are you Man or Woman [m/w]? : $’
mes_man db 0Dh,0Ah,»Hello, Strong Man!»,0Dh,0Ah,’$’ ; строка для вывода. Вместо ASCII смвола ‘$’ можно написать машинный код 24h
mes_womn db 0Dh,0Ah,»Hello, Beautyful Woman!»,0Dh,0Ah,’$’ ; строка для вывода
mes_gobl db 0Dh,0Ah,»Hello, Strong and Beautyful GOBLIN!»,0Dh,0Ah,24h ; строка для вывода. 24h = ‘$’ .
len = $ — mes_gobl
end start
1 |
;goblin.asm .modeltiny; for СОМ .code; code segment start org100h; offset in memory = 100h (for COM) startmainproc begin movah,09h movdx,offsetprompt int21h inpt movah,01h int21h cmpal,’m’ jemode_man cmpal,’w’ jemode_woman callgoblin jmpbegin mode_man movaddrs,offsetman; указатель на процедуру в addrs jmpcont mode_woman movaddrs,offsetwoman; указатель на процедуру в addrs cont callwordptraddrs; косвенный вызов процедуры movax,4c00h int21h mainendp manproc movah,09h movdx,offsetmes_man int21h ret manendp womanproc movah,09h movdx,offsetmes_womn int21h ret womanendp goblinproc movah,09h movdx,offsetmes_gobl int21h ret goblinendp addrsdw;for procedure adress promptdb’Are you Man or Woman [m/w]? : $’ mes_mandb0Dh,0Ah,»Hello, Strong Man!»,0Dh,0Ah,’$’; строка для вывода. Вместо ASCII смвола ‘$’ можно написать машинный код 24h mes_womndb0Dh,0Ah,»Hello, Beautyful Woman!»,0Dh,0Ah,’$’; строка для вывода mes_gobldb0Dh,0Ah,»Hello, Strong and Beautyful GOBLIN!»,0Dh,0Ah,24h; строка для вывода. 24h = ‘$’ . len=$-mes_gobl endstart |
Умножение в Assembler
Для умножения чисел без знака предназначена команда MUL. У этой команды только один операнд — второй множитель, который должен находиться в регистре или в памяти. Местоположение первого множителя и результата задаётся неявно и зависит от размера операнда:
Отличие умножения от сложения и вычитания в том, что разрядность результата получается в 2 раза больше, чем разрядность сомножителей. Также и в десятичной системе — например, умножая двухзначное число на двухзначное, мы можем получить в результате максимум четырёхзначное. Запись «DX:AX» означает, что старшее слово результата будет находиться в DX, а младшее — в AX.
Если старшая часть результата равна нулю, то флаги CF и ОF будут иметь нулевое значение. В этом случае старшую часть результата можно отбросить. Это свойство можно использовать в программе, если результат должен быть такого же размера, как множители.
Если аргументом команды MUL является 1-байтовый регистр (например MUL bl), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.
Если аргументом является регистр из 2 байт(например MUL bx), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.
Если аргументом является регистр из 4 байт(например MUL ebx), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.
Непрямая адресация памяти
Обычно для этой цели используются базовые регистры EBX, EBP (или BX, BP) и индексные регистры (DI, SI), используемые в квадратных скобках для ссылок на память.
Непрямая адресация обычно используется для переменных, содержащих несколько элементов, таких как массивы. Начальный адрес массива хранится, например, в регистре EBX.
В следующем примере мы получаем доступ к разным элементам переменной:
MY_TABLE TIMES 10 DW 0 ; выделяем 10 слов (2 байта), каждое из которых инициализируем значением 0
MOV EBX, ; помещаем эффективный адрес MY_TABLE в EBX
MOV , 110 ; MY_TABLE = 110
ADD EBX, 2 ; EBX = EBX +2
MOV , 123 ; MY_TABLE = 123
1 |
MY_TABLETIMES10DW; выделяем 10 слов (2 байта), каждое из которых инициализируем значением 0 MOVEBX,MY_TABLE; помещаем эффективный адрес MY_TABLE в EBX MOVEBX,110; MY_TABLE = 110 ADDEBX,2; EBX = EBX +2 MOVEBX,123; MY_TABLE = 123 |
2.5 Константы
NASM знает четыре различных типа констант: числовые, символьные, строковые и с плавающей точкой.
2.5.1 Числовые константы
Числовая константа — это просто число. NASM позволят определять числа в различных системах счисления и различными способами: вы можете использовать суффиксы H, Q или O, и B для шестнадцатеричных, восьмеричных и двоичных чисел соответственно; можете использовать для шестнадцатеричных чисел префикс 0x в стиле С, а также префикс $ в стиле Borland Pascal. Однако имейте в виду, что префикс $ может быть также префиксом идентификаторов (см. параграф ), поэтому первой цифрой шестнадцатеричного числа при использовании этого префикса должна быть обязательно цифра, а не буква.
Некоторые примеры числовых констант:
mov ax,100 ; десятичная
mov ax,0a2h ; шестнадцатеричная
mov ax,$0a2 ; снова шестнадцатеричная: нужен 0
mov ax,0xa2 ; опять шестнадцатеричная
mov ax,777q ; восьмеричная
mov ax,777o ; снова восьмеричная
mov ax,10010011b ; двоичная
2.5.2 Символьные константы
Символьная константа содержит от одного до четырех символов, заключенных в одиночные или двойные кавычки. Тип кавычек для NASM несущественен, поэтому если используются одинарные кавычки, двойные могут выступать в роли символа и, соответственно, наоборот. Символьная константа, содержащая более одного символа, будет загружаться в обратном порядке следования байт: если вы пишете
mov eax,’abcd’
сгенерированной константой будет не 0x61626364, а 0x64636261, поэтому если сохранить эту константу в память, а затем прочитать, получится снова abcd, но никак не dcba. Это также влияет на инструкцию CPUID Пентиумов.
2.5.3 Строковые константы
Строковые константы допустимы только в некоторых псевдо-инструкциях, а именно в семействе DB и инструкции INCBIN.
Строковые константы похожи на символьные, только длиннее. Они обрабатываются как сцепленные друг с другом символьные константы. Так, например, следующие строки кода эквивалентны.
db ‘hello’ ; строковая константа
db ‘h’,’e’,’l’,’l’,’o’ ; эквивалент из символьных констант
Следующие строки также эквивалентны:
dd ‘ninechars’ ; строковая константа в двойное слово
dd ‘nine’,’char’,’s’ ; три двойных слова
db ‘ninechars’,0,0,0 ; и действительно похоже
Обратите внимание, что когда используется db, константа типа ‘ab’ обрабатывается как строковая, хотя и достаточно коротка, чтобы быть символьной, потому что иначе db ‘ab’ имело бы тот же смысл, какой и db ‘a’, что глупо. Соответственно, трех- или четырехсимвольные константы, являющиеся операндами инструкции dw, обрабатываются также как строки
2.5.4 Константы с плавающей точкой
Константы с плавающей точкой допустимы только в качестве аргументов DW, DD, DQ и DT. Выражаются они традиционно: цифры, затем точка, затем возможно цифры после точки, и наконец, необязательная E с последующей степенью. Точка обязательна, т.к. dd 1 NASM воспримет как объявление целой константы, в то время как dd 1.0 будет воспринята им правильно.
Несколько примеров:
dw -0.5 ;
dd 1.2 ; «простое» число
dq 1.e10 ; 10,000,000,000
dq 1.e+10 ; синоним 1.e10
dq 1.e-10 ; 0.000 000 000 1
dt 3.141592653589793238462 ; число pi
В процессе компиляции NASM не может проводить вычисления над константами с плавающей точкой (это сделано с целью переносимости). Несмотря на то, что NASM генерирует код для х86 процессоров, сам по себе ассемблер может работать на любой системе с ANCI C компилятором. Само собой, ассемблер не может гарантировать присутствия устройства, обрабатывающего числа с плавающей точкой в формате Intel, поэтому стало бы необходимо включить собственный полный набор подпрограмм для работы с такими числами, что неизбежно привело бы к значительному увеличению размера самого ассемблера, хотя польза от этого была бы минимальна.
Системные вызовы Linux
В своих программах на ассемблере вы можете использовать системные вызовы Linux. Для этого нужно:
поместить номер системного вызова в регистр EAX;
сохранить аргументы системного вызова в регистрах EBX, ECX и т.д.;
вызвать соответствующее прерывание (80h);
результат обычно возвращается в регистр EAX.
Есть шесть регистров, в которых хранятся и используются аргументы необходимого системного вызова:
EBX
ECX
EDX
ESI
EDI
EBP
Эти регистры принимают последовательные аргументы. Если есть более шести аргументов, то ячейка памяти, содержащая первый аргумент, сохраняется в регистре EBX.
В следующем примере мы будем использовать системный вызов :
mov eax,1 ; номер системного вызова (sys_exit)
int 0x80 ; вызов ядра
1 |
moveax,1; номер системного вызова (sys_exit) int0x80; вызов ядра |
А в следующем — :
mov edx,4 ; длина сообщения
mov ecx,msg ; сообщение для вывода на экран
mov ebx,1 ; файловый дескриптор (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра
1 |
movedx,4; длина сообщения movecx,msg; сообщение для вывода на экран movebx,1; файловый дескриптор (stdout) moveax,4; номер системного вызова (sys_write) int0x80; вызов ядра |
Все системные вызовы перечислены в вместе с их номерами (значение, которое помещается в EAX перед вызовом ).
В следующей таблице приведены некоторые системные вызовы, которые мы будем использовать:
%eax | Название | %ebx | %ecx | %edx | %esx | %edi |
1 | sys_exit | int | — | — | — | — |
2 | sys_fork | struct pt_regs | — | — | — | — |
3 | sys_read | unsigned int | char * | size_t | — | — |
4 | sys_write | unsigned int | const char * | size_t | — | — |
5 | sys_open | const char * | int | int | — | — |
6 | sys_close | unsigned int | — | — | — | — |
В следующей программе мы запрашиваем число, а затем выводим его на экран:
section .data ; сегмент данных
userMsg db ‘Please enter a number: ‘ ; сообщение с просьбой ввести число
lenUserMsg equ $-userMsg ; длина сообщения
dispMsg db ‘You have entered: ‘
lenDispMsg equ $-dispMsg
section .bss ; неинициализированные данные
num resb 5
section .text ; сегмент кода
global _start
_start: ; запрашиваем пользовательский ввод
mov eax, 4
mov ebx, 1
mov ecx, userMsg
mov edx, lenUserMsg
int 80h
; Считываем и сохраняем пользовательский ввод
mov eax, 3
mov ebx, 2
mov ecx, num
mov edx, 5 ; 5 байт информации
int 80h
; Выводим сообщение ‘You have entered: ‘
mov eax, 4
mov ebx, 1
mov ecx, dispMsg
mov edx, lenDispMsg
int 80h
; Выводим число пользователя
mov eax, 4
mov ebx, 1
mov ecx, num
mov edx, 5
int 80h
; Код выхода
mov eax, 1
mov ebx, 0
int 80h
1 |
section.data; сегмент данных userMsgdb’Please enter a number: ‘; сообщение с просьбой ввести число lenUserMsgequ$-userMsg; длина сообщения dispMsgdb’You have entered: ‘ lenDispMsgequ$-dispMsg section.bss; неинициализированные данные numresb5 section.text; сегмент кода global_start _start; запрашиваем пользовательский ввод moveax,4 movebx,1 movecx,userMsg movedx,lenUserMsg int80h ; Считываем и сохраняем пользовательский ввод moveax,3 movebx,2 movecx,num movedx,5; 5 байт информации int80h ; Выводим сообщение ‘You have entered: ‘ moveax,4 movebx,1 movecx,dispMsg movedx,lenDispMsg int80h ; Выводим число пользователя moveax,4 movebx,1 movecx,num movedx,5 int80h ; Код выхода moveax,1 movebx, int80h |
Результат выполнения программы:
Деление в Assembler
Для умножения чисел без знака предназначена команда DIV, которая относится к группе команд целочисленной арифметики и производит целочисленное деление с остатком беззнаковых целочисленных операндов.
Делимое, частное и остаток задаются неявно. Делимое является переменной в регистре (или регистровой паре) AX, DX:AX или EDX:EAX в зависимости от кода команды и размера операнда (что также определяет и разрядность делителя). Единственный явный операнд команды — операнд-источник (SRC), задающий делитель — может быть переменной в регистре или в памяти.
Целая часть частного помещается в регистр AL, AX или EAX в зависимости от заданного размера делителя (8, 16 или 32 бита). При этом остаток от целочисленного деления помещается в регистр AH, DX или EDX соответственно.
Действие команды DIV зависит от размера операнда-источника следующим образом:
Если частное, получаемое в результате деления, оказывается слишком велико, чтобы поместиться в целевом регистре-назначении (то есть имеет место переполнение), или если делитель равен нулю, то генерируется особая ситуация #DE.
Если аргументом команды div является 1-байтовый регистр (например DIV bl), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah.
Если аргументом является регистр из 2 байт(например DIV bx), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx.
Если же аргументом является регистр из 4 байт(например DIV ebx), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx.
Режимы адресации
Большинство инструкций на языке ассемблера требуют обработки операндов. Адрес операнда предоставляет место, где хранятся данные, подлежащие обработке. Некоторые инструкции не требуют операнда, в то время как другие могут требовать один, два или три операнда. В тех случаях, когда инструкции требуется два операнда, первый операнд обычно является местом назначения, содержащий данные в регистре или в ячейке памяти, а второй — источником. Источник содержит либо данные для доставки (немедленная адресация), либо адрес (в регистре или в памяти) данных. Как правило, исходные данные остаются неизменными после операции.
Есть три основных режима адресации:
регистровая адресация;
прямая (или «непосредственная») адресация;
адресация памяти.
Правила деления в Assembler
Почти аналогично реализуется и деление, вот примеры:
- Если аргументом команды div является 1-байтовый регистр (например div bl ), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah. ax/bl = al, ah
- Если аргументом является регистр из 2 байт(например div bx ), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx. (dx,ax)/bx = ax, dx
- Если же аргументом является регистр из 4 байт(например div ebx ), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx. (edx,eax)/ebx = eax, edx
Адресация памяти
Когда в режиме адресации памяти операнды определены, то требуется прямой доступ к основной памяти (обычно к сегменту данных) — этот вариант является медленным. Чтобы обнаружить точное местоположение данных в памяти, нам нужен начальный адрес сегмента, который обычно находится в регистре DS, и значение смещения. Это значение смещения также называется эффективным адресом.
В режиме прямой адресации значение смещения указывается как часть инструкции (например, имя переменной). Ассемблер вычисляет значение смещения и поддерживает таблицу символов, в которой хранятся значения смещения всех переменных, используемых в программе.
При прямой адресации памяти один из операндов ссылается на ячейку памяти, а другой — на регистр. Например:
ADD BYTE_VALUE, DL ; добавляем регистр в ячейку памяти
MOV BX, WORD_VALUE ; операнд из памяти скопирован в регистр
1 |
ADDBYTE_VALUE,DL; добавляем регистр в ячейку памяти MOVBX,WORD_VALUE; операнд из памяти скопирован в регистр |
Быть или не быть?
Так, нужно ли изучать ассемблер современному программисту? Если вы уже не новичок в программировании, и у вас серьёзные амбиции, то изучение ассемблера, внутреннего устройства операционных систем и функционирования железа (особенно процессоров, памяти), а также использование различных инструментов для дизассемблирования, отладки и анализа кода полезно тем, кто хочет писать действительно эффективные программы. Иначе будет сложно в полной мере понять, что происходит «под капотом» любимого компилятора (хотя бы в общих чертах), как оптимизировать программы на любом языке программирования и какой приём стоит предпочесть. Необязательно погружаться слишком глубоко в эту тему, если вы пишете на Python или JavaScript. А вот если ваш язык – C или C++, хорошенько изучить ассемблер будет полезно.
Вместе с тем, необходимо помнить не только о «тактике», но и о «стратегии» написания кода, поэтому не менее важно изучать и алгоритмы (правильный выбор которых зачастую более важен для создания эффективных программ, нежели низкоуровневая оптимизация), шаблоны проектирования и многие другие технологии, без которых программист не может считать себя современным
Битовые операции
Мнемоника | Описание | Операция | Флаги |
---|---|---|---|
CBR Rd, K | Очистка разрядов регистра | Rd ← Rd and (0FFH – K) | Z, N, V |
SBR Rd, K | Установка разрядов регистра | Rd ← Rd or K | Z, N, V |
CBI P, b | Сброс разряда I/O-регистра | P.b ← 0 | — |
SBI P, b | Установка разряда I/O-регистра | P.b ← 1 | — |
BCLR s | Сброс флага SREG | SREG.s ← 0 | SREG.s |
BSET s | Установка флага SREG | SREG.s ← 1 | SREG.s |
BLD Rd, b | Загрузка разряда регистра из флага T | Rd.b ← T | — |
BST Rr, b | Запись разряда регистра во флаг T | T ← Rd.b | T |
CLC | Сброс флага переноса | C ← 0 | C |
SEC | Установка флага переноса | C ← 1 | C |
CLN | Сброс флага отрицательного числа | N ← 0 | N |
SEN | Установка флага отрицательного числа | N ← 1 | N |
CLZ | Сброс флага нуля | Z ← 0 | Z |
SEZ | Установка флага нуля | Z ← 1 | Z |
CLI | Общий запрет прерываний | I ← 0 | I |
SEI | Общее разрешение прерываний | I ← 1 | I |
CLS | Сброс флага знака | S ← 0 | S |
SES | Установка флага знака | S ← 1 | S |
CLV | Сброс флага переполнения дополнительного кода | V ← 0 | V |
SEV | Установка флага переполнения дополнительного кода | V ← 1 | V |
CLT | Сброс пользовательского флага T | T ← 0 | T |
SET | Установка пользовательского флага T | T ← 1 | T |
CLH | Сброс флага половинного переноса | H ← 0 | H |
SEH | Установка флага половинного переноса | H ← 1 | H |
3.2.5 TIMES: Повторение инструкций или данных
Префикс TIMES заставляет инструкцию ассемблироваться
несколько раз. Данная псевдо-инструкция отчасти представляет NASM-эквивалент
синтаксиса DUP, поддерживающегося MASM-совместимыми
ассемблерами. Вы можете написать, например
или что-то подобное; однако TIMES более разносторонняя
инструкция. Аргумент TIMES не просто числовая
константа, а числовое выражение, поэтому вы можете писать следующие вещи:
При этом будет резервироваться строго определенное пространство, начиная от
метки buffer и длиной 64 байта. Наконец, TIMES
может использоваться в обычных инструкциях, так что вы можете писать тривиальные
развернутые циклы:
Заметим, что нет никакой принципиальной разницы между times
100 resb 1 и resb 100 за исключением того,
что последняя инструкция будет обрабатываться примерно в 100 раз быстрее из-за
внутренней структуры ассемблера.
Операнд псевдо-инструкции TIMES, подобно EQU
и RESB, является критическим выражением ().
Имейте также в виду, что TIMES не применима в
макросах: причиной служит то, что TIMES обрабатывается
после макро-фазы, позволяющей аргументу TIMES содержать
выражение, подобное 64-$+buffer. Для повторения
более одной строки кода или в сложных макросах используйте директиву препроцессора
%rep.
3.3 Эффективные адреса
Эффективный адрес это любой операнд инструкции со ссылкой на память.
Эффективные адреса в NASM имеют очень простой синтаксис: они содержат выражение
(в результате вычислений которого получается нужный адрес), обрамленное квадратными
скобками. Например:
Любая другая ссылка, не соответствующая этой простой системе, для NASM недействительна,
например es:wordvar.
Более сложные эффективные адреса, когда вовлечено более одного регистра, работают
точно также:
NASM способен воспринимать алгебру таких выражений, поэтому он правильно транслирует
вещи, выглядящие на первый взгляд недопустимыми:
Некоторые варианты эффективных адресов имеют более одной ассемблерной формы;
в большинстве таких ситуаций NASM будет генерировать самую короткую из них.
Например, у нас имеются простые ассемблерные инструкции
и . NASM будет генерировать последнюю
из них, т.к. первый вариант требует дополнительно 4 байта для хранения нулевого
смещения.
NASM имеет механизм подсказок, позволяющий создавать из
и разные инструкции; это порой полезно,
т.к. например и
по умолчанию имеют разные сегментные регистры.
Несмотря на это, вы можете заставить NASM генерировать требуемые формы эффективных
адресов при помощи ключевых слов BYTE, WORD, DWORD
и NOSPLIT. Если вам нужно, чтобы
ассемблировалась со смещением в двойное слово, вместо одного байта по умолчанию,
вы можете написать . Точно также при
помощи вы можете заставить NASM
использовать байтовые смещения для небольших значений, не определяемых при первом
проходе (см. пример такого кода в ).
В особых случаях, будет кодироваться
как с нулевым байтовым смещением, а будет кодироваться с нулевым смещением в двойное слово. Обычная
форма, , будет оставлена без смещения.
NASM будет разделять на ,
т.к. это позволяет избежать использования поля смещения и сэкономить некоторое
пространство; соответственно, будет
разделено на . При помощи ключевого
слова NOSPLIT вы можете запретить такое поведение
NASM: будет буквально оттранслировано
в .
Обзор группы арифметических команд и данных
Рис. 1. Классификация
арифметических команд
Группа арифметических целочисленных команд работает с двумя типами чисел:
-
целыми двоичными числами. Числа могут иметь знаковый разряд
или не иметь такового, то есть быть числами со знаком или без знака; - целыми десятичными числами.
Целые двоичные числа
Размерность целого двоичного числа может составлять 8,
16 или 32 бит. Знак двоичного числа определяется тем, как интерпретируется
старший бит в представлении числа. Это 7-й, 15-й или 31-й биты для чисел
соответствующей размерности (см. Типы данных
). При этом интересно то, что среди арифметических команд есть
всего две команды, которые действительно учитывают этот старший разряд
как знаковый, — это команды целочисленного умножения и деления imul и idiv.
В остальных случаях ответственность за действия со знаковыми числами и,
соответственно, со знаковым разрядом ложится на программиста. К этому вопросу
мы вернемся чуть позже. Диапазон значений двоичного числа зависит от его
размера и трактовки старшего бита либо как старшего значащего бита числа,
либо как бита знака числа (табл. 1).
Таблица 1. Диапазон значений двоичных чисел
Размерность поля | Целое без знака | Целое со знаком |
байт | 0…255 | –128…+127 |
слово | 0…65 535 | –32 768…+32 767 |
двойное слово | 0…4 294 967 295 | –2 147 483 648…+2 147 483 647 |
Это делается с использованием директив
описания данных. К примеру, последовательность описаний двоичных
чисел из сегмента данных листинга 1 (помните о принципе “младший байт по
младшему адресу”) будет выглядеть в памяти так, как показано на рис. 2.
Листинг 1. Числа с фиксированной точкой ;prg_8_1.asm masm model small stack 256 .data ;сегмент данных per_1 db 23 per_2 dw 9856 per_3 dd 9875645 per_4 dw 29857 .code ;сегмент кода main: ;точка входа в программу mov ax,@data ;связываем регистр dx с сегментом mov ds,ax ;данных через регистр ax exit: ;посмотрите в отладчике дамп сегмента данных mov ax,4c00h ;стандартный выход int 21h end main ;конец программы |
Рис. 2. Дамп
памяти для сегмента данных листинга 1
Десятичные числа
-
упакованном формате — в этом формате каждый байт содержит
две десятичные цифры. Десятичная цифра представляет собой двоичное значение
в диапазоне от 0 до 9 размером 4 бита. При этом код старшей цифры числа
занимает старшие 4 бита. Следовательно, диапазон представления десятичного
упакованного числа в одном байте составляет от 00 до 99; -
неупакованном формате — в этом формате каждый байт содержит
одну десятичную цифру в четырех младших битах. Старшие четыре бита имеют
нулевое значение. Это так называемая зона. Следовательно, диапазон представления
десятичного неупакованного числа в одном байте составляет от 0 до 9.
Рис. 3. Представление
BCD-чисел
Как описать двоично-десятичные числа в программе?
Для этого можно использовать только две директивы описания
и инициализации данных — db и dt. Возможность применения только этих директив
для описания BCD-чисел обусловлена тем, что к таким числам также применим
принцип “младший байт по младшему адресу”, что, как мы увидим далее, очень
удобно для их обработки. И вообще, при использовании такого типа данных
как BCD-числа, порядок описания этих чисел в программе и алгоритм их обработки
— это дело вкуса и личных пристрастий программиста. Это станет ясно после
того, как мы ниже рассмотрим основы работы с BCD-числами. К примеру, приведенная
в сегменте данных листинга 2 последовательность описаний BCD-чисел будет
выглядеть в памяти так, как показано на рис. 4.
Листинг 2. BCD-числа ;prg_8_2.asm masm model small stack 256 .data ;сегмент данных per_1 db 2,3,4,6,8,2 ;неупакованное BCD-число 286432 per_3 dt 9875645 ;упакованное BCD-число 9875645 .code ;сегмент кода main: ;точка входа в программу mov ax,@data ;связываем регистр dx с сегментом mov ds,ax ;данных через регистр ax exit: ;посмотрите в отладчике дамп сегмента данных mov ax,4c00h ;стандартный выход int 21h end main ;конец программы |
Рис. 4. Дамп
памяти для сегмента данных листинга 2
После столь подробного обсуждения объектов, с которыми
работают арифметические операции, можно приступить к рассмотрению средств
их обработки на уровне системы команд микропроцессора.