Взаимодействие с кодом на C/C++
Ассемблер MASM позволяет использовать гибридный подход, при котором часть программы пишется на ассемблере, а часть — на С/С++. Это позволяет сократить и упростить программу. Например, определим файл app.c с кодом на языке C:
А в файле app.c определим следующий код:
#include // подключаем внешнюю функцию hello из файла hello.asm extern void hello(void); int main()
Это обычная программа на языке С, в которой с помощью оператора extern подключаем некоторую внешнюю функцию hello, которая ничего не возвращает и не принимает никаких параметров. В функции main вызываем эту внешнюю функцию hello.
Теперь в той же папке определим файл hello.asm , который будет содержать следующий код на ассемблере:
.code option casemap:none ; определяем процедуру hello public hello hello proc ret ; возвращаемся в вызывающий код hello endp end
Секция .code здесь начинается с директивы option
option casemap:none
Директива option указывает MASM сделать все символы чувствительными к регистру. Это необходимо, потому что MASM по умолчанию нечувствителен к регистру и отображает все идентификаторы в верхнем регистре (поэтому hello() станет HELLO()). Но язык C чувствителен к регистру и рассматривает hello() и HELLO() как два разных идентификатора.
Далее идет определение функции hello. С помощью оператора public указываем, что функция hello будет видна вне исходного/объектного файла MASM:
public hello
Без этого оператора функция hello() была бы доступна только внутри текущего файла, и при компиляции компилятор не смог бы ее найти.
Затем между операторами hello proc и hello endp определяется функция hello, которая ничего не делает.
Скомпилируем оба файла — app.c и hello.asm в одну программу. Рассмотрим напримере ассемблера MASM64 и компилятора для Visual C++ в Visual Studio. Для компиляции откроем программу x64 Native Tools Command Prompt for VS 2022 , которая устанавливается вместе с Visual Studio, и перейдем в ней к папке, где располагаются файлы. Затем выполним следующюю команду
ml64 /c hello.asm
Опция /c говорит, что файл hello.asm надо только скомпилировать в объектный файл. И после этой команды в папке программы будет скомпилирован файл hello.obj :
********************************************************************** ** Visual Studio 2022 Developer Command Prompt v17.5.5 ** Copyright (c) 2022 Microsoft Corporation ********************************************************************** [vcvarsall.bat] Environment initialized for: 'x64' C:\Program Files\Microsoft Visual Studio\2022\Community>cd c:\asm c:\asm>ml64 /c hello.asm Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm c:\asm>
Далее используем скомпилируем всю программу с помощью команды:
cl app.c hello.obj
В итоге будет скомпилирован файл app.exe, который мы можем запустить и который при запуске выполнит ассемблерную функцию hello:
c:\asm>cl app.c hello.obj Microsoft (R) C/C++ Optimizing Compiler Version 19.35.32217.1 for x64 Copyright (C) Microsoft Corporation. All rights reserved. app.c Microsoft (R) Incremental Linker Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. /out:app.exe app.obj hello.obj c:\asm>app Hello starts Hello ends c:\asm>
Вывод строки на консоль
Программа на C может вызывать код из файла на ассемблере, так и код на ассемблере может вызывать функции языка С. Например, используем стандартную функцию printf() для вывода строки на консоль в программе на ассемблере. Для этого определим в файле hello.asm следующий код:
option casemap:none .data text byte "Hello, world!", 10, 0 ; определяем выводимые данные .code ; подключаем определение функции printf() из C/C++ externdef printf:proc ; определяем процедуру hello public hello hello proc sub rsp, 40 ; резервируем в стеке 40 байт lea rcx, text ; в регистр rcx загружаем адрес строки text call printf ; вызываем функцию printf add rsp, 40 ; восстанавливаем значение в стеке ret ; возвращаемся в вызывающий код hello endp end
Здесь для хранения данных определяем секцию .data
.data text byte "Hello, world!", 10, 0 ; определяем выводимые данные
Здесь определена строка text, которая представляет набор байт. Причем предпоследний байт — представляет число 10 — это числовой код перевода строки. Последний байт — 0, так как строки в Си должны завершаться нулевым байтом.
Для использования функции printf подключаем ее с помощью директивы.
externdef printf:proc
Директива externdef имеет следующую форму
externdef symbol:type
Где symbol — это внешный идентификатор, который мы хотим определить — в данном случае функция printf . А type — тип этого идентификатора (для функций это тип proc ).
Стоит отметить, что директива externdef должна применяться в секции .code
Функция printf() принимает как минимум один параметр — выводимую строку. Эта строка передается в функцию через регистр RCX. Для загрузки адреса строки в регистр rcx применяем инструкцию lea и затем вызываем функцию printf.
lea rcx, text call printf
Снова сначала скомпилируем код ассемблера из файла hello.asm с помощью команды:
ml64 /c hello.asm
И скомпилируем файл app.c в файл приложения с помощью команды:
cl app.c hello.obj
После компиляции запустим файл app.exe:
c:\asm>ml64 /c hello.asm Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm c:\asm>cl app.c hello.obj Microsoft (R) C/C++ Optimizing Compiler Version 19.35.32217.1 for x64 Copyright (C) Microsoft Corporation. All rights reserved. app.c Microsoft (R) Incremental Linker Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. /out:app.exe app.obj hello.obj c:\asm>app Hello starts Hello, world! Hello ends c:\asm>
- Глава 1. Введение в ассемблер Intel x86-64
- Архитектура Intel x86-64
- Представление данных, биты и байты
- Регистры процессора
- Ассемблер MASM. Установка и начало работы
- Первая программа на MASM
- Определение данных и их типы. Секция .data
- Инструкция mov. Копирование данных
- Сложение и вычитание. add и sub
- Переходы. Инструкция jmp
- Флаги состояния и условные переходы
- Сравнение. Инструкция CMP
- Инструкции условного копирования
- Логические операции
- Сдвиг и вращение
- Умножение. mul и imul
- Деление. Инструкции div и idiv
- Установка битов по условию. setc
- Манипуляции битами. BMI
- Константы
- Преобразование данных
- Символы
- Режимы адресации. Косвенная адресация
- Выравнивание
- Стек
- Указатели
- Строки
- Сортировка массива
- Многомерные массивы
- xlat и поиск в таблицах
- Структуры
- Объединения union
- Условные конструкции
- Булевые конструкции
- Имитация конструкции switch..case
- Машина состояний
- Циклы
- Сложение и вычитание больших чисел
- Сравнение больших чисел
- Умножение больших чисел
- Деление больших чисел
- Поразрядные операции с большими числами
- Операции сдвига больших чисел
- Определение и вызов процедур
- Сохранение регистров и переменных при вызове процедур
- Параметры
- Результат процедуры
- Фрейм стека и локальные переменные
- Указатель на процедуру
- Вставка кода и директива include
- Разбиение программы на модули и подключение внешних модулей
- Библиотеки
- Регистры FPU
- Числа с плавающей точкой
- Загрузка данных и преобразование чисел в FPU
- Сложение чисел с плавающей точкой в FPU
- Вычитание чисел с плавающей точкой в FPU
- Умножение чисел с плавающей точкой в FPU
- Деление чисел с плавающей точкой в FPU
- Дополнительные математические инструкции FPU
- Сравнение чисел с плавающей точкой в FPU
- Расширения SSE для чисел с плавающей точкой
- Арифметика чисел с плавающей точкой в SSE
- Сравнение чисел с плавающей точкой в SSE
- Десятичная арифметика и двоично-десятичный формат BCD
- Расширения SSE и AVX/AVX2
- Копирование данных с помощью инструкций SIMD
- Копирование чисел с плавающей точкой
- Переупорядочивание данных
- Логические операции SSE/AVX
- Операции сдвига SSE/AVX
- Сложение с помощью инструкций SSE/AVX
- Вычитание с помощью инструкций SSE/AVX
- Умножение с помощью инструкций SSE/AVX
- Математические инструкции SSE/AVX
- Сравнение целых чисел в SSE/AVX
- Преобразования целых чисел в SSE/AVX
- Арифметические операции с плавающей точкой в SSE/AVX
- Сравнение чисел с плавающей точкой в SIMD
- Преобразование чисел с плавающей точкой в целые числа и обратно
- Загрузка в регистры константных значений
- Сохранение состояния регистров SSE/AVX
- Операции со строками
- Сравнение строк
- Поиск в строке
- Сохранение и получение элементов строк
- Определение макросов
- Параметры макроса
- Метки в макросах
- Условная компиляция и циклы
- Генерация таблиц для поиска
- Первая программа на MASM/C
- Передача параметров из ассемблера в функцию C/C++
- Возвращение результата и передача параметров в процедуру ассемблера в коде на C/C++
- Практически пример. Обработка консольного ввода
- Запись в файл и вывод на консоль
- Считывание файла и ввод с консоли
- Практический пример. Обработка ввода
- Создание, открытие и закрытие файла
- Запись в текстовый файл
- Чтение текстового файла
- Графическое приложение. Окно сообщения
- Формат COFF-файла
- Исследование файла с помощью dumpbin
- Исследование файла с помощью objdump
Где писать на ассемблере
Создадим первую программу с помощью ассемблера MASM. При создании программы на ассемблере стоит понимать, что это не высокоуровневый язык, где достаточно вызвать одну функцию, которая выполнит всю сложную работу. В ассемблере, чтобы выполнить довольно простые вещи, придется писать довольно много инструкций. И здесь есть разные подходы: мы можем написать весь код только на ассемблере — вариант, который в реальности втречается редко, либо мы можем какие-то части писать на ассемблере, а какие-то на языке высокого уровня, например, на С. В данном случае рассмотрим создание программы целиком на ассемблере, но в дальнейшем посмотрим на второй вариант на примере взаимодействия с языками С и С++.
Итак, напишем программу на ассемблере, которая выводит строку на консоль. Для этого определим на жестком диске папку для файлов с исходным кодом. Допустим, она будет называться C:\asm . И в этой папке создадим новый файл, который назовем hello.asm и в котором определим следующий код:
includelib kernel32.lib ; подключаем библиотеку kernel32.lib ; подключаем функции WriteFile и GetStdHandle extrn WriteFile: PROC extrn GetStdHandle: PROC .code text byte "Hello METANIT.COM!" ; выводимая строка main proc sub rsp, 40 ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт) mov rcx, -11 ; Аргумент для GetStdHandle - STD_OUTPUT call GetStdHandle ; вызываем функцию GetStdHandle mov rcx, rax ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли lea rdx, text ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX mov r8d, 18 ; Третий параметр WriteFile - длина строки для записи в регистре R8D xor r9, r9 ; Четвертый параметр WriteFile - адрес для получения записанных байтов mov qword ptr [rsp + 32], 0 ; Пятый параметр WriteFile call WriteFile ; вызываем функцию WriteFile add rsp, 40 ret main endp end
Откроем программу x64 Native Tools Command Prompt for VS 2022 и перейдем в ней к папке, где располагается файл hello.asm. Затем выполним следующюю команду
ml64 hello.asm /link /entry:main
В результате ассемблер скомпилирует ряд файлов
********************************************************************** ** Visual Studio 2022 Developer Command Prompt v17.5.5 ** Copyright (c) 2022 Microsoft Corporation ********************************************************************** [vcvarsall.bat] Environment initialized for: 'x64' C:\Program Files\Microsoft Visual Studio\2022\Community>cd c:\asm c:\asm>ml64 hello.asm /link /entry:main Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm Microsoft (R) Incremental Linker Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. /OUT:hello.exe hello.obj /entry:main c:\asm>
В итоге в каталоге программы будут сгенерированы объектный файл hello.obj и собственно файл программы — hello.exe. Запустим этот файл, и консоль выведет строку:
********************************************************************** ** Visual Studio 2022 Developer Command Prompt v17.5.5 ** Copyright (c) 2022 Microsoft Corporation ********************************************************************** [vcvarsall.bat] Environment initialized for: 'x64' C:\Program Files\Microsoft Visual Studio\2022\Community>cd c:\asm c:\asm>ml64 hello.asm /link /entry:main Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm Microsoft (R) Incremental Linker Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. /OUT:hello.exe hello.obj /entry:main c:\asm>hello Hello METANIT.COM! c:\asm>
Вкратце разеберем программу. Вначале идет инструкция
includelib kernel32.lib
Эта инструкция подключает библиотеку kernel32.lib . Это библиотека Windows включает нативные функции GetStdHandle и WriteFile, которые использует программа для вывода строки на консоль. Установка Visual Studio включает этот файл, а файл vcvars64.bat при запуске должным образом установить пути к нему, чтобы компоновщик мог его найти при сборке файла программы смог бы его найти.
Чтобы воспользоваться функциями подключаем их с помощью оператора extrn
extrn WriteFile: PROC extrn GetStdHandle: PROC
С помощью директивы .code открываем секцию кода и затем определяем используемые в программе данные — выводимую строку
.code text byte "Hello METANIT.COM!" ; выводимая строка
Строка называется text, а каждый ее элемент (символ) имеет тип byte (8-битное число).
Далее идут собственно выполняемые программой действия. Они помещаются между инструкциями main proc и main endp
main proc ; действия программы main endp
Далее нам надо должным образом настроить стек. В частности, резервируем в стеке 40 байт для параметров функций GetStdHandle и WriteFile и при этом учитываем выравнивание стека по 16-байтной границе. Указатель на верхушку стека хранится в регистре rsp . Поэтому вычитаем с помощью инструкции sub из значения в регистре rsp 40 байт
sub rsp, 40
Почему 40? Прежде всего при вызове функций WinAPI (как в данном случае функций GetStdHandle и WriteFile) необходимо зарезервировать в стеке как минимум 32 байта — так называемое «shadow storage» (теневое хранилище). Далее нам надо учитывать количество параметров функции. Пеовые 4 параметра функций передаются через регистры, а параметры начиная с 5-го передаются через стек. Соответственно для 5-го и последующих параметров надо выделить в стеке область. Для каждого параметра вне зависимости от его размера выделяется 8 байт. Функция WriteFile как раз принимает 5 параметров, поэтому для нее надо выделить дополнительные 8 байт в стеке. Поэтому получаем 32 байта + 8 байт (5-й параметр WriteLine) = 40 байт. Количество параметров смотрим по функции с наибольшим количеством параметров. Третий момент — нам надо учитывать, что перед вызовом функций WinAPI стек имел выравнивание по 16 байтовой границе, то есть значение в RSP должно быть кратно 16. По умолчанию при вызове функции в стек помещается адрес возврата функии размером 8 байт. Поэтому наши 40 байт + 8 байт (адрес возврата из функции) дадут 48 байт — число кратное 16.
Вначале нам надо использовать встроенную функцию GetStdHandle() , которая позволяет получить дескриптор на устройство ввода-вывода. Она имеет следующее определение на C:
HANDLE WINAPI GetStdHandle( _In_ DWORD nStdHandle );
Функция GetStdHandle() получает числовой код устройства, с которым мы хотим взаимодействовать. В нашем случае нам надо получить устройство стандартного вывода (для вывода строки), которым по умолчанию является консоль. Для обращения к консоли надо передать число -11, которое надо поместить в регистр rcx :
mov rcx, -11 ; Аргумент для GetStdHandle - STD_OUTPUT
После установки параметра этой функции вызываем ее с помощью инструкции call :
call GetStdHandle
В результате выполнения функция GetStdHandle возвращает дескриптор — объект, через который мы можем взаимодействовать с консолью. Этот дескриптор помещается в регистр rax . Получив этот дескриптор, используем его для вывода на консоль строки с помощью функции WriteFile . Для справки ее определение на С++
BOOL WriteFile( [in] HANDLE hFile, [in] LPCVOID lpBuffer, [in] DWORD nNumberOfBytesToWrite, [out, optional] LPDWORD lpNumberOfBytesWritten, [in, out, optional] LPOVERLAPPED lpOverlapped );
Вызов функции GetStdHandle помещает в регистр rax дескриптор консоли. И для вывода строкии на консоль с помощью функции WriteFile нам надо поместить этот дескриптор в регистр rcx
mov rcx, rax
Затем с помощью инструкции lea загружаем в регистр rdx адрес выводимой строки
lea rdx, text
Далее в регистр r8d помещаем длину выводимой строки в байтах — в данном случае это 18 байт:
mov r8d, 18
Затем в регистре r9 устанавливаем адрес четвертого параметра функции WriteFile:
xor r9, r9
В данном случае нам не нужно количество считанных байтов, и с помощью инструкции xor обнуляем значение регистра r9.
Последний — пятый параметр функции WriteFile должен иметь значение NULL, по сути 0. Поэтому устанавливаем для него значение 0, смещаясь в стеке вперед на 32 байта (4 параметра * 8):
mov qword ptr [rsp + 32], 0
Инструкция mov помещает значение в определенное место. Здесь в качестве значения служит число 0. А место определяется выражением qword ptr [rsp + 32] . qword ptr указывает, что этот операнд описывает адрес размером в четыре слова, что означает 8 байтов (слово имеет длину 2 байта). ptr указывает, что значение операнда следует рассматривать как адрес. То есть число 0 представляет 8-байтное значение и помещается по адресу rsp + 32 .
И далее собственно вызываем функцию WriteFile:
call WriteFile
Этот вызов должен привести к выводу строки на консоль. После этого восстанавливаем значение верхушки стека. Для этого с помощью инструкции add прибавляем к значению в регстре rsp ранее отнятые 40 байт:
add rsp, 40
И с помощью инструкции ret выходим из программы.
Записки программиста
Шпаргалка по основным инструкциям ассемблера x86/x64
12 октября 2016
В прошлой статье мы написали наше первое hello world приложение на асме, научились его компилировать и отлаживать, а также узнали, как делать системные вызовы в Linux. Сегодня же мы познакомимся непосредственно с ассемблерными инструкциями, понятием регистров, стека и вот этого всего. Ассемблеры для архитектур x86 (a.k.a i386) и x64 (a.k.a amd64) очень похожи, в связи с чем нет смысла рассматривать их в отдельных статьях. Притом акцент я постараюсь делать на x64, попутно отмечая отличия от x86, если они есть. Далее предполагается, что вы уже знаете, например, чем стек отличается от кучи, и объяснять такие вещи не требуется.
Регистры общего назначения
Регистр — это небольшой (обычно 4 или 8 байт) кусочек памяти в процессоре с чрезвычайно большой скоростью доступа. Регистры делятся на регистры специального назначения и регистры общего назначения. Нас сейчас интересуют регистры общего назначения. Как можно догадаться по названию, программа может использовать эти регистры под свои нужды, как ей вздумается.
На x86 доступно восемь 32-х битных регистров общего назначения — eax, ebx, ecx, edx, esp, ebp, esi и edi. Регистры не имеют заданного наперед типа, то есть, они могут трактоваться как знаковые или беззнаковые целые числа, указатели, булевы значения, ASCII-коды символов, и так далее. Несмотря на то, что в теории эти регистры можно использовать как угодно, на практике обычно каждый регистр используется определенным образом. Так, esp указывает на вершину стека, ecx играет роль счетчика, а в eax записывается результат выполнения операции или процедуры. Существуют 16-и битные регистры ax, bx, cx, dx, sp, bp, si и di, представляющие собой 16 младших бит соответствующих 32-х битных регистров. Также доступны и 8-и битовые регистры ah, al, bh, bl, ch, cl, dh и dl, которые представляют собой старшие и младшие байты регистров ax, bx, cx и dx соответственно.
Рассмотрим пример. Допустим, выполняются следующие три инструкции:
(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 соответственно.
Про адресацию
Как уже отмечалось, регистры могут трактоваться, как указатели на данные в памяти. Для разыменования таких указателей используется специальный синтаксис:
mov ( % rsp ) , % rax
Эта запись означает «прочитай 8 байт по адресу, записанному в регистре rsp, и сохрани их в регистр rax». При запуске программы rsp указывает на вершину стека, где хранится число аргументов, переданных программе (argc), указатели на эти аргументы, а также переменные окружения и кое-какая другая информация. Таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций) в rax будет записано количество аргументов, с которыми была запущена программа.
В одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:
mov 8 ( % rsp ) , % rax
Эта запись означает «возьми rsp, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в rax». Таким образом, в rax будет записан адрес строки, представляющей собой первый аргумент программы, то есть, имя исполняемого файла.
При работе с массивами бывает удобно обращаться к элементу с определенным индексом. Соответствующий синтаксис:
# инструкция xchg меняет значения местами
xchg 16 ( % rsp ,% rcx , 8 ) , % raxЧитается так: «посчитай rcx*8 + rsp + 16, и поменяй местами 8 байт (размер регистра) по получившемуся адресу и значение регистра rax». Другими словами, rsp и 16 все так же играют роль смещения, rcx играет роль индекса в массиве, а 8 — это размер элемента массива. При использовании данного синтаксиса допустимыми размерами элемента являются только 1, 2, 4 и 8. Если требуется какой-то другой размер, можно использовать инструкции умножения, бинарного сдвига и прочие, которые мы рассмотрим далее.
Наконец, следующий код тоже валиден:
.data
msg :
. ascii «Hello, world!\n»
. text. globl _start
_start :
# обнуление rcx
xor % rcx , % rcx
mov msg ( ,% rcx , 8 ) , % al
mov msg , % ahВ смысле, что можно не указывать регистр со смещением или вообще какие-либо регистры. В результате выполнения этого кода в регистры al и ah будет записан ASCII-код буквы H, или 0 x 48.
В этом контексте хотелось бы упомянуть еще одну полезную ассемблерную инструкцию:
# rax := rcx*8 + rax + 123
lea 123 ( % rax ,% rcx , 8 ) , % raxИнструкция lea очень удобна, так как позволяет сразу выполнить умножение и несколько сложений.
Fun fact! На x64 в байткоде инструкций никогда не используются 64-х битовые смещения. В отличие от x86, инструкции часто оперируют не абсолютными адресами, а адресами относительно адреса самой инструкции, что позволяет обращаться к ближайшим +/- 2 Гб оперативной памяти. Соответствующий синтаксис:
movb msg ( % rip ) , % al
Сравним длины опкодов «обычного» и «относительного» mov ( objdump -d ):
4000b0: 8a 0c 25 e8 00 60 00 mov 0x6000e8,%cl
4000b7: 8a 05 2b 00 20 00 mov 0x20002b(%rip),%al # 0x6000e8Как видите, «относительный» mov еще и на один байт короче! Что это за регистр такой rip мы узнаем чуть ниже.
Для записи же полного 64-х битового значения в регистр предусмотрена специальная инструкция:
movabs $ 0x1122334455667788 , % rax
Другими словами, процессоры x64 так же экономно кодируют инструкции, как и процессоры x86, и в наше время нет особо смысла использовать процессоры x86 в системах, имеющих пару гигабайт оперативной памяти или меньше (мобильные устройства, холодильники, микроволновки, и так далее). Скорее всего, процессоры x64 будут даже более эффективны за счет большего числа доступных регистров и большего размера этих регистров.
Арифметические операции
Рассмотрим основные арифметические операции:
# инциализируем значения регистров
mov $ 123 , % rax
mov $ 456 , % rcx# инкремент: rax = rax + 1 = 124
inc % rax# декремент: rax = rax — 1 = 123
dec % rax# сложение: rax = rax + rcx = 579
add % rcx , % rax# вычитание: rax = rax — rcx = 123
sub % rcx , % rax# изменение знака: rcx = — rcx = -456
neg % rcxЗдесь и далее операндами могут быть не только регистры, но и участки памяти или константы. Но оба операнда не могут быть участками памяти. Это правило применимо ко всем инструкциям ассемблера x86/x64, по крайней мере, из рассмотренных в данной статье.
mov $ 100 , % al
mov $ 3 , % cl
mul % clВ данном примере инструкция 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# rax = rax * rcx = 56088
imul % rcx , % rax# rcx = rax * 10 = 560880
imul $ 10 , % rax , % rcxИнструкции div и idiv производят действия, обратные mul и imul. Например:
mov $ 0 , % rdx
mov $ 456 , % rax
mov $ 123 , % rcx# rax = rdx:rax / rcx = 3
# rdx = rdx:rax % rcx = 87
div % rcxКак видите, был получен результат целочисленного деления, а также остаток от деления.
Это далеко не все арифметические инструкции. Например, есть еще adc (сложение с учетом флага переноса), sbb (вычитание с учетом займа), а также соответствующие им инструкции, выставляющие и очищающие соответствующие флаги (ctc, clc), и многие другие. Но они распространены намного меньше, и потому в рамках данной статьи не рассматриваются.
Логические и битовые операции
Как уже отмечалось, особой типизации в ассемблере x86/x64 не предусмотрено. Поэтому не стоит удивляться, что в нем нет отдельных инструкций для выполнения булевых операций и отдельных для выполнения битовых операций. Вместо этого есть один набор инструкций, работающих с битами, а уж как интерпретировать результат — решает конкретная программа.
Так, например, выглядит вычисление простейшего логического выражения:
mov $ 0 , % rax # a = false
mov $ 1 , % rbx # b = true
mov $ 0 , % rcx # c = false# rdx := a || !(b && c)
mov % rcx , % rdx # rdx = c
and % rbx , % rdx # rdx &= b
not % rdx # rdx = ~ rdx
or % rax , % rdx # rdx |= a
and $ 1 , % rdx # rdx &= 1Заметьте, что здесь мы использовали по одному младшему биту в каждом из 64-х битовых регистров. Таким образом, в старших битах образуется мусор, который мы обнуляем последней командой.
Еще одна полезная инструкция — это xor (исключающее или). В логических выражениях xor используется нечасто, однако с его помощью часто происходит обнуление регистров. Если посмотреть на опкоды инструкций, то становится понятно, почему:
4000b3: 48 31 db xor %rbx,%rbx
4000b6: 48 ff c3 inc %rbx
4000b9: 48 c7 c3 01 00 00 00 mov $0x1,%rbxКак видите, инструкции xor и inc кодируются всего лишь тремя байтами каждая, в то время, как делающая то же самое инструкция mov занимает целых семь байт. Каждый отдельный случай, конечно, лучше бенчмаркать отдельно, но общее эвристическое правило такое — чем короче код, тем больше его помещается в кэши процессора, тем быстрее он работает.
В данном контексте также следует вспомнить инструкции побитового сдвига, тестирования битов (bit test) и сканирования битов (bit scan):
# положим что-нибудь в регистр
movabs $ 0xc0de1c0ffee2beef , % rax# сдвиг влево на 3 бита
# rax = 0x0de1c0ffee2beef0
shl $ 4 , % rax# сдвиг вправо на 7 бит
# rax = 0x001bc381ffdc57dd
shr $ 7 , % rax# циклический сдвиг вправо на 5 бит
# rax = 0xe800de1c0ffee2be
ror $ 5 , % rax# циклический сдвиг влево на 5 бит
# rax = 0x001bc381ffdc57dd
rol $ 5 , % rax# положить в CF (см далее) значение 13-го бита
# CF = !!(0x1bc381ffdc57dd & (1 bt $ 13 , % rax# то же самое + установить бит (bit test and set)
# rax = 0x001bc381ffdc77dd, CF = 0
bts $ 13 , % rax# то же самое + сбросить бит (bit test and reset)
# rax = 0x001bc381ffdc57dd, CF = 1
btr $ 13 , % rax# то же самое + инвертировать бит (bit test and complement)
# rax = 0x001bc381ffdc77dd, CF = 0
btc $ 13 , % rax# найти самый младший ненулевой байт (bit scan forward)
# rcx = 0, ZF = 0
bsf % rax , % rcx# найти самый старший ненулевой байт (bit scan reverse)
# rdx = 52, ZF = 0
bsr % rax , % rdx# если все биты нулевые, ZF = 1, значение rdx неопределено
xor % rax , % rax
bsf % rax , % rdxЕще есть битовые сдвиги со знаком (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
xor % rax , % rax
jmp next
# эта инструкция будет пропущена
inc % rax
next :
inc % raxВ результате значение rax будет равно единице, так как первая инструкция inс будет пропущена. Заметьте, что адрес перехода также может быть записан в регистре:
xor % rax , % rax
mov $next , % rcx
jmp *% rcx
inc % rax
next :
inc % raxВпрочем, на практике такого кода лучше избегать, так как он ломает предсказание переходов и потому менее эффективен.
Примечание: GAS позволяет давать меткам цифирные имена типа 1: , 2: , и так далее, и переходить к ближайшей предыдущей или следующей метке с заданным номером инструкциями вроде jmp 1b и jmp 1f . Это довольно удобно, так как иногда бывает трудно придумать меткам осмысленные имена. Подробности можно найти здесь.
Условные переходы обычно осуществляются при помощи инструкции cmp, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je, jg и подобных:
je 1f # перейти, если равны (equal)
jl 1f # перейти, если знаково меньше (less)
jb 1f # перейти, если беззнаково меньше (below)
jg 1f # перейти, если знаково больше (greater)
ja 1f # перейти, если беззнаково больше (above)Существует также инструкции jne (перейти, если не равны), jle (перейти, если знаково меньше или равны), jna (перейти, если беззнаково не больше) и подобные. Принцип их именования, надеюсь, очевиден. Вместо je / jne часто пишут jz / jnz, так как инструкции je / jne просто проверяют значение ZF. Также есть инструкции, проверяющие другие флаги — js, jo и jp, но на практике они используются редко. Все эти инструкции вместе взятые обычно называют jcc. То есть, вместо конкретных условий пишутся две буквы «c», от «condition». Здесь можно найти хорошую сводную таблицу по всем инструкциям jcc и тому, какие флаги они проверяют.
Помимо cmp также часто используют инструкцию test:
test % rax , % rax
jz 1f # перейти, если rax == 0
js 2f # перейти, если rax < 0
1 :
# какой-то код
2 :
# какой-то еще кодFun fact! Интересно, что cmp и test в душе являются теми же sub и and, только не изменяют своих операндов. Это знание можно использовать для одновременного выполнения sub или and и условного перехода, без дополнительных инструкций cmp или test.
Еще из инструкций, связанных с условными переходами, можно отметить следующие.
jrcxz 1f
# какой-то код
1 :Инструкция jrcxz осуществляет переход только в том случае, если значение регистра rcx равно нулю.
cmovge % rcx , % rax
Инструкции семейства cmovcc (conditional move) работают как mov, но только при выполнении заданного условия, по аналогии с jcc.
Инструкции setcc присваивают однобайтовому регистру или байту в памяти значение 1, если заданное условие выполняется, и 0 иначе.
cmpxchg % rcx , ( % rdx )
Сравнить rax с заданным куском памяти. Если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере rcx. Иначе очистить ZF и загрузить значение из памяти в rax. Также оба операнда могут быть регистрами.
cmpxchg8b ( % rsi )
cmpxchg16b ( % rsi )Инструкция cmpxchg8b главным образом нужна в x86. Она работает аналогично cmpxchg, только производит compare and swap сразу 8-и байт. Регистры edx:eax используются для сравнения, а регистры ecx:ebx хранят то, что мы хотим записать. Инструкция cmpxchg16b по тому же принципу производит compare and swap сразу 16-и байт на x64.
Важно! Примите во внимание, что без префикса lock все эти compare and swap инструкции не атомарны.
mov $ 10 , % rcx
1 :
# какой-то код
loop 1b
# loopz 1b
# loopnz 1bИнструкция loop уменьшает значение регистра rcx на единицу, и если после этого rcx != 0 , осуществляет переход на заданную метку. Инструкции loopz и loopnz работают аналогично, только условия более сложные — (rcx != 0) && (ZF == 1) и (rcx != 0) && (ZF == 0) соответственно.
Не нужно быть семи пядей во лбу, чтобы изобразить при помощи этих инструкций конструкцию if-then-else или циклы for / while, поэтому двигаемся дальше.
«Строковые» операции
Рассмотрим следующий кусок кода:
mov $str1 , % rsi
mov $str2 , % edi
cld
cmpsbВ регистры rsi и rdi кладутся адреса двух строк. Командой cld очищается флаг направления (DF). Инструкция, выполняющая обратное действие, называется std. Затем в дело вступает инструкция cmpsb. Она сравнивает байты (%rsi) и (%rdi) и выставляет флаги в соответствии с результатом сравнения. Затем, если DF = 0, rsi и rdi увеличиваются на единицу (количество байт в том, что мы сравнивали), иначе — уменьшаются. Аналогичные инструкции cmpsw, cmpsl и cmpsq сравнивают слова, длинные слова и четверные слова соответственно.
Инструкции cmps интересны тем, что могут использоваться с префиксом rep, repe (repz) и repne (repnz). Например:
mov $str1 , % rsi
mov $str2 , % edi
mov $len , % rcx
cld
repe cmpsb
jne not_equalПрефикс 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, то поменять местами значение двух регистров можно так:
push % rax
mov % rcx , % rax
pop % rcxСуществуют инструкции, помещающие на стек и извлекающие с него регистр rflags / eflags:
pushf
# делаем что-то, что меняет флаги
popf
# флаги восстановлены, самое время сделать jccА так, к примеру, можно получить значение флага CF:
pushf
pop % rax
and $ 1 , % raxНа x86 также существуют инструкции pusha и popa, сохраняющие на стеке и восстанавливающие с него значения всех регистров. В x64 этих инструкций больше нет. Видимо, потому что регистров стало больше и сами регистры теперь длиннее — сохранять и восстанавливать их все стало сильно дороже.
Процедуры, как правило, «создаются» при помощи инструкций call и ret. Инструкция call кладет на стек адрес следующей инструкции и передает управление по указанному в аргументе адресу. Инструкция ret читает со стека адрес возврата и передает по нему управление. Например:
someproc :
# типичный пролог процедуры
# для примера выделяем 0x10 байт на стеке под локальные переменные
# rbp — указатель на фрейм стека
push % rbp
mov % rsp , % rbp
sub $ 0x10 , % rsp# тут типа какие-то вычисления .
mov $ 1 , % rax# типичный эпилог процедуры
add $ 0x10 , % rsp
pop % rbp# выход из процедуры
ret_start :
# как и в случае с jmp, адрес перехода может быть в регистре
call someproc
test % rax , % rax
jnz errorПримечание: Аналогичный пролог и эпилог можно написать при помощи инструкций 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 }
return hash ;
}Дизассемблерный листинг (при компиляции с -O0 , комментарии мои):
# типичный пролог процедуры
# регистр rsp не изменяется, так как процедура не вызывает никаких
# других процедур
400950: 55 push %rbp
400951: 48 89 e5 mov %rsp,%rbp# инициализация локальных переменных:
# -0x08(%rbp) — const unsigned char *data (8 байт)
# -0x10(%rbp) — const size_t data_len (8 байт)
# -0x14(%rbp) — unsigned int hash (4 байта)
# -0x18(%rbp) — int i (4 байта)
400954: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400958: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40095c: c7 45 ec 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
400963: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)# rax := i. если достигли data_len, выходим из цикла
40096a: 48 63 45 e8 movslq -0x18(%rbp),%rax
40096e: 48 3b 45 f0 cmp -0x10(%rbp),%rax
400972: 0f 83 28 00 00 00 jae 4009a0# eax := (hash 400978: 8b 45 ec mov -0x14(%rbp),%eax
40097b: c1 e0 05 shl $0x5,%eax
40097e: 03 45 ec add -0x14(%rbp),%eax# eax += data[i]
400981: 48 63 4d e8 movslq -0x18(%rbp),%rcx
400985: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400989: 0f b6 34 0a movzbl (%rdx,%rcx,1),%esi
40098d: 01 f0 add %esi,%eax# hash := eax
40098f: 89 45 ec mov %eax,-0x14(%rbp)# i++ и перейти к началу цикла
400992: 8b 45 e8 mov -0x18(%rbp),%eax
400995: 83 c0 01 add $0x1,%eax
400998: 89 45 e8 mov %eax,-0x18(%rbp)
40099b: e9 ca ff ff ff jmpq 40096a# возвращаемое значение (hash) кладется в регистр eax
4009a0: 8b 45 ec mov -0x14(%rbp),%eax# типичный эпилог
4009a3: 5d pop %rbp
4009a4: c3 retqЗдесь мы встретили две новые инструкции — movs и movz. Они работают точно так же, как mov, только расширяют один операнд до размера второго, знаково и беззнаково соответственно. Например, инструкция movzbl (%rdx,%rcx,1),%esi читайт байт (b) по адресу (%rdx,%rcx,1) , расширяет его в длинное слово (l) путем добавления в начало нулей (z) и кладет результат в регистр esi.
Как видите, два аргумента были переданы процедуре через регистры rdi и rsi. По всей видимости, используется конвенция под названием System V AMD64 ABI. Утверждается, что это стандарт де-факто под x64 на *nix системах. Я не вижу смысла пересказывать описание этой конвенции здесь, заинтересованные читатели могут ознакомиться с полным описанием по приведенной ссылке.
Заключение
Само собой разумеется, в рамках одной статьи, описать весь ассемблер x86/x64 не представляется возможным (более того, я не уверен, что сам знаю его прямо таки весь). Как минимум, за кадром остались такие темы, как операции над числами с плавающей точкой, MMX-, SSE- и AVX-инструкции, а также всякие экзотические инструкции вроде lidt, lgdt, bswap, rdtsc, cpuid, movbe, xlatb, или prefetch. Я постараюсь осветить их в следующих статьях, но ничего не обещаю. Следует также отметить, что в выводе objdump -d для большинства реальных программ вы очень редко увидите что-то помимо описанного выше.
Еще интересный топик, оставшийся за кадром — это атомарные операции, барьеры памяти, спинлоки и вот это все. Например, compare and swap часто реализуется просто как инструкция cmpxchg с префиксом lock. По аналогии реализуется атомарный инкремент, декремент, и прочее. Увы, все это тянет на тему для отдельной статьи.
В качестве источников дополнительной информации можно рекомендовать книгу Modern X86 Assembly Language Programming, и, конечно же, мануалы от Intel. Также довольно неплоха книга x86 Assembly на wikibooks.org.
Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:
- http://ref.x86asm.net/;
- http://www.felixcloutier.com/x86/;
- http://x86.renejeschke.de/;
- https://en.wikipedia.org/wiki/X86_instruction_listings;
Дополнение: В продолжение темы вас может заинтересовать пост Учебный микропроцессорный комплект УМК-80. В нем рассказывается об ассемблере 8080, 8-и битном предке ассемблера x86/x64.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.
Ассемблер. Базовый синтаксис
Программы на языке ассемблере могут быть разделены на три секции: data, bss, text.
Секция data используется для объявления инициализированных данных или констант. Данные в этой секции НЕ могут быть изменены во время выполнения программы. Вы можете хранить константные значения и названия файлов в этой секции. Синтаксис объявления:
section . data
Секция bss используется для объявления переменных. Синтаксис объявления:
section . bss
Секция text используется для хранения кода программы. Данная секция должна начинаться с объявления global_start , которое сообщает ядру, откуда нужно начинать выполнение программы. Синтаксис объявления:
section . text
global _startКомментарии
Комментарии в ассемблере должны начинаться с точки с запятой ( ; ). Они могут содержать любой печатный символ, включая пробел. Комментарий может находиться как на отдельной строке:
; эта программа выводит сообщение на экран
Так и на строке со стейтментом:
add eax , ebx ; добавляет ebx к eax
Стейтменты
В ассемблере есть три вида стейтментов:
Выполняемые инструкции (или просто «инструкции») — сообщают процессору, что нужно делать. Каждая инструкция хранит в себе код операции (или «опкод») и генерирует одну инструкцию на машинном языке.
Директивы ассемблера — сообщают программе об аспектах компиляции. Они не генерируют инструкции на машинном языке.
Макросы — являются простым механизмом вставки кода.
В ассемблере на одну строку приходится один стейтмент, который должен соответствовать следующему формату:
[ метка ] mnemonic [ операнды ] [ ; комментарий ]
Базовая инструкция состоит из названия инструкции ( mnemonic ) и операндов (они же «параметры»). Вот примеры типичных стейтментов ассемблера:
INC COUNT ; выполняем инкремент переменной памяти COUNT
MOV TOTAL , 48 ; перемещаем значение 48 в переменную памяти TOTAL
ADD AH , BH ; добавляем содержимое регистра BH к регистру AH
AND MASK1 , 128 ; выполняем операцию AND с переменной MASK1 и 128
ADD MARKS , 10 ; добавляем 10 к переменной MARKS
MOV AL , 10 ; перемещаем значение 10 в регистр ALПервая программа
Следующая программа на языке ассемблера выведет строку Hello, world! на экран:
section . text
global _start ; необходимо для линкера ( ld )
_start : ; сообщает линкеру стартовую точку
mov edx , len ; длина строки
mov ecx , msg ; строка
mov ebx , 1 ; дескриптор файла ( stdout )
mov eax , 4 ; номер системного вызова ( sys_write )
int 0x80 ; вызов ядра
mov eax , 1 ; номер системного вызова ( sys_exit )
int 0x80 ; вызов ядра
section . data
msg db ‘Hello, world!’ , 0xa ; содержимое строки для вывода
len equ $ — msg ; длина строкиРезультат выполнения программы:
Сборка программ
Убедитесь, что у вас установлен NASM. Запишите вашу программу в текстовом редакторе и сохраните её как hello.asm. Затем:
убедитесь, что вы находитесь в той же директории, в которой вы сохранили hello.asm;
чтобы собрать программу, введите команду nasm -f elf hello.asm ;
если не было ошибок, то создастся объектный файл вашей программы под названием hello.o;
чтобы ваш объектный файл прошел линкинг и создался исполняемый файл под названием hello, введите команду ld -m elf_i386 -s -o hello hello.o ;
запустите программу командой ./hello .
Если всё прошло успешно, то вам выведется Hello, world! .
Если у вас нет возможности скомпилировать программу, например, у вас нет Linux и вы пока не хотите на него переходить, то можете использовать одну из следующих онлайн-IDE:
Примечание: Запоминать две вышеприведенные команды для сборки программы на ассемблере для некоторых может быть несколько затруднительно, поэтому вы можете написать скрипт для сборки программ на ассемблере. Для этого создайте файл под названием Makefile со следующим содержимым:
nasm – f elf $ ( source )
ld – m elf _ i386 – s – o $ ( source ) $ ( source ) . o
rm $ ( source ) . oДля сборки hello.asm выполните следующие действия:
убедитесь, что вы находитесь в той же директории, в которой вы сохранили hello.asm и Makefile;
введите команду make source=hello .
(97 оценок, среднее: 4,77 из 5)
Ассемблер. Настройка среды разработки
Ассемблер. Сегменты памяти и регистры
Комментариев: 21
Visual Code 1.82.3-1696245001 выдаёт ошибку «You don’t have an extension for debugging ‘x86 and x86_64 assembly’» Установки расширений ошибку не устраняют.
Круто! Спасибо большое!
cmake_minimum_required ( VERSION 3.5 )
project ( test LANGUAGES ASM_NASM )
set ( CMAKE_NASM_LINK _ EXECUTABLE «ld-o » )
add_executable ( test test . asm )
set_target_properties ( test PROPERTIES LINKER_LANGUAGE NASM )
set_target_properties ( test PROPERTIES NASM_OBJ _ FORMAT «elf32» LINK _ FLAGS «-m elf_i386» )
Код для автоматизации процесса сборки через CMakeЭм полчаса дебажил, оказывается вместо спейсов нужен таб на строчках и символ — в исходнике отличается от консольного, вот рабочий вариант all:
nasm — f elf $ ( source ) . asm
ld — m elf_i386 — s — o $ ( source ) $ ( source ) . o
rm $ ( source ) . oХех, спасибо! Да, символы дефиса в примере не валидный для консоли, просто скопипастить в Makefile не получится. Тоже не сразу сообразил.
Я использую вот такой скрипт:
https://gist.github.com/MinerChAI/62c60442d4bb904bb87e0ca5fa1b9473
Его удобнее использовать, т.к. не надо указывать source и как аргумент он получает полное имя файла, таким образом можно компилить файлы с другими расширениями (можно даже просто точку в конце имени файла написать) + работает автодополнениеДля тех, кому нужно автоматизировать процесс сборки и запуска с помощью bash-скрипта:
создайте скрипт > touch Makefile
отредактируйте его с помощью vim > vim Makefile
впишите туда это: #!/bin/bash
fileasm=$(ls *.asm)
nasm -f elf $fileasm
file=$
fileo=$(ls *.o)
ld -m elf_i386 -s -o $file $fileo
rm $fileo
./$file ой, а как отсюда выйти? ХДХДХД
сохраните и выйдите нажав esc и введя :wq
выдайте права на исполнение > sudo chmod +x Makefile
А теперь просто запустите его > ./Makefile
Он создаст исполняемый файл и запустит его, а ненужный файл object удалит. При обновлении самого кода на асме, скрипт перезапишет и запустит новую версию, без косяков и лишних файлов. ТОЛЬКО работает он, если в текущей директории нет других файлов с расширением .asmА почему Makefile? Тут же не make используется
«чтобы ваш объектный файл прошёл линкинг и создался исполняемый файл под названием hello, введите команду ld -m elf_i386 -s -o hello hello.o;» «username@PC:~/Документи/1$ ld -m elf_i386 -s -o hello.s hello.o
bash: ld: команду не обнаружено» стопорнулся на даном моменте.
что может быть не так.
(работаю с debian)