Общий формат инструкций на примере инструкции MOV:
31 | 30 | 29 | 28-24 | 23-22 | 21 | 20-16 | 15-10 | 9-5 | 4-0 |
Bits | Opcode | Condition Code | Opcode | Shift | 0 | Rm | imm6 | Rn | Rd |
Каждая бинарная инструкция ARM имеет длину в 32 бита и содержит ряд полей:
Bits
: разрядность. Если равно 0, то регистры рассматриваются как 32-битные регистры. Если равно 1, то регистры рассматриваются как 64-битные регистры.
Причем все регистры в одной инструкции должны быть либо 32-битными, либо 64-битными.
Opcode
: код выполняемой операции (например, ADD
(сложение) или MUL
(умножение)).
Condition Code
: бит, который указывает, должна ли инструкция обновить флаг условия (1 - флаг условия обновляется, 0 - не обновляется)
Shift
: два бита, которые определяют применяемые операции сдвига.
Rm, Rn
: регистры, которые хранят операнды операции. Поскольку мы имеем 32 регистра (31 регистр общего пользования X0-X30 и
регистр указателя стека SP / нулевой регистр XZR), то для указания регистра требуется 5 бит.
Rd
: регистр для помещения результата операции.
Imm6
: непосредственный операнд операции, которые обычно представляют некоторые константные значения и для которых не нужна загрузка в регистры
В зависимости от конкретной инструкции и используемых ею значений конкретное содержимое будет меняться.
Возьмем простейшую программу из первой главы, которая использовала прерывание Linux для вывода строки на консоль:
.global _start // устанавливаем стартовый адрес программы _start: mov X0, #1 // 1 = StdOut - поток вывода ldr X1, =hello // строка для вывода на экран mov X2, #19 // длина строки mov X8, #64 // устанавливаем функцию Linux svc 0 // вызываем функцию Linux для вывода строки mov X0, #0 // Устанавливаем 0 как код возврата mov X8, #93 // код 93 представляет завершение программы svc 0 // вызываем функцию Linux для выхода из программы .data hello: .ascii "Hello METANIT.COM!\n" // данные для вывода
Допустим, код этой программы находится в файле hello.s. Перейдем к папке файла и скопилируем программу в файл hello.o:
aarch64-none-elf-as hello.s -o hello.o
Теперь разберем скопилированный объектный файл hello.o. Для этих целей в папке с компилятором мы можем найти утилиту objdump. В завимости от типа пакета компилятора
конкретное название утилиты будет отличаться. Оно формируется по типу [название_пакета]-objdump
Например, для пакета типа baremetal, который не привяан к Linux, она называется aarch64-none-elf-objdump.exe. Выполним
команду:
aarch64-none-elf-objdump -s -d hello.o
И в результате консоль выведет что-то наподобие:
c:\arm>aarch64-none-elf-objdump -s -d hello.o hello.o: file format elf64-littleaarch64 Contents of section .text: 0000 200080d2 e1000058 620280d2 080880d2 ......Xb....... 0010 010000d4 000080d2 a80b80d2 010000d4 ................ 0020 00000000 00000000 ........ Contents of section .data: 0000 48656c6c 6f204d45 54414e49 542e434f Hello METANIT.CO 0010 4d210a M!. Disassembly of section .text: 0000000000000000 <_start>: 0: d2800020 mov x0, #0x1 // #1 4: 580000e1 ldr x1, 20 <_start+0x20> 8: d2800262 mov x2, #0x13 // #19 c: d2800808 mov x8, #0x40 // #64 10: d4000001 svc #0x0 14: d2800000 mov x0, #0x0 // #0 18: d2800ba8 mov x8, #0x5d // #93 1c: d4000001 svc #0x0 ... c:\arm>
Первая часть вывода отображает двоичные данные файла в шестнадцатеричной форме, в том числе его инструкции, а также строку из секции .data
. Затем идет дизассемблированный код секции
.text
. Например, в первой строке двоичных данных
0000 200080d2 e1000058 620280d2 080880d2 ......Xb.......
Инструкция 200080d2
будет представлять инструкцию mov, которая скомпилирована в код 0xd2800020
(обратите внимание,
что во втором случае биты расположены в обратном порядке - 200080d2
- 0xd2800020
все потому что arm использует порядок little-endian)
Переложим ее в двоичный код, чтобы понять ее содержимое:
Шестнадцатеричный код | d | 2 | 8 | 0 | 0 | 0 | 2 | 0 |
Двоичный код | 1101 | 0010 | 1000 | 0000 | 0000 | 0000 | 0010 | 0000 |
То есть в итоге мы получаем инструкцию в бинарной форме 11010010100000000000000000100000
, или, если разбить по частям, 1_1_0_100101_00_0000000000000001_00000
Самый первый бит равен 1, что значит, что данная инструкция использует 64-битные регистры (X0, а не W0)
Второй бит в комбинации с битами с 4-ого по 9-й образуют код операции mov - 1_100101
.
Третий бит равен 0, что значит, что инструкция не устанавливает никаких флагов, которые могли бы повлиять на ветвление программы
Следующие два бита 00
указывают, что никаких операций сдвига не выполняется
Следующие 16 бит представляют непосредственный операнд для команды mov
- 0000000000000001
- число 1
И последние 5 бит представляют регистр для загрузки. Значение 00000
или грубо говоря 0 представляет регистр X0
Ряд использумых инструкций фактически является псевдонимами и сокращениями от других инструкций. Иногда возникает необходимость видеть код без псевдонимов. Например, в коде выше инструкция
mov фактически представляет псевдоним. Чтобы выполнить дизассемблирование без псевдонимов, добавим флаг no-aliases
aarch64-none-elf-objdump -s -d -M no-aliases hello.o
И в результате консоль выведет что-то наподобие:
c:\arm>aarch64-none-elf-objdump -s -d -M no-aliases hello.o hello.o: file format elf64-littleaarch64 Contents of section .text: 0000 200080d2 e1000058 620280d2 080880d2 ......Xb....... 0010 010000d4 000080d2 a80b80d2 010000d4 ................ 0020 00000000 00000000 ........ Contents of section .data: 0000 48656c6c 6f204d45 54414e49 542e434f Hello METANIT.CO 0010 4d210a M!. Disassembly of section .text: 0000000000000000 <_start>: 0: d2800020 movz x0, #0x1 4: 580000e1 ldr x1, 20 <_start+0x20> 8: d2800262 movz x2, #0x13 c: d2800808 movz x8, #0x40 10: d4000001 svc #0x0 14: d2800000 movz x0, #0x0 18: d2800ba8 movz x8, #0x5d 1c: d4000001 svc #0x0 ... c:\arm>