Основу программы составляют инструкции, которые и определяют поведение программы, ее действия. Наиболее распространенной инструкцией в ассемблере является инструкция копирования данных или инструкции mov. По разным оценкам, она составляет от 25% до 40% всех инструкций программы. Она копирует данные из одного места в другое и имеет следующий синтаксис:
mov destination, source
Инструкция принимает два операнда. Первый операнд - destination
представляет расположение, куда надо поместить данные. В качестве такого места может выступать
регистр процессора или адрес в памяти. Второй операнд - source
указывает на источник данных, в качестве которого может выступать регистр процессора, адрес в
памяти или непосредственный операнд - число. То инструкция mov копирует данные из source в destination. При этом оба операнда не могут быть одновременно адресами в памяти.
Например, скопируем в регистр rax число 5:
mov rax, 5
Или скопируем в регистр rbx значение из регистра rax:
mov rax, 5 mov rbx, rax ; rbx=rax=5
Проверим, что в регистре именно то число, которое мы положили в него. Вывод содержимого регистра на консоль требует ряда других инструкций. Поэтому на начальном этапе можно проверять значения с помощью статусного кода возврата: при выполнения программы в определенный регистр помещается код статуса выполнения программы. В Windows это регистр RAX, в Linux/MacOS - это регистр RDI. Обычно считается, что если этот регистр содержит 0, то программа успешно завершила свое выполнение. Другое число обычно означает код ошибки. В реальности число может быть произвольным. Но в нашем случае мы можем использовать указанный регистр и число в нем для проверки выполнения инструкций ассемблера.
Например, на Linux определим следующий файл hello.asm.
global _start ; делаем метку метку _start видимой извне section .text ; объявление секции кода _start: ; объявление метки _start - точки входа в программу mov rdi, 23 ; помещаем в регистр rdi код возврата - 23 mov rax, 60 ; 60 - номер системного вызова exit syscall ; выполняем системный вызов exit
В данном случае помещаем в регистр RAX число 60 - номер системного вызова Linux закрытия программы. А в регистр RDI помещается число 23 - произвольный код статуса выполнения
программы. После выполнения программы его можно получить командой echo $?
. Пример компиляции и работы программы:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# ld -o hello hello.o root@Eugene:~/asm# ./hello root@Eugene:~/asm# echo $? 23 root@Eugene:~/asm#
На Windows для проверки статусного кода применяется команда echo %ERRORLEVEL%. Так, определим на Windows аналогичный файл hello.asm со следующим кодом
global _start ; делаем метку метку _start видимой извне section .text ; объявление секции кода _start: ; метка _start - точка входа в программу mov rax, 23 ; помещаем в регистр rax код возврата - 23 ret ; выход из программы
В данном случае помещаем в регистр RAX число 23. Пример компиляции и работы программы:
c:\asm>nasm -f win64 hello.asm -o hello.o c:\asm>ld hello.o -o hello.exe c:\asm>hello.exe c:\asm>echo %ERRORLEVEL% 23 c:\asm>
Как можно увидеть, отличие программы на Windows от программы на Linux только в том, что на Linux для завершения надо выполнить системный вызов 60, а на Windows достаточно использовать инструкцию ret. И естественно меняется регистр для статусного кода возврата - в одному случае rdi, в другом случае rax.
Оба операнда инструкции mov должны быть одинакового размера. То есть можно поместить значение из 8-битного регистра в другой 8-битный регистр или 8-битное значение в 8-битный регистр. Подобным образом можно копировать данные между 16-битными или между 32-битными или между 64-битными операндами, но использовать в инструкции операнды разных размеров нельзя. Непосредственные операнды могут быть меньшего размера, чем регистр, в который они помещаются. Например::
mov ecx, 525 ; помещаем в регистр ecx число 525 mov eax, ecx ; помещаем в регистр eax число из ecx (525)
Здесь число 525 помещается в 32-разрядный регистр ECX. Число 525 укладывается в размер регистра ECX, поэтому никаких проблем не возникнет. Далее значение из регистра ecx копируется в регистр eax. Оба регистра 32-разрядные, поэтому проблем опять не возникнет, и в регистре eax окажется число 525.
Но возьмем другой пример:
mov cl, 525 ; помещаем в регистр CL число 525 mov eax, cl
Первая инструкция здесь помещает число 525 в регистр CL. Но регистр CL - 8-разрядный и может принимать только 8-разрядные числа. Максимальное 8-разрядное положительное число - 255. Соответственно мы НЕ можем поместить число 525 в регистр CL. И при компиляции NASM сгенерирует предупреждение:
warning: byte data exceeds bounds [-w+number-overflow]
Причем это не ошибка, а предупреждение, которое говорит, что что-то может быть не так. При наличии предупреждений программа все компилируются: NASM его ужмет до 8 бит и только тогда поместит в регистр CL. В итоге в регистре CL окажется число 13. Однако лучше не игнорировать подобные предупреждения, так как несмотря на компиляцию программы она может совершать неправильные расчеты. Так, число 525 и числа 13 - совершенно разные.
Вторая инструкция помещает значение регистра CL в регистр EAX (то есть значение из 8-разрядного регистра в 32-разрядный регистр). Даже учитывая, что 8 -разрядное число вполне может поместиться в 32-разрядный регистр, мы все равно при компиляции получим ошибку:
error: invalid combination of opcode and operands
При наличии ошибок программа не компилируется.
Инструкция mov имеет ограничение - операнды должны совпадать по размеру. Но что, если нам надо поместить в 64-разрядный регистр 8-битное значение,
ведь 8-битное значение вполне помещается в 64 разряда?
Архитектура x86-64 также предоставляет дополнительные расширения инструкции mov
- movsx и movzx.
movsx выполняет перемещение с расширением знаковым битом,
которое копирует данные и расширяет данные по знаку во время их копирования. Синтаксис инструкции movsx аналогичен синтаксису mov:
movsxd dest, source ; если dest - 64-разрядный операнд и source - 32-разрядный movsx dest, source ; для всех остальных комбинаций операндов
При этом первый операнд - dest должен быть по разрядности не меньше второго операнда и обязательно должен предоставлять регистр. Эта инструкция также не допускает константные значения. Например
mov al, -1 movsx rdi, al
Здесь значение из регистра AL помещается в регистр RDI (в младший байт, так как AL - 8-разрядный регистр). Поскольку в AL число отрицательное, то используем расширение со знаком. В результате в RDI в младшем байте сохранится число -1, а все остальные байты (семь старших байтов) заполняются знаковым битом - в данном случае числом 1 (знаковый бит отрицательного числа).
Для беззнакового расширения нулями есть другая инструкция - movzx:
mov al, 5 movzx rdi, cx
В данном случае значение из регистра AL помещается в самый младший байт регистра RDI. Остальные байты (7 байт) регистра RDI заполняются нулями.
Если надо расширить 32-битный регистр до 64-битного, можно просто скопировать (32-битный) регистр в самого себя:
mov rax, 0ffffffffffffffffh mov eax, eax ; RAX = 00000000ffffffffh - в RAX остались только младшие 32 бита
По умолчанию ассемблер NASM воспринимает числа как значения в десятичной системе, но также позволяет определять числа в других системах - шестнадцатеричной и двоичной. Это делается с помощью специальных префиксов, либо суффиксов. В общем случае, когда перед числом нет никакого префикса, число использует десятичную систему:
mov rdi, 11
Здесь в регистр RDI помещается десятичное число 11.
Если надо определить число в двоичном формате, то перед числом указывается префикс 0b:
mov rdi, 0b1011
Здесь бинарное число 10112
, которое в десятичной системе представляет число 1110
, помещается в RDI.
В качестве альтернативы можно использовать суффикс b:
mov rdi, 1011b ; rdi = 11
Если надо определить число в шестнадцатеричном формате, то перед числом указывается префикс 0x:
mov rdi, 0x1f ; rdi = 31
Здесь шестнадцатеричное число 1f16
, которое в десятичной системе также представляет число 3110
, помещается в RDI.
В качестве альтернативы можно использовать суффикс h:
mov rdi, 1eh ; rdi = 30