Как сделать сложение в ассемблере

Добавил пользователь Cypher
Обновлено: 05.10.2024

Привет всем! У меня есть прога по сложению двух чисел на ассемблере:

.model tiny
.data
ch1 db 6,'$'
ch2 db 6,'$'
.code
main proc
mov ax,@data
mov ds,ax

mov ah,0Ah
mov dx,offset ch1
int 21h

mov bx,offset ch1

mov ah,0Ah
mov dx,offset ch2
int 21h

mov cx,offset ch2

mov ah,09h
mov dx,offset bx
int 21h

mov ah,08h
int 21h

mov ax,4C00h
int 21h

main endp
end main

Но данная прога работает некорректно, а почему не могу понять? Может кто подскажет?

дануконешно. Вот формат буфера, на который указывают bx/cx:

Format of DOS input buffer :

Offset Size Description ( Table 01344 )
00h BYTE maximum characters buffer can hold
01h BYTE ( call ) number of chars from last input which may be recalled
( ret ) number of characters actually read , excluding CR
02h N BYTEs actual characters read , including the final carriage return

Да, и не факт, что bx переживёт вызов int 21 невредимым.

Ну даже если допустить, что каким-то чудом в bx попал результат сложения, и что транслятор проглотит этот наивный offset, девятая функция 21-го прерывания ну никак не переведёт число из dx в строку символов, заканчивающихся $.

Шпаргалка по основным инструкциям ассемблера x86/x64

12 октября 2016

В прошлой статье мы написали наше первое hello world приложение на асме, научились его компилировать и отлаживать, а также узнали, как делать системные вызовы в Linux. Сегодня же мы познакомимся непосредственно с ассемблерными инструкциями, понятием регистров, стека и вот этого всего. Ассемблеры для архитектур x86 (a.k.a i386) и x64 (a.k.a amd64) очень похожи, в связи с чем нет смысла рассматривать их в отдельных статьях. Притом акцент я постараюсь делать на x64, попутно отмечая отличия от x86, если они есть. Далее предполагается, что вы уже знаете, например, чем стек отличается от кучи, и объяснять такие вещи не требуется.

Регистры общего назначения

Регистр — это небольшой (обычно 4 или 8 байт) кусочек памяти в процессоре с чрезвычайно большой скоростью доступа. Регистры делятся на регистры специального назначения и регистры общего назначения. Нас сейчас интересуют регистры общего назначения. Как можно догадаться по названию, программа может использовать эти регистры под свои нужды, как ей вздумается.

Рассмотрим пример. Допустим, выполняются следующие три инструкции:

(gdb) x/3i $pc
=> 0x8048074: mov $0xaabbccdd,%eax
0x8048079: mov $0xee,%al
0x804807b: mov $0x1234,%ax

Значения регистров после записи в eax значения 0 x AABBCCDD:

(gdb) p/x $eax
$1 = 0xaabbccdd
(gdb) p/x $ax
$2 = 0xccdd
(gdb) p/x $ah
$3 = 0xcc
(gdb) p/x $al
$4 = 0xdd

Значения после записи в регистр al значения 0 x EE:

(gdb) p/x $eax
$5 = 0xaabbccee
(gdb) p/x $ax
$6 = 0xccee
(gdb) p/x $ah
$7 = 0xcc
(gdb) p/x $al
$8 = 0xee

Значения регистров после записи в ax числа 0 x 1234:

(gdb) p/x $eax
$9 = 0xaabb1234
(gdb) p/x $ax
$10 = 0x1234
(gdb) p/x $ah
$11 = 0x12
(gdb) p/x $al
$12 = 0x34

Как видите, ничего сложного.

Примечание: Синтаксис GAS позволяет явно указывать размеры операндов путем использования суффиксов b (байт), w (слово, 2 байта), l (длинное слово, 4 байта), q (четверное слово, 8 байт) и некоторых других. Например, вместо команды mov $ 0xEE , % al можно написать movb $ 0xEE , % al , вместо mov $ 0x1234 , % ax — movw $ 0x1234 , % ax , и так далее. В современном GAS эти суффиксы являются опциональными и я лично их не использую. Но не пугайтесь, если увидите их в чужом коде.

На x64 размер регистров был увеличен до 64-х бит. Соответствующие регистры получили название rax, rbx, и так далее. Кроме того, регистров общего назначения стало шестнадцать вместо восьми. Дополнительные регистры получили названия r8, r9, …, r15. Соответствующие им регистры, которые представляют младшие 32, 16 и 8 бит, получили название r8d, r8w, r8b, и по аналогии для регистров r9-r15. Кроме того, появились регистры, представляющие собой младшие 8 бит регистров rsi, rdi, rbp и rsp — sil, dil, bpl и spl соответственно.

Про адресацию

Как уже отмечалось, регистры могут трактоваться, как указатели на данные в памяти. Для разыменования таких указателей используется специальный синтаксис:

В одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:

При работе с массивами бывает удобно обращаться к элементу с определенным индексом. Соответствующий синтаксис:

Наконец, следующий код тоже валиден:

.data
msg :
. ascii "Hello, world!\n"
. text

В смысле, что можно не указывать регистр со смещением или вообще какие-либо регистры. В результате выполнения этого кода в регистры al и ah будет записан ASCII-код буквы H, или 0 x 48.

В этом контексте хотелось бы упомянуть еще одну полезную ассемблерную инструкцию:

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

Fun fact! На x64 в байткоде инструкций никогда не используются 64-х битовые смещения. В отличие от x86, инструкции часто оперируют не абсолютными адресами, а адресами относительно адреса самой инструкции, что позволяет обращаться к ближайшим +/- 2 Гб оперативной памяти. Соответствующий синтаксис:

Для записи же полного 64-х битового значения в регистр предусмотрена специальная инструкция:

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

Арифметические операции

Рассмотрим основные арифметические операции:

Здесь и далее операндами могут быть не только регистры, но и участки памяти или константы. Но оба операнда не могут быть участками памяти. Это правило применимо ко всем инструкциям ассемблера x86/x64, по крайней мере, из рассмотренных в данной статье.

В данном примере инструкция mul умножает al на cl, и сохраняет результат умножения в пару регистров al и ah. Таким образом, ax примет значение 0 x 12C или 300 в десятичной нотации. В худшем случае для сохранения результата перемножения двух N-байтовых значений может потребоваться до 2*N байт. В зависимости от размера операнда результат сохраняется в al:ah, ax:dx, eax:edx или rax:rdx. Притом в качестве множителей всегда используется первый из этих регистров и переданный инструкции аргумент.

Знаковое умножение производится точно так же при помощи инструкции imul. Кроме того, существуют варианты imul с двумя и тремя аргументами:

mov $ 123 , % rax
mov $ 456 , % rcx

Инструкции div и idiv производят действия, обратные mul и imul. Например:

mov $ 0 , % rdx
mov $ 456 , % rax
mov $ 123 , % rcx

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

Это далеко не все арифметические инструкции. Например, есть еще adc (сложение с учетом флага переноса), sbb (вычитание с учетом займа), а также соответствующие им инструкции, выставляющие и очищающие соответствующие флаги (ctc, clc), и многие другие. Но они распространены намного меньше, и потому в рамках данной статьи не рассматриваются.

Логические и битовые операции

Как уже отмечалось, особой типизации в ассемблере x86/x64 не предусмотрено. Поэтому не стоит удивляться, что в нем нет отдельных инструкций для выполнения булевых операций и отдельных для выполнения битовых операций. Вместо этого есть один набор инструкций, работающих с битами, а уж как интерпретировать результат — решает конкретная программа.

Так, например, выглядит вычисление простейшего логического выражения:

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

Еще одна полезная инструкция — это xor (исключающее или). В логических выражениях xor используется нечасто, однако с его помощью часто происходит обнуление регистров. Если посмотреть на опкоды инструкций, то становится понятно, почему:

Как видите, инструкции xor и inc кодируются всего лишь тремя байтами каждая, в то время, как делающая то же самое инструкция mov занимает целых семь байт. Каждый отдельный случай, конечно, лучше бенчмаркать отдельно, но общее эвристическое правило такое — чем короче код, тем больше его помещается в кэши процессора, тем быстрее он работает.

В данном контексте также следует вспомнить инструкции побитового сдвига, тестирования битов (bit test) и сканирования битов (bit scan):

Еще есть битовые сдвиги со знаком (sal, sar), циклические сдвиги с флагом переноса (rcl, rcr), а также сдвиги двойной точности (shld, shrd). Но используются они не так уж часто, да и утомишься перечислять вообще все инструкции. Поэтому их изучение я оставляю вам в качестве домашнего задания.

Условные выражения и циклы

Выше несколько раз упоминались какие-то там флаги, например, флаг переноса. Под флагами понимаются биты специального регистра eflags / rflags (название на x86 и x64 соответственно). Напрямую обращаться к этому регистру при помощи инструкций mov, add и подобных нельзя, но он изменяется и используется различными инструкциями косвенно. Например, уже упомянутый флаг переноса (carry flag, CF) хранится в нулевом бите eflags / rflags и используется, например, в той же инструкции bt. Еще из часто используемых флагов можно назвать zero flag (ZF, 6-ой бит), sign flag (SF, 7-ой бит), direction flag (DF, 10-ый бит) и overflow flag (OF, 11-ый бит).

Еще из таких неявных регистров следует назвать eip / rip, хранящий адрес текущей инструкции. К нему также нельзя обращаться напрямую, но он виден в GDB вместе с eflags / rflags, если сказать info registers , и косвенно изменяется всеми инструкциям. Большинство инструкций просто увеличивают eip / rip на длину этой инструкции, но есть и исключения из этого правила. Например, инструкция jmp просто осуществляет переход по заданному адресу:

В результате значение rax будет равно единице, так как первая инструкция inс будет пропущена. Заметьте, что адрес перехода также может быть записан в регистре:

Впрочем, на практике такого кода лучше избегать, так как он ломает предсказание переходов и потому менее эффективен.

Примечание: GAS позволяет давать меткам цифирные имена типа 1: , 2: , и так далее, и переходить к ближайшей предыдущей или следующей метке с заданным номером инструкциями вроде jmp 1b и jmp 1f . Это довольно удобно, так как иногда бывает трудно придумать меткам осмысленные имена. Подробности можно найти здесь.

Условные переходы обычно осуществляются при помощи инструкции cmp, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je, jg и подобных:

Помимо cmp также часто используют инструкцию test:

Fun fact! Интересно, что cmp и test в душе являются теми же sub и and, только не изменяют своих операндов. Это знание можно использовать для одновременного выполнения sub или and и условного перехода, без дополнительных инструкций cmp или test.

Еще из инструкций, связанных с условными переходами, можно отметить следующие.

Инструкция jrcxz осуществляет переход только в том случае, если значение регистра rcx равно нулю.

Инструкции семейства cmovcc (conditional move) работают как mov, но только при выполнении заданного условия, по аналогии с jcc.

Инструкции setcc присваивают однобайтовому регистру или байту в памяти значение 1, если заданное условие выполняется, и 0 иначе.

Сравнить rax с заданным куском памяти. Если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере rcx. Иначе очистить ZF и загрузить значение из памяти в rax. Также оба операнда могут быть регистрами.

Важно! Примите во внимание, что без префикса lock все эти compare and swap инструкции не атомарны.

Инструкция loop уменьшает значение регистра rcx на единицу, и если после этого rcx != 0 , осуществляет переход на заданную метку. Инструкции loopz и loopnz работают аналогично, только условия более сложные — (rcx != 0) && (ZF == 1) и (rcx != 0) && (ZF == 0) соответственно.

Не нужно быть семи пядей во лбу, чтобы изобразить при помощи этих инструкций конструкцию if-then-else или циклы for / while, поэтому двигаемся дальше.

Рассмотрим следующий кусок кода:

В регистры rsi и rdi кладутся адреса двух строк. Командой cld очищается флаг направления (DF). Инструкция, выполняющая обратное действие, называется std. Затем в дело вступает инструкция cmpsb. Она сравнивает байты (%rsi) и (%rdi) и выставляет флаги в соответствии с результатом сравнения. Затем, если DF = 0, rsi и rdi увеличиваются на единицу (количество байт в том, что мы сравнивали), иначе — уменьшаются. Аналогичные инструкции cmpsw, cmpsl и cmpsq сравнивают слова, длинные слова и четверные слова соответственно.

Инструкции cmps интересны тем, что могут использоваться с префиксом rep, repe (repz) и repne (repnz). Например:

Префикс rep повторяет инструкцию заданное в регистре rcx количество раз. Префиксы repz и repnz делают то же самое, но только после каждого выполнения инструкции дополнительно проверяется ZF. Цикл прерывается, если ZF = 0 в случае c repz и если ZF = 1 в случае с repnz. Таким образом, приведенный выше код проверяет равенство двух буферов одинакового размера.

Аналогичные инструкции movs перекладывает данные из буфера, адрес которого указан в rsi, в буфер, адрес которого указан в rdi (легко запомнить — rsi значит source, rdi значит destination). Инструкции stos заполняет буфер по адресу из регистра rdi байтами из регистра rax (или eax, или ax, или al, в зависимости от конкретной инструкции). Инструкции lods делают обратное действие — копируют байты по указанному в rsi адресу в регистр rax. Наконец, инструкции scas ищут байты из регистра rax (или соответствующих регистров меньшего размера) в буфере, адрес которого указан в rdi. Как и cmps, все эти инструкции работают с префиксами rep, repz и repnz.

На базе этих инструкций легко реализуются процедуры memcmp, memcpy, strcmp и подобные. Интересно, что, например, для обнуления памяти инженеры Intel рекомендуют использовать на современных процессорах rep stosb , то есть, обнулять побайтово, а не, скажем, четверными словами.

Работа со стеком и процедуры

Со стеком все очень просто. Инструкция push кладет свой аргумент на стек, а инструкция pop извлекает значение со стека. Например, если временно забыть про инструкцию xchg, то поменять местами значение двух регистров можно так:

Существуют инструкции, помещающие на стек и извлекающие с него регистр rflags / eflags:

А так, к примеру, можно получить значение флага CF:

На x86 также существуют инструкции pusha и popa, сохраняющие на стеке и восстанавливающие с него значения всех регистров. В x64 этих инструкций больше нет. Видимо, потому что регистров стало больше и сами регистры теперь длиннее — сохранять и восстанавливать их все стало сильно дороже.

Примечание: Аналогичный пролог и эпилог можно написать при помощи инструкций enter $ 0x10 , $ 0 и leave . Но в наше время эти инструкции используются редко, так как они выполняются медленнее из-за дополнительной поддержки вложенных процедур.

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

Для примера рассмотрим ассемблерный код, сгенерированный CLang 3.8 для простой программки на языке C под x64. Так выглядит одна из процедур:

unsigned int
hash ( const unsigned char * data , const size_t data_len ) <
unsigned int hash = 0x4841434B ;
for ( int i = 0 ; i data_len ; i ++ ) <
hash = ( ( hash 5 ) + hash ) + data [ i ] ;
>
return hash ;
>

Дизассемблерный листинг (при компиляции с -O0 , комментарии мои):

Здесь мы встретили две новые инструкции — movs и movz. Они работают точно так же, как mov, только расширяют один операнд до размера второго, знаково и беззнаково соответственно. Например, инструкция movzbl (%rdx,%rcx,1),%esi читайт байт (b) по адресу (%rdx,%rcx,1) , расширяет его в длинное слово (l) путем добавления в начало нулей (z) и кладет результат в регистр esi.

Заключение

Само собой разумеется, в рамках одной статьи, описать весь ассемблер x86/x64 не представляется возможным (более того, я не уверен, что сам знаю его прямо таки весь). Как минимум, за кадром остались такие темы, как операции над числами с плавающей точкой, MMX-, SSE- и AVX-инструкции, а также всякие экзотические инструкции вроде lidt, lgdt, bswap, rdtsc, cpuid, movbe, xlatb, или prefetch. Я постараюсь осветить их в следующих статьях, но ничего не обещаю. Следует также отметить, что в выводе objdump -d для большинства реальных программ вы очень редко увидите что-то помимо описанного выше.

Еще интересный топик, оставшийся за кадром — это атомарные операции, барьеры памяти, спинлоки и вот это все. Например, compare and swap часто реализуется просто как инструкция cmpxchg с префиксом lock. По аналогии реализуется атомарный инкремент, декремент, и прочее. Увы, все это тянет на тему для отдельной статьи.

Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:

OP1 - это всегда регистр, OP2 - непосредственный операнд, регистр или память.

При умножении результат имеет удвоенный формат по отношению к сомножителям. Иногда точно известно, что результат может уместиться в формат сомножителей, тогда его можно извлекать из AL, AX, EAX.

Размер результата можно выяснить с помощью флагов OF и CF.

Если OF = CF = 1, то результат занимает двойной формат,

и OF = СF = 0 в противном случае. Остальные флаги не изменяются.

Деление беззнаковых чисел:

DIV OP2 ; OP2 это r m

Деление знаковых чисел.

IDIV OP2 ; OP2 это r m

В результате выполнения этих команд содержимое регистров

(AX) (DX:AX) (EDX:EAX) делится на указанный операнд и результат помещается в AL AX EAX, остаток помещается в AH DX EDX. Значение флагов не меняется, но может наступить деление на ноль, или переполнение, например:

Приведем пример программы с использованием команд деления: цифры целого беззнакового байтового числа N записать в байты памяти, начиная с адреса D как символы.

Если цифры трехзначного числа обозначить буквами a, b, c - N(abc), то получить их можно по формулам:

b = (N div 10) mod 10

a = (N div 10) div 10

Перевести цифру в символ можно так: код(i) = код (`0') + I, тогда фрагмент программы может быть таким:

MOV BL, 10 ; делитель

MOV AL, N ; делимое

MOV AH, 0 ; расширяем делимое до слова

; или CBW AH конвертируем до слова

DIV BL ; A L = ab, AH = c

DIV BL ; AL = a, AH = b

Директивы внешних ссылок

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

определяет указанные имена как глобальные величины, к которым можно обратиться из других модулей. Имена это имена меток и переменных, определенных с помощью директивы `=' и EQU.

Если некоторое имя определено в модуле А как глобальное и к нему нужно обратиться из других модулей В и С, то в этих модулях должна быть директива вида

Здесь имя тоже, что и в Public, а тип определяется следующим образом: если - это имя переменной, то типом может быть:

BYTE, WORD, DWORD, FWORD, QWORD, TWORD;

если - это имя метки, то типом может быть NEAR, FAR.

Директива EXTRN говорит о том, что перечисленные имена являются внешними для данного модуля.

Пусть в модуле А содержится:

Чтобы обратиться из В и С к переменной TOT, в них должна быть директива EXTRN TOT:WORD

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

Файл Prim.ASM, расположенный в указанной директории, на этапе ассемблирования записывается на место этой директивы.

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

Команда безусловной передачи управления имеет вид JMP ,

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

JMP M1 ; по умолчанию М1 имеет тип NEAR

Если метка содержится в другом сегменте, то в том сегменте, в который передается управление, должна быть директива Public M1, а в том сегменте из которого передается управление должна быть директива - EXTRN M1:FAR. Кроме того, передачу можно осуществлять с использованием прямой адресации (JMP M1) или с использованием косвенной адресации (JMP [BX]). Команда, осуществляющая близкую передачу, занимает 3 байта памяти, а дальнюю - 5 байтов. Но если передача осуществляется не далее чем на -128 или 127 байтов, то можно использовать команду безусловной передачи управления JMP Short , занимающую 1 байт памяти.

Замечание: команда, следующая за командой безусловной передачи управления, должна иметь метку, иначе к ней нельзя будет возвратиться.

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

Процедура типа NEAR может быть вызвана только из того кодового сегмента, в котором содержится ее описание. Процедура типа FAR Может быть вызвана из любого сегмента. Поэтому тип вызова процедуры (дальность) определяется следующим образом: главная программа всегда имеет тип FAR, т.к. обращаются к ней из ОС или отладчика, если процедур несколько, и они содержатся в одном кодовом сегменте, то все остальные, кроме главной, имеют тип NEAR (пример 1). Если процедура описана в кодовом сегменте с другим именем, то у нее должен быть тип FARи должны быть использованы директивы внешних ссылок (пример 2).


Команда вызова процедуры или подпрограммы имеет вид: CALL ;

Адресация может быть использована как прямая, так и косвенная. При обращении к процедуре типа NEAR в стеке сохраняется адрес возврата, адрес команды, следующей за CALL, содержится в IP или EIP. При обращении к процедуре типа FAR в стеке сохраняется полный адрес возврата CS:EIP. Возврат из процедуры реализуется с помощью команды RET Она может иметь один из следующих видов:

RET [n] ; возврат из процедуры типа NEAR, и из процедуры типа FAR

RETN [n] ; возврат только из процедуры типа NEAR

RETF [n] ; возврат только из процедуры типа FAR.

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

Примеры прямого и косвенного перехода


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

Команды условной передачи управления

Команды условной передачи управления можно разделить на 3 группы:

• команды, используемые после команд сравнения

• команды, используемые после команд, отличных от команд сравнения, но реагирующие на значения флагов

• команды, реагирующие на начальное значение регистра CX.

В общем виде команду условной передачи управления можно записать так: Jx ;

Здесь х - это одна, две, или три буквы определяют условия передачи управления. Метка, указанная в поле операнда, должна отстоять от команды не далее чем -128 ? +127 байт.

JE M1 ; передача управления на команду с меткой М1, если ZF=1

JNE M2 ; передача управления на команду с меткой М2, если ZF=0

JC M3 ; передача управления на команду с меткой М3, если CF=1

JNC M4 ; передача управления на команду с меткой М4, если CF=0

Если в результате сложения CF =1, то управление передается на команду с меткой М, иначе - на команду, следующую за JC.

Если результатом вычитания будет 0, то ZF = 1 и управление передается на команду с меткой Мet.

Часто команды передачи управления используются после команд сравнения CMP OP1, OP2 ;

По этой команде выполняется (OP1) - (OP2) и результат никуда не посылается, формируются только флаги. Команды условной передачи управления для беззнаковых и знаковых чисел можно представить в виде таблицы:

Хотите улучшить этот вопрос? Переформулируйте вопрос так, чтобы он был сосредоточен только на одной проблеме.

Закрыт 3 года назад .

Какие функции сложения присутствуют на ассемблере, в частности в NASM'е?

2 ответа 2

Плохо помню ассемблер, но вроде все просто


Осмелюсь предположить, что сложение производится командой:
add (к примеру "add ax,bx")

Всё ещё ищете ответ? Посмотрите другие вопросы с метками ассемблер nasm или задайте свой вопрос.

Похожие

дизайн сайта / логотип © 2022 Stack Exchange Inc; материалы пользователей предоставляются на условиях лицензии cc by-sa. rev 2022.1.28.41300

Читайте также: