Ассемблеры для Linux: Сравнение GAS и NASM
Параллельное рассмотрение GNU Assembler (GAS) и Netwide Assembler (NASM)
Введение
В отличие от других языков для программирования на ассемблере необходимо знание архитектуры процессора машины, для которой выполняется программирование. Программы на ассемблере совершенно непортируемы, часто очень трудоемки в поддержке и понимании и, как правило, содержат большое количество строк кода. Однако наряду с этими ограничениями они имеют преимущество в скорости и размере запускаемого на машине двоичного кода.
Хотя по программированию под Linux на уровне ассемблера доступно большое количество информации, эта статья подробно показывает разницу между синтаксисами двух вариантов ассемблера в таком виде, чтобы помочь вам проще выполнять преобразование из одного вида ассемблера в другой. Эта статья родилась на основе моих собственных попыток освоить такое преобразование.
В этой статье используется ряд примеров программ. Каждая программа иллюстрирует те или иные особенности и сопровождается обсуждением и сравнением синтаксиса. Несмотря на то, что охватить все различия NASM и GAS невозможно, я попытаюсь раскрыть основные моменты и заложить основу для дальнейшего изучения этого вопроса. Даже если вы знакомы и с NASM, и с GAS, вы можете найти здесь что-нибудь полезное, например, макросы.
В этой статье предполагается, что вы по меньшей мере знакомы с терминологией ассемблера и программировали на ассемблере с использованием синтаксиса Intel®, возможно, с помощью NASM под Linux или Windows. Из этой статьи вы не узнаете, как вводить код в редактор или как транслировать и компоновать код (однако во врезке приведено краткое напоминание). Вы должны быть знакомы с операционной системой Linux (подойдет любой дистрибутив Linux; я использовал Red Hat и Slackware) и основными инструментами GNU, такими как gcc и ld, а также вы должны программировать на машине x86.
Теперь я расскажу о том, что вы сможете найти в этой статье, а чего не сможете.
В этой статье описываются:
- Основные синтаксические различия NASM и GAS
- Общие конструкции на уровне ассемблера, такие как переменные, циклы, метки и макросы
- Немного о вызове внешних подпрограмм на C и использовании функций
- Использование и различия мнемоник ассемблера
- Методы адресации памяти
В этой статье не рассматриваются:
- Набор инструкций процессора
- Различные виды макросов и компоненты, специфичные для того или иного ассемблера
- Директивы ассемблера, характерные только для NASM либо GAS
- Возможности, не являющиеся широко употребительными, или имеющиеся в одном ассемблере и отсутствующие в другом
Дополнительную информацию можно найти в официальных руководствах к ассемблерам (ссылки можно найти в разделе Ресурсы), так как они являются наиболее полными источниками информации.
Базовая структура
В листинге 1 показана очень простая программа, которая просто завершается с кодом выхода 2. Эта простая программа описывает базовую структуру программ на ассемблерах GAS и NASM.
Строка | NASM | GAS |
---|---|---|
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 | ; Начало текстового сегмента section .text global _start ; Точка входа в программу _start: ; Передача кода системного вызова mov eax, 1 ; Возвращаемое значение mov ebx, 2 ; Вызов ОС int 80h | # Начало текстового сегмента .section .text .globl _start # Точка входа в программу _start: # Передача кода системного вызова movl $1, %eax /* Возвращаемое значение */ movl $2, %ebx # Вызов ОС int $0x80 |
Теперь немного пояснений.
Одним из основных различий между NASM и GAS является синтаксис. В GAS используется относительно старый синтаксис AT&T, характерный для GAS и некоторых старых ассемблеров, тогда как NASM использует синтаксис Intel, поддерживаемый большинством ассемблеров, в том числе TASM и MASM. (Современные версии GAS поддерживают директиву .intel_syntax
, которая позволяет использовать синтаксис Intel в GAS.)
Ниже приведены некоторые из наиболее значимых отличий, изложенные в сжатом виде на основании руководства GAS:
- В синтаксисе AT&T и Intel используется различный порядок операндов источника и получателя. Например:
- Intel:
mov eax, 4
- AT&T:
movl $4, %eax
- Intel:
- В синтаксисе AT&T перед непосредственными операндами указывается
$
; в синтаксисе Intel такого обозначения непосредственных операндов нет. Например:- Intel:
push 4
- AT&T:
pushl $4
- Intel:
- В синтаксисе AT&T перед операндами регистров указывается
%
; в синтаксисе Intel такого обозначения нет. - В синтаксисе AT&T величина операндов памяти определяется по последнему символу названия кода операции. Суффикс кода операции
b
,w
илиl
обозначает ссылку на ячейку памяти величиной в байт (8 бит), слово (16 бит) или двойное слово (32 бита) соответственно. В синтаксисе Intel размер указывается префиксами операндов памяти (а не самих кодов операции):byte ptr
,word ptr
иdword ptr
. Таким образом:- Intel:
mov al, byte ptr foo
- AT&T:
movb foo, %al
- Intel:
- В синтаксисе AT&T дальние переходы и вызовы в непосредственной форме выглядят как
lcall/ljmp $section, $offset
; в синтаксисе Intel этоcall/jmp far section:offset
. Инструкция дальнего возврата в синтаксисе AT&T выглядит какlret $stack-adjust
, тогда как в Intel используетсяret far stack-adjust
.
В обоих ассемблерах названия регистров одинаковы, но различается синтаксис их использования, равно как и синтаксис режимов адресации. Кроме того, директивы ассемблера GAS начинаются с ".", а в NASM это не так.
Секция .text
– это место, где начинается выполнение кода процессором. Для того чтобы символ был виден компоновщику и доступен для других компонуемых модулей, используется ключевое слово global
(в GAS также .globl
и .global
) . На стороне NASM листинга 1 global _start
отмечает символ _start
как видимый идентификатор, поэтому компоновщик знает, где находится вход в программу и начинается исполнение. Так же, как и NASM, GAS ищет метку _start
как точку входа в программу по умолчанию. И в GAS, и в NASM метки всегда заканчиваются двоеточием.
Прерывания - это способ сообщить ОС о том, что требуются ее услуги. Эту работу в нашей программе выполняет инструкция int
в строке 16. Обозначения прерываний в GAS и в NASM одинаковы. Для обозначения шестнадцатеричного числа в GAS используется префикс 0x
, а в NASM - суффикс h
. Поскольку в GAS у непосредственных операндов указывается префикс $
, для вызова Linux и запроса сервиса используется шестнадцатеричное число 80 - $0x80
.
int $0x80
(или 80h
в NASM). Код сервиса записывается в регистр EAX. Для запроса выхода из программы в регистр EAX записывается значение 1 (для системного вызова Linux "выход"). В регистр EBX записывается код выхода (в нашем случае, 2), число, которое будет возвращаться в ОС. (Вы можете отследить это число, введя в командной строке echo $?
.)
И, наконец, немного о комментариях. GAS поддерживает комментарии в стиле C (/* */
), в стиле C++ (//
) и в стиле shell (#
). NASM поддерживает комментарии в одну строку, начинающиеся с символа ";".
Переменные и доступ к памяти
Этот раздел начнем с примера программы, которая находит максимальное из трех чисел.
Строка | NASM | GAS |
---|---|---|
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 | ; Начало сегмента данных section .data var1 dd 40 var2 dd 20 var3 dd 30 section .text global _start _start: ; Перемещение содержимого переменных mov ecx, [var1] cmp ecx, [var2] jg check_third_var mov ecx, [var2] check_third_var: cmp ecx, [var3] jg _exit mov ecx, [var3] _exit: mov eax, 1 mov ebx, ecx int 80h | // Начало сегмента данных .section .data var1: .int 40 var2: .int 20 var3: .int 30 .section .text .globl _start _start: # Перемещение содержимого переменных movl (var1), %ecx cmpl (var2), %ecx jg check_third_var movl (var2), %ecx check_third_var: cmpl (var3), %ecx jg _exit movl (var3), %ecx _exit: movl $1, %eax movl %ecx, %ebx int $0x80 |
Вы можете увидеть несколько различий в приведенных выше объявлениях переменных памяти. В NASM для объявления 32-, 16- и 8-разрядных чисел используются директивы dd
, dw
и db
соответственно, тогда как в GAS для той же цели используются .long
,
.int
и .byte
. В GAS также используются другие директивы, такие как .ascii
,
.asciz
и .string
. В GAS переменные объявляются так же, как и любые другие метки (с помощью двоеточия), а в NASM просто вводится название переменной (без двоеточия) перед директивой выделения памяти (dd
, dw
и т.д.), после чего указывается значение переменной.
В строке 18 листинга 2 показан пример режима косвенной адресации памяти. Для разыменования значения по указанному адресу в памяти, в NASM используются квадратные скобки: [var1]
. Для разыменования значения в GAS используются круглые скобки: (var1)
. Использование других режимов адресации будет рассмотрено ниже в этой статье.
Использование макросов
В листинге 3 показана модель для этого раздела; на вход она принимает имя пользователя и возвращает приветствие.
Строка | NASM | GAS |
---|---|---|
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 | section .data prompt_str db 'Enter your name: ' ; $ - счетчик адресов STR_SIZE equ $ - prompt_str greet_str db 'Hello ' GSTR_SIZE equ $ - greet_str section .bss ; Резервирование 32 байт памяти buff resb 32 ; Макрос с двумя параметрами ; Реализует системный вызов для записи %macro write 2 mov eax, 4 mov ebx, 1 mov ecx, %1 mov edx, %2 int 80h %endmacro ; Реализует системный вызов для чтения %macro read 2 mov eax, 3 mov ebx, 0 mov ecx, %1 mov edx, %2 int 80h %endmacro section .text global _start _start: write prompt_str, STR_SIZE read buff, 32 ; Чтение возвращает длину в eax push eax ; Вывод приветсвия write greet_str, GSTR_SIZE pop edx ; edx = длина, возвращенная чтением write buff, edx _exit: mov eax, 1 mov ebx, 0 int 80h | .section .data prompt_str: .ascii "Enter Your Name: " pstr_end: .set STR_SIZE, pstr_end - prompt_str greet_str: .ascii "Hello " gstr_end: .set GSTR_SIZE, gstr_end - greet_str .section .bss // Резервирование 32 байт памяти .lcomm buff, 32 // Макрос с двумя параметрами // Реализует системный вызов для записи .macro write str, str_size movl $4, %eax movl $1, %ebx movl \str, %ecx movl \str_size, %edx int $0x80 .endm // Реализует системный вызов для чтения .macro read buff, buff_size movl $3, %eax movl $0, %ebx movl \buff, %ecx movl \buff_size, %edx int $0x80 .endm .section .text .globl _start _start: write $prompt_str, $STR_SIZE read $buff, $32 // Чтение возвращает длину в eax pushl %eax // Вывод приветсвия write $greet_str, $GSTR_SIZE popl %edx // edx = длина, возвращенная чтением write $buff, %edx _exit: movl $1, %eax movl $0, %ebx int $0x80 |
Заголовок этого раздела предполагает обсуждение макросов, и оба ассемблера, и NASM, и GAS поддерживают их. Однако прежде чем переходить к макросам, будет полезно сравнить несколько других особенностей.
В листинге 3 показана модель неинициализированной памяти, определенной с помощью директивы раздела .bss
(строка 14). BSS означает "block storage segment" ("блочный сегмент хранения", изначально означал "block started by symbol" - "блок, начатый с символа"), и при запуске программы память, зарезервированная под раздел BSS, инициализируется нулями. У объектов в разделе BSS есть только название и размер, но нет значения. Переменные, объявленные в разделе BSS, фактически не занимают пространства, в отличие от сегментов данных.
Для выделения пространства байтов, слов или двойных слов в разделе BSS в NASM используются ключевые слова resb
, resw
и resd
. С другой стороны, в GAS для определения байтового пространства используется ключевое слово .lcomm
. Обратите внимание на способ объявления названия переменной в обеих версиях программы. В NASM перед названием переменной указывается ключевое слово resb
(или resw
, или resd
), после чего указывается величина резервируемого пространства, тогда как в GAS название переменной следует за ключевым словом .lcomm
, после чего через точку с запятой указывается величина резервируемого пространства. Разница показана ниже:
NASM: varname resb size
GAS: .lcomm varname, size
В листинге 2 также введено понятие счетчика адресов (строка 6).
В NASM для управления счетчиком адресов предоставляется специальная переменная (переменные $
и $$
). Методов управления счетчиком адресов в GAS нет, поэтому для расчета следующей ячейки хранения (данных, инструкций и т.п.) следует использовать метки.
Например, для расчета длины строки в NASM нужно использовать следующее выражение:
prompt_str db 'Enter your name: '
STR_SIZE equ $ - prompt_str ; $ - счетчик адресов
$
возвращает текущее значение счетчика адресов, и вычитание значения метки (все названия переменных являются метками) из этого счетчика дает количество байт между объявлением метки и текущей ячейкой. Для определения значения переменной STR_SIZE для следующего выражения используется директива equ
. Такое же выражение в GAS будет выглядеть следующим образом:
prompt_str:
.ascii "Enter Your Name: "
pstr_end:
.set STR_SIZE, pstr_end - prompt_str
Конечная метка (pstr_end
) задает адрес следующей ячейки, и вычитание из него адреса начальной метки дает размер. Также стоит обратить внимание, что при использовании .set
для инициализации значения переменной STR_SIZE выражением после названия переменной указывается запятая. Также может быть использована соответствующая директива .equ
. В NASM нет директивы, аналогичной set
в GAS.
Как я упоминал выше, в листинге 3 используются макросы (строка 21). В NASM и GAS существует несколько различных технологий макросов, в том числе макросы одной строкой и переопределение макросов, но здесь я рассматриваю только простейший тип. В основном макросы в ассемблере используются для ясности кода. Вместо того чтобы вводить один и тот же кусок кода раз за разом, вы можете создать макрос и использовать его, что исключает такое повторение и, убирая нагромождения, улучшает внешний вид и удобство чтения кода.
Пользователи NASM могут быть знакомы с объявлением макросов с помощью директивы %beginmacro
и завершением их с помощью директивы %endmacro
. После директивы %beginmacro
указывается название макроса. После названия вводится число, обозначающее ожидаемое количество аргументов макроса. Аргументы макроса в NASM нумеруются последовательно, начиная с 1. Таким образом, первым аргументом макроса будет %1, вторым -- %2, третьим -- %3 и так далее. Например:
%beginmacro macroname 2
mov eax, %1
mov ebx, %2
%endmacro
Этот код создает макрос с двумя аргументами, первым из которых является %1
, а вторым %2
.
Таким образом, вызов определенного выше макроса будет выполняться следующим образом:
macroname 5, 6
Макрос также может создаваться и без аргументов, в этом случае никаких чисел не указывается.
Теперь давайте рассмотрим, как макросы используются в GAS. Для определения макросов в GAS имеются директивы .macro
и .endm
. После директивы .macro
указывается название макроса, у которого могут быть аргументы, а могут и не быть. Аргументы макроса в GAS обозначаются именами. Например:
.macro macroname arg1, arg2
movl \arg1, %eax
movl \arg2, %ebx
.endm
При использовании названия аргумента в коде макроса перед этим названием ставится обратная косая черта. Если этого не сделать, компоновщик будет трактовать названия как метки, а не как аргументы, вследствие чего будет возникать ошибка.
Функции, внешние подпрограммы и стек
Пример программы для этого раздела выполняет сортировку массива целых чисел методом выбора.
Строка | NASM | GAS |
---|---|---|
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | section .data array db 89, 10, 67, 1, 4, 27, 12, 34, 86, 3 ARRAY_SIZE equ $ - array array_fmt db " %d", 0 usort_str db "unsorted array:", 0 sort_str db "sorted array:", 0 newline db 10, 0 section .text extern puts global _start _start: push usort_str call puts add esp, 4 push ARRAY_SIZE push array push array_fmt call print_array10 add esp, 12 push ARRAY_SIZE push array call sort_routine20 ; Корректировка указателя стека add esp, 8 push sort_str call puts add esp, 4 push ARRAY_SIZE push array push array_fmt call print_array10 add esp, 12 jmp _exit extern printf print_array10: push ebp mov ebp, esp sub esp, 4 mov edx, [ebp + 8] mov ebx, [ebp + 12] mov ecx, [ebp + 16] mov esi, 0 push_loop: mov [ebp - 4], ecx mov edx, [ebp + 8] xor eax, eax mov al, byte [ebx + esi] push eax push edx call printf add esp, 8 mov ecx, [ebp - 4] inc esi loop push_loop push newline call printf add esp, 4 mov esp, ebp pop ebp ret sort_routine20: push ebp mov ebp, esp ; Выделение в стеке пространства под слово sub esp, 4 ; Получение адреса массива mov ebx, [ebp + 8] ; Запись размера массива mov ecx, [ebp + 12] dec ecx ; Подготовка внешнего цикла xor esi, esi outer_loop: ; Сохраняем индекс минимального элемента mov [ebp - 4], esi mov edi, esi inc edi inner_loop: cmp edi, ARRAY_SIZE jge swap_vars xor al, al mov edx, [ebp - 4] mov al, byte [ebx + edx] cmp byte [ebx + edi], al jge check_next mov [ebp - 4], edi check_next: inc edi jmp inner_loop swap_vars: mov edi, [ebp - 4] mov dl, byte [ebx + edi] mov al, byte [ebx + esi] mov byte [ebx + esi], dl mov byte [ebx + edi], al inc esi loop outer_loop mov esp, ebp pop ebp ret _exit: mov eax, 1 mov ebx, 0 int 80h | .section .data array: .byte 89, 10, 67, 1, 4, 27, 12, 34, 86, 3 array_end: .equ ARRAY_SIZE, array_end - array array_fmt: .asciz " %d" usort_str: .asciz "unsorted array:" sort_str: .asciz "sorted array:" newline: .asciz "\n" .section .text .globl _start _start: pushl $usort_str call puts addl $4, %esp pushl $ARRAY_SIZE pushl $array pushl $array_fmt call print_array10 addl $12, %esp pushl $ARRAY_SIZE pushl $array call sort_routine20 # Корректировка указателя стека addl $8, %esp pushl $sort_str call puts addl $4, %esp pushl $ARRAY_SIZE pushl $array pushl $array_fmt call print_array10 addl $12, %esp jmp _exit print_array10: pushl %ebp movl %esp, %ebp subl $4, %esp movl 8(%ebp), %edx movl 12(%ebp), %ebx movl 16(%ebp), %ecx movl $0, %esi push_loop: movl %ecx, -4(%ebp) movl 8(%ebp), %edx xorl %eax, %eax movb (%ebx, %esi, 1), %al pushl %eax pushl %edx call printf addl $8, %esp movl -4(%ebp), %ecx incl %esi loop push_loop pushl $newline call printf addl $4, %esp movl %ebp, %esp popl %ebp ret sort_routine20: pushl %ebp movl %esp, %ebp # Выделение в стеке пространства под слово subl $4, %esp # Получение адреса массива movl 8(%ebp), %ebx # Запись размера массива movl 12(%ebp), %ecx decl %ecx # Подготовка внешнего цикла xorl %esi, %esi outer_loop: # Сохраняем индекс минимального элемента movl %esi, -4(%ebp) movl %esi, %edi incl %edi inner_loop: cmpl $ARRAY_SIZE, %edi jge swap_vars xorb %al, %al movl -4(%ebp), %edx movb (%ebx, %edx, 1), %al cmpb %al, (%ebx, %edi, 1) jge check_next movl %edi, -4(%ebp) check_next: incl %edi jmp inner_loop swap_vars: movl -4(%ebp), %edi movb (%ebx, %edi, 1), %dl movb (%ebx, %esi, 1), %al movb %dl, (%ebx, %esi, 1) movb %al, (%ebx, %edi, 1) incl %esi loop outer_loop movl %ebp, %esp popl %ebp ret _exit: movl $1, %eax movl 0, %ebx int $0x80 |
На первый взгляд листинг 4 может показаться огромным, но на самом деле он очень прост. Листинг представляет понятие функций, различных схем адресации памяти, стека и работы с функциями внешних библиотек. Программа сортирует массив из 10 чисел и с помощью функций puts
и printf
из внешней библиотеки C выводит содержимое несортированного и отсортированного массива. Для обеспечения модульности и представления понятия функций подпрограмма сортировки реализована отдельной процедурой, равно как и подпрограмма печати массива. Давайте разбираться с ними по очереди.
Выполнение программы начинается с вызова puts
(строка 31) после объявления данных. Функция puts
выводит строку на консоль. Её единственным аргументом является адрес отображаемой строки, который передается путем добавления адреса строки в стек (строка 30).
Любая метка в NASM, которая не является частью программы и должна быть разрешена во время компоновки, должна быть определена заранее, для чего используется ключевое слово extern
(строка 24). В GAS такого требования нет. После этого адрес строки usort_str
отправляется в стек (строка 30). Переменные памяти в NASM, например, usort_str
, представляют адрес ячейки памяти, и, таким образом, выполнение команды push usort_str
фактически помещает адрес в вершину стека. С другой стороны, в GAS перед переменной usort_str
должен указываться префикс $
, после чего она трактуется как непосредственный адрес. Если префикс $
не указывается, то в стек вместо адреса помещаются фактические байты, представленные переменной памяти.
Поскольку помещение переменной в стек фактически смещает указатель стека на двойное слово, указатель стека корректируется путем добавления к нему числа 4 (величина двойного слова) (строка 32)
Теперь в стек помещены три аргумента и вызывается функция print_array10
(строка 37). Функции в NASM и GAS определяются одинаково. По существу, они являются обычными метками, которые вызываются с помощью инструкции call
.
После вызова функции в регистре ESP содержится начало стека. Значение esp + 4
представляет адрес возврата, а значениеesp + 8
- первый аргумент функции. Доступ ко всем последующим аргументам выполняется путем добавления размера переменной двойного слова к указателю стека (то есть esp + 12
,
esp + 16
и так далее).
Внутри функции путем копирования esp
в ebp
(строка 62) создается локальный кадр стека. Вы также можете выделить пространство для локальных переменных, как это сделано в программе (строка 63). Это выполняется путем вычитания количества необходимых байт из esp
. Значение esp – 4
представляет пространство из четырех байт, выделенное для локальной переменной, и такое выделение может продолжаться до тех пор, пока в стеке достаточно пространства для локальных переменных.
В листинге 4 показан базовый режим косвенной адресации (строка 64), названный так потому, что вы начинаете с базового адреса и добавляете к нему смещение для указания нужного адреса. На стороне листинга NASM примером такой адресации является [ebp + 8]
, а также [ebp – 4]
(строка 71). Адресация в GAS чуть более лаконична: 4(%ebp)
и -4(%ebp)
, соответственно.
В подпрограмме print_array10
вы можете увидеть еще один режим адресации, используемый после метки push_loop
(строка 74). Эта строка представлена в NASM и GAS следующим образом:
NASM: mov al, byte [ebx + esi]
GAS: movb (%ebx, %esi, 1), %al
Такой режим адресации называется базово-индексной адресацией. В нем используются три сущности: первая -- это базовый адрес, вторая -- индексный регистр, а третья -- множитель. Поскольку определить количество байт, вызываемых из определенного места памяти, не представляется возможным, необходим способ определения объема адресуемой памяти. NASM использует байтовый оператор для того, чтобы сообщить ассемблеру о том, что этот байт данных должен быть перемещен. В GAS та же проблема решается с помощью множителя и использования в мнемонических командах суффикса b
, w
или l
(например, movb
). Синтаксис GAS на первый взгляд может показаться несколько сложным.
Общая форма базово-индексной адресации в GAS выглядит следующим образом:
%сегмент:АДРЕС (, индекс, множитель)
или
%сегмент:(смещение, индекс, множитель)
или
%сегмент:АДРЕС(база, индекс, множитель)
Конечный адрес рассчитывается по следующей формуле:
АДРЕС или смещение + база + индекс * множитель.
Таким образом, для того, чтобы обратиться к байту, используется множитель 1, для слова 2 и для двойного слова 4. Конечно же, синтаксис, используемый в NASM, проще. Таким образом, изложенная выше формула в NASM будет иметь следующий вид:
Сегмент:[АДРЕС или смещение + база + индекс * множитель]
Для доступа к 1, 2 или 4 байтам памяти перед адресом памяти указывается префикс byte
, word
или dword
соответственно.
Прочее
Программа, приведенная в листинге 5, считывает перечень аргументов командной строки, записывает их в память и выводит на экран.
Строка | NASM | GAS |
---|---|---|
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 | section .data ; Таблица команд для хранения до ; 10 аргументов командной строки cmd_tbl: %rep 10 dd 0 %endrep section .text global _start _start: ; Создание кадра стека mov ebp, esp ; На вершине стека содержится количество ; аргументов командной строки. ; По умолчанию значение 1 mov ecx, [ebp] ; Выход, если аргументов больше 10 cmp ecx, 10 jg _exit mov esi, 1 mov edi, 0 ; Сохранение аргументов командной ; строки в таблице команд store_loop: mov eax, [ebp + esi * 4] mov [cmd_tbl + edi * 4], eax inc esi inc edi loop store_loop mov ecx, edi mov esi, 0 extern puts print_loop: ; Выделение локального пространства sub esp, 4 ; функция, повреждающая ecx mov [ebp - 4], ecx mov eax, [cmd_tbl + esi * 4] push eax call puts add esp, 4 mov ecx, [ebp - 4] inc esi loop print_loop jmp _exit _exit: mov eax, 1 mov ebx, 0 int 80h | .section .data // Таблица команд для хранения до // 10 аргументов командной строки cmd_tbl: .rept 10 .long 0 .endr .section .text .globl _start _start: // Создание кадра стека movl %esp, %ebp // На вершине стека содержится количество // аргументов командной строки. // По умолчанию значение 1 movl (%ebp), %ecx // Выход, если аргументов больше 10 cmpl $10, %ecx jg _exit movl $1, %esi movl $0, %edi // Сохранение аргументов командной // строки в таблице команд store_loop: movl (%ebp, %esi, 4), %eax movl %eax, cmd_tbl( , %edi, 4) incl %esi incl %edi loop store_loop movl %edi, %ecx movl $0, %esi print_loop: // Выделение локального пространства subl $4, %esp // функция, повреждающая ecx movl %ecx, -4(%ebp) movl cmd_tbl( , %esi, 4), %eax pushl %eax call puts addl $4, %esp movl -4(%ebp), %ecx incl %esi loop print_loop jmp _exit _exit: movl $1, %eax movl $0, %ebx int $0x80 |
В листинге 5 показана конструкция, повторяющая инструкции ассемблера. Достаточно естественным образом она называется конструкцией повторения. Конструкция повторения в GAS начинается с директивы .rept
(строка 6). Эта директива закрывается с помощью директивы .endr
(строка 8). После .rept
в GAS указывается число раз, которое должна быть повторена конструкция, заключенная в .rept/.endr
. Любые инструкции, размещенные в этой конструкции, эквивалентны написанию этих инструкций count
раз в отдельных строках.
Например, если количество (count) равно 3:
.rept 3
movl $2, %eax
.endr
Это соответствует:
movl $2, %eax
movl $2, %eax
movl $2, %eax
В NASM похожая инструкция используется на этапе предварительной обработки. Она начинается с директивы %rep
и заканчивается директивой %endrep
. После директивы %rep
указывается выражение (в отличие от GAS, где за директивой .rept
следует количество):
%rep <expression>
nop
%endrep
В NASM также есть альтернативный вариант - директива times
. Так же, как и %rep
, она работает на уровне ассемблера и после нее также указывается выражение. Например, приведенная выше конструкция %rep
соответствует следующему:
times <expression> nop
А эта:
%rep 3
mov eax, 2
%endrep
эквивалентна следующему:
times 3 mov eax, 2
и обе они эквивалентны:
mov eax, 2
mov eax, 2
mov eax, 2
Для создания в памяти области данных величиной 10 двойных слов в листинге 5 используется директива .rept
(или %rep
). После этого из стека поочередно извлекаются аргументы командной строки и сохраняются в области памяти, пока таблица команд не заполнится.
Доступ к аргументам командной строки в обоих ассемблерах осуществляется одинаковым образом. В ESP или на вершине стека хранится количество аргументов командной строки, переданных в программу, по умолчанию указывается 1 (если аргументов командной строки нет). esp + 4
содержит первый аргумент командной строки, которым всегда является название программы, вызванной в командной строке. esp + 8
,
esp + 12
и далее содержат остальные аргументы командной строки.
Кроме того, обратите внимание, как выполняется доступ к таблице команд в обеих сторонах листинга 5. Здесь для доступа к таблице команд используется режим косвенной адресации памяти (строка 33), вместе со смещением ESI (и EDI) и множителем. Таким образом, [cmd_tbl + esi * 4]
в NASM эквивалентно cmd_tbl(, %esi, 4)
в GAS.
Заключение
Даже несмотря на то, что различия между двумя этими ассемблерами значительны, преобразовать код из одной формы в другую не так сложно. Сначала вам может показаться, что синтаксис AT&T очень сложен в понимании, но после освоения он будет так же прост, как и синтаксис Intel.
Ресурсы для скачивания
Похожие темы
- Оригинал статьи Linux assemblers: A comparison of GAS and NASM (EN).
- Чтобы досконально познакомиться с этими ассемблерами, обратитесь к руководствам по NASM и GAS(EN):
- GAS: GNU Assembler
- NASM: Netwide Assembler
- Прочтите описание сортировки методом выбора в Википедии.(EN)
- В разделе Linux сайта developerWorks можно найти дополнительные ресурсы для разработчиков Linux, а также самые популярные среди наших читателей статьи и руководства(EN).
- Посмотрите все советы по Linux (EN) и руководства Linux на сайте developerWorks.
- Используйте в своем следующем проекте разработки для Linux ознакомительные версии программного обеспечения IBM, которые можно скачать непосредственно с developerWorks.