Примеры кода на ассемблере

На этой практике мы потрогаем ассемблер.

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".

Теперь давайте говорить про код:

  1. В этом коде мы видим section:

    Исполняемые файлы в нынешнее время большинство имеют отдельное место под код (условно говоря .text), отдельное место для неизменяемой памяти (.rodata) и другие.

  2. Когда наша программа компилируется, она будет запускаться из функции _start.

  3. global _start нужен для того, чтобы линковщик мог к ней обратиться (иначе файл бы не запустился, т.к. не видел бы начало программы).

  4. Мы распихиваем с помощью mov в какие-то регистры какие-то числа, а потом делаем syscall. Что делает syscall? Просит ядро операционной системы вызвать какую-то команду в зависимости от значения на регистре rax. Какую вы можете посмотреть сюда: Linux_System_Call_Table (https://en.wikipedia.org/wiki/System_call#Linux_kernel)

    Когда мы хотим сделать какой-то системный вызов, мы читаем rax и в зависимости от него делаем команду.

  5. Рассмотрим эту часть кода:

    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  ; Буфер для записи результата

Данную практику писал Чепелин Вячеслав. Скопирован файл Алисы с нашей практики и на нем объяснен код