- •Аппаратно-ориентированное программирование
- •Ббк 32.973.73
- •Удк 681.3 ббк 32.973.73ф 73
- •1. Основы программирования на ассемблере
- •1.1. Принципы построения ассемблерных программ
- •1.2. Понятие архитектуры компьютера
- •1.3. Регистры программиста в ia32
- •1.4. Описание сегментной структуры программы
- •2. Простейшие средства ассемблера
- •2.1. Средства описания данных
- •2.2. Обращения к функциям ос посредством прерываний
- •2.3. Средства преобразования в исполняемый файл
- •2.4. Управление строками при выводе и ввод данных
- •2.5. Простейшие способы адресации
- •3. Архитектурные элементы для построения программ
- •3.1. Организация условных переходов
- •3.2. Средства организации циклов
- •3.3. Особенности команд умножения и деления
- •3.4. Организация процедур
- •3.5. Неарифметические операции над кодами
- •4. Использование неэлементарных способов адресации
- •4.1. Косвенно-регистровая адресация
- •4.2. Использование индексной адресации данных
- •4.3. Базовая и индексно базовая адресации
- •4.4. Адресация с масштабированием
- •5. Взаимосвязи программных единиц
- •5.1. Многомодульная разработка программ
- •5.2. Использование библиотек объектных модулей
- •5.3. Организация стекового кадра подпрограммы
- •5.4. Программный доступ к системным функциям Win32
- •5.5. Особенности использования объектных файлов формата coff
- •5.6. Стандартный доступ к системным функциям Unix
- •6. Вспомогательные средства базовой архитектуры
- •6.1. Использование строковых команд пересылки
- •6.2. Применение строковых команд сравнения
- •7. Использование ассемблерных отладчиков
- •7.1. Особенности отладчика gdb для программ в Linux
- •7.2. Отладчики текстового режима для Windows
- •Библиографический список
- •Оглавление
5. Взаимосвязи программных единиц
5.1. Многомодульная разработка программ
Только самые простые программы изготавливаются из одного файла исходной программы. Более сложные программы в процессе разработки разбиваются на множество исходных модулей, как наиболее обще называют отдельные файлы исходной программы. Такое разбиение помогает справиться с общей сложностью программы и выполнять разработку по частям: вначале написать и отладить только часть программы, затем аналогичное выполнить с другой частью и т.д. Все современные языки предоставляют средства для такого исходного разбиения программы и указания связи получаемых частей. (Заметим, что Паскаль - в отличие от Турбо Паскаля и его модификаций - не позволял разбивать программу на отдельно транслируемые модули.)
Проблема связей программ, которые в совокупности после соединения составляют единую выполняемую программу, особенно четко просматривается именно с позиций программирования на ассемблере и машинных структур объектных файлов. Большинство современных систем программирования работают по принципу компиляция-компоновка-выполнение. Это значит, что непосредственным результатом обработки файла исходной программы служит файл специального служебного формата, называемый объектным модулем (или объектным файлом). В типовых системах разработки программ для операционных систем Windows, OS/2, MS-DOS имена объектных файлов традиционно имеют расширение OBJ, а в Unix-системах - расширение имени, задаваемое единственной буквой o.
Объектный файл представляет собой полузаконченную форму исполняемого файла. Он содержит, в основном, двоичные коды машинных команд и данных, но, кроме того, вспомогательную информацию для связывания множества объектных модулей в единый исполняемый файл. Проблема компоновки объектных модулей становится понятной только при рассмотрении проблем перехода к машинным кодам команд и данных. Сейчас же мы остановимся на внешней стороне этой проблемы, рассматривая, какую дополнительную информацию и как следует задавать для последующей конкретной связи объектных модулей в исполняемую программу.
Из языков высокого уровня, подобных Алголу, читателю должно быть известно, что никакие информационные объекты нельзя использовать без их описания как данных. Такое описание задается специальными конструкциями, которые неявно задают размер этих объектов в памяти компьютера и место, занимаемое ими в этой памяти. На ассемблере подобные описания, как мы уже видели, представляют собой конструкции на основе директив RESx и Dx, где символ x обобщенно обозначает одну из букв B, W D. Именно эти директивы явно задают место в памяти компьютера для данных.
Встает вопрос, что нужно делать, если разработчик хочет использовать в программе одного исходного модуля данные, описанные подобными конструкциями в другом исходном модуле. Нетрудно понять, что описать их тем же самым способом в первом из указанных модулей нельзя - одни и те же данные (по смыслу их использования и, главное, по имени области их размещения) нельзя размещать в разных местах памяти. Нельзя потому, что в машинных командах указываются действия над данными, расположенными по конкретным адресам, и не может быть данных, расположенных по двум или более различным адресам для одной исполняемой программы. Поэтому в исходном модуле программ для данных, определенных в другом месте и приходится записывать буквально (с учетом английского диалекта, на котором записаны ключевые слова программ), что данные с таким-то именем в действительности определены и размещены в другом исходном модуле.
Такая запись на языке ассемблера NASM делается в директиве EXTERN. Последняя имеет вид
EXTERN имя_внешнего_имени
или, в общем случае,
EXTERN перечисление_внешних_имен
где перечисление_внешних_имен представляет собой перечисление через запятую набора различных внешних имен.
В свою очередь те имена, которые предполагается использовать для доступа к данным и подпрограммам из другого исходного модуля, необходимо дополнительно сопровождать соответствующей информацией для системы разработки. Эта информация на ассемблере NASM задается в директиве GLOBAL, с которой читатель уже частично знаком. Именно, все имена, которые будут использоваться как внешние (или которые только могут использоваться как внешние), следует включить в состав перечисления после служебного слова GLOBAL. Имена, перечисленные в одной из директив GLOBAL, называются глобальными. Существенной особенностью таких имен является то, что они попадают в объектный модуль, сохраняя в нем свое обозначение. В пояснение заметим, что все остальные (не глобальные) имена не сохраняются в объектном модуле, а превращаются в нем лишь в числовые смещения от начала соответствующего сегмента. Те имена исходного файла, которые, согласно сделанным пояснениям, программист должен объявить глобальными или внешними, могут быть произвольно сгруппированы в одном, нескольких или во многих директивах (соответственно, GLOBAL или EXTERN).
Рассмотрим для дальнейшего изучения описанных средств демонстрационную программу, которая образуется двумя исходными файлами prim5.asm и prim5a.asm на ассемблере. Текст этих программ приведен в листингах 5.1.1 и 5.1.2.
; Многомодульные программы (использование GLOBAL и EXTERN)
; Главная программа - файл prim5.asm
GLOBAL _start
GLOBAL sla, slb, sum
EXTERN funa
SEGMENT .code
_start: mov DWORD [sla], 27
mov WORD [slb], 19
call funa
mov eax, [sum]
; сюда следует вставить преобразование
; в десятичную систему и вывод результата на экран
mov esi,10 ; base of position digit system
mov ecx, 0 ; reset digit counter
pov: mov edx, 0 ; null into left part of devident
div esi ; divide for next digit = rest
add dl, '0'
push edx
inc ecx ; step into counter
cmp eax, 0
jne pov
mov [cnt], ecx
mov ebx, numtxt
izv: pop edx
mov byte [ebx],dl
inc ebx
loop izv
mov eax,4 ; N function=write
mov ebx,1 ; N handle=1 (stdout)
mov ecx, numtxt ; address of text
mov edx,[cnt] ; number of byte
int 80h
mov eax, 1
int 80h ; function=exit
SEGMENT .data
sla DD 0
slb DW 0
sum DD 0
cnt dd 0
SEGMENT .bss
numtxt times 10 db 0
Листинг 5.1.1. Файла prim5.asm многомодульной программы для Linux
; Многомодульные программы (использование GLOBAL и EXTERN)
; Дополнительный файл Prima.asm:
SEGMENT .data
EXTERN sla, slb, sum
GLOBAL funa
SEGMENT .code ; may be other segment, for example, .codes
; procedure funa
funa:
push eax
push ebx
mov eax,[sla]
mov ebx, 0
mov bx, [slb]
add eax, ebx
mov [sum], eax
pop ebx
pop eax
ret
; end procedure funa
Листинг 5.1.2. Файла prim5a.asm многомодульной программы для Linux
В первом модуле этой программы, названном для определенности prim5.asm, определены именованные области данных sla, slb и sum. Причем в предположении их использования в другом модуле эти имена объявлены директивой GLOBAL как глобальные. В этом же исходном модуле используется имя funa, которому в программе исходного модуля не соответствует никакой метки и вообще никакого конкретного места внутри программы. Если бы в программу исходного модуля не была вставлена директива EXTERN funa, то при ее трансляции появилась бы ошибка, что имя funa не определено. Последняя же директива поясняет компилятору, что программист совсем не ошибся, а место в программе, обозначаемое меткой funa, будет находиться где-то в другом исходном модуле, и окончательное согласование ее использования выполняет уже компоновщик.
Во втором модуле программы, названном для определенности prim5a.asm, имя funa определено как метка (начинающая по существу процедуру). Чтобы сделать это имя доступным для использования компоновщику, в этом втором модуле имя funa указано в директиве GLOBAL. Если такое указание не сделано в этом модуле, то при его компиляции ошибка не появится - для глобального имени все равно, будет оно использоваться извне или нет. Но при выполнении компоновки обоих получающихся объектных модулей появится сообщение, что в программе модуля prim5.o сделана ссылка на несуществующее внешнее имя.
Кроме того, в исходном модуле prim5a.asm используются имена областей данных sla, slb и sum, которым в этом модуле не отведено никакое место в памяти. В то же время этот модуль содержит директиву EXTERN, в которой указанные имена перечисляются, обозначаясь как внешние. Если директиву EXTERN опустить, ошибка появится уже при компиляции исходного модуля prim5a.asm, так как компилятор имеет все основания полагать, что программист забыл о каком-либо определении или объявлении упоминаемых имен.
Заметим, что в языках высокого уровня делается терминологическое отличие определения имен и объявления имен. Более ясно эта проблема смотрится на ассемблере. При определении имени ему оно соотносится с конкретным местом в памяти. Программист может явно (буквально, «пальцем») показать, какие данные или команды идут перед местом, обозначенным этим именем, а какие - после него (в действительности имени соотносится совершенно конкретное смещение в сегменте данных или команд, соответственно). При объявлении имени (директивой EXTERN в NASM) лишь объявляется, что где-то в другом участке результирующей программы будет определено место, которое будет обозначаться этим именем. Таким образом, объявление имени содержит скорее намерение в дальнейшем определить имя, чем его действительное определение (другое дело, что модуль, в котором это имя определено, может быть изготовлен по времени гораздо раньше - компилятор не знает и не может знать об этом).
Существенной деталью использования директивы GLOBAL является необходимость ее указания до места использования перечисленных в ней имен (иначе возникают ошибки на стадии компоновки). (Возможно, эта особенность будет устранена в следующих версиях NASM, так как рациональной причины оставлять эту особенность не видно.) Для ассемблеров MASM и TASM не следует размещать директиву EXTRN в сегменте, отличном от того, в котором действительно будут определены перечисленные в ней имена. В старых версиях операционных систем такое требование было связано с устремлением обеспечить, по возможности, максимальную степень контроля над именами в ходе разработки. (Сейчас при одном сегменте изменяемых данных и одном сегменте машинных команд большого смысла в подобном требовании нет.)
Если порознь выполнить компиляцию исходных модулей, приведенных в листингах 5.1.1 и 5.1.2, используя рекомендованные выше соглашения, то получатся объектные модули prim5.o и prim5a.o. Для связывания (компоновки) их в единую выполняемую программу следует использовать вызов компоновщика в виде команды
ld -o prim5.exe prim5.o prim5a.o
Автоматизировать процесс разработки данной программы можно с помощью командного файла, текст которого приведен в листинге 5.1.3.
nasm -f elf $1.asm -l $1.lst
nasm -f elf $2.asm -l $2.lst
ld -o $1.exe $1.o $2.o
Листинг 5.1.3. Командный файла nasml2 для изготовления программы
из двух исходных моделей в Linux
Применение такого командного файла особенно удобно при многократных ошибках в составлении программы и необходимости многократного запуска системы разработки. Если данный командный файл назван nasml2, то его использование для данной задачи заключается в вызове с помощью командной строки
nasml2 prim5 prim5a
При выполнении командного файла первый и второй аргумент этого вызова используются вместо формальных аргументов $1 и $2 командного файла. Этот же командный файл можно использовать для разработки любой программы, состоящей из двух исходных модулей.
В ассемблерах MASM и TASM вместо директивы EXTERN используется директива EXTRN. Кроме отсутствия в ее названии одной буквы (для сокращения) в директиве EXTRN необходимо для каждого имени указывать его атрибут. Эти атрибуты задаются служебными словами BYTE, WORD, DWORD, NEAR, FAR и должны следовать за именем через разделяющее двоеточие. Для аналога приведенного примера в указанных ассемблерах следует записать
EXTRN funa: NEAR
EXTRN sla: DWORD , slb:BYTE, sum: DWORD
Таким образом, в указанных ассемблерах внутри директивы EXTRN задаются не только внешние имена, но и характеристики размера памяти, связанной с каждым из этих имен.
Вместо директивы GLOBAL в указанных ассемблерах имеется соответствующая директива PUBLIC, применение которой ничем кроме ключевого слова не отличается от уже рассмотренной.