Самый короткий мультфильм про программирование.
Самый короткий мультфильм про программирование.

Пролог

Решил поделиться своими мыслями и кратким двухдневным опытом написания (собирания по частям) программы на Ассемблере без чтения учебников, больших статей и в целом без опыта программирования на этом языке. На одном из форумов я набрёл на задачу вывода десятичного числа в консоль. Если на языке C или PHP эта операция совершенно элементарна, то на Ассемблере всё не так просто, как может показаться на первый взгляд. Для решения задачи я выбрал nasm (правда, выбора и не было), немножко поигравшись предварительно с вставками nasm (синтаксис AT&T) в код C (ссылка на форум с моими опытами в конце статьи).

Философское отступление
Остановись, дорогой читатель! И прежде чем читать дальше, задай себе вопрос: возможно ли начать ковать без обучения кузнечному делу?!

Я оставлю этот вопрос без ответа. Только скажу, что в нём нет ни капли иронии, издёвки, намёка на назидательность и т.д. Это вопрос без какого-либо дополнительного подтекста.

Поиски
Информации по Ассемблеру в Интернете очень много и заблудиться в разных видах Ассемблера (для различных систем) крайне просто. Я не единственный задавался вопросом в поиске «how to print a number in asm». Ответы на разных диалектах языка относительно легко можно найти на Stack Overflow, однако это совершенно не означает, что будет легко запустить найденный код на своей машине. Велика вероятность того, что что-нибудь не сойдётся. Научиться отличать синтаксис AT&T и intel можно за несколько минут, а вот с узнаванием tasm, fasm, masm, nasm - несколько сложнее. Единственное, что можно предположить и (почти) не прогадать: базовые инструкции во всех Ассемблерах имеют (почти) одинаковые мнемоники.

Ассоциации и первые впечатления

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

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

Итак, задача:
Собрать программу, печатающую на стандартный вывод (stdout) число.

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

  1. Число делится на 10.

  2. В регистр edx заносится остаток, в eax остальная часть.

  3. Остаток в цикле записывается в буфер (stroka), начиная с конца, используется декремент, затем выводится

  4. Дополнительно: при использовании инкремента можно напечатать строку в обратном порядке.

Код nasm (собран на архитектуре 64b, с моими и чужими комментариями)

section .data
 	 stroka db 4;буфер для вывода
section .text

global _start  	;must be declared for using gcc

_start:            ;tell linker entry point
	push rbp       ;Работа со стеком
	mov rbp, rsp
	mov eax, 311  	; 311 - делимое
	mov edi, 3      ; Переменная цикла.
loop:               ; Начало цикла
      dec edi
					 ; Декремент, чтобы  записать все значения остатка в stroka
    mov ebx, 10      ; Делитель. Запишем  в цикл, чтобы вернуть ему значение на новой итерации. 
                     ; Можно использовать для делителя только часть регистра ebx, а именно bx   
    xor edx, edx     ; Обнулим остаток
    div ebx          ; Делим
    add edx, 30h     ; Добавим в остаток 0, равносильно add edx, '0'
    mov  [stroka + edi], dl ; Пишем в строку остаток в обратном порядке.
    cmp edi,  0       ; Выходим
jne loop              ; Возврат в цикл

                        ; Собрали строку и далее выводим

  	mov ecx, stroka   ; Кладём
    mov     edx, 4      ; Длина выводимой строки  4 (видимо, в байтах)
    mov     ebx, 1      ; file descriptor (stdout)
    mov     eax, 4      ; system call number (sys_write)
    int     0x80         ; call kernel

    mov     eax, 1      ;system call number (sys_exit)
    int     0x80        ;call kernel

Команда для сборки, линковки и выполнения

nasm -f elf64 print_num.asm -o print_num.o; ld -o print_num print_num.o; ./print_num

  • *** Для остатка можно использовать только часть регистра edx, а именно dx. Это же верно и для делителя - можно использовать bx, так как число 10 умещается в 2 байта (16 бит).

  • *** Для счётчика цикла, наверное, лучше использовать регистр ecx (как более канонический вариант), а остановку цикла осуществлять, когда в ax будет 0.

Оставлю для таких же как я дополнительную задачку и подсказку.

Задача: изменить программу так, чтобы число печаталось наоборот.

Подсказка

Hidden text

Менять нужно mov edi, 3; dec edi; cmp edi, 0

Дополнительно: как сделать из нашего "hello world" что-нибудь полезное?

Если немножко почитать про стек, а также регистры rsp, rbp и поиграть со смещением, то можно быстро переделать программу в генератор псевдослучайных чисел:

	mov rbp, rsp
	mov eax, [rbp +17]  	; Забираем из стека по адресу 17 значение
	add eax, [rbp +18]  	; И ещё сместимся на 1 (видимо,1 байт) и добавим в eax

Далее самописный псевдогенератор

section .data
 	 stroka db 4 ;буфер для вывод
section .text

global _start  	 ;must be declared for using gcc

_start:            ;tell linker entry point
	push rbp       ;Работа со стеком
	mov rbp, rsp
	mov eax, [rbp +17]  	; Забираем из стека по адресу 17 значение
	add eax, [rbp +18]  	; И ещё сместимся 1 одно значение, добавим в eax
	mov edi, 3         ; Переменная цикла.
loop:                  ; Начало цикла
      dec edi
					 ;Декремент, чтобы  записать все значения остатка в stroka
    mov ebx, 10      ; Делитель. Запишем  в цикл, чтобы вернуть ему значение на новой итерации
    xor edx, edx     ; Обнулим остаток
    div ebx          ; Делим
    add edx, 30h     ; Добавим в остаток 0, равносильно add edx, '0'
    mov  [stroka + edi], dl ; Пишем в строку остаток в обратном порядке.
    cmp edi,  0          ; Выходим
jne loop               	 ; Возврат в цикл

                         ; Собрали строку и далее выводим

  	mov ecx, stroka     ; Кладём
    mov     edx, 4      ; Длина выводимой строки  4 (видимо, в байтах)
    mov     ebx, 1      ; file descriptor (stdout)
    mov     eax, 4      ; system call number (sys_write)
    int     0x80        ; call kernel

    mov     eax, 1      ;system call number (sys_exit)
    int     0x80        ;call kernel

Источники:

1) https://acm.mipt.ru/twiki/bin/view/Asm/PrintIntFunction

2) http://av-assembler.ru/asm/afd/asm-cpu-registers.htm

3) https://wasm.in/

4) https://codetown.ru/assembler/delenie-umnozhenie/ (про умножение, деление и регистры)

Темы с моими опытами:

https://wasm.in/threads/vyvod-desjatichnogo-chisla.34675/

https://wasm.in/threads/assmembler-vstavkami.34672/

Update:

Добавлю решённый мной пример поиска и замены значения в массиве (для таких же как я, на примере можно понять ветвления прогрммы (пропуски, переходы)): https://gitflic.ru/project/dcc0/mix-c-89-php/blob?file=search_and_change_in_array.asm

Данной заметкой цикл статей для хабра завершаю.

Всем спасибо.

Комментарии (0)