Особенности низкоуровневого программирования в 64-битовых Unix-подобных операционных системах

ИННОВАЦИИ В ОБРАЗОВАНИИ И НАУКЕ

УДК 004

В.Ю. Пирогов, г. Шадринск

Особенности низкоуровневого программирования в 64-битовых Unix-подобных операционных системах

Массовый переход к 64-битовым операционным системам требует изменений и в технологии программирования. Статья посвящена особенностям программной модели 64-битовых Unix-подобных операционных систем. В качестве инструмента программирования взят кроссплатформенный ассемблер Fasm. Автор останавливается на особенностях 64-битового программирования с использованием данного ассемблера и особенностях его установки в разных системах Unix. Во второй части статьи рассматривается вопрос о соглашении вызова процедур в 64-битовых операционных системах Unix. Рассматривается стандартная структура стека в 64-битовых операционных системах Unix. Проводится сравнение с аналогичной структурой в 64-битовых системах Windows.

Программирование, ассемблер, Fasm, 64-битовые системы, анализ кода.

V. U. Pirogov, Shadrinsk

Features of low-level programming in 64-bit Unix-like operating

systems

Mass migration to 64-bit operating systems requires changes in the programming technology. The paper deals with features of the programing model of 64-bit Unix-like operating systems. As a programming tool, the cross-platform assembler Fasmis taken.The author dwells on the peculiarities of 64-bit programming using the assembler and the peculiarities of its installing this assembler in different Unixsystems. The second part of the paper is devoted to the issue of calling conventions in 64-bit Unix operating systems.A comparison is made with the similar structure in 64-bit Windows systems.

Key words: programming, assembler, Fasm, 64-bit systems, code analysis.

Особенности 64-битового программирования в системах Unix мы рассмотрим на примере кроссплатформенного ассемблера FASM. Последние версии ассемблера можно найти на сайте http://flatassembler.net.

Развертывание FASM в Linux

Развертывание пакета FASM в Linux также просто, как в Windows, так как на сайте Fasm имеется отдельный архив, созданный именно для Linux, который содержит исполняемый модуль FASM. Достаточно развернуть архив, в какой-нибудь папке и далее вы можете использовать транслятор без какой-либо дополнительной настройки. Данный алгоритм работает и для 32-битовой системы Linux и для 64-битовой системы. Хотя сам исполняемый модуль FASM, имеет 32-битовый формат, это, разумеется, не мешает ему создавать 64-битовые приложения.

Развертывание FASM в системе FreeBSD

Развертывание ассемблера FASM в других (не Linux) Unix-системах может столкнуться с некоторыми трудностями. С сайта http://flatassembler.net можно скачать пакет, предназначенный для других Unix-систем. В частности, рассмотрим, как данный пакет может быть установлен в операционной системеFreeBDS. В составе скаченного пакета имеется объектный модуль FASM.o, который с помощью команды gccFASM.o -oFASM следует превратить в исполняемый модуль, т.е. транслятор FASM для конкретной системы. Однако для

FreeBSD это не единственный способ установки пакета FASM. Там имеется также и порт, с помощью которого можно автоматически установить FASM в системе. Порт располагается в каталоге /usr/ports/lang/FASM. Достаточно войти туда и выполнить команду makeinstall и пакет будет установлен. Однако, и тот и другой способы хороши, только для FreeBSD32 версии 8.x и старше и для FreeBSD62 версии 9.x и старше. Для того, чтобы установить FASM на 64-битную FreeBSD версии 8.x необходимо сделать следующее.

1. Создать каталог /usr/lib32 если у вас в системе его нет.

2. Скопировать туда архив библиотек, который располагается по адресу ftp://ftp.freebsd.Org/pub/FreeBSD/releases/amd64/8.1-RELEASE/lib32/

3. Войти в каталог /usr/lib32 и развернуть архив командой shinstall.sh.

4.После этого войти в каталог /usr/ports/lang/FASM и выполнить командуmakeinstall.

После этого вы получите возможность транслировать программы, написанные на FASM.

Имеется также возможность инсталлировать FASM с помощью команды pkg_add -K -rfFASM.

Пример программы на языке FASM

BUnix-системах широко используются библиотеки языка C, которые играют роль, похожую на роль API-функций в операционной системе Windows, обеспечивая совместимость приложений на уровне текста программ. Можно написать программу в одной Unix-системе, а далее перенести ее в другую систему, перекомпилировав ее там. Использование системных вызовов напротив может привести к несовместимости программ, и на уровне двоичного кода, и на уровне текста программы. Предпочтительнее поэтому писать программы с использованием интерфейса библиотек языка C (подробности программирования в Unix можно найти в книгах[1,2,3]).

Обратимся теперь к программе из листинга 1. В программе используются две библиотечных функции языка C: exit - окончание работы программы, printf -форматный вывод текстовой информации на стандартное устройство вывода, т.е. консоль.

Листинг 1. Простая программа. Unix64.

;тип исполняемого файла

formatELF64

;сегмент кода

section '.text' executable

public main

;внешняяфункция

extrnprintf

extrn exit

main:

;вызовбиблиотечнойфункции C

movrdi, sa

callprintf

;выход

mov rdi,0

call exit

;сегментданных section '.data' writeable ftdb "Privet!",0xA,0

Комментарий к программе из листинга 1.

s В FASM предусмотрено два способа трансляции программ в Unix-системах, которые обращаются к библиотечным функциям языка C. Первый способ, представленный в пакете FASM, предусматривает применение макросов interpreter, needed, import. Мы не будем использовать этот метод. Более интересным, на наш взгляд подходом, является подход, базирующийся на использовании транслятора gcc ([5]) в качестве редактора связей. Запись formatELF64 (ELF -ExecutableandLinkableFormat (формат исполняемых и компонуемых модулей). Фор-мат исполняемых модулей, широко используемый в Unix-системах.) означает, что транслятор FASM должен создавать объектный модуль. На основе этого объектного модуля с помощью транслятора gcc можно создать (скомпоновать) исполняемый модуль. Для того, чтобы получить исполняемый модуль в Unix-системе следует таким образом выполнить следующие две строки. /home/rootl/FASM/FASM prog.asm prog.o gcc -oprogprog.o

Транслятор gcc преобразует объектный модуль в исполняемый модуль, автоматически связывая вызовы библиотечных функций с библиотеками языка C (GCC - GNU CompilerCollection, т.е. gcc это целый набор различных компиляторов, для разных языков программирования. В данном случае gcc автоматически вызывает компоновщик, для создания исполняемого модуля путем связывания объектного модуля с библиотеками C).

s Программа, как и в случае с операционной системойWindows, состоит из сегментов (секций), назначение которых легко понять: сегмент для хранения кода, сегмент для хранения данных. Обратим внимание на директиву publicmain, которая объявляет, что метка main должна быть видна для других модулей. Метка с таким именем по умолчанию является точкой входа в программу для компоновщика gcc. Еще одной важной директивой является объявление внешних процедур (имен), например, так extrnprintf. Данная директива также предназначена для компоновщика, который должен в последствие связать имя printf с адресом вызова этой функции в библиотеке C.

s Обратим внимание, как отправляются параметры в функцию printf: первый параметр - регистр rdi, второй параметр - регистр rsi. Далее мы подробно поговорим о передаче параметров в процедуры. Вызов процедур в 64-битовых Unix-системах

В 64-битовых Unix-системах,как и в Windows произошел переход к соглашению о передаче параметров посредством регистров (см. [4]). Для передачи параметров, представляющих целые числа (в том числе и ссылки) используются последовательно шесть регистров: rdi, rsi, rdx, rcx, r8,r9. Если количество параметров превышает шесть, то оставшиеся параметры передаются через стек. При этом, следует помнить о выравнивании стека по границе кратной 16 байтов. Кроме этого, для передачи параметров типа double предлагается использовать восемь регистров xmm0-xmm7. Причем механизм использования этих регистров в корне отличается от аналогичного механизма в 64-битовой системе Windows — регистры xmm не замещают регистры общего назначения, а дополняют их. Таким образом, в системах Unix-64 для передачи параметров одновременно может быть использовано до 14 регистров(см. [6]).

Рисунок 1. Структура стека при вызове функции в Unix64

На рисунке 1 представлена схема структуры стека при вызове функций C в Unix64. Основным отличием от подобной же структуры стека в 64-битовых операционных системах Windows является то, что в случае необходимости (только если необходимо) хранения параметров в стеке (параметров, передаваемых через регистры), стек резервируется непосредственно в вызванной функции (это называется красная зона - redzone). Красная зона располагается в младших по отношению к указателю стека адресах. Размер красной зоны составляет 128 байтов. Смысл этой области заключается в том, что она не должна затираться какими-либо прерываниями, обрабатываемыми в момент выполнения кода процедуры. Но поскольку при вызове следующей процедуры эта область фактически будет затерта, использовать ее есть смысл только в последней процедуре из цепочки вызываемых. Заметим, что в Windowsдля параметров, передаваемых через регистры, стек резервируется всегда перед вызовом функции. При этом в обеих системах:

1. Стек резервируемый до вызова функции, освобождается после того, как управление передается в вызывающий модуль.

2. Стек, резервируемый в вызывающей функции, освобождается в ней же.

Переменные в программировании на ассемблере

Переменные в программировании играют роль временной памяти для хранения данных, необходимых для проведения тех или иных действий, заложенных в программе, хранения промежуточных данных. После окончания работы программы содержимое переменных не сохраняется. Поэтому для долговременного хранения данных используется такой объект как файл. Для временного хранения данных при программировании на языке ассемблера могут быть использованы следующие области памяти:

s Сегмент (или сегменты) данных. В языке ассемблера FASM для этой цели используется директива section. Области для таких данных резервируются при помощи этой директивы, и существуют с начала запуска программы (создания процесса) и до ее окончания. Например, если мы указали qdq 0, то это значит, что область размером 8 байтов будет существовать все время работы процесса. Но

можно сказать и по-другому, мы используем переменную, время жизни которой совпадает с временем жизни всего процесса. При этом область видимости и область действия переменной охватывают весь исполняемый процесс (переменная в программе характеризуется тремя параметрами: время жизни, область видимости и область действия. Область видимости и область действия переменная не все-гда совпадают, как это может показаться на первый взгляд. Например, (язык C++), ес-ли локальная переменная имеет тоже имя, что и глобальная переменная, то она тем са-мым сужает область видимости глобальной переменной. При этом доступ к глобальной переменной все же остается, но посредством другого имени.) Такие переменные обычно называют статическими.

s Динамическое выделение памяти. Существуют механизмы, позволяющие выделять память динамически. Например, в языке C, существует стандартная библиотечная функция malloc, позволяющая в любом месте программы выделить память для хранения данных. Особенность такого рода памяти заключается в том, что она может не только выделяться, но и освобождаться в процессе выполнения программы. Такие переменные можно назвать динамическими. Мы будем говорить о динамическом выделении памяти в главе 3 нашей книги.

s Использование области стека. Область стека очень удобная для хранения данных, используемых в конкретной функции. Для этого достаточно выделить стек инструкцией subrsp,N, где N - количество выделенных байтов. При выходе из функции, стек должен быть восстановлен и, соответственно, хранимые в нем данные будут удалены. В дальнейшем мы обратимся к такому виду переменных, особенно удобных при программировании рекурсивных алгоритмов.

s Хранение данных в регистрах. Хотя архитектура x86-64 и радует нас значительным увеличением количества рабочих регистров, их все равно мало. Поэтому регистры используют для хранения промежуточных данных. При этом следует учитывать, какие регистры сохраняются при вызове внешних процедур (библиотечных или API-функций). Хранение данных в регистрах может значительно ускорить работу программы. Собственно, этот один из способов оптимизации. Однако, надо иметь в виду, что если фрагмент программы достаточно длинен, то использование регистров для хранения данных может явиться дополнительных фактором, способствующим усложняющим понимание алгоритма программы и, в конечном итоге, приводящим к возникновению ошибок. Тут важно соблюсти определенный баланс между хранением данных в регистре и в памяти.

ЛИТЕРАТУРА

1. Рочкинд, Марк. Дж. Программирование для Unix [Текст] / Марк. Дж. Рочкинд. - СПб. : БХВ, 2005.

2. Стивенс, Уильям. UNIX. Взаимодействие процессов [Текст] / Уильям Стивенс. - СПб. : Питер, 2003.

3. Элджер, Джефф. C++ : библиотека программиста [Текст] / Джефф Элджер. - СПб. : Питер,

2000.

4. Fog, Agner. Calling conventions for différent C++ compilers and operating systems [Text] / Agner Fog: Technical University of Denmark, 2014. - 57 p.

5. GCC online documentation [Electronic resource]. - Access mode: http://gcc.gnu.org/onlinedocs. -13.03.2016.

6. Пирогов, В.Ю. Операционные системы на базе набора команд x86-64 в контексте низкоуровневого программирования [Текст] /В.Ю. Пирогов // Прикладная информатика. - № 6 (60). - 2015.