Sasm
Содержание:
Введение
В отличие от других языков для программирования на ассемблере необходимо знание архитектуры процессора машины, для которой выполняется программирование. Программы на ассемблере совершенно непортируемы, часто очень трудоемки в поддержке и понимании и, как правило, содержат большое количество строк кода. Однако наряду с этими ограничениями они имеют преимущество в скорости и размере запускаемого на машине двоичного кода.
Хотя по программированию под Linux на уровне ассемблера доступно большое количество информации, эта статья подробно показывает разницу между синтаксисами двух вариантов ассемблера в таком виде, чтобы помочь вам проще выполнять преобразование из одного вида ассемблера в другой. Эта статья родилась на основе моих собственных попыток освоить такое преобразование.
В этой статье используется ряд примеров программ. Каждая программа иллюстрирует те или иные особенности и сопровождается обсуждением и сравнением синтаксиса. Несмотря на то, что охватить все различия NASM и GAS невозможно, я попытаюсь раскрыть основные моменты и заложить основу для дальнейшего изучения этого вопроса. Даже если вы знакомы и с NASM, и с GAS, вы можете найти здесь что-нибудь полезное, например, макросы.
В этой статье предполагается, что вы по меньшей мере знакомы с терминологией ассемблера и программировали на ассемблере с использованием синтаксиса Intel, возможно, с помощью NASM под Linux или Windows. Из этой статьи вы не узнаете, как вводить код в редактор или как транслировать и компоновать код (однако во врезке приведено ). Вы должны быть знакомы с операционной системой Linux (подойдет любой дистрибутив Linux; я использовал Red Hat и Slackware) и основными инструментами GNU, такими как gcc и ld, а также вы должны программировать на машине x86.
Теперь я расскажу о том, что вы сможете найти в этой статье, а чего не сможете.
Сборка примеров
Трансляция: GAS:
NASM:
Компоновка (общая для обоих типов ассемблера):
Компоновка при использовании внешней библиотеки C:
В этой статье описываются:
- Основные синтаксические различия NASM и GAS
- Общие конструкции на уровне ассемблера, такие как переменные, циклы, метки и макросы
- Немного о вызове внешних подпрограмм на C и использовании функций
- Использование и различия мнемоник ассемблера
- Методы адресации памяти
В этой статье не рассматриваются:
- Набор инструкций процессора
- Различные виды макросов и компоненты, специфичные для того или иного ассемблера
- Директивы ассемблера, характерные только для NASM либо GAS
- Возможности, не являющиеся широко употребительными, или имеющиеся в одном ассемблере и отсутствующие в другом
Дополнительную информацию можно найти в официальных руководствах к ассемблерам (ссылки можно найти в разделе ), так как они являются наиболее полными источниками информации.
Примеры программы Hello, world! под разные ОС
Примеры программы Hello, world!, которая выводит соответствующее сообщение и завершается.
Под операционную систему Linux
SECTION .data
msg db "Hello, world!",0xa
len equ $ - msg
SECTION .text
global _start ; the program entry point
_start:
mov eax, 4 ; 'write' syscall
mov ebx, 1 ; file descr. 1 (stdout)
mov ecx, msg ; pointer to the data
mov edx, len ; amount of data
int 0x80 ; call to the kernel
mov eax, 1 ; '_exit' syscall
mov ebx, ; zero exit code (success)
int 0x80 ; call to the kernel
Под операционную систему Linux (x64)
global _start
section .text
_start:
mov rax, 1 ; system call 1 is write
mov rdi, 1 ; file handle 1 is stdout
mov rsi, message ; address of string to output
mov rdx, 13 ; number of bytes
syscall ; invoke operating system to do the write
mov eax, 60 ; system call 60 is exit
xor rdi, rdi ; exit code 0
syscall ; invoke operating system to exit
message:
db "Hello, World", 10 ; note the newline at the end
Под операционную систему DOS
SECTION .text org 0x100 ; эта директива нужна только в случае .com файла, в котором нет никаких секций mov ah, 0x9 mov dx, hello int 0x21 mov ax, 0x4c00 ; ah == 0x4c al == 0x00 int 0x21 SECTION .data hello DB "Hello, world!",0xd,0xa,'$'
Под операционную систему Windows (obj)
%include 'WIN32N.INC' EXTERN MessageBoxA Import MessageBoxA user32.dll EXTERN ExitProcess Import ExitProcess kernel32.dll SECTION CODE USE32 CLASS=CODE ..start: push UINT MB_OK push LPCTSTR title push LPCTSTR banner push HWND NULL call MessageBoxA push UINT NULL call ExitProcess SECTION DATA USE32 CLASS=DATA banner db 'Hello, world!',0xD,0xA, title db 'Hello',
Под операционную систему Windows x64 (obj)
; Hello.asm
EXTERN MessageBoxW
EXTERN ExitProcess
SECTION .text USE64
start:
sub rsp, 28h ; Microsoft x64 calling convention "shadow space"
xor rcx, rcx ; HWND hWnd = NULL
lea rdx, banner ; LPCTSTR lpText = banner
lea r8, title ; LPCTSTR lpCaption = title
xor r9, r9 ; UINT uType = MB_OK
call MessageBoxW ; MessageBox(hWnd, lpText, lpCaption, uType)
xor rcx, rcx ; UINT uExitCode = 0
call ExitProcess ; ExitProcess(uExitCode)
SECTION .data
banner dw __utf16__('Hello, world!'),
title dw __utf16__('Hello!'),
>nasm -f win64 Hello.asm
>golink Hello.obj kernel32.dll user32.dll
Под операционную систему FreeBSD
SECTION .data
msg db "Hello, world!",0xa
len equ $ - msg
SECTION .text
global _start ; the program entry point
_start:
push dword len
push dword msg
push dword 1 ; 1 is the file descriptor of stdout
mov eax, 4 ; 4 is the 'write' syscall
push eax ; we must leave an extra dword on the stack
int 0x80 ; call to the kernel
add esp, 16 ; clean up the stack
push dword ; 0 is the exit code (success)
mov eax, 1 ; 1 is the '_exit' syscall
push eax ; extra dword on the stack
int 0x80 ; call to the kernel
; no cleanup - we will never return
Под операционную систему KolibriOS
bits 32
%include 'mos.inc'
section .text
MOS_HEADER01 main,image_end,memory_end,stacktop,,
main:
redraw:
call draw_window
wait_event:
MOS_WAITEVENT
dec eax
jz redraw
dec eax
jz key
;button pressed; we have only one button, close
MOS_EXIT
key:
;key pressed, read it and ignore
mov eax, MOS_SC_GETKEY
int 0x40
jmp wait_event
draw_window:
MOS_STARTREDRAW
xor eax, eax
mov ebx, 10*65536 + 150
mov ecx, 40*65536 + 50
mov edx, 0x33FFFFFF
mov edi, header
int 0x40 ;define&draw window
mov eax, MOS_SC_WRITETEXT
mov ebx, 30*65536 + 10
mov ecx, 0x80000000
mov edx, string
int 0x40 ;display string
MOS_ENDREDRAW
ret
section .data
header db 'HelloWorld test',
string db 'Hello, World!',
image_end:
section .bss
alignb 4
stack resb 1024
stacktop:
memory_end:
Examples of programs for various operating systems
This is a «Hello, world!» program for the DOS operating system.
section .text org 0x100 mov ah, 0x9 mov dx, hello int 0x21 mov ax, 0x4c00 int 0x21 section .data hello: db 'Hello, world!', 13, 10, '$'
An equivalent program for Linux:
global _start section .text _start: mov eax, 4 ; write mov ebx, 1 ; stdout mov ecx, msg mov edx, msg.len int 0x80 ; write(stdout, msg, strlen(msg)); xor eax, msg.len ; invert return value from write() xchg eax, ebx ; value for exit() mov eax, 1 ; exit int 0x80 ; exit(...) section .data msg: db "Hello, world!", 10 .len: equ $ - msg
An example of a similar program for Microsoft Windows:
global _main extern _MessageBoxA@16 extern _ExitProcess@4 section code use32 class=code _main: push dword ; UINT uType = MB_OK push dword title ; LPCSTR lpCaption push dword banner ; LPCSTR lpText push dword ; HWND hWnd = NULL call _MessageBoxA@16 push dword ; UINT uExitCode call _ExitProcess@4 section data use32 class=data banner: db 'Hello, world!', title: db 'Hello',
Below is a 64-bit program for Apple OS X that inputs a keystroke and shows it on the screen
global _start section .data query_string: db "Enter a character: " query_string_len: equ $ - query_string out_string: db "You have input: " out_string_len: equ $ - out_string section .bss in_char: resw 4 section .text _start: mov rax, 0x2000004 ; put the write-system-call-code into register rax mov rdi, 1 ; tell kernel to use stdout mov rsi, query_string ; rsi is where the kernel expects to find the address of the message mov rdx, query_string_len ; and rdx is where the kernel expects to find the length of the message syscall ; read in the character mov rax, 0x2000003 ; read system call mov rdi, ; stdin mov rsi, in_char ; address for storage, declared in section .bss mov rdx, 2 ; get 2 bytes from the kernel's buffer (one for the carriage return) syscall ; show user the output mov rax, 0x2000004 ; write system call mov rdi, 1 ; stdout mov rsi, out_string mov rdx, out_string_len syscall mov rax, 0x2000004 ; write system call mov rdi, 1 ; stdout mov rsi, in_char mov rdx, 2 ; the second byte is to apply the carriage return expected in the string syscall ; exit system call mov rax, 0x2000001 ; exit system call xor rdi, rdi syscall
Прочее
Программа, приведенная в листинге 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,
; Выход, если аргументов больше 10
cmp ecx, 10
jg _exit
mov esi, 1
mov edi, 0
; Сохранение аргументов командной
; строки в таблице команд
store_loop:
mov eax,
mov , eax
inc esi
inc edi
loop store_loop
mov ecx, edi
mov esi, 0
extern puts
print_loop:
; Выделение локального пространства
sub esp, 4
; функция, повреждающая ecx
mov , ecx
mov eax,
push eax
call puts
add esp, 4
mov ecx,
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 начинается с директивы (строка 6). Эта директива закрывается с помощью директивы (строка 8). После в GAS указывается число раз, которое должна быть повторена конструкция, заключенная в . Любые инструкции, размещенные в этой конструкции, эквивалентны написанию этих инструкций раз в отдельных строках.
Например, если количество (count) равно 3:
Это соответствует:
В NASM похожая инструкция используется на этапе предварительной обработки. Она начинается с директивы и заканчивается директивой . После директивы указывается выражение (в отличие от GAS, где за директивой следует количество):
В NASM также есть альтернативный вариант — директива . Так же, как и , она работает на уровне ассемблера и после нее также указывается выражение. Например, приведенная выше конструкция соответствует следующему:
А эта:
эквивалентна следующему:
и обе они эквивалентны:
Для создания в памяти области данных величиной 10 двойных слов в листинге 5 используется директива (или ). После этого из стека поочередно извлекаются аргументы командной строки и сохраняются в области памяти, пока таблица команд не заполнится.
Доступ к аргументам командной строки в обоих ассемблерах осуществляется одинаковым образом. В ESP или на вершине стека хранится количество аргументов командной строки, переданных в программу, по умолчанию указывается 1 (если аргументов командной строки нет). содержит первый аргумент командной строки, которым всегда является название программы, вызванной в командной строке. ,
и далее содержат остальные аргументы командной строки.
Кроме того, обратите внимание, как выполняется доступ к таблице команд в обеих сторонах листинга 5. Здесь для доступа к таблице команд используется режим косвенной адресации памяти (строка 33), вместе со смещением ESI (и EDI) и множителем
Таким образом, в NASM эквивалентно в GAS.
Компиляция и компоновка
NASM компилирует программы под различные операционные системы в пределах x86-совместимых процессоров. Находясь в одной операционной системе, можно беспрепятственно откомпилировать исполняемый файл для другой.
Компиляция программ в NASM состоит из двух этапов. Первый — ассемблирование, второй — компоновка. На этапе ассемблирования создаётся объектный код. В нём содержится машинный код программы и данные, в соответствии с исходным кодом, но идентификаторы (переменные, символы) пока не привязаны к адресам памяти. На этапе компоновки из одного или нескольких объектных модулей создаётся исполняемый файл (программа). Операция компоновки связывает идентификаторы, определённые в основной программе, с идентификаторами, определёнными в остальных модулях, после чего всем идентификаторам даются окончательные адреса памяти или обеспечивается их динамическое выделение.
Для компоновки объектных файлов в исполняемые в Windows можно использовать свободный бесплатно распространяемый компоновщик alink(для 64-х битных программ компоновщик GoLink), а в Linux — компоновщик ld, который есть в любой версии этой операционной системы.
Для ассемблирования файла нужно ввести следующую команду:
nasm -f format filename -o output
Инструкции перехода
Компилятор обрабатывает текст программы в несколько проходов, благодаря чему можно инструкции перехода размещать до объявления соответствующих меток.
В командах условного и безусловного () переходов используется по умолчанию ближний тип переходов — . Поэтому при возможности короткого перехода, чтобы не завысить размер программы на лишний байт, необходимо специально указать тип перехода . С версии 0.98.09b были добавлены опции оптимизации -Ox, которые позволяют автоматически оптимизировать размер инструкций перехода, в более ранних версиях или без таких опций минимальный размер программы можно получить только ручной модификацией исходного кода.
История
NASM был создан Саймоном Тэтхемом совместно с Юлианом Холлом и в настоящее время развивается небольшой командой разработчиков на SourceForge.net. Первоначально он был выпущен согласно его собственной лицензии, но позже эта лицензия была заменена на GNU LGPL после множества проблем, вызванных выбором лицензии. Начиная с версии 2.07 лицензия заменена на «упрощённую BSD» (BSD из 2 пунктов).
NASM может работать на платформах, отличных от x86, таких как SPARC и PowerPC, однако код он генерирует только для x86 и x86-64.
NASM успешно конкурирует со стандартным в Linux- и многих других UNIX-системах ассемблером gas. Считается, что качество документации у NASM выше, чем у gas. Кроме того, ассемблер gas по умолчанию использует AT&T-синтаксис, ориентированный на процессоры не от Intel, в то время как NASM использует вариант традиционного для x86-ассемблеров Intel-синтаксиса; Intel-синтаксис используется всеми ассемблерами для под DOS/Windows, например, MASM, TASM, fasm.
Синтаксис языка
В NASM используется Intel-синтаксис записи инструкций. Предложение языка ассемблера NASM (строка программы) может состоять из следующих элементов:Метка, Инструкция, Операнды, Комментарий.
Операнды отделяются между собой запятой. Перед строкой и после инструкции можно использовать любое количество пробельных символов. Комментарий начинается с точки с запятой, а концом комментария считается конец строки. В качестве инструкции может использоваться команда или псевдокоманда (директива компилятора). Если строка очень длинная, то её можно перенести на следующую, используя обратный слеш ( \ ), подобно тому, как это делается в языке Си.
Структура программы на MIPS-ассемблере
директивыметкикомментарииинструкциипсевдоинструкции
Основные директивы
- объявляет символ sym глобальным и позволяет обращатся к нему из других файлов;
- объявляет, что данные, которые хранятся в sym имеют размер size, и делает sym глобальной меткой (см. предыдущую директиву);
- сохраняет строку str в памяти, не добавляя нулевой символ (\0) в конец;
- сохраняет строку str и добавляет в конец нулевой символ (\0);
- последовательно сохраняет в памяти байты b1, b2, …, bn;
- последовательно сохраняет в памяти 16-битные значения h1, h2, …, hn;
- последовательно сохраняет в памяти 32-битные значения w1, w2, …, wn;
- последовательно сохраняет в памяти 64-битные значения dw1, dw2, …, dwn;
- сохраняет в памяти числа с плавающей запятой f1, f2, …, fn;
- сохраняет в памяти числа с плавающей запятой (двойная точность) d1, d2, …, dn;
- выделить n байт в данном сегменте данных;
- выровнять все следующие данные до 2^n байт.
Регистры
- $zero ($0) – регистр, всегда содержащий значение 0 и доступный только для чтения;
- $at ($1) – временный регистр процессора;
- $v0-$v1 ($2-$3) – для результатов, возвращаемых функциями;
- $a0-$a3 ($4-$7) – для аргументов функций;
- $t0-$t9 ($8-$15, $24-$25) – для временных данных, можно использовать как угодно;
- $s0-$s8 ($16-$23, $30) – для постоянных данных, можно использовать как угодно;
- $k0-$k1 ($26-$27) – зарезервировано для ядра операционной системы;
- $gp ($28) – поинтер для глобальных переменных, практически не используется;
- $sp ($29) – поинтер стека, его значение всегда равно верхнему адресу стека;
- $ra ($31) – бог солнца адрес инструкции, из которой была вызвана функция;
- $f0 – для результатов, возвращаемых функцями, с плавающей запятой;
- $f4, $f6, $f8, $f10, $f16, $f18 – для временных данных с плавающей запятой;
- $f12, $f14 – для параметров функций с плавающей запятой
Формат выходных файлов
NASM поддерживает множество форматов выходных файлов, среди них:
- bin — файл произвольного формата, определяемого только исходным кодом. Пригоден как для файлов данных, так и для модулей с исполняемыми кодами — например, системных загрузчиков, образов ПЗУ, модулей операционных систем, драйверов .SYS в MS-DOS или исполняемых файлов .COM.
- obj — объектный модуль в формате OMF, совместимый с MASM и TASM.
- win32 и win64 — объектный модуль для 32- и 64-битного кода, совместимый с Win32- и Win64-компиляторами Microsoft.
- aout — объектный модуль в варианте формата a.out, использовавшемся в ранних Linux-системах.
- aoutb — версия формата a.out для BSD-совместимых операционных систем.
- coff — объектный модуль в формате COFF, совместимом с компоновщиком из DJGPP.
- elf32 и elf64 — объектный модуль в форматах ELF32 и ELF64, используемых в Linux и Unix System V, включая Solaris x86, UnixWare и SCO Unix.
Формат выходного файла можно задать с помощью ключа командной строки -f. Форматы могут расширять синтаксис некоторых инструкций и добавлять собственные инструкции.