Как написать на ассемблере hello world

Так сложилось, что я совсем не знаю ассемблера. Даже несмотря на то, что я разрабатываю компиляторы, на уровень близкий к аппаратуре я почти не спускаюсь. Была пара попыток его выучить, но я просто не находил подходящего материала. В итоге решил что если его нет, то нужно написать самому. В этой заметке я планирую показать как написать простой Hello world на ассемблере.

В данной статье я преследую несколько целей:

  • Изучить основы работы с ассемблером
  • Сравнить ассемблеры процессоров различных архитектур и, как следствие, показать разные аппаратные особенности
  • Написать материал по которому новички далее смогут самостоятельно продолжить изучение ассемблера

Содержание:

  1. Введение
  2. amd64
  3. sparc v9
  4. Эльбрус
  5. Послесловие
  6. Источники

1. Введение

Я буду стараться давать минимум теории, т.к. её рассказывают много где, гораздо более подробно и понятно. Поэтому буду описывать только то, что касается данного примера.
Итак, задача: написать программу, выводящую на экран сообщение «Hello, world». В качестве эталона возьмём программу на C:


#include <unistd.h>

int main()
{
        const char * msg = "Hello, worldn";
        write(0, msg, 13);
        return 0;
}

Сборка и запуск:


$ gcc t.c && ./a.out
Hello, world


Здесь специально не использована стандартная библиотека, а применён системный вызов write. Подробнее про него можно прочесть по команде man 2 write.

2. amd64

В качестве процессора на данной архитектуре применяется Intel Core i5, операционная система — Gentoo GNU/Linux, синтаксис AT&T. По моей любимой привычке сначала напишем программу, а потом будем думать.


.section .data
        hello_str:
                .string "Hello, worldn"
                .set hello_str_len, . - hello_str - 1

.section .text
        .globl _start

_start:
        # Здесь подготавливаем и вызываем write
        mov $1, %rax
        mov $1, %rdi
        mov $hello_str, %rsi
        mov $hello_str_len, %rdx
        syscall

        # Здесь подготавливаем и вызываем exit
        mov $60, %rax
        mov $0, %rdi
        syscall

Сборка и запуск:


$ as tt.s -o tt.o && ld tt.o && ./a.out
Hello, world

Теперь попытаемся понять что произошло.

Краткое описание синтаксиса:
На каждой строчке находятся команды (statement). Команда начинается с нуля и более меток, после которых находится ключевой символ, обозначающий тип команды. Всё что начинается с точки `.’ является директивой ассемблера. Всё что начинается с буквы является инструкцией ассемблера и транслируется в машинный код. Комментарии бывают многострочными `/**/’ и однострочными `#’.

Директивы .section обозначают начало секций. Секция — это диапазон адресов без пробелов, содержащий в себе данные, предназначенные для одной цели [as]. Объектный файл, сгененрированный as имеет как минимум три секции: .text, .data, .bss. Внутри объектного файла по адресу 0 располагается секция .text, за ней идёт секция .data, а за ней секция .bss. Все адреса as вычисляет как (адрес начала секции) + (смещение внутри секции). Итак, что же означают секции:

  • .data — в этой секции обычно хранятся константы
  • .text — в этой секции обычно хранятся инструкции программы
  • .bss — содержит обнулённые байты и применяется для хранения неинициализированной информации

В начале секции .data у нас стоит метка hello_str, которая указывает на начало строки.

Далее идёт директива .string. Это псевдо операция, копирующая байты в объектник.

Директива .set присваивает символу значение выражения. Т.о. мы говорим что символ hello_str_len равен выражению . - hello_str - 1. Символ `.‘ означает текущий адрес. Вычитая из него адрес метки hello_str получаем длину строки с завершающим нулём. Чтобы он не попал на печать вычитаем 1.

Директива .globl говорит что данный символ должен быть виден ld. Т.е. теперь символ _start сможет быть слинкован. Это нужно, т.к. вход в программу осуществляется именно через этот символ.

После метки _start начинаются непосредственно ассемблерные инструкции. И теперь опять вернёмся к теории.

Данная программа написана под процессор Intel архитектуры amd64 (она же x86_64). Это 64-х битное расширение архитектуры IA-32. Описание самой архитектуры процессора находится в [intel1]. Подробное описание команд процессора находится в [intel2].

Итак, в данной программе мы оперируем регистрами — внутренней памятью процессора. Архитектура amd64 содержит очень мало регистров — всего 16 64-х разрядных регистров общего назначения: RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP, R8DR15D.

Операция mov предназначена для копирования первого операнда во второй (заметьте, что это особенность синтаксиса AT&T, и интеловский синтаксис имеет обратный порядок операндов). Мы можем скопировать константу, значение общего или сегментного регистра или значение из памяти. Копировать можно в общий или сегментный регистр или память. Для обозначения констант используется символ $, а для регистров — % Чуть позже станет понятно что куда и зачем мы копировали.

Далее идёт операция syscall. Она делает системный вызов. Системный вызов — это функция из ядра ОС. Каждый системный вызов производится по номеру. Он должен находиться в регистре rax. Номера системных вызовов можно посмотреть в таблицах [syscall1][syscall2]. Но можно выяснить самому. Их конкретное местоположение зависит от дистрибутива. В моём случае они, например, находятся в файле /usr/include/asm/unistd_64.h. Вот выдержка из этого файла:


...
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
...
#define __NR_execve 59
#define __NR_exit 60
#define __NR_wait4 61
...

Понятно, что помимо номеров нам нужны ещё аргументы этих вызовов. Их можно найти следующим образом:


$ cd /usr/src/linux/
$ grep -rA3 'SYSCALL_DEFINE.?(write,' *
fs/read_write.c:SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
fs/read_write.c-           size_t, count)
fs/read_write.c-{
fs/read_write.c-   struct fd f = fdget_pos(fd);

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

Итак, видно, что вызов write требует 3 аргумента. Первый — это дескриптор файла вывода. Он кладётся на регистр rdi. Мы на rdi кладём 1, что является дескриптором stdout. На регистр rsi кладётся указатель на адрес строки. И на регистр rdx кладётся длина строки. Всё, теперь, когда все регистры подготовлены, можно делать syscall и нам будет выведено сообщение.

Далее нужно выйти из программы. Для этого используется системный вызов exit. Он имеет номер 60 и требует код возврата в качестве первого аргумента. Мы завершаемся с кодом 0, как и положено успешно выполненной программе.

3. Sparc v9

Не устали? Теперь внезапно рассмотрим sparc. Меня эта платформа интересует, т.к. одна из линеек процессоров Эльбрус основана на этой архитектуре. Я тестировался на процессорах TI UltraSparc III+ (Cheetah+) с ОС Gentoo и процессорах Эльбрус R1000 c ОС Эльбрус. Итак, смотрим:


.section .data
hello_str:
    .ascii "Hello, worldn"
    .set hello_str_len, . - hello_str

.global _start

.section .text

_start:
    ! Подготавливаем и вызываем write
    mov 1, %o0
    set hello_str, %o1
    mov hello_str_len, %o2
    mov 4, %g1
    ta 0x10

    ! Подготавливаем и вызываем exit
    mov 0, %o0
    mov 1, %g1
    ta 0x10



Сборка и запуск:


$ as -Av9 -64 t1.s -o t1.o && ld -Av9 -m elf64_sparc t1.o && ./a.out
Hello, world

Вроде как отличий немного. Синтаксис as был описан в блоке amd64, разве что здесь однострочные комментарии задаются символом !, поэтому его опускаем и переходим сразу к отличиям. Сразу скажу, что речь идёт о Sparc v9 если не оговорено другое. v9 является 64-х битным расширением архитектуры sparc v8. Начнём с регистров. Их здесь больше чем в amd64 — целых 32 общего назначения, доступных пользователю. Сами регистры называются %r0%r31, но у них есть логическое разделение:

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

Название Имя внутри окна Имя r-регистра
Глобальные (global) %g0 — %g7 %r0 — %r7
Выходные (out) %o0 — %o7 %r8 — %r15
Локальные (local) %l0 — %l7 %r16 — %r23
Входные (in) %i0 — %i7 %r24 — %r31

Данные регистры называются r регистрами и используются для целочисленных вычислений. Плавающие регистры называются f регистрами, они расположены отдельно, и о них мы сегодня говорить не будем. Интересно отметить, что сама архитектура предполагает от 64 до 528 r регистров, но регистровое окно содержит только 24. Чтение %g0 всегда возвращает 0, а запись в него не даёт эффекта. Вообще на спарке регистры сделаны очень круто, но их очень долго описывать, советую прочитать документацию [sparcv9].

Переходим к инструкциям. Начнём с инструкции mov. От интела эта инструкция отличается тем, что её нет в Спарке. Sparc — это RISC архитектура с малым количеством команд, но для удобства программистов ассемблер поддерживает синтетические инструкции. В частности приведённый mov возможно будет оттранслирован следующим образом (есть несколько способов трансляции в зависимости от аргументов):
mov 1, %o1 -> or %g0, 1, %o1
Синтетические инструкции не являются частью стандарта, но входят в информационное приложение к нему, так что их можно смело использовать.
Следующая инструкция set, являющаяся синонимом к инструкции setuw, которая тоже является синтетической инструкцией. Её раскрытие возможно выглядит следующим образом:

set hello_str %o2 ->
sethi %hi(hello_str), %o2
or %o2, %lo(hello_str), %o2

Инструкция sethi поместит старшие 22 бита hello_str (т.е. её адрес) на регистр %o2. Инструкция or поместит туда младший остаток. Обозначения %hi и %lo нужны для взятия старших и младших битов соответственно. Такие сложности возникают из-за того что инструкция кодируется 32 битами, и просто не может включать в себя 32-х битную константу.

Далее мы кладём значение 4 на глобальный регистр %g1. Можно догадаться что это номер вызова write. Системный возов будет искать номер вызова именно там.

Операция ta инициирует системное прерывание. Её аргументом является тип системного прерывания. Скажу честно — я не нашёл нормального описания системных вызовов для v9, а то что туда надо подавать 0x10 выяснил случайно из архивов какой-то переписки. Поэтому придётся просто это запомнить :)

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

UPD:

Спасибо уважаемому Анониму за версию данной программы для SunOS 5.10:


.section ".text"
.global _start
_start:
mov 4,%g1 ! 4 is SYS_write
mov 1,%o0 ! 1 is stdout
set .msg,%o1 ! pointer to buffer
mov (.msgend-.msg),%o2 ! length
ta 8

mov 1,%g1 ! 1 is SYS_exit
clr %o0 ! return status is 0
ta 8

.msg:
.ascii "Hello world!n"
.msgend:

Запуск:
$ as t1.s -o t1.o && ld t1.o && ./a.out
Hello world!

4. Эльбрус

Ну и, собственно, жемчужина коллекции — процессор Эльбрус. Работа проводилась на процессоре Эльбрус-4С, который имеет архитектуру команд v3 (наше внутреннее обозначение). Управляется машина ОС Эльбрус. Про сам Эльбрус можно почитать в [elbrus], про какую-либо документацию, находящуюся в открытом доступе мне неизвестно.

Как и Sparc, архитектура Эльбруса рассчитана в первую очередь на то что оптимальный код выдаст компилятор. Но в отличает от Sparc, ассемблер Эльбруса вообще не предназначен для людей. Итак, вот наш пример:


.section ".data"

$hello_msg:
    .ascii    "Hello, worldn00"

.section ".text"
    .global _start

_start:
    ! Подготавливаем вызов write
    {
      sdisp %ctpr1, 0x3
      addd, 0 0x0, 13, %b[3]
      addd, 2 0x0, [ _f64, _lts1 $hello_msg ], %b[2]
      addd, 1 0x0, 0x1, %b[1]
      addd, 3 0x0, 0x4, %b[0]
    }

    ! Вызываем write
    {
      call %ctpr1, wbs = 0x4
    }

    ! Подготавливаем вызов exit
    {
      sdisp %ctpr2, 0x1
      addd, 0 0x0, 0x0, %b[1]
      addd, 1 0x0, 0x1, %b[0]
    }

    ! Вызываем exit
    {
      call %ctpr2, wbs = 0x4
    }



Сборка и запуск:


$ las t.s -o t.o && ld t.o && ./a.out
Hello, world


Начнём с изменения синтаксиса.

Мы видим что к синтаксису добавились фигурные скобки. Процессоры Эльбрус основаны на VLIW архитектуре, а значит могут исполнять множество статически спланированных команд за такт. Набор таких команд называется широкой командой (ШК) и заключается в фигурные скобки. Остальной синтаксис более или менее идентичен.

Если посмотреть
на команду сборки, то вместо as используется las. Это наш местный ассемблер, но сейчас идёт процесс перехода на gas, поэтому скоро он станет неактуален (отдел, занимающийся ассемблером уже сейчас ругается если я его использую, но в дистрибутиве пока именно он).

Чтобы процессор мог исполнять много команд за такт, ему нужно много регистров. Согласен, что их никогда не бывает много, но для программы на Эльбрусе регистровый файл содержит 256 регистров общего назначения размером 64 бита. Из них 224 предназначены для процедурного стека, а 32 являются глобальными регистрами. В Эльбрусе нет отдельных регистров для плавающих вычислений, все они выполняются на одном конвейере и хранятся в общих
регистрах. Именование регистров идёт следующим образом:

  • %r<номер> — прямоадресуемые регистры текущего окна. <номер> является индексом относительно базы текущего окна
  • %b[<номер>] — вращаемые регистры текущего окна. <номер> — индекс относительно текущей базы
  • %g<номер> — глобальные регистры. <номер> является индексом относительно базы текущей глобальной области

Иногда в ассемблере регистры имеют  различные префиксы. Подобные названия не влияют ни на что и нужны только для наглядности. Префиксы бывают следующие:

  • s одинарный формат регистра — 32 бита (Single)
  • d двойной формат регистра — 64 бита (Double)
  • x расширенный двойной регистра — 80 бит (Extended)
  • q квадро формат регистра — 128 бит (Quadro)

Существует программное соглашение, согласно которому для передачи аргументов в вызываемую процедуру мы используем вращающиеся регистры.

Итак теперь переходим к самой программе. Думаю первые несколько строк и так понятны, поэтому рассмотрим сразу первую ШК:


_start:
    {
      sdisp %ctpr1, 0x3
      addd, 0 0x0, 13, %b[3]
      addd, 2 0x0, [ _f64, _lts1 $hello_msg ], %b[2]
      addd, 1 0x0, 0x1, %b[1]
      addd, 3 0x0, 0x4, %b[0]
    }


Рассмотрим первую команду sdisp %ctpr1, 0x3. А чтобы понять что это такое и что оно делает нужно ещё немного рассказать про механизм работы переходов в Эльбрусе. В процессорах Эльбрус вызов функции является дорогим удовольствием, поэтому переходы следует готовить заранее. Для этого существует два типа команд — ctp (подготовка перехода) и ct — фактический переход. Нам доступно три регистра перехода: %ctpr1%ctpr3, т.е. за раз мы можем подготовить три маршрута для прыжка. Существует несколько команд подготовки перехода, нас здесь интересует sdisp. Эта команда подготавливает переход для системного вызова. Первым аргументом идёт регистр перехода, по которому мы будем совершать прыжок. Вторым аргументом — точка входа в операционную систему, нам она нужна равной 3 (64-х битный вход в ОС).

Далее рассмотрим команды addd. Как я уже говорил, ассемблер Эльбруса не предназначен для людей, и общепринятых мнемоников здесь пока нет. Так в ассемблере нет команды MOV. Чтобы положить значение на регистр применяется команда add. Она производит сложение регистров или констант и записывает их в регистр.

Для Эльбруса одновременно доступно 6 арифметико-логических каналов (АЛК), т.е. за такт мы можем производить до 6 сложений. Итак, в первой операции мы кладём число 13 в регистр %b[3] — это длина нашей строки. (В версиях для других архитектур мы вычисляли это программно, и в Эльбрусе можно сделать также, но для las у меня это так и не получилось, хотя в gas всё заработало). Далее на регистр %b[2] мы кладём адрес начала нашего сообщения. Затем в %b[1] кладём идентификатор устройства вывода, и, наконец, в %b[0] кладём номер системного вызова. В целом аналогия с другими архитектурами прослеживается.

Далее может возникнуть вопрос зачем в команде addd третья d. В мнемониках команд, реализованных для нескольких форматов операндов, последняя буква обозначает используемый формат. В данном случае мы работаем в double формате, т.е. с полноценным 64-х битным регистром.

Отдельно рассмотрим команду addd, 2 0x0, [ _f64, _lts1 $hello_msg ], %b[2], которая, как можно догадаться, кладёт в регистр %b[2] адрес печатаемого сообщения. Для того чтобы закодировать адрес в памяти используется аргумент [ _f64, _lts1 $hello_msg ]. Квадратные скобки означают взятие адреса. Внутри расположен длинный литерал. Его содержимое означает следующее:

  • _f64 — формат литерала. В данном случае мы говорим что это литерал размера 64 (хотя он уместится и в 32 бита)
  • _lts1 — литеральный слог, кодирующий константное значение. Всего доступно 4 литеральных слога, так что в одной ШК мы не сможем поместить более 4 длинных литералов (в случае формата _f64 — не более 2).
  • $hello_msg — идентификатор, обозначающий нашу метку

Во второй ШК у нас производится операция call %ctpr1, wbs = 0x4, которая вызывает функцию, переход на которую подготовлен на регистре %ctpr1. т.е. вызывается наш write. Второй аргумент задаёт смещение для новой базы регистрового окна. Здесь я не буду объяснять что это значит, т.к. это займёт много времени, просто пока придётся запомнить что это должно быть так (на самом деле это очень частный случай и нужно понимать как его высчитывать)

В третьей ШК мы аналогичным образом подготавливаем переходы для вызова exit, и в четвёртой ШК мы его вызываем.

Всё, проще некуда.

Послесловие

Как я уже говорил в начале, данный материал появился потому что я не смог найти чего-то подобного в сети. На самом деле многое я взял из этого [0xax] блога — описание примера на x86 и вообще саму идею. Для остальных архитектур пришлось изворачиваться :) Позже, во время работы над заметкой, я нашёл это [mechasm] неплохое описание, но оно уже было неактуально.

Вообще я планировал написать эту заметку за неделю-две и перейти на следующий пример. Более того хотел ещё включить описание llvm IR. Но внезапно простенькая заметка про hello world заняла у меня несколько месяцев. Преимущественно из-за Эльбруса. Тут оказалось много нового и непонятного при почти полном отсутствии читабельной документации. И тут хотелось бы сказать огромное спасибо многим моим коллегам, которые терпеливо в течении долгого времени разъясняли мне простейшие вещи.

В данной заметки могут быть неточности, ошибки и вообще фиг знает что, поэтому если что-то не так — пишите, я поправлю :)

Источники

[intel1] Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 1: Basic Architecture
[intel2] Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 3A, 3B, 3C and 3D
[syscall1] Таблица системных вызовов linux
[syscall2] Другая таблица системных вызовов linux
[as] Мануал по ассемблеру
[0xax] Серия постов про написание hello world на ассемблере amd64. Во многом при написании заметки я смотрел именно в этот пост, там весьма подробное и доходчивое описание с замечаниями в комментах
[mechasm]Аналогичный пост на русском, который я нашёл не сразу и не пользовался им. Но стиль изложения мне нравится
[sparcv9]The SPARC Architecture Manual Version 9
[sparcv9asm] SPARC Assembly Language Reference Manual
[oracle] Актуальная документация от Oracle
[sparcasmbook]SPARC Architecture, Assembly Language Programming, and C. Очень хороший учебник по ассемблеру и по спарку
[elbrus] Микропроцессоры и вычислительные комплексы семейства «Эльбрус»

Время на прочтение
6 мин

Количество просмотров 8.2K

Эта серия статей посвящена изучению и практике программирования на языке ассемблера.

Материал рассчитан на новичков в ассемблере, студентов, которым пришлось столкнуться с «динозавром» в виде MS-DOS, и может быть интересен тем, кто хочет немного узнать как функционировали операционные системы на заре своего существования.

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

Программа будет выполнять следующие функции:

  • вывод текста вниз экрана по таймеру,

  • переключение режима отображения шрифта: italic/normal,

  • русификация,

  • запрет на ввод прописных русских букв,

  • резидентные часы,

  • вывод бинарного представления символа.

Предисловие

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

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

Немного оговорок. Далее под ассемблером будет пониматься язык ассемблера, а не программа компилятор. MS-DOS часто будет заменяться на dos/дос.

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

Про MS-DOS. Всех, наверное, пугает это слово в современном мире. Операционная система, которая уже как 20 лет мертва, но не все так однобоко как кажется на первый взгляд. Минусы понятны: изучение технологии, которая уже сгнила и разложилась, не используемая модель памяти. Но что насчет положительных моментов:

  • Ассемблер он и в Африке ассемблер, основные концепции программирования на нем будут везде одинаковы, да где-то будут расширенные регисты, где-то другой интерфейс по работе с операционной системой.

  • MS-DOS очень простая операционная система, которая в начале своего существования умещалась в 50 тысяч строк кода, причем ассемблерных (Майкрософт выложила исходники 2-х версий на github). График ее изучения имеет дно, в отличие от современных операционных систем. Аналогией может служить C и C++, последний, наверное, не знает в полной мере со всеми тонкостями ни один человек в мире.

  • Операционка работает в реальном режиме процессора, то есть в 16-битном. Это означает, что нет виртуальной памяти, адреса сразу преобразуются в физические с использованием сегментной адресаци памяти. Нет защиты процессов друг от друга, можно обратиться по любому адресу, посмотреть, что там лежит, можно делать с осью все, что тебе вздумается, но могут быть последствия ;). Плюс этот режим до сих пор не вымер, при запуске системы процессор начинает работу именно в этом режиме. Так что это не просто знакомство с историей.

  • Из предыдущего пункта понятно, что систему легко сломать, например, переписать адрес аппаратного прерывания по работе с клавиатурой, но в режиме эмуляции dos очень быстро запускается, что очень удобно в таких случаях

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

  • в MS-DOS мало встроенной функциональности, она работает в режиме терминала (печатной машинки), и уже первые шаги в написании ассемблерных программ позволяют видеть пользу от них.

  • Ассемблер актуален в MS-DOS, и это радует, когда работаешь в ней, потому что иных средств разработки программ не так много там. Но в настоящее время ассемблер используется только в виде вставок в языке Си или в микроконтроллерах.

  • Простой формат бинарного файла, точнее его попросту нет. Текст программы компилируется напрямую в машинный код, и получается исполняемый файл .COM, готовый к запуску. Очень удобно начинать обучение с этого, не забивая себе голову всякими разными дерективами, секции, которые необходимы в современных форматах.

Немного про компилятор. Использоваться будет NASM, хотя логичнее было бы использовать досовские компиляторы TASM, MASM, но они не поддерживают мою рабочую операционную систему Линукс, а разрабатываться хочется все-таки в удобстве, поэтому взят nasm. Он популярный, современный, кроссплатформенный (запускается везде, компилируется подо все, включая дос), более умный — позволяет опускать какие-то вещи в синтаксисе, имеет фичи в виде локальных меток, контекстов, всяких других директив.

Настройка

Для начала нам потребуется эмулятор операционной системы DOS под названием DOSBox. Скачать можно здесь, версия 0.74-3. После установки и запуска вы увидите, что-то похожее на это:

Стартовый экран DOSBox

Стартовый экран DOSBox

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

Windows: Z:> mount c: C:UsersUsernameDesktopdos

Linux: Z:> mount c: /home/username/dos

Получаем сообщение Drive C is mounted...Теперь все содержимое папочки dos будет отображаться в диске С: в эмуляторе. Перейти в диск C с диска Z можно командой Z:>c:. Это действие придется делать каждый раз при запуске эмулятора, поэтому мы можем положить эту команду в файл конфигурации в секцию autoexec. На линуксе файл находится в /home/username/.dosbox. На виндовс C:UsersUsernameAppDataLocalDOSbox. Открываем файл dosbox-0.74-3.conf и в конец прописываем команду монтирования и перехода в диск C вот таким образом:

[autoexec]
# Lines in this section will be run at startup.
# You can put your MOUNT lines here.

mount c: /home/username/dos
c:

Hello world

Напишем первую программу на ассемблере, которая будет выводить на экран избитую фразу hello world:

org 100h

mov ah, 09h
mov dx, message
int 21h
int 20h

message: db 'Hello, world!$'

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

nasm hello_world.asm -o hello_world.com

Бинарный .com файл нужно положить в нашу папочку dos, перезапустить дос или запустить в работующем эмуляторе команду rescan, чтобы дос подхватил изменения. Запустить команду можно, начав вводить первые символы имени файла и нажав Tab. Вводить название файла целиком самостоятельно не стоит, потому что долго и потому что с файлами, у которых в имени больше 8 символов, начинаются проблемы. Регистр букв не важен. После запуска, на экране можно будет увидеть фразу Hello, world!.

Теперь о том, что делает каждая строчка, 1-я строка org 100h это указание компилятору на смещение начала инстукций, будет понятно, что это означает, когда мы рассмотрим устройство .com файла и механизм работы процессора в реальном режиме.

8-я строка содержит метку message: , метки это своего рода переменные, в них помещается адрес текущей инструкции, после компиляции, места, где были ссылки на метки будут заменяться реальными адресами. Двоеточие в метках опционально. Далее идет псевдо-инструкция db (define byte), она не является инструкцией процессора, служит для того, чтобы в текущее место исполняемого файла записать блок данных побайтово. db принимает сколько угодно операндов (аргументов), разделенных запятыми. В нашем случае это один операнд, являющийся строкой из 14 символов (байт), можно было бы записать строку и посимвольно. В конце строки ставится знак $, который дает понять внутренней функции доса, что наступил конец строки. В следующей части поговорим, о том почему у нас данные находятся в конце файла.

3-5 строки подготовка для вызова прерывания 21h и непосредственно сам вызов, прерывание мы обсудим в 3-ей части, в нашем случае это попросту вызов функции операционной системы. В строке 3 мы помещаем число 09h (h значит шестнадцатиричное) в регистр ah. 09h — это номер функции.

В строке 4 записываем в dx адрес начала строки, которую хотим вывести на экран. Теперь понятно зачем нужен $, начало строки дос знает, конец нет.

В строке 5 передаем управление операционной системе с помощью прерывания, по номеру функции дос понимает, что нужно сделать (вывести строку на экран).

В строке 6 используем прерывание 20h для завершения программы, этот способ не совсем корректный, но он простой и хорошо подходит для .com программ.

Не думаю, что стало сильно понятно. Поэтому в следующих частях мы рассмотрим теоретические аспекты: сегментную адресацию памяти, формат файла .com, дебаггер, интерфейс вызовов функций дос, прерывания и снова вернемся к примеру с hello world.

До встречи!

Hey there!

This year I studied computer’s organisation II in College. I’ve been fascinated thus far with how computation is done on an assembler language and I want to share with you a little of my knowledge.

Maybe this will become a series because I’m finding myself really captivated with this subject and I think a lot of people will understand better how programming works with this info.

But first things first

❓ What is ASM?

Alt Text

ASM, short for Assembler (or assembly), is not a unique language such as C, Java, Go or whatever, it is instead a program that converts code into machine language. This means there’s assembler languages for the different types of machines. For example: There is assembler for the Intel and AMD processor’ architectures (x86_64) and there’s another for ARM architectures.

This tutorial is going to be oriented toward Intel’s Architecture

🔧 What are we going to use?

Alt Text

For this short program we are going to use NASM and whatever text editor you like. In my case, I’m going to use VS Code since it has some nice plugins.

To install NASM on Debian systems (Ubuntu, PopOs!, Linux Mint, etc..)

sudo apt-get update -y
sudo apt-get install -y nasm

I’m only going to show this example in a linux system since the sys calls are different for mac, hence the example won’t work in that system (believe me, this post was intended for mac as well…)

🏗️ Structure of an ASM program

Ok, now we have our assembler and our Text Editor or IDE. What now?

Let’s create a new file and name it helloWorld.asm

Alt Text

Now that we have our empty file. We need to determine how the file is going to be used. In ASM each file has 4 sections. This sections will always exist even if you don’t define them. However, if you need one, you will have to do it.

The 4 sections are:

  • .data : where we are going to declare our global initialised variables

  • .rodata : where we are going to declare our global un-itialised constants

  • .bss : where we are going to declare our global un-initialised variables

  • .text : where we are going to define our code

👏 Hands On

Ok, so what we are trying to build here is a CLI program that prints Hello World!. Sounds fairly easy. But in order to do so, we need to inform the processor that this function that we are going to name ‘start’ is global to all the system. so we add our .text section with the ‘start’ function and the global statement outside the section. Like this:

Alt Text

Since we don’t want to use any fancy C functions, nor none of those other high level languages functions for the matter, we are going to rely on Syscalls.

Without digging that deep, Syscalls are just calls to the OS. We need to call the 0x80 interruption (on UNIX systems) and pass to that interruption the parameters we want it to handle.

For the function that we are going to use (sys_write) the interruption receives 4 parameters:

  1. The function number (RAX)
  2. Where do we want it to execute (RBX)
  3. The direction of the memory we want to execute (RCX)
  4. The size of the message in bytes (RDX)

RAX, RBX, RCX and RDX are just multi-purpose registers that we are going to use and that I’m going to explain in further chapters of this series. So bare with me for now.

So let’s define the message first and let’s call int 0x80 after that.

Alt Text

A lot of new info here. Let’s go line by line.

This is the section were we are going to define our ‘Hello World!’ string variable. Since it will be already initialised, we declare in .data

msg: DB 'Hello World!', 10

This is our new string. It’s declared under the name msg and we initialise it with DB (define byte) the characters that will be displayed and a ‘, 10’ which is going to be our n character. What I want you to get out of this step is that Each char comprising ‘Hello World!’ takes one byte of memory. So by using DB we are asking the processor for a memory slot that will take 13 bytes (counting the space and n char).

This one is a little bit tougher. We are declaring a variable call msgSize that is going to step on the right end of Hello World! ($) and will subtract the address were your msg variable began. Thus leaving us with the bytes used for msg

We have our message, let’s display it now!

Alt Text

Again, let’s explain what is happening here

    mov rax, 4          ; function 4
    mov rbx, 1          ; stdout
    mov rcx, msg        ; msg
    mov rdx, msgSize    ; size
    int 0x80

Intel has a very weird way of doing things most of the time. So each line of text will be divided into 4 fragments again

  1. Mov : an instruction which moves the elements from B to A
  2. A : the destiny Register/Memory
  3. B : the origin Register/Memory
  4. comments : where the comments are :p

So what we are doing in here is moving the number 4 to our RAX register (because sys_write is our function number 4 on UNIX). We move the number 1 to RBX (representing STDOUT). Then the memory in which msg is defined will be stored on RCX and finally the size on RCX. By calling int 0x80 we are asking the interruption 0x80 to handle all the parameters we threw to it and do what it’s supposed to do.

Alt Text

Our final step is to exit the program. And guess what? that requires another Syscall. In this case, our function will be number 1 (exit) and our parameter will be 0 (because that’s the number we want to return. 0 usually means that the program was executed successfully while 1 means that it wasn’t)

    mov rax, 1          ; function 1
    mov rbx, 0          ; code
    int 0x80

🔗 Assembling and Linking

Let’s save our file as helloWorld.asm and head over to the terminal.

If you have already installed NASM, head to the folder where you saved your .asm file and assemble and link it.

Linux:

nasm -f elf64 -g -F DWARF helloWorld.asm
ld -e start -o helloWorld helloWorld.o
./helloWorld

And that’s it for today. You should get a ‘Hello World message on your terminal.

Alt Text

If you want to see the full code, here is the Repository: Hello World!

Первая программа на ассемблере.

Наша первая программа на ассемблере.

Наша первая программа на ассемблере будет в формате *.COM — как мы уже знаем, исполняемые файлы указанного формата очень крохотные (tiny) по размеру и  состоят из одного сегмента, в котором размещаются код, данные и стек.

Ещё мы знаем, что в указанном формате пишутся резидентные программы, драйверы и вирусы.

Резидентная (TSR-программа, от англ. Terminate and Stay Resident) — это программа, которая после запуска передает управление операционной системе, но сама не завершается, а остаётся в оперативной памяти, реагируя на определённые действия пользователя. Например, при нажатии сочетания горячих клавиш делает снимок экрана.

Код в статьях отображается в удобочитаемой форме: каждая строка имеет свой номер, строки и код подсвечиваются. Чтобы скопировать «чистый исходник», наведите курсор мыши на текст, дождитесь всплывающего меню и нажмите в меню кнопочку «копировать» (изображение двух листочков бумаги с текстом). Чистый код не содержит нумерации строк!

Наша первая программа выведет на экран монитора (консоль) надпись «Hello, World!». Итак, как говорил Юрий Алексеевич, поехали!

Создаём исполняемый файл PRG.COM.

Для достижения нашей цели делаем следующее.

  • Скачиваем с нашего сайта архив (DOS-1.rar) с предустановленными DOSBox и программами. Запускаем DOSBox. Стартует эмулятор MS-DOS и Norton Commander пятой версии.
  • В папке D:TASM.2_0TASM находим текстовый файл PRG.ASM. Это обычный текстовый файл, который можно создать
    с помощью любого текстового редактора, с расширением ASM вместо TXT.
  • В файл вносим код:

;Строка, после точки с запятой является комментарием

;и не обрабатывается ассемблером

; prg.asm — название файла.

.model tiny ; создаём программу типа СОМ

.code ; начало сегмента кода

org 100h ; начальное значение смещения программы в памяти — 100h

start:

mov ah,9 ; номер функции DOS — в АН

mov dx,offset message ; адрес строки — в DX

int 21h ; вызов т.н. «прерывания» — системной функции DOS

ret ; завершение СОМ-программы

message db «Hello, World!»,0Dh,0Ah,‘$’ ; строка для вывода

end start ; конец программы.

  • В папке D:TASM.2_0TASM находим «батник» ASM-COM.BAT со следующим текстом:

tasm.exe prg.asm

tlink.exe /t /x prg.obj

Первая строка — запуск транслятора с названием нашего файла с кодом, расположенного в одной директории с транслятором.

Вторая строка — запуск компилятора с параметрами /t /x и название объектного файла — prg.obj, получившегося в результате выполнения первой команды.

Чтобы посмотреть список всех возможных параметров с пояснениями для файлов tasm.exe и tlink.exe необходимо запустить эти программы без параметров. Если вы сделаете это, не выходя из оболочки NC, то, чтобы просмотреть чистое окно DOS нажмите Ctrl+O, чтобы вернуться в NC, нажмите сочетание клавиш повторно.

  • После запуска ASM-COM.BAT в этой же директории появится файл prg.com. Запустив его мы увидим сообщение «Hello World!» в окне MS-DOS (при необходимости просмотра, снова применяем Ctrl+O).

Батник ASM-EXE.BAT предназначен для создания исполняемого файла формате *.EXE (предусматривает раздельную сегментацию для кода, данных и стека — наиболее распространённый формат исполняемых файлов DOS).

Батник COMPLEX.BAT предназначен для создания исполняемых файлов из двух файлов кода (названия обязательно должны быть prg.asm, prg1.asm).

Наша первая программа на ассемблере прекрасно работает!

TASMED (Tasm Editor) — среда разработки приложений DOS на ассемблере.

Выше мы рассмотрели стандартный подход к программированию на TASM в системе MS-DOS. Указанным алгоритмом создания программ можно пользоваться и далее.

Для более удобной работы с кодом целесообразно применять какую-либо среду разработки. Среда разработки — это громко сказано для времён MS-DOS, правильнее сказать — специфический редактор.

Можете попробывать TASMED в папке D:UTILSTASMED. Программа уже настроена и готова к использованию.

Первая программа на ассемблере.

Первая программа на ассемблере в среде разработки TASMED.

Основные плюсы:

  • подсветка ассемблерного синтаксиса;
  • возможность сохранения проектов под любым именем и в любой директории;
  • работа как с TASM, так и MASM.

Минусы:

  • только английский язык интерфейса, но английский программист должен знать лучше русского;
  • слишком много настроек для текстового редактора.
    Хотя, в принципе, настройки — не проблема. Основное, что необходимо настроить — это соответствующие пути:
    Options->External->Assembler
    Options->External->Linker
    В общем, разобраться не сложно.

Практические советы: группирование проектов, русский язык в MS-DOS.

Для удобства группирования создаваемых программ можно создать отдельную папку (мы создали папку PROJECTS) в которой создавать папки названий проектов, куда копировать соответствующие файлы. Пока у нас — это PRG.ASM, PRG.OBJ, PRG.EXE. Однако, в зависимости от параметров и наших программ их может быть больше (PRG.MAP, PRG.SYM и др.).

В нашем случае, все программы, рассматриваемые в курсе обучения будут группироваться в директории D:WORK в соответствующих папках. Например, наша первая программа в папке D:WORKPRGCOM (файлы prg.asm и prg.com). Папку D:TASM.2_0PROJECTS оставляем пустой для ваших проектов и экспериментов.

Для того, чтобы в исходниках, которые вы будете просматривать в системе MS-DOS с использованием текстовых редакторов DOS и различных сред разработок нормально отображалась кириллица (например в комментариях к строкам кода) необходимо проделать следующие действия.

  1. Запустить драйвер русской раскладки клавиатуры. В нашей запущенной MS-DOS системе на базе DOSBox это файл C:KEYRUSkeyrus.com. Впрочем, при запуске MS-DOS согласно нашим настройкам DOSBox, он запустится автоматически. При этом будет обеспечено не только отображение русского текста в текстовых редакторах, но и русскоязычная раскладка клавиатуры. Переключение раскладки Eng->Rus и наоборот — горячая клавиша «правый CTRL».
  2. Текст исходников необходимо писать в текстовых редакторах или средах разработки DOS.
  3. Если исходники пишутся в Windows редакторах, должна быть обеспечена русскоязычная кодировка текста — ASCII для DOS (CP866 или OEM866).

Русскоязычная кодировка текста ASCII для DOS (CP866 или OEM866).

Русскоязычная кодировка текста — программа просмотра файлов Total Commander.
Русскоязычная кодировка текста для DOS.
Русскоязычная кодировка текста — используем Notepad++.

Конечно вопрос снимается сам собой, если комментарии писать на английском.

В следующей статье мы разберём код нашей первой программы на ассемблере.

Ты решил освоить ассемблер, но не знаешь, с чего начать и какие инструменты для этого нужны? Сейчас расскажу и покажу — на примере программы «Hello, world!». А попутно объясню, что процессор твоего компьютера делает после того, как ты запускаешь программу.

Содержание

  1. Основы ассемблера
  2. Если наборы инструкций у процессоров разные, то на каком учить ассемблер лучше всего?
  3. Что и как процессор делает после запуска программы
  4. Регистры процессора: зачем они нужны, как ими пользоваться
  5. Подготовка рабочего места
  6. Написание, компиляция и запуск программы «Hello, world!»
  7. Инструкции, директивы
  8. Метки, условные и безусловные переходы
  9. Комментарии, алгоритм, выбор регистров
  10. Взаимодействие с пользователем: получение данных с клавиатуры
  11. Полезные мелочи: просмотр машинного кода, автоматизация компиляции
  12. Выводы

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

Я буду исходить из того, что ты уже знаком с программированием — знаешь какой-нибудь из языков высокого уровня (С, PHP, Java, JavaScript и тому подобные), тебе доводилось в них работать с шестнадцатеричными числами, плюс ты умеешь пользоваться командной строкой под Windows, Linux или macOS.

Если наборы инструкций у процессоров разные, то на каком учить ассемблер лучше всего?

Знаешь, что такое 8088? Это дедушка всех компьютерных процессоров! Причем живой дедушка. Я бы даже сказал — бессмертный и бессменный. Если с твоего процессора, будь то Ryzen, Core i9 или еще какой-то, отколупать все примочки, налепленные туда под влиянием технологического прогресса, то останется старый добрый 8088.

SGX-анклавы, MMX, 512-битные SIMD-регистры и другие новшества приходят и уходят. Но дедушка 8088 остается неизменным. Подружись сначала с ним. После этого ты легко разберешься с любой примочкой своего процессора.

РЕКОМЕНДУЕМ:
Лучшие игры для программистов и технарей

Больше того, когда ты начинаешь с начала — то есть сперва выучиваешь классический набор инструкций 8088 и только потом постепенно знакомишься с современными фичами, — ты в какой-то миг начинаешь видеть нестандартные способы применения этих самых фич.

Что и как процессор делает после запуска программы

После того как ты запустил софтину и ОС загрузила ее в оперативную память, процессор нацеливается на первый байт твоей программы. Вычленяет оттуда инструкцию и выполняет ее, а выполнив, переходит к следующей. И так до конца программы.

Некоторые инструкции занимают один байт памяти, другие два, три или больше. Они выглядят как-то так:

90

B0 77

B8 AA 77

C7 06 66 55 AA 77

Вернее, даже так:

90 B0 77 B8 AA 77 C7 06 66 55 AA 77

Хотя погоди! Только машина может понять такое. Поэтому много лет назад программисты придумали более гуманный способ общения с компьютером: создали ассемблер.

Благодаря ассемблеру ты теперь вместо того, чтобы танцевать с бубном вокруг шестнадцатеричных чисел, можешь те же самые инструкции писать в мнемонике:

nop

mov al, 0x77

mov ax, 0x77AA

mov word [0x5566], 0x77AA

Согласись, такое читать куда легче. Хотя, с другой стороны, если ты видишь ассемблерный код впервые, такая мнемоника для тебя, скорее всего, тоже непонятна. Но мы сейчас это исправим.

Регистры процессора: зачем они нужны, как ими пользоваться

Что делает инструкция
mov? Присваивает число, которое указано справа, переменной, которая указана слева.

Переменная — это либо один из регистров процессора, либо ячейка в оперативной памяти. С регистрами процессор работает быстрее, чем с памятью, потому что регистры расположены у него внутри. Но регистров у процессора мало, так что в любом случае что-то приходится хранить в памяти.

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

У процессора 8088 регистры 16-битные, их восемь штук (в скобках указаны типичные способы применения регистра):

  • AX — общего назначения (аккумулятор);
  • BX — общего назначения (адрес);
  • CX — общего назначения (счетчик);
  • DX — общего назначения (расширяет
    AX до 32 бит);
  • SI — общего назначения (адрес источника);
  • DI — общего назначения (адрес приемника);
  • BP — указатель базы (обычно адресует переменные, хранимые на стеке);
  • SP — указатель стека.

Несмотря на то что у каждого регистра есть типичный способ применения, ты можешь использовать их как заблагорассудится. Четыре первых регистра —
AX,
BX,
CX и
DX — при желании можно использовать не полностью, а половинками по 8 бит (старшая
H и младшая
L):
AH,
BH,
CH,
DH и
AL,
BL,
CL,
DL. Например, если запишешь в
AX число
0x77AA (
mov ax, 0x77AA), то в
AH попадет
0x77, в
AL
0xAA.

С теорией пока закончили. Давай теперь подготовим рабочее место и напишем программу «Hello, world!», чтобы понять, как эта теория работает вживую.

Подготовка рабочего места

  1. Скачай компилятор NASM с www.nasm.us. Обрати внимание, он работает на всех современных ОС: Windows 10, Linux, macOS. Распакуй NASM в какую-нибудь папку. Чем ближе папка к корню, тем удобней. У меня это
    c:nasm (я работаю в Windows). Если у тебя Linux или macOS, можешь создать папку
    nasm в своей домашней директории.
  2. Тебе надо как-то редактировать исходный код. Ты можешь пользоваться любым текстовым редактором, который тебе по душе: Emacs, Vim, Notepad, Notepad++ — сойдет любой. Лично мне нравится редактор, встроенный в Far Manager, с плагином Colorer.
  3. Чтобы в современных ОС запускать программы, написанные для 8088, и проверять, как они работают, тебе понадобится DOSBox или VirtualBox.

Написание, компиляция и запуск программы «Hello, world!»

Сейчас ты напишешь свою первую программу на ассемблере. Назови ее как хочешь (например,
first.asm) и скопируй в папку, где установлен
nasm.

Основы ассемблера. Hello World

Если тебе непонятно, что тут написано, — не переживай. Пока просто постарайся привыкнуть к ассемблерному коду, пощупать его пальцами. Чуть ниже я все объясню. Плюс студенческая мудрость гласит: «Тебе что-то непонятно? Перечитай и перепиши несколько раз. Сначала непонятное станет привычным, а затем привычное — понятным».

Теперь запусти командную строку, в Windows это cmd.exe. Потом зайди в папку
nasm и скомпилируй программу, используя вот такую команду:

nasm f bin first.asm o first.com

Если ты все сделал правильно, программа должна скомпилироваться без ошибок и в командной строке не появится никаких сообщений.
NASM просто создаст файл
first.com и завершится.

Чтобы запустить этот файл в современной ОС, открой DOSBox и введи туда вот такие три команды:

Само собой, вместо
c:nasm тебе надо написать ту папку, куда ты скопировал компилятор. Если ты все сделал правильно, в консоли появится сообщение «Hello, world!».

Основы ассемблера. Hello World

Инструкции, директивы

В нашей с тобой программе есть только три вещи: инструкции, директивы и метки.

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

Директивы (в нашей программе их две:
org и
db) — это распоряжения, которые ты даешь компилятору. Каждая отдельно взятая директива говорит компилятору, что на этапе ассемблирования нужно сделать такое-то действие. В машинный код директива не переводится, но она влияет на то, каким образом будет сгенерирован машинный код.

Директива
org говорит компилятору, что все инструкции, которые последуют дальше, надо размещать не в начале сегмента кода, а отступить от начала столько-то байтов (в нашем случае 0x0100).

Директива
db сообщает компилятору, что в коде нужно разместить цепочку байтов. Здесь мы перечисляем через запятую, что туда вставить. Это может быть либо строка (в кавычках), либо символ (в апострофах), либо просто число.

В нашем случае:
db «Hello, world», ‘!’, 0.

Обрати внимание, символ восклицательного знака я отрезал от остальной строки только для того, чтобы показать, что в директиве
db можно оперировать отдельными символами. А вообще писать лучше так:

Метки, условные и безусловные переходы

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

Что значит «прыгать из других мест программы»? В норме процессор выполняет инструкции последовательно, одну за другой. Но если тебе надо организовать ветвление (условие или цикл), ты можешь задействовать инструкцию перехода. Прыгать можно как вперед от текущей инструкции, так и назад.

РЕКОМЕНДУЕМ:
Язык программирования Ада

У тебя в распоряжении есть одна инструкция безусловного перехода (
jmp) и штук двадцать инструкций условного перехода.

В нашей программе задействованы две инструкции перехода:
je и
jmp. Первая выполняет условный переход (Jump if Equal — прыгнуть, если равно), вторая (Jump) — безусловный. С их помощью мы организовали цикл.

Обрати внимание: метки начинаются либо с буквы, либо со знака подчеркивания, либо со знака собаки. Цифры вставлять тоже можно, но только не в начало. В конце метки обязательно ставится двоеточие.

Комментарии, алгоритм, выбор регистров

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

Как добавлять комментарии? Просто поставь точку с запятой, и все, что напишешь после нее (до конца строки), будет комментарием. Давай добавим комментарии в нашу программу.

Основы ассемблера. Комментарии, алгоритм, выбор регистров

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

  1. Поместить в
    BX адрес строки.
  2. Поместить в
    AL очередную букву из строки.
  3. Если вместо буквы там 0, выходим из программы — переходим на 6-й шаг.
  4. Выводим букву на экран.
  5. Повторяем со второго шага.
  6. Конец.

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

Взаимодействие с пользователем: получение данных с клавиатуры

От программ, которые не могут взаимодействовать с пользователем, толку мало. Так что смотри, как можно считывать данные с клавиатуры. Сохрани вот этот код как
second.asm.

Основы ассемблера. Получение данных с клавиатуры

Потом иди в командную строку и скомпилируй его в NASM:

nasm f bin second.asm o second.com

Затем запусти скомпилированную программу в DOSBox:

Как работает программа? Две строки после метки
@@start вызывают функцию BIOS, которая считывает символы с клавиатуры. Она ждет, когда пользователь нажмет какую-нибудь клавишу, и затем кладет ASCII-код полученного значения в регистр
AL. Например, если нажмешь заглавную
A, в
AL попадет
0x41, а если строчную
A
0x61.

Дальше смотрим: если нажата клавиша с кодом 0x1B (клавиша ESC), то выходим из программы. Если же нажата не ESC, вызываем ту же функцию, что и в предыдущей программе, чтобы показать символ на экране. После того как покажем — прыгаем в начало (
jmp):
start.

Обрати внимание, инструкция
cmp (от слова compare — сравнить) выполняет сравнение, инструкция
je (Jump if Equal) — прыжок в конец программы.

Полезные мелочи: просмотр машинного кода, автоматизация компиляции

Если тебе интересно, в какой машинный код преобразуются инструкции программы, скомпилируй исходник вот таким вот образом (добавь опцию
l):

nasm f bin second.asm l second.lst o second.com

Тогда NASM создаст не только исполняемый файл, но еще и листинг:
second.lst. Листинг будет выглядеть как-то так.

Основы ассемблера. Просмотр машинного кода, автоматизация компиляции

Еще тебе наверняка уже надоело при каждом компилировании вколачивать в командную строку длинную последовательность одних и тех же букв. Если ты используешь Windows, можешь создать батник (например,
m.bat) и вставить в него вот такой текст.

Основы ассемблера. Автоматизация компиляции

Теперь ты можешь компилировать свою программу вот так:

Само собой, вместо
first ты можешь подставить любое имя файла.

Выводы

Итак, ты теперь знаешь, как написать простейшую программу на ассемблере, как ее скомпилировать, какие инструменты для этого нужны. Конечно, прочитав одну статью, ты не станешь опытным программистом на ассемблере. Чтобы придумать и написать на нем что-то стоящее — вроде Floppy Bird и «МикроБ», которые написал я, — тебе предстоит еще много пройти. Но первый шаг в эту сторону ты уже сделал.

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (2 оценок, среднее: 5,00 из 5)

Загрузка…

Многие считают, что Assembler – уже устаревший и нигде не используемый язык, однако в основном это молодые люди, которые не занимаются профессионально системным программированием. Разработка ПО, конечно, хорошо, но в отличие от высокоуровневых языков программирования, Ассемблер научит глубоко понимать работу компьютера, оптимизировать работку с аппаратными ресурсами, а также программировать любую технику, тем самым развиваясь в направлении машинного обучения. Для понимания этого древнего ЯП, для начала стоит попрактиковаться с простыми программами, которые лучше всего объясняют функционал Ассемблера.

IDE для Assembler

Первый вопрос: в какой среде разработки программировать на Ассемблере? Ответ однозначный – MASM32. Это стандартная программа, которую используют для данного ЯП. Скачать её можно на официальном сайте masm32.com в виде архива, который нужно будет распаковать и после запустить инсталлятор install.exe. Как альтернативу можно использовать FASM, однако для него код будет значительно отличаться.

Перед работой главное не забыть дописать в системную переменную PATH строчку:

С:masm32bin

Программа «Hello world» на ассемблере

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

.386
.model flat, stdcall
option casemap: none
 
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
 
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib
 
.data
msg_title db "Title", 0
msg_message db "Hello world", 0
 
.code
start:
invoke MessageBox, 0, addr msg_message, addr msg_title, MB_OK
invoke ExitProcess, 0
end start

Для начала запускаем редактор qeditor.exe в папке с установленной MASM32, и в нём пишем код программы. После сохраняем его в виде файла с расширением «.asm», и билдим программу с помощью пункта меню «Project» → «Build all». Если в коде нет ошибок, программа успешно скомпилируется, и на выходе мы получим готовый exe-файл, который покажет окно Windows с надписью «Hello world».

Сложение двух чисел на assembler

В этом случае мы смотрим, равна ли сумма чисел нулю, или же нет. Если да, то на экране появляется соответствующее сообщение об этом, и, если же нет – появляется иное уведомление.

.486
.model flat, stdcall
option casemap: none
 
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
 
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib

include /masm32/macros/macros.asm 
uselib masm32, comctl32, ws2_32

.data

.code
start:

mov eax, 123
mov ebx, -90 
add eax, ebx

test eax, eax

jz zero 
invoke MessageBox, 0, chr$("В eax не 0!"), chr$("Info"), 0
jmp lexit

zero:
invoke MessageBox, 0, chr$("В eax 0!"), chr$("Info"), 0

lexit:
invoke ExitProcess, 0

end start

Здесь мы используем так называемые метки и специальные команды с их использованием (jz, jmp, test). Разберём подробнее:

  • test – используется для логического сравнения переменных (операндов) в виде байтов, слов, или двойных слов. Для сравнения команда использует логическое умножение, и смотрит на биты: если они равны 1, то и бит результата будет равен 1, в противном случае – 0. Если мы получили 0, ставятся флаги совместно с ZF (zero flag), которые будут равны 1. Далее результаты анализируются на основе ZF.
  • jnz – в случае, если флаг ZF нигде не был поставлен, производится переход по данной метке. Зачастую эта команда применяется, если в программе есть операции сравнения, которые как-либо влияют на результат ZF. К таким как раз и относятся test и cmp.
  • jz – если флаг ZF всё же был установлен, выполняется переход по метке.
  • jmp – независимо от того, есть ZF, или же нет, производится переход по метке.

Программа суммы чисел на ассемблере

Примитивная программа, которая показывает процесс суммирования двух переменных:

.486
.model flat, stdcall
option casemap: none
 
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
 
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib

include /masm32/macros/macros.asm 
uselib masm32, comctl32, ws2_32

.data
msg_title db "Title", 0
A DB 1h
B DB 2h
buffer db 128 dup(?)
format db "%d",0

.code
start:

MOV AL, A
ADD AL, B

invoke wsprintf, addr buffer, addr format, eax
invoke MessageBox, 0, addr buffer, addr msg_title, MB_OK

invoke ExitProcess, 0

end start

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

Получение значения из командной строки на ассемблере

Одно из важных основных действий в программировании – это получить данные из консоли для их дальнейшей обработки. В данном случае мы их получаем из командной строки и выводим в окне Windows:

.486
.model flat, stdcall
option casemap: none
 
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
 
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib

include /masm32/macros/macros.asm 
uselib masm32, comctl32, ws2_32

.data

.code
start:

call GetCommandLine ; результат будет помещен в eax
 
push 0
push chr$("Command Line")
push eax ; текст для вывода берем из eax
push 0
call MessageBox

push 0
call ExitProcess

end start

Также можно воспользоваться альтернативным методом:

.486
.model flat, stdcall
option casemap: none
 
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
 
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib

include /masm32/macros/macros.asm 
uselib masm32, comctl32, ws2_32

.data

.code
start:

call GetCommandLine ; результат будет помещен в eax
 
invoke GetCommandLine
invoke MessageBox, 0, eax, chr$("Command Line"), 0
invoke ExitProcess, 0

push 0
call ExitProcess

end start

Здесь используется invoke – специальный макрос, с помощью которого упрощается код программы. Во время компиляции макрос-команды преобразовываются в команды Ассемблера. Так или иначе, мы пользуемся стеком – примитивным способом хранения данных, но в тоже время очень удобным. По соглашению stdcall, во всех WinAPI-функциях переменные передаются через стек, только в обратном порядке, и помещаются в соответствующий регистр eax.

Циклы в ассемблере

Вариант использования:

.data

msg_title db "Title", 0
A DB 1h
buffer db 128 dup(?)
format db "%d",0

.code
start:

mov AL, A
.REPEAT
inc AL

.UNTIL AL==7

invoke wsprintf, addr buffer, addr format, AL
invoke MessageBox, 0, addr buffer, addr msg_title, MB_OK
 
invoke ExitProcess, 0

end start
.data

msg_title db "Title", 0
buffer db 128 dup(?)
format db "%d",0

.code
start:

mov eax, 1
mov edx, 1

.WHILE edx==1
inc eax
.IF eax==7
.BREAK
.ENDIF
.ENDW

invoke wsprintf, addr buffer, addr format, eax
invoke MessageBox, 0, addr buffer, addr msg_title, MB_OK
 
invoke ExitProcess, 0

Для создания цикла используется команда repeat. Далее с помощью inc увеличивается значение переменной на 1, независимо от того, находится она в оперативной памяти, или же в самом процессоре. Для того, чтобы прервать работу цикла, используется директива «.BREAK». Она может как останавливать цикл, так и продолжать его действие после «паузы». Также можно прервать выполнение кода программы и проверить условие repeat и while с помощью директивы «.CONTINUE».

Сумма элементов массива на assembler

Здесь мы суммируем значения переменных в массиве, используя цикл «for»:

.486
.model flat, stdcall
option casemap: none
 
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
 
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib

include /masm32/macros/macros.asm 
uselib masm32, comctl32, ws2_32

.data

msg_title db "Title", 0
A DB 1h
x dd 0,1,2,3,4,5,6,7,8,9,10,11
n dd 12

buffer db 128 dup(?)
format db "%d",0

.code
start:
mov eax, 0
mov ecx, n
mov ebx, 0
L: add eax, x[ebx]
add ebx, type x
dec ecx
cmp ecx, 0
jne L

invoke wsprintf, addr buffer, addr format, eax
invoke MessageBox, 0, addr buffer, addr msg_title, MB_OK
 
invoke ExitProcess, 0

end start

Команда dec, как и inc, меняет значение операнда на единицу, только в противоположную сторону, на -1. А вот cmp сравнивает переменные методом вычитания: отнимает одно значение из второго, и, в зависимости от результата ставит соответствующие флаги.

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

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


Post Views:
56 008

These are Win32 and Win64 examples using Windows API calls. They are for MASM rather than NASM, but have a look at them. You can find more details in this article.

This uses MessageBox instead of printing to stdout.

Win32 MASM

;---ASM Hello World Win32 MessageBox

.386
.model flat, stdcall
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib

.data
title db 'Win32', 0
msg db 'Hello World', 0

.code

Main:
push 0            ; uType = MB_OK
push offset title ; LPCSTR lpCaption
push offset msg   ; LPCSTR lpText
push 0            ; hWnd = HWND_DESKTOP
call MessageBoxA
push eax          ; uExitCode = MessageBox(...)
call ExitProcess

End Main

Win64 MASM

;---ASM Hello World Win64 MessageBox

extrn MessageBoxA: PROC
extrn ExitProcess: PROC

.data
title db 'Win64', 0
msg db 'Hello World!', 0

.code
main proc
  sub rsp, 28h  
  mov rcx, 0       ; hWnd = HWND_DESKTOP
  lea rdx, msg     ; LPCSTR lpText
  lea r8,  title   ; LPCSTR lpCaption
  mov r9d, 0       ; uType = MB_OK
  call MessageBoxA
  add rsp, 28h  
  mov ecx, eax     ; uExitCode = MessageBox(...)
  call ExitProcess
main endp

End

To assemble and link these using MASM, use this for 32-bit executable:

ml.exe [filename] /link /subsystem:windows 
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:Main

or this for 64-bit executable:

ml64.exe [filename] /link /subsystem:windows 
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:main

Why does x64 Windows need to reserve 28h bytes of stack space before a call? That’s 32 bytes (0x20) of shadow space aka home space, as required by the calling convention. And another 8 bytes to re-align the stack by 16, because the calling convention requires RSP be 16-byte aligned before a call. (Our main‘s caller (in the CRT startup code) did that. The 8-byte return address means that RSP is 8 bytes away from a 16-byte boundary on entry to a function.)

Shadow space can be used by a function to dump its register args next to where any stack args (if any) would be. A system call requires 30h (48 bytes) to also reserve space for r10 and r11 in addition to the previously mentioned 4 registers. But DLL calls are just function calls, even if they’re wrappers around syscall instructions.

Fun fact: non-Windows, i.e. the x86-64 System V calling convention (e.g. on Linux) doesn’t use shadow space at all, and uses up to 6 integer/pointer register args, and up to 8 FP args in XMM registers.


Using MASM’s invoke directive (which knows the calling convention), you can use one ifdef to make a version of this which can be built as 32-bit or 64-bit.

ifdef rax
    extrn MessageBoxA: PROC
    extrn ExitProcess: PROC
else
    .386
    .model flat, stdcall
    include kernel32.inc
    includelib kernel32.lib
    include user32.inc
    includelib user32.lib
endif
.data
caption db 'WinAPI', 0
text    db 'Hello World', 0
.code
main proc
    invoke MessageBoxA, 0, offset text, offset caption, 0
    invoke ExitProcess, eax
main endp
end

The macro variant is the same for both, but you won’t learn assembly this way. You’ll learn C-style asm instead. invoke is for stdcall or fastcall while cinvoke is for cdecl or variable argument fastcall. The assembler knows which to use.

You can disassemble the output to see how invoke expanded.

Понравилась статья? Поделить с друзьями:
  • Как написать на армянском языке я тебя люблю
  • Как написать на армянском языке русскими буквами
  • Как написать на армянском языке как жизнь
  • Как написать на армянском хорошего дня
  • Как написать на армянском ты мне нравишься