Одно из главных отличий ассемблера MacOS ARM64 от стандартного ассемблера ARM64 от ассемблера GNU GCC заключается в загрузке данных. Рассмотрим детально это отличие.
Прежде всего для загрузки адреса данных в GNU GCC можно применять инструкцию ldr со знаком равно:
ldr x0, =message // загружаем в регистр Х0 адрес объекта message
На MacOS этот подход НЕ работает. Взамен мы можем использовать другие способы в зависимости от того, откуда загружаются данные.
Если данные расположены в разделе кода, то есть в разделе .text (он же раздел .code в GNU GCC), то для загрузки адреса данных можно применяеть инструкцию ADR:
// // METANIT.COM. Программа на ассемблере для Mac OS Silicon, которая // выводит на консоль строку "Hello METANIT.COM!" // .global _start // Устанавливаем точку входа в программу для компоновщика .align 2 // Для MacOS требуется смещение в 2 байта // _start - точка входа в программу _start: mov x0, #1 // значение 1 представляет стандартный поток вывода (консоль) adr x1, message // передаем адрес строки для вывода на консоль mov x2, count // размер строки в байтах mov x16, #4 // номер системного вызова Unix для записи в поток (на консоль) svc #0x80 // вызываем системную функцию с номером 4 // выход из программы mov x16, #1 // системный вызов 1 завершает программу svc #0x80 // вызываем системную функцию с номером 1 message: .ascii "Hello METANIT.COM!\n" .equ count, . - message // длина строки
Здесь значение message
расположено в одном разделе с кодом программы, то есть фактически в разделе .text.
Вроде все просто, но это накладывает ограничения, поскольку раздел кода - раздел только для чтения, и изменить значение message мы не можем. Фактически message выступает здесь как константа. Например, попробуем заменить первый символ строки message на букву "a":
.global _start // Устанавливаем точку входа в программу для компоновщика .align 2 // Для MacOS требуется смещение в 2 байта // _start - точка входа в программу _start: adr x1, message // передаем адрес строки для вывода на консоль mov w0, #'a' strb w0, [x1] // ! Error - пытаемся заменить по адресу [X1] символ на 'a' mov x0, #1 // значение 1 представляет стандартный поток вывода (консоль) mov x2, count // размер строки в байтах mov x16, #4 // номер системного вызова Unix для записи в поток (на консоль) svc #0x80 // вызываем системную функцию с номером 4 // выход из программы mov x16, #1 // системный вызов 1 завершает программу svc #0x80 // вызываем системную функцию с номером 1 message: .ascii "Hello METANIT.COM!\n" .equ count, . - message // длина строки
Несмотря на то, что и компилятор, и компоновщик отработают без ошибок, на этапе выполнения мы столкнемся с ошибкой. И суть этой ошибки будет в том, что message - константа, и ее нельзя изменить, поэтому следующая операция недействительна
strb w0, [x1] // ! Error - пытаемся заменить по адресу [X1] символ на 'a'
Кроме того, загрузка адреса с помощью инструкции adr
имеет и другие ограничения - она ограничена диапазоном ±1Mб относительно текущей инструкции.
Если мы хотим, чтобы статические данные можно было изменить, то их следует размещать в разделе .data. Однако сам процесс загрузки усложняется. Он разбивается на 2 инструкции и получает следующую форму:
adrp x1, message@PAGE add x1, x1, message@PAGEOFF
Вначале для получения адреса используется инструкция ADRP. Эта инструкция загружает выровненный адрес страницы (4 КБ) относительно текущего счетчика программ. Далее инструкция ADD используется для добавления младших 12 бит - смещения message к этому адресу страницы, что дает правильный адрес. Это позволяет нам оперировать диапазоном адресов ±4Гб. Директивы @PAGE и @PAGEOFF, которые добавляются к имени переменной, задают тип перемещения. Так, директива @PAGE указывает компоновщику подставить адрес страницы, на которой расположен символ (message). А директива @PAGEOFF сообщает компоновщику подставить смещение этого символа внутри страницы.
В итоге, таким образом, в регистр X1 будет помещен корректный адрес переменной message. Рассмотрим полный код:
// раздел кода .text .global _start // Устанавливаем точку входа в программу для компоновщика .align 2 // Для MacOS требуется смещение в 2 байта // _start - точка входа в программу _start: adrp x1, message@PAGE // передаем адрес страницы переменной message add x1, x1, message@PAGEOFF // прибавляем смещение message относительно страницы mov w0, #'a' strb w0, [x1] // заменяем по адресу [X1] первый символ на 'a' mov x0, #1 // значение 1 представляет стандартный поток вывода (консоль) mov x2, count // размер строки в байтах mov x16, #4 // номер системного вызова Unix для записи в поток (на консоль) svc #0x80 // вызываем системную функцию с номером 4 // выход из программы mov x16, #1 // системный вызов 1 завершает программу svc #0x80 // вызываем системную функцию с номером 1 // раздел данных .data message: .ascii "Hello METANIT.COM!\n" .equ count, . - message // длина строки
Здесь выводим на консоль строку message. Но перед этим для теста заменяем в ней первый символ на "a". Результат работы программы:
eugene@MacBook-Pro-Eugene arm64 % as -o hello.o hello.s eugene@MacBook-Pro-Eugene arm64 % ld -o hello hello.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start eugene@MacBook-Pro-Eugene arm64 % ./hello aello METANIT.COM! eugene@MacBook-Pro-Eugene arm64 %
В примере выше мы загружали адрес строки. Однако также нередко может возникнуть необходимость загрузить сами данные. Например, в примере выше константу count, которая хранит размер строки, определим как переменную:
// раздел кода .text .global _start // Устанавливаем точку входа в программу для компоновщика .align 2 // Для MacOS требуется смещение в 2 байта // _start - точка входа в программу _start: adrp x1, message@PAGE // передаем адрес страницы переменной message add x1, x1, message@PAGEOFF // прибавляем смещение message относительно страницы mov x0, #1 // значение 1 представляет стандартный поток вывода (консоль) adrp x2, count@PAGE // передаем в Х2 адрес страницы переменной count ldr x2, [x2, count@PAGEOFF] // загружаем само значение по адресу страницы + смещение mov x16, #4 // номер системного вызова Unix для записи в поток (на консоль) svc #0x80 // вызываем системную функцию с номером 4 // выход из программы mov x16, #1 // системный вызов 1 завершает программу svc #0x80 // вызываем системную функцию с номером 1 // раздел данных .data message: .ascii "Hello METANIT.COM!\n" .align 3 // выравнивание в 8 байт count: .quad 19 // длина строки
Итак, для вывода строки на консоль нам надо загрузить длину строки - значение переменной count в регистр X2. По сравнению с ассемблером GNU GCC на MacOS процесс занимает также 2 инструкции:
adrp x2, count@PAGE // передаем в Х2 адрес страницы переменной count ldr x2, [x2, count@PAGEOFF] // загружаем само значение по адресу страницы + смещение
Для большей ясности этот код можно развернуть на три инструкции
adrp x2, count@PAGE // передаем адрес страницы переменной count add x2, x2, count@PAGEOFF // прибавляем смещение count относительно страницы ldr x2, [x2] // загружаем само значение по адресу в Х2
Опять же получаем адрес переменной count с помощью инструкций ADRP+ADD, а затем собственно загружаем данные с помощью ldr
.
Также обратите внимание, на выравнивание переменной count по 8 байтам. Причем если две инструкции с загрузкой count в X2 развернуть в 3, то выравнивание не понадобится.
Поучение адреса и загрузка значения переменной могут сильно нагружать код, особенно если мы работаем с несколькими переменными:
.text .global _start .align 2 _start: // выводим первое сообщение mov x0, #1 adrp x1, message1@PAGE add x1, x1, message1@PAGEOFF adrp x2, count1@PAGE ldr x2, [x2, count1@PAGEOFF] mov x16, #4 svc #0x80 // выводим второе сообщение mov x0, #1 adrp x1, message2@PAGE add x1, x1, message2@PAGEOFF adrp x2, count2@PAGE ldr x2, [x2, count2@PAGEOFF] mov x16, #4 svc #0x80 // выход из программы mov x16, #1 svc #0x80 // раздел данных .data count1: .quad 19 message1: .ascii "Hello METANIT.COM!\n" .align 3 count2: .quad 13 message2: .ascii "Hello ARM64!\n"
В данном случае при выводе только двух сообщений конструкции получения адреса сообщения и загрузки его размера существенно утяжеляют код. И было бы неплохо его упростить. Как обычно, для группировки какой-то определенной программной логики мы можем применять два подхода: функции и макросы. Используем макросы:
.text .global _start .align 2 .macro loadAddr reg, variable adrp \reg, \variable@PAGE add \reg, \reg, \variable@PAGEOFF .endm .macro loadVal reg, variable adrp \reg, \variable@PAGE ldr \reg, [\reg, \variable@PAGEOFF] .endm _start: // выводим первое сообщение mov x0, #1 loadAddr x1, message1 loadVal x2, count1 mov x16, #4 svc #0x80 // выводим второе сообщение mov x0, #1 loadAddr x1, message2 loadVal x2, count2 mov x16, #4 svc #0x80 // выход из программы mov x16, #1 svc #0x80 // раздел данных .data count1: .quad 19 message1: .ascii "Hello METANIT.COM!\n" .align 3 count2: .quad 13 message2: .ascii "Hello ARM64!\n"
В данном случае макрос loadAddr загружает адрес переменной в регистр, а макрос loadVal - значение переменной в регистр.