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

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

В этой статье объясняются некоторые наиболее важные синтаксические и семантические различия двух самых популярных ассемблеров для Linux® - GNU Assembler (GAS) и Netwide Assembler (NASM), а также различия в базовом синтаксисе, переменных и доступе к памяти, обработке макросов, функциях и внешних подпрограммах, работе со стеком и методиках простого повторения блоков кода.

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

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



11.12.2007

Введение

В отличие от других языков для программирования на ассемблере необходимо знание архитектуры процессора машины, для которой выполняется программирование. Программы на ассемблере совершенно непортируемы, часто очень трудоемки в поддержке и понимании и, как правило, содержат большое количество строк кода. Однако наряду с этими ограничениями они имеют преимущество в скорости и размере запускаемого на машине двоичного кода.

Хотя по программированию под 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
СтрокаNASMGAS
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. Программа, которая находит максимальное из трех чисел
СтрокаNASMGAS
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. Программа, считывающая строку и отображающая приветствие пользователя
СтрокаNASMGAS
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. Реализация сортировки массива целых чисел методом выбора
СтрокаNASMGAS
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. Программа, которая считывает перечень аргументов командной строки, записывает их в память и выводит на экран.
СтрокаNASMGAS
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.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=276310
ArticleTitle=Ассемблеры для Linux: Сравнение GAS и NASM
publish-date=12112007