Страница с сайта Владислава Пирогова Ассемблер и не только.

Ассемблеры для Linux: Сравнение GAS и NASM

Параллельное рассмотрение GNU Assembler (GAS) и Netwide Assembler (NASM)

Рэм Нараян, инженер-программист, IBM

Рэм закончил постдипломный курс в области информатики и работает инженером-программистом в индийской лаборатории программного обеспечения IBM, в подразделении Rational, разрабатывая и добавляя новые возможности в Rational ClearCase. Он имеет опыт работы с различными версиями Linux, UNIX и Windows, а также операционных систем реального времени для мобильных устройств, в том числе Symbian и Windows Mobile. В свободное время он осваивает Linux и читает книги.

Описание: В этой статье объясняются некоторые наиболее важные синтаксические и семантические различия двух самых популярных ассемблеров для 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

В этой статье описываются:

В этой статье не рассматриваются:

Дополнительную информацию можно найти в официальных руководствах к ассемблерам (ссылки можно найти в разделе Ресурсы), так как они являются наиболее полными источниками информации.

Базовая структура

В листинге 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:

В обоих ассемблерах названия регистров одинаковы, но различается синтаксис их использования, равно как и синтаксис режимов адресации. Кроме того, директивы ассемблера 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 и читает книги.