- •ВВЕДЕНИЕ
- •1. ОСНОВЫ ПРОГРАММИРОВАНИЯ НА АССЕМБЛЕРЕ
- •1.1. Принципы построения ассемблерных программ
- •1.2. Понятие архитектуры компьютера
- •1.3. Регистры программиста в IA32
- •1.4. Описание сегментной структуры программы
- •2. ПРОСТЕЙШИЕ СРЕДСТВА АССЕМБЛЕРА
- •2.1. Средства описания данных
- •2.4. Управление строками при выводе и вводе данных
- •2.3. Средства преобразования в исполняемый файл
- •2.5. Простейшие способы адресации
- •3. АРХИТЕКТУРНЫЕ ЭЛЕМЕНТЫ ДЛЯ ПОСТРОЕНИЯ ПРОГРАММ
- •3.1. Организация условных переходов
- •3.2. Средства организации циклов
- •3.3. Особенности команд умножения и деления
- •3.4. Организация процедур
- •3.5. Неарифметические операции над кодами
- •3.6. Архитектура AMD64 процессоров в ассемблерных Linux программах
- •4. ИСПОЛЬЗОВАНИЕ НЕЭЛЕМЕНТАРНЫХ СПОСОБОВ АДРЕСАЦИИ
- •4.1. Косвенно-регистровая адресация и ее использование
- •4.2. Использование индексной адресации данных
- •4.3. Базовая и индексно-базовая адресации
- •5. ВЗАИМОДЕЙСТВИЕ ПРОГРАММНЫХ КОМПОНЕНТОВ
- •5.1. Многомодульная разработка программ
- •5.2. Организация стекового кадра подпрограммы
- •5.3. Программный доступ к системным функциям Win32
- •5.4. Использование свободно распространяемых утилит для Win32
- •5.5. Вызов функций из стандартных библиотек Linux
- •6. БИБЛИОТЕКИ ОБЪЕКТНЫХ МОДУЛЕЙ
- •6.1. Использование библиотек объектных модулей в Linux
- •6.2. Использование библиотек объектных модулей в Win32
- •7. РАЗДЕЛЯЕМЫЕ БИБЛИОТЕКИ ВЫПОЛНЯЕМЫХ ПРОГРАММ
- •7.1. Понятие о статической и динамической компоновке
- •7.2. Конструкция библиотеки динамической компоновки
- •7.3. Компоновка времени загрузки с использованием GoLink
- •КОНТРОЛЬНЫЕ ВОПРОСЫ
- •ЗАКЛЮЧЕНИЕ
- •БИБЛИОГРАФИЧЕСКИЙ СПИСОК
7.РАЗДЕЛЯЕМЫЕ БИБЛИОТЕКИ ВЫПОЛНЯЕМЫХ ПРОГРАММ
7.1.Понятие о статической и динамической компоновке
Одним из важнейших компонентов многозадачной операционной системы являются разделяемые библиотеки выполняемых программ, которые в ряде ОС, включая Windows, называются библиотеками динамической компоновки (DynamicLinkingLibrary). Чтобы освоиться с ними, следует предварительно рассмотреть, как выполняется окончательное построение двоичных кодов машинных команд. В процессе автоматического выполнения приложения (программных кодов с их данными и вспомогательной информацией) аппаратура компьютера имеет дело практически только с машинными кодами – двоичными кодами, которые являются для нее командами процессора, командами отдельных контроллеров и внутренними представлениями данных. Имен, привычных для человека,– последовательностей символов латинского или русского алфавита – аппаратура использовать непосредственно не может. Вместо них аппаратура использует адреса и номера позиций в различных таблицах (начало таких таблиц для аппаратуры указывается опять же адресами).
В то же время современное программирование применяет для обозначения частей обрабатываемой информации и частей программ почти исключительно имена, ориентированные на человека. Основную работу по переходу от таких имен к внутренним обозначениям адресов и номеров берет на себя компилятор. Компилятор преобразовывает программу с языка программирования в промежуточную форму объектных файлов. Начинающий или не задумывавшийся ранее программист может спросить, почему бы не заставить компилятор преобразовывать исходную программу с языка программирования сразу в машинные коды, которые могут после этого выполняться аппаратурой непосредственно без каких-либо дополнительных преобразований.
Причин две. Во-первых, машинный код исполняемой программы хранится (при выключенном компьютере) во внешней памяти, а при необходимости выполнения переносится в оперативную память с непосредственно адресуемыми ячейками. При этом в другой раз или на другом компьютере этот исполняемый машинный код может оказаться в оперативной памяти начиная с другого начального адреса. Нет никакой возможности договориться, условиться о всегда постоянном месте начала
124
программы в оперативной памяти, так как в другой раз или на другой машине перед ней могут оказаться другие программы. Современные ОС многопрограммные и стараются эффективно использовать память. Отказ от фиксации размещения программы в оперативной памяти – на момент начала ее выполнения – требует настройки части адресной информации в ней при перенесении кодов программы из внешней памяти в оперативную. Такую работу обычно выполняет специальная программа – системный загрузчик, которая обязательно входит в состав операционной системы, но, как правило, не заметна пользователям.
Второй причиной недостаточности одной компиляции для получения правильно работающего машинного кода является то, что скольконибудь сложная программа реально собирается не только из тех явных указаний, которые сделаны в основной части исходной программы. Кроме последних частей, она собирается и из заранее подготовленных заготовок, созданных разработчиками операционной системы и разработчиками системы программирования. Эти заготовки реализуют программы вводавывода, типовые преобразования информации, например преобразования данных из одного типа в другой, и ряд более сложных действий. Трудоемкость изготовления таких заготовок колоссальна, их создавали большие коллективы профессиональных программистов, и повторять их работу не практично, а по времени и невозможно. Все эти заготовки собраны в специальные наборы, называемые библиотеками. Библиотеки содержат внутренние каталоги, которые облегчают использование их компонентов для других программ. Исполняемый файл программы формируется из одного или нескольких объектных файлов (модулей), разработанных текущим программистом, и множества заготовок из библиотек. (Заметим, что в последних операционных системах исполняемый файл имеет расширение EXE.) Это формирование выполняет компоновщик.
Рассмотрим работу компоновщика более детально. Если программистом в программе на языке ассемблера написано
CALL FUNA
и где-то для той же программы записано
EXTERN FUNA
но функция FUNA – как последовательность реализующих ее действий – описана в другом модуле (другой единице компиляции), то в этом исходном тексте нет достаточной информации о начальном адресе машинных
125
кодов этой функции. Компилятор с языка ассемблера для Win32 преобразует оператор вызова функции в 5 байтов с шестнадцатеричным кодом
E8 00000000
На этапе же выполнения этого машинного кода вместо нулей должен стоять действительный адрес начала машинного кода требуемой функции, но получить его пока – по крайней мере до соединения (компоновки) с заготовкой машинных кодов подпрограммы FUNA – нет никакой возможности.
Запись строки листинга исходной программы для рассматриваемого примера вызова программы FUNA будет иметь вид
A7A6A5A4A3A2A1A0E8 00000000 CALL FUNA
гдезаписьюA7A6A5A4A3A2A1A0условно представлены шестнадцатеричные цифры относительного адреса команды (например, этот адрес может оказаться 00000049). Заметим, что сразу после нулевых (не окончательно заполненных) байтов машинного кода услужливый компилятор MASM или TASM ставит для понимающего программиста условное обозначение буквой e, что означает – данное поле отвечает внешнему имени (описываемому директивой EXTRN). В конце листинга – таблице символических имен – некоторые компиляторы (например,TASM) вставляли строку вида
FUNA |
Near32FLAT:----Extern |
В объектном файле, кроме части, полностью воспроизводящей код машинных команд из листинга, присутствует информациясловаря внешних имен и словаря перемещений. В современных системах программирования эта информация стандартизована в небольшом числе используемых форматов. Практически в объектном файле для рассматриваемого примера присутствует специальная запись. В ней условным кодом задано, что имя, записываемое как FUNA, внешнее, имеет атрибут типа NEAR и это имя используется в поле с адресом B7B6B5B4B3B2B1B0, который в нашем примере при трансляции вычисляется как значение, на единицу большее адреса A7A6A5A4A3A2A1A0. (Большее на единицу, потому что длина кода команды перед полем операнда равна в этом примере 1).
Таким образом, в объектном коде присутствует информационная конструкция, которая условно может быть описана как
|Внешнее имя| Атрибут | Определено вне| отн_адрес_использования|
126
В нашем примере эта информация имеет общий вид (не обязательно повторяясь в деталях)
|FUNA| Тип | Определено вне| 0000004a|
Для имен, определенных с помощью директивы GLOBAL, также строится специальная, уже несколько иная запись в объектном файле, она описательно представляется как
|Внешнее имя| Тип | Определено здесь| отн_адрес_ места_ определения| Компоновщик, используя информацию записей двух таких типов, строит результирующий код исполняемого файла, который после дополнительной настройки загрузчиком может автоматически выполняться процессором, обращаясь при необходимости к встроенным функциям
операционной системы.
Например, если процедура FUNA размещается в кодах объектного файла со смещением 00000240 от начала этого кода, то в листинге старых систем компилятора для программного модуля, содержащего FUNA, это изображается в виде
FUNA |
Тип 00000240 |
где FLAT – стандартное наименование группы сегмента кодов в программах, которые строились компиляторами с языка Си. А содержание соответствующей записи вобъектом файле будет
|FUNA| Тип | Определено здесь| 00000240|
Пусть теперь при компоновке двоичные коды модуля, содержащего процедуру FUNA, оказываются при последовательной раскладке кодов всех соединяемых модулей, начиная с адреса 00003200 общего двоичного кода. Пусть двоичные коды модуля, содержащего рассмотренный выше вызов этой функции, размещаются, начиная с адреса 00002160. Тогда действительный адрес начала процедуры FUNA будет 00003440 (вычисляясь, как результат сложения (базовый адрес модуля + смещение), т. е. 3200 + 240), а действительный адрес начала тех четырех байтов машинного кода команды CALLFUNA, которые следует заполнить этим действительным адресом 3440, равен 000021Aa (2160 + 4a). Все числовые значения адресов
вданном примере приведены в шестнадцатеричном коде, как принято
всистемном программировании, а главное, отвечают двоичному кодированию адресов в аппаратуре компьютера.
Всовременных средствах разработки программ информация о данных словарях внешних имен строится более сложным образом. Наглядное
127
представление о ней можно получить с помощью утилиты DUMPBIN
вWindows и утилиты nm в Linux. Утилита DUMPBIN при использовании опции /SYMBOLS выдает данные о внешних именах в следующей форме. В 5-м столбце вывода утилиты содержится служебное слово External, которое и указывает, что строка описывает именно внешнее, а не внутреннее имя. (Для внутренних имен используются обозначения Static.) В первом столбце вывода – номер по порядку перечисления строк вывода данных об именах, во втором столбце – относительный адрес в сегменте формируемого машинного кода (в сегменте листинга) или нулевое значение (когда имя только объявляется, но не определяется), в третьем столбце – где имя определено (не просто в виде определено здесь или определено вне, а либо текстом SECT номер секции, либо UNDEF). Таким образом, несколько условно указываются обозначения отдельных секций, позволяющие понять для двух различных внешних имен, определены они в одном и том же сегменте объектного кода или в различных. В 3-м столбце может находиться вместо указанных значений слово ABS, обозначающее, что описываемое имя – константа. Само имя (внешнее или внутреннее) задается
впоследнем – шестом – столбце, в 4-м столбце обычно находится слово notype, обозначающее, что объектный код не содержит явную информацию о типе данных этого имени.
Получается, что ключевое слово EXTERN для определения имени
вассемблерном коде порождает для вывода утилитой DUMPBIN строку вида
номер смещение UNDEF тип External | внешнее_имя или SECTn или Static
По результату использования ключевого слова GLOBAL строится выводная строка вида
номер смещение SECTn тип External | внешнее_имя
где смещение, как правило, не нулевое (нулевое получается только для имен, определяемых в самом начале сегмента; обычно это _start и имя первой переменной в сегменте данных).
Далее приведены фрагменты подобного вывода для исходных программ, заданных ранее листингами 6.2.5 и 6.2.8:
007 00000000 |
UNDEF |
notype |
External | _GetStdHandle@4 |
008 00000000 |
UNDEF |
notype |
External | _WriteFile@20 |
009 00000000 |
SECT1 |
notype |
External | writestd |
128
00A 00000000 SECT2 |
notype |
Static |
| hstdout |
00B 00000004 SECT2 |
notype |
Static |
| actlen |
00C FFFFFFF5 ABS |
notype |
Static | STD_OUTPUT_HANDLE |
|
007 00000000 UNDEF |
notype |
External | writestd |
|
008 00000000 UNDEF |
notype |
External | readstd |
|
009 00000000 UNDEF |
notype |
External | exitproc |
|
00A 00000000 SECT1 |
notype |
External | _start |
|
00B 00000000 SECT2 |
notype |
Static |
| buf |
00C 00000050 SECT2 |
notype |
Static |
| prmpt |
При простейшем варианте работы компоновщика все составные компоненты (все объектные файлы, подготовленные программистом и извлекаемые из библиотек) соединяются как последовательности машинных кодов в общий исполняемый файл. Для любого используемого в этой сборке объектного файла можно найти участок, являющийся его копией, может быть с некоторой коррекцией отдельных кодов (там, где в первоначальном объектном коде были нули или которые отвечали именам, смещения которых изменились в результате сборки). Этот вариант называют статической компиляцией. Название будет понятно из дальнейшего изложения. Пока же заметим, что если, например, в программе было указано использование функции printf языка Си, то команды реализации действий этой функции – со всеми средствами преобразования форматов и обращения к средствам операционной системы для вывода образуемых символов – включаются в коды исполняемого файла программы. При загрузке такой программы на выполнение все эти коды (суммарно значительной длины) будут перенесены в оперативную память.
Если в многозадачной ОС одновременно выполняется несколько программ и в каждой такой программе ее разработчик использует функцию printf и статическую компоновку, то в оперативной памяти компьютера присутствуют несколько абсолютно идентичных экземпляров машинных кодов реализации этой функции. Так как библиотечные подпрограммы, особенно описывающие функции ввода-вывода, имеют значительный объем, то память используется чрезвычайно расточительно.
Напрашивается рекомендация разработчикам системного обеспечения сделать как-то так, чтобы различные программы могли использовать единственный экземпляр программы, в частности той же printf, в оперативной памяти. Эта рекомендация уже выполнена и в результате получилась динамическая компоновка. Идея динамической компоновки в том,
129
