Основы ассемблера NASM

Инструкция MOV. Копирование данных

Последнее обновление: 11.10.2023

Основу программы составляют инструкции, которые и определяют поведение программы, ее действия. Наиболее распространенной инструкцией в ассемблере является инструкция копирования данных или инструкции 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 и расширение чисел нулем и знаком

Инструкция 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850