Особенности разработки для MacOS ARM64. Загрузка данных

Последнее обновление: 01.08.2023

Одно из главных отличий ассемблера 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 - значение переменной в регистр.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850