 | Рэм Нараян, инженер-программист, IBM
11.12.2007 В этой статье объясняются некоторые наиболее важные синтаксические и семантические различия двух самых популярных ассемблеров для Linux® - GNU Assembler (GAS) и Netwide Assembler (NASM), а также различия в базовом синтаксисе, переменных и доступе к памяти, обработке макросов, функциях и внешних подпрограммах, работе со стеком и методиках простого повторения блоков кода.
Введение
В отличие от других языков для программирования на ассемблере необходимо знание архитектуры процессора машины, для которой выполняется программирование. Программы на ассемблере совершенно непортируемы, часто очень трудоемки в поддержке и понимании и, как правило, содержат большое количество строк кода. Однако наряду с этими ограничениями они имеют преимущество в скорости и размере запускаемого на машине двоичного кода.
Хотя по программированию под Linux на уровне ассемблера доступно большое количество информации, эта статья подробно показывает разницу между синтаксисами двух вариантов ассемблера в таком виде, чтобы помочь вам проще выполнять преобразование из одного вида ассемблера в другой. Эта статья родилась на основе моих собственных попыток освоить такое преобразование.
В этой статье используется ряд примеров программ. Каждая программа иллюстрирует те или иные особенности и сопровождается обсуждением и сравнением синтаксиса. Несмотря на то, что охватить все различия NASM и GAS невозможно, я попытаюсь раскрыть основные моменты и заложить основу для дальнейшего изучения этого вопроса. Даже если вы знакомы и с NASM, и с GAS, вы можете найти здесь что-нибудь полезное, например, макросы.
В этой статье предполагается, что вы по меньшей мере знакомы с терминологией ассемблера и программировали на ассемблере с использованием синтаксиса Intel®, возможно, с помощью NASM под Linux или Windows. Из этой статьи вы не узнаете, как вводить код в редактор или как транслировать и компоновать код (однако во врезке приведено краткое напоминание). Вы должны быть знакомы с операционной системой Linux (подойдет любой дистрибутив Linux; я использовал Red Hat и Slackware) и основными инструментами GNU, такими как gcc и ld, а также вы должны программировать на машине x86.
Теперь я расскажу о том, что вы сможете найти в этой статье, а чего не сможете.
 |
Сборка примеров
Трансляция:
GAS:
as –o program.o program.s
NASM:
nasm –f elf –o program.o program.asm
Компоновка (общая для обоих типов ассемблера):
ld –o program program.o
Компоновка при использовании внешней библиотеки C:
ld –-dynamic-linker /lib/ld-linux.so.2 –lc –o program program.o
|
|
В этой статье описываются:
- Основные синтаксические различия NASM и GAS
- Общие конструкции на уровне ассемблера, такие как переменные, циклы, метки и макросы
- Немного о вызове внешних подпрограмм на C и использовании функций
- Использование и различия мнемоник ассемблера
- Методы адресации памяти
В этой статье не рассматриваются:
- Набор инструкций процессора
- Различные виды макросов и компоненты, специфичные для того или иного ассемблера
- Директивы ассемблера, характерные только для NASM либо GAS
- Возможности, не являющиеся широко употребительными, или имеющиеся в одном ассемблере и отсутствующие в другом
Дополнительную информацию можно найти в официальных руководствах к ассемблерам (ссылки можно найти в разделе Ресурсы), так как они являются наиболее полными источниками информации.
Базовая структура
В листинге 1 показана очень простая программа, которая просто завершается с кодом выхода 2. Эта простая программа описывает базовую структуру программ на ассемблерах GAS и NASM.
Листинг 1. Программа, которая выходит с кодом 2
| Строка | 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
- В синтаксисе AT&T перед непосредственными операндами указывается
$; в синтаксисе Intel такого обозначения непосредственных операндов нет. Например:
- Intel:
push 4
- AT&T:
pushl $4
- В синтаксисе 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
- В синтаксисе 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 поддерживает комментарии в одну строку, начинающиеся с символа ";".
Переменные и доступ к памяти
Этот раздел начнем с примера программы, которая находит максимальное из трех чисел.
Листинг 2. Программа, которая находит максимальное из трех чисел
| Строка | 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 показана модель для этого раздела; на вход она принимает имя пользователя и возвращает приветствие.
Листинг 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
При использовании названия аргумента в коде макроса перед этим названием ставится обратная косая черта. Если этого не сделать, компоновщик будет трактовать названия как метки, а не как аргументы, вследствие чего будет возникать ошибка.
Функции, внешние подпрограммы и стек
Пример программы для этого раздела выполняет сортировку массива целых чисел методом выбора.
Листинг 4. Реализация сортировки массива целых чисел методом выбора
| Строка | 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, считывает перечень аргументов командной строки, записывает их в память и выводит на экран.
Листинг 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.
Ресурсы Научиться
Получить продукты и технологии
Обсудить
Об авторе  | |  | Рэм закончил постдипломный курс в области информатики и работает инженером-программистом в индийской лаборатории программного обеспечения IBM, в подразделении Rational, разрабатывая и добавляя новые возможности в Rational ClearCase. Он имеет опыт работы с различными версиями Linux, UNIX и Windows, а также операционных систем реального времени для мобильных устройств, в том числе Symbian и Windows Mobile. В свободное время он осваивает Linux и читает книги. |
Выскажите мнение об этой странице
|  |