Примеры кода на ассемблере
На этой практике мы потрогаем ассемблер.
Hello World
Справа вы можете видеть код:
section .text
global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, msg_size
syscall
mov rax, 60
xor rdi, rdi
syscall
section .rodata
msg: db "Hello, world!", 0x0a
msg_size: equ $ - msgbash
Давайте сперва покажем, как компилировать программу на ассемблере:
nasm -felf64 hello.asm
nasm - сам компилятор, -felf64 - формат кодирования для 64-битного линукса и название файла.
Теперь, если вы заметите, то у вас появился файл hello.o - объектный файл.
Чтобы создать файлик, который мы можем запускать, мы должны написать:
ld hello.o
ld - ссылочный компилятор, hello.o - объектный файл, который мы создали ранее. Пока вы не знаете, что это такое, но позже по курсу узнаете. Так вот, эта команда создаст файл a.out, который уже можно будет запустить и он выведет наш "Hello, World".
Теперь давайте говорить про код:
-
В этом коде мы видим section:
Исполняемые файлы в нынешнее время большинство имеют отдельное место под код (условно говоря .text), отдельное место для неизменяемой памяти (.rodata) и другие.
-
Когда наша программа компилируется, она будет запускаться из функции _start.
-
global _start нужен для того, чтобы линковщик мог к ней обратиться (иначе файл бы не запустился, т.к. не видел бы начало программы).
-
Мы распихиваем с помощью mov в какие-то регистры какие-то числа, а потом делаем syscall. Что делает syscall? Просит ядро операционной системы вызвать какую-то команду в зависимости от значения на регистре rax. Какую вы можете посмотреть сюда: Linux_System_Call_Table (https://en.wikipedia.org/wiki/System_call#Linux_kernel)
Когда мы хотим сделать какой-то системный вызов, мы читаем rax и в зависимости от него делаем команду.
-
Рассмотрим эту часть кода:
mov rax, 1 mov rdi, 1 mov rsi, msg mov rdx, msg_size syscall
Что у нас происходит? Мы кладем в rax - 1, откуда это команда write, потом в rdi кладем 1, в rsi кладем указатель на сообщение, а в rdx передаем msg_size, как от нас и требуется по Linux_System_Call_Table и вызываем syscall.
И мы побеждаем!
Вот мы и разобрали весь код HelloWorld. Это очень простой пример, но он показывает основные концепции ассемблера.
Подсчет новых строк в тексте
Давайте напишем программу, которая будет подсчитывать количество переносов строк в тексте. Код с подробными комментариями приведен снизу:
section .text
global _start
SYS_EXIT: equ 60
SYS_WRITE: equ 1
STDOUT: equ 1
BUFFER_SIZE: equ 4096
EXIT_FAILURE: equ 1
_start:
sub rsp, BUFFER_SIZE ; Создаем место на стеке для буфера
xor ebx, ebx ; Вводим регистр, который будет считать количество новых строчек
mov rsi, rsp ; И ставим указатель на место в буфере в регистр rsi
.read_loop:
xor eax, eax ; Очищаем rax, чтобы подготовиться к вызову системного вызова sys_read
xor edi, edi ; Устанавливаем дескриптор файла (STDIN) для системного вызова sys_read
mov rdx, BUFFER_SIZE ; Устанавливаем размер буфера (BUFFER_SIZE) для системного вызова sys_read
syscall ; Считываем BUFFER_SIZE байт из stdin на адрес выделенный памяти (rsi)
; syscall возвращает в регистр rax количество байт, которые он считал.
;Подробнее читайте в документации на Linux_System_Call_Table
test rax, rax ; Проверяем, вернул ли sys_read 0 (EOF)
jz .quit ; Если да, переходим к .quit
js .error ; Если вернул отрицательное значение - что-то сломалось
; Поэтому надо перейти в .error
xor ecx, ecx ; Очищаем rcx
; Сейчас у нас в коде будет что-то типо цикла:
; for(rcx = 0; rcx < rax; rcx++)
.count_newlines:
cmp byte [rsp + rcx], 0x0a ; Сравниваем текущий символ с символом перехода строки
jne .skip ; Если не равны, переходим к .skip
inc rbx ; Если равны, увеличиваем счетчик переводов строки (rbx)
.skip:
inc rcx ; Увеличиваем счетчик цикла (rcx)
cmp rcx, rax ; Сравниваем счетчик цикла с количеством прочитанных байт
jb .count_newlines ; Если меньше, переходим обратно к .count_newlines
jmp .read_loop ; Переходим обратно к .read_loop, чтобы прочитать еще данные
.quit:
; Сюда мы попадаем, когда выходим из цикла и все данные прочитаны корректно
; Теперь нам осталось вывести наше число в десятичном виде
mov rax, rbx ; Перемещаем счетчик переводов строк (rbx) в rax для деления
lea rcx, [write_buffer + write_buffer_size - 1]
; Устанавливаем адрес буфера (rcx) для записи результата
mov byte [rcx], 0x0a ; Добавляем символ новой строки в конец буфера
mov rbx, 10 ; Устанавливаем основание для деления (rbx) в 10
.print_num:
xor edx, edx ; Очищаем rdx, чтобы подготовиться к делению
div rbx ; Делим rax на rbx
add rdx, '0' ; Преобразуем остаток в ASCII
dec rcx ; Уменьшаем адрес буфера (rcx)
mov byte [rcx], dl ; Сохраняем ASCII-символ в буфер
test rax, rax ; Проверяем, является ли частное число 0
jnz .print_num ; Если нет, переходим обратно к .print_num
mov rax, SYS_WRITE ; Устанавливаем номер системного вызова для sys_write
mov rdi, STDOUT ; Устанавливаем дескриптор файла (STDOUT) для sys_write
mov rsi, rcx ; Устанавливаем адрес буфера (rcx) для sys_write
lea rdx, [write_buffer + write_buffer_size] ; Устанавливаем размер буфера для sys_write
sub rdx, rcx ; Вычисляем количество байт для записи
syscall ; Выводим результат на экран
mov rax, SYS_EXIT ; Устанавливаем номер системного вызова для sys_exit
xor rdi, rdi ; Устанавливаем статус выхода (0) для sys_exit
syscall ; Выходим
.error:
; О нет, вы проиграли
mov rax, SYS_WRITE ; Устанавливаем номер системного вызова для sys_write
mov rdi, STDOUT ; Устанавливаем дескриптор файла (STDOUT) для sys_write
mov rsi, err_msg ; Устанавливаем адрес сообщения об ошибке (err_msg) для sys_write
mov rdx, err_msg_len ; Устанавливаем длину сообщения об ошибке (err_msg_len) для sys_write
syscall
mov rax, SYS_EXIT ; Устанавливаем номер системного вызова для sys_exit
mov rdi, EXIT_FAILURE ; Устанавливаем статус выхода (EXIT_FAILURE) для sys_exit
syscall
section .rodata
err_msg: db "Read Error", 0x0a ; Сообщение об ошибке для системного вызова sys_write
err_msg_len: equ $ - err_msg ; Длина сообщения об ошибке
section .bss
write_buffer_size: equ 21 ; Размер буфера для записи результата
write_buffer: resb write_buffer_size ; Буфер для записи результата
Данную практику писал Чепелин Вячеслав. Скопирован файл Алисы с нашей практики и на нем объяснен код