Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Textnew2

.pdf
Скачиваний:
13
Добавлен:
06.02.2018
Размер:
1.48 Mб
Скачать

При программировании для операционных систем типа MS (MS-DOS, OS/2, Windows) используются утилиты библиотекарей с типовым названием lib.exe. Первым дополнительным символом в имени такого библиотекаря могут быть символы фирменной идентификации утилиты. Так библиотекарь для MS-DOS и OS/2 фирмы Borland/Inprise имеет название TLIB.EXE. Синтаксис команд управления библиотекарем в указанных и ряде других современных ОС имеет в общем случае вид

имя_библиотекаря имя_библиотеки команда объектный файл

где последние два параметра могут повторятся. Элемент командного вызова команда здесь представляется одним или двумя служебными символами, причем для добавления объектного модуля к библиотеке используется символ '+', а для исключения из библиотеки используется символ '-' (дефис). В частности, действия, аналогичные рассмотренному примеру для Unix, для библиотекаря TLIB запишутся в виде команды

tlib myliba.lib +stwrite.obj +stread.obj +stexit.obj

Причем стандартным расширением библиотек в этих системах разработки является буквосочетание lib, а obj используется как стандартное расширение объектных файлов. Эти стандартные расширения могут опускаться при задании команд обработки для утилиты.

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

5.3. Организация стекового кадра подпрограммы

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

Принципиальным решение по передаче аргументов стало использование для этого стека. Пусть некоторая подпрограмма с именем funa описана на некотором языке высоко уровня, например Паскаль, использует при вызове фактические аргументы arg1, arg2, arg3 и пусть все они для простоты картины имеют целочисленный тип. Вызов этой функции, который на языке высоко уровня запишется в виде funa(arg1, arg2, arg3), реализуется с помощью стека следующей последовательностью команд

push значение arg1 push значение arg2 push значение arg3 call funa

В результате все значения фактических аргументов окажутся в стеке и вызванной подпрограмме останется только извлекать их оттуда. С этим извлечением возникают, казалось бы, некоторые проблемы. Извлекать их непосредственно командами POP нельзя, так как над значениями этих аргументов в стеке оказывается

81

значение адреса возврата, записываемое туда командой CALL. Извлекая "по дороге" с вершины стек адрес возврата, подпрограмма лишила бы себя возможности вернуть управление в вызвавшую ее программу (команде RET должно быть что снимать с вершины стека). Какие-то манипуляции с стеком, отличные от классических операций PUSH, POP теоретически возможны, но такое решение не удобно.

С учетом динамического характера заполнения стека (в свое время) было принято красивое решение связывать участок заполнения стека для процедуры со специальным регистром - указателем. Этот регистр называют указателем фрейма или указателем кадра, а в архитектуре Intel его назвали просто базовым указателем, подразумевая базовый указатель фрейма в стеке. Символическое обозначение этого регистра здесь - EBP (от сочетания слов - Base Pointer). Обратим внимание, что использование этого регистра специализированно назначением, хотя временно его можно использовать и как регистр общего назначения (но не рекомендуется, так как при этом нужно учитывать его определенные особенности).

Специализация регистра EBP заключается прежде всего в том, что подпрограммы стандартной структуры должны в качестве первых двух своих командах выполнить следующие

PUSH EPB MOV EBP, ESP

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

Посмотрим с помощью рис. 5.3.1, к какому строению участка стека привело выполнение указанных выше двух первых команд процедуры.

Рис. 5.3.1. Строение кадра стека после стандартного пролога в подпрограмме

Из этого рисунка, как и из внимательного рассмотрения действий заполнения стека, следует, что содержимое четырехбайтового поля стека, где размещено значение аргумента arg3, может быть обозначено с помощью базового способа адресации в виде [EBP+8]. (Старое значение регистра EBP обозначается как [EBP+0], значение адреса возврата как [EBP+4], а далее следует как раз значение аргумента arg3.) Содержимое аналогичного поля стека, где содержится значение аргумента

82

arg2 обозначится как [EBP+12], а содержимое поля стека, где находится значение аргумента arg1 - как [EBP+16].

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

Заметим, что в общем случае могут использоваться более экзотические вызовы процедур, так называемые дальние (FAR), при которых адрес возврата занимает не одно четырехбайтовое поле в стеке, а в два раза больше. В таких случаях указанные рассуждения приводят к обозначению [EBP+12] уже для самого верхнего аргумента в стеке.

Используя рассмотренные содержательные связи, вычисление суммы первых двух аргументов процедуры можно выполнить с помощью двух команд

MOV eax, [ebp+16] ; arg1 ADD eax, [ebp+12] ; arg2

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

PROCEDURE funa(arg1: INTEGER; arg2: INTEGER; VAR arg3 : INTEGER);

Для данной процедуры аргументы arg1 и arg2 передаются по значению, а аргумент arg3 - по ссылке (иначе говоря, по адресу). Поэтому обращение к процедуре funa записывается машинными командами, записанными на ассемблере NASM, как

PUSH DWORD [arg1] PUSH DWORD [arg2] PUSH DWORD arg3

Запись того же фрагмента на ассемблере TASM/MASM будет иметь несколько иной вид, а именно

PUSH arg1 PUSH arg2

PUSH OFFSET arg3

(Здесь используется возможность неявного задания атрибутов данных, определенных при их описании в области данных, что позволило отказаться от уточните- ля-модификатора DWORD, и использования вспомогательного служебного слова OFFSET для указания использования адреса именованного объекта вместо его самого.)

Сложение оператора Паскаля arg3:=arg1+arg2;

выполнится в этом примере командами

83

MOV eax, [ebp+16]

; arg1

ADD eax, [ebp+12]

; arg2

MOV ebx, [ebp+10]

; адрес собственно arg3

MOV [ebx], eax

 

причем в реальной подпрограмме необходимо было предварительно сохранить (а перед выходом из подпрограммы - восстановить - значения регистров eax и ebx).

Перед выходом из процедуры, если она использует стандартное строение кадра процедуры, следует дополнительно выполнять команду POP EBP - для восстановления старого значения регистра EBP, которое нужно будет после возврата в вызывавшую программу. С учетом необходимого сохранения рабочих регистров для расширенного примера содержимое собственно запишется фрагментом

funa: ;; процедура funa

 

push ebp

 

 

mov ebp, esp

 

push eax

 

 

push ebx

 

 

mov eax, [ebp+16]

; arg1

add eax,

[ebp+12]

; arg2

mov ebx, [ebp+10]

; адрес собственно arg3

mov [ebx], eax

 

pop ebx

 

 

pop eax

 

 

pop ebp

 

 

ret

; end procedure

Таким образом, как мы видим, в кадре процедуры хранятся аргументы ее вызова, адрес возврата и сохраняемые значения регистров. В действительности часть кадра процедуры еще систематически выделяется для хранения локальных переменных. Если переменные, определенные на языках высокого уровня вне подпрограмм, размещаются в сегменте данных, статические данные (данные описанные со служебным словом static) в языке Си хранятся также в сегменте данных, то локальные переменные (определенные внутри подпрограммы) размещаются обязательно внутри кадра процедуры.

С этой целью непосредственно после команды MOV EBP,ESP в начале машинного кода процедуры выполняется команда

SUB ESP, размер_области_локальных_переменных

Например, если в подпрограмме определены две локальные переменные i,j целочисленного типа для 32-битной архитектуры (т.е. по 4 байта каждая), то под них необходимо зарезервировать 8 байтов, задав размер области равным восьми. В результате изменения значения регистра ESP для указателя вершины стека, новая вершина будет располагаться на 8 байтов ближе к началу сегмента стека. Причем зарезервированные таким образом четырехбайтовые поля можно обозначить в операн-

84

дах команд как [EBP-4] и [EBP-8]. На рис. 5.3.2 изображено строение кадра процедуры для данного частного примера.

Рис. 5.3.2. Строение кадра стека после выделения области для локальных переменных

Резервирование области для локальных переменных в стеке требует определенных обратных действий перед выходом их подпрограммы (иначе эти поля, оставшись на вершине стека, не позволят автоматически сделать вершиной стека то место в нем, где хранится адрес возврата (и запомненное "старое" значение EBP). Такие обратные восстановительные действия могут быть выполнены одним из двух следующих вариантов. Следует либо выполнить команду

ADD ESP, размер_области_локальных_переменных

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

MOV ESP, EBP

после которой регистр ESP будет указывать на то место в стеке, на которое указывал до тех пор регистр EBP (после этого можно выполнять команду POP EBP и команду RET).

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

встеке над ней остаются не снятыми данные).

Теоретически для последней проблемы возможно два решения: очисткой кадра процедуры от аргументов занимаются команды процедуры и очисткой кадра процедуры от аргументов занимаются команды вызвавшей программы. Для реализации первого варианта нужно было освобождать стек после снятия адреса возврата, но до возвращения в вызвавшую процедуру. Но так как снятие адреса возврата выполняет команда RET, а к концу выполнения этой команды последовательность управления выборкой следующей команды должна вернуться в вызывающую программу (что называется просто возвратом управлением ), то такая реализация может быть вложена только внутрь выполнения самой команды RET. Именно так и поступили разработчики архитектуры и дополнительно к обычной команде RET ввели в набор команд еще и команду, записываемую в мнемонике как

RET число

где операнд число задает сколько байтов нужно удалить из стека в ходе выполнения команды RET после снятия из стека адреса возврата. С учетом того, что из стека может быть излечено только четное число байтов (слово или двойное слово), то не-

85

посредственный операнд число должен обязательно задаваться четным числовым значением. В нашем демонстрационном примере следовало выполнить команду RET 12. (Размер области аргументов процедуры funa есть 12 байтов.)

Второе из упомянутых выше решений заключается в выполнении команды ADD ESP, размер_области_аргументов_процедуры

после команды CALL. В частности, по этому варианту в нашем примере следует выполнить команду ADD ESP, 12.

Для разработчика программного обеспечения встает естественный вопрос, какой вариант из рассмотренных использовать в конкретной ситуации. Для очистки стека от аргументов процедуры предпочтительным является первый вариант, так как вызовы подпрограммы используются многократно и по второму варианту за каждой командой вызова CALL должна помещаться дополнительная команда ADD ESP, размер, а команда RET число будет присутствовать в процедуре в единственном экземпляре.

В то же время существуют ситуации когда приходится использовать второй вариант очистки стека от аргументов. Он используется когда предполагается применение подпрограммы с переменным числом аргументов. Напомним, что в языке С++ допускаются подпрограммы, которые можно использовать, задав только часть аргументов, а именно опустив сколько-то последних из них. Более того, даже в обычном языке Си неявно допускаются такие подпрограммы с переменных числом аргументов, среди которых можно назвать обычные системные функции типа printf.

Рассмотрим, что получиться, если предложенную выше подпрограмму примера funa вызвать не с тремя ее, а только с двумя аргументами. Обращаясь к последовательности укладки аргументов в стек и модифицированному этим допущением схеме, аналогичной рис. 5.3.1, получим, что arg1 должен теперь внутри команд процедуры обозначаться как [EBP+12], а arg2 - как [EBP+8]. Но разработчик программы, а следовательно и машинный код никак не могут знать с каким числом аргументов в действительности вызвана подпрограмм и поэтому выбрать правильное обозначение для доступа к аргументом нет никакой возможности.

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

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

86

ваемого вариантом языка Си, вызов подпрограммы funa в машинных командах запишется как

push значение arg3 push значение arg2 push значение arg1 call funa

В результате такой укладки аргументов на верху области аргументов в стеке окажется как раз аргумент arg1, а последний из списка аргументов - в низу области внутри стека. Поэтому теперь аргументу arg1 будет соответствовать обозначение операнда [EBP+8], аргументу arg2 - обозначение [EBP+12], а аргументу arg1 - обозначение [EBP+16]. В результате не укладки последнего аргумента последовательность этих обозначений нисколько не изменится, за исключением того, что неуложенный аргумент не обозначать, ни использовать будет нельзя (но это и не является неожиданностью или неудобством).

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

Последнее менее значимое соглашение касается возвращаемой информации из подпрограммы. В тех случаях, когда заголовок подпрограммы на языке высоко уровня предполагает возврат в качестве собственного значения ее (как подпрограм- мы-функции) простого значения, размещаемого в одном, двух, четырех байтах или восьми байтов, то для передачи этого значения используются регистры AL, AX, EAX или пара регистров EAX, EDX, соответственно.

5.4. Программный доступ к системным функциям Win32

Доступ к системным функциям операционной системы MS Windows организован через специализированные библиотеки этой системы, которые содержат подпрограммы, вызываемые с помощью машинных команд CALL. Для конкретного доступа библиотеки предоставляют специализированные имена этих функций. Такие имена с подробным описанием аргументов, включая их типы, задаются документацией на API MS Windows с учетом особенностей системы программирования. Следует иметь в виду, что стандартные для документации имена API предоставляются для языка Си, а на уровне фактических имен для ассемблера может иметь место определенное отличие от этих имен, что будет рассмотрено дальше.

87

Обращение к API функциям в MS Windows осуществляется по соглашению Си для укладки аргументов в стек и по соглашению Паскаля для освобождения стека от этих аргументов. Такое комбинированное соглашение для обращения к подпрограмм в ассемблерах TASM и MASM обозначается также условным именем STDCALL. Все аргументы системной функции должны быть обязательно заданы (иначе нельзя было бы использовать соглашение Паскаля по освобождению области аргументов). Еще раз обратим внимание, что укладка аргументов по соглашению STDCALL выполняется в порядке "от конца к началу" в прототипе функции, а после возврата из системной функции никаких корректирующих действий в вызывающей программе предпринимать не нужно.

Для получения исполняемых программ, предназначенных выполняться под управлением ОС Windows, проще всего воспользоваться форматом выходного файла ассемблера NASM, задаваемого обозначением OBJ. Этот формат отвечает соглашениям, принятым для первых, тогда еще 16-битных версий продуктов Microsoft, и приспособленных в дальнейшем для 32-битных программных продуктов. Тот же самый формат объектных файлов используют системы разработки фирмы Borland/Inprise. Ориентация объектных модулей этого формата как на 16-бит- ную архитектуру для исполняемых файлов, так и на 32-битную приводит к необходимости некоторых дополнительных указаний в самом тексте исходной программы.

В частности, приходится уточнять назначение сегментов, явно указывая их предназначение для 32-битной архитектуры современных версий Windows и задавая детали, существенные для конкретных соглашений компоновщика, выполняющего построение исполняемого файла для операционной системы MS Windows.

При использовании свободно распространяемого компоновщика ALINK.EXE (Copyright 1998/9 Anthony A.J. Williams.), входящего в комплект системы NASMW, ограничений оказывается меньше и необходимо обязательно указывать только 32битную разрядность сегментов. Для этих целей сегменты кода и данных можно определить с помощью директив

SEGMENT .text USE32

и

SEGMENT .data USE32 соответственно.

При использовании компоновщика TLINK32.EXE, входящего в состав систем программирования Borland/Inprise, ограничений оказывается больше. Во-первых, необходимо дополнительно указывать параметр CLASS=CODE при описании сегмента кодов. Во-вторых, при размещении сегментов данных перед сегментом кодов в исходной программе, необходимо использовать дополнительную директиву GROUP, вводя с ее помощью общее описание группы сегментов со стандартным именем DGROUP. Таким образом, для компоновки объектных файлов, формируемых NASM для его формата obj, следует использовать в качестве описания сегмента данных пару директив

GROUP DGROUP DATA

88

SEGMENT DATA USE32

а для описания сегмента кодов директиву SEGMENT .text USE32 CLASS=CODE

При использовании, кроме единственного инициализируемого сегмента данных, еще и других сегментов данных, в частности сегмента неинициализированных данных с именем BSS, все имена сегментов данных должны быть перечислены - через запятые - в директиве GROUP.

В частности, для двух сегментов данных DATA и BSS получается директива GROUP DGROUP DATA, BSS

Таким образом, структура исходного текста программы NASM для последующего использования компоновщика TLINK32.EXE будет иметь вид

GROUP DGROUP DATA, BSS SEGMENT DATA USE32 содержимое сегмента данных SEGMENT BSS USE32

содержимое сегмента не инициализируемых данных SEGMENT .text USE32 CLASS=CODE

содержимое сегмента кодов

Заметим, что вместо имени .text при обозначении сегмента кодов для указанного типа объектных файлов чаще всего применяют имя CODE.

Еще раз подчеркнем, что более простое построение программы получается, если сегмент кодов размещать раньше сегментов данных. В этом случае даже для TLINK32.EXE структура исходного текста программы будет отличаться от ранее рассмотренных вариантов только дополнительным параметром USE32 в директивах сегментов и еще параметром CLASS=CODE в описании сегмента кодов. Все перечисленные чисто технические сложности использования компоновщика TLINK32.EXE для наших целей обусловлены тем, что его построение (неявно для пользователей) базируется на традиционном в 16-битных программных архитектурах использовании директивы GROUP. Последнее было целесообразно в указанных старых архитектурах, где такой подход давал возможность эффективней использовать очень ограниченные для программиста возможности. Для сравнения компоновщик ALINK.EXE, созданный уже в новых условиях, неявно не ориентирован на такие ограничения и обеспечивает больше возможностей, поэтому (по крайней мере в данном курсе) рекомендуется использовать именно его.

В качестве стандартной метки начала запуска программы для формата OBJ следует использовать имя ..start. Причем то, что это имя оказывается глобальным, принимает по умолчанию сам ассемблер.

Существенными в Windows являются отличия программ, использующих стандартный ввод-вывод, от рассмотренных ранее для других операционных систем. Они заключаются в том, что хэндлы стандартных файлов не предоставляются операционной системой автоматически и с фиксированными значениями. Для получе-

89

ния хэндлов стандартного ввода и вывода необходимо выполнить вызов специальной функции с именем GetStdHandle. Единственный аргумент этой функции указывает хэндл для какого стандартного файла - для стандартного ввода, стандартного вывода или стандартного вывода ошибок требуется получить. В качестве этих констант в системах разработки на языке Си используются символические константы, определенные в заголовочном файле WINCON.H. Они имеют в этом файле обозначения STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE, а отвечают им в действительности числовые значения -10, -11 и -12, соответственно.

Универсальные функции для ввода и вывода с помощью хэндлов файлов имею в этой ОС прототипы

BOOL ReadFile(HANDLE hFile, void *buf, dword size, dword *actlen, OVERLAPPED *Overlapped);

BOOL Write(HANDLE hFile, void *buf, dword size, dword *actlen, OVERLAPPED *Overlapped);

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

В качестве системной функции завершения программы Windows содержит функцию с прототипом

VOID ExitProcess(UINT uExitCode)

где аргумент также является двойным словом Все системные функции обязательно должны быть перечислены в директивах

EXTERN, так они соответствуют именам определенным вне программы их использования.

На рис. 5.4.1 приведен исходный текст простейшей программы для Windows, которая выводит текст, описанный в именованной области данных txt и - сразу за ним - один символ из области symbl.

EXTERN GetStdHandle, WriteFile, ExitProcess SEGMENT .text USE32 CLASS=CODE

..start:

push dword STD_OUTPUT_HANDLE call GetStdHandle

mov [hstdout],eax

;--- WriteFile(hstdout, txt, 7, &actlen, NULL) push dword 0

push dword actlen

push dword 7 ; number of bytes

90

Соседние файлы в предмете Операционные системы