Условные переходы по меткам

ОС - Ubuntu 18.04.3 LTS
синтаксис языка - AT&T
компилятор - GNU ассемблер, версия 2.30 (x86_64-linux-gnu)
компоновщик - GNU ld 2.30

# НАЗНАЧЕНИЕ: Эта программа находит максимальное число в наборе чисел.

.section .data

data_items:
    .long 3,67,34,222,45,75,54,34,44,37,0

.section .text
.globl _start

_start:
    movl $0, %edi
    movl data_items(,%edi,4), %eax
    movl %eax, %ebx

start_loop:
    cmpl $0, %eax
    je loop_exit

    incl %edi
    movl data_items(,%edi,4), %eax
    cmpl %ebx, %eax
    jle start_loop

    movl %eax, %ebx
    jmp start_loop

loop_exit:
    movl $1, %eax
    int $0x80

Разбираем код

Для начала смотрим область данных

.section .data

data_items:
    .long 3,67,34,222,45,75,54,34,44,37,0

data_items является меткой, которая ссылается на ячейку памяти, которая идёт сразу после неё. Далее идёт директива ассемблера .long. Она указывает ассемблеру зарезервировать память для чисел, которые идут после неё. Метка data_items ссылается на первое число в этом списке. Так как data_items является меткой, то каждый раз, когда нам надо сослаться на этот адрес, то мы можем использовать символ data_items и ассемблер в процессе сборки заменит его на адрес с которого начинаются числа. Например, инструкция movl data_items, %eax поместит в регистр EAX значение 3. Существует несколько различных типов размещения данных в памяти отличных от .long, которые можно зарезервировать. Перечислим основные:

.byte
Байт занимает одну ячейку памяти для каждого числа. Он ограничен числами между 0 и 255.
.int
Целые числа (не путать с инструкцией int) занимают две ячейки памяти для каждого числа. Диапазон значений между 0 и 65535.
.long
Длинное целое число занимает четыре ячейки памяти. Данная программа написана для 32-битных регистров, поэтому здесь использовалось именно это значение. Диапазон значений от 0 до 4294967295.
.ascii
Данная инструкция нужна для внесения символов в память. каждый символ занимает одну ячейку памяти (они преобразовываются к типу .byte). Поэтому, если вы напишете директиву .ascii "Hello, there\0", тогда ассемблер зарезервирует 12 ячеек памяти (байтов). (Примечание: символы UTF-8 тоже корректно выводит.) Первый байт содержит числовой код для "H", второй байт содержит числовой код для "e" и так далее. Последний символ представлен "\0" и он является завершающим символом (он никогда не отображается, а просто говорит другим частям программы что это конец символов). Буквы и цифры которые начинаются с символа обратный слеш "\", представляют символы, которые нельзя ввести с клавиатуры или нельзя легко представить на экране. Например, \n; ссылается на символ "новая строка", который говорит компьютеру начать вывод со следующей строки и \t gпредставляет собой символ "табуляция". Все символы директивы .ascii необходимо заключать в двойные кавычки.

В нашем примере ассемблер резервирует в памяти 11 областей типа .long. Каждую друг за другом. Так как каждый long занимает 4 байта, то это означает что весь список занимает 44 байта. Эти цифры мы будем сравнимать для нахождения максимального значения. data_items используется ассемблером как ссылка на адрес первого значения из списка.

Заметьте, что последним элементом списка является ноль. Решено использовать ноль чтобы сказать программе что она достигла конца списка. Это можно было реализовать другими путями. Например, иметь значение размера списка, которое будет вшито в программу. Также можно было указать длину списка в его первом элементе или в какой-нибудь другой ячейке памяти. Ещё можно было сделать специальный символ, который бы указывал на адрес последнего элемента списка. Неважно как можно было это сделать, но должен был быть метод определения конца списка. Компьютер не знает ничего - он делает только то, что ему сказано делать. Он не остановится пока ему не подадут какой-нибудь сигнал. Иначе он продолжит выполнение и после окончания списка, считывая данные, которые идут после него и даже в ячейках, в которых нет никаких данных.

...

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

.section .text
.globl _start

Начало выполнения кода. Запоминаем индекс (порядковый номер смещения) сегмента памяти, который хранит первый элемент списка числел. Считываем первое число в регистр EAX, а потом копируем его в регистр EBX как изначально самое большое.

_start:
    movl $0, %edi
    movl data_items(,%edi,4), %eax
    movl %eax, %ebx

Входим в цикл. На начало цикла указывает метка start_loop

start_loop:
    cmpl $0, %eax
    je loop_exit

    incl %edi
    movl data_items(,%edi,4), %eax
    cmpl %ebx, %eax
    jle start_loop

    movl %eax, %ebx
    jmp start_loop

Инструкция cmpl сранивает два значения. В данном случае мы сравниваем значение 0 (которое является признаком конца списка) со значением текущего элемента списка, которое мы сохранили в регистр EAX. Эта инструкция сравнения влияет на регистр, который ранее не был упомянут, а именно регистр EFLAGS (%eflags). Он также известен как регистр статусов. Результат сравнения сохраняется в данный регистр.

Следующей строкой идёт инструкция, контролирующая поток выполнения программы, которая предписывает переместиться (перепрыгнуть, j, jump) к ячейке с меткой loop_exit, если сравниваемые значения эквивалентны (e, equal). Отсюда и название условного оператора перехода - je (Jump if Equal). Здесь не используются, но есть и другие операторы условного и безусловного перехода.

je
Перепрыгнуть (Jump), если значения эквивалентны (Equal)
jg
Перепрыгнуть (Jump), если второе значение больше (Greater) первого
jge
Перепрыгнуть (Jump), если второе значение больше (Greater) первого или эквивалентно (Equal) ему
jl
Перепрыгнуть (Jump), если второе значение меньше (Less) первого
jle
Перепрыгнуть (Jump), если второе значение меньше (Less) первого или эквивалентно (Equal) ему
jmp
Перепрыгнуть (JuMP) несмотря ни на что (безусловный переход)

Инкрементируем (incl) индекс, указывающий на начало следующего элемента в списке. Получаем значение этого элемента и копируем его в регистр EAX. Сравниваем значения регистров EAX (текущий элемент) и EBX (текущее максимальное значение). Если значение меньше или эквивалентно (jle) максимальному, то возвращаемся к метке start_loop начала цикла. Если перехода не было, то это означает, что текущий элемент больше максимального и его надо запомнить. Возвращаемся к метке start_loop.

Последняя часть области кода отмечена меткой loop_exit и переход к ней выполняется после достижения последнего элемента списка, который равен 0. Записываем в регистр EAX код системного вызова 1 (exit). Linux завершает программу и, так как обычно в регистр EBX записывается код выхода из программы, то в нашем случае туда попадёт число 222, что можно проверить, выведя на консоль код завершения последнего приложения.

loop_exit:
    movl $1, %eax
    int $0x80

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

host$ vim maximum.s # пишем исходный код в своём редакторе host$ as maximum.s -o maximum.o # собираем объектный код ассемблером host$ ld maximum.o -o maximum # компоновщиком создаём исполняемый файл приложения host$ ./maximum # запускаем приложение host$ echo $? # выводим статус завершения последнего приложения 222 host$

Далее

Вернуться к общему описанию ассемблера

Поддержите проект, если он помог вам

Проект продвигается за счёт личных средств и времени авторского коллектива. Если вы нашли здесь то, что искали, то вы можете выразить свою благодарность финансово. Даже небольшой платёж помогает авторам в их труде, сохраняя их вовлечённость и высокую мотивацию чтобы строить открытый мир равных возможностей для всех неравнодушных людей вокруг.