Textnew2
.pdftxt |
db 'We are into Dll .. ',10 |
lentxt equ $-txt |
|
frmt |
db 'Text argument is <%s>',10,0 |
txtinit db 'Inialize DLL-so-library..',10 lentxtinit equ $-txtinit
txtfinidb 'Terminate this DLL-so-library..',10 lentxtfini equ $-txtfini
wiwoda: ;;аналог функции wiwoda(char *pt) push ebp
mov ebp,esp push ebx push ecx push edx mov eax,4 mov ebx,1
mov ecx, txt ; для вызова функции printf(frmt, pt) mov edx, lentxt
int 80h
push dword [ebp+8] push dword frmt call printf
add esp, 8 pop edx pop ecx pop ebx mov eax,37 pop ebp ret
_init: pusha
mov eax,4 mov ebx,1 mov ecx, txtinit
mov edx, lentxtinit int 80h
popa ret
_fini: pusha
mov eax,4 mov ebx,1 mov ecx, txtfini
211
mov edx, lentxtfini int 80h
popa ret
Рис. 8.5.2. Программа upd4.asm со служебными процедурами
Аналогичная программа на языке Си может быть задана программой, приведенной на рис. 8.5.3
#include <stdio.h> int wiwoda(char* text)
{printf("'We are into Dll ...\n"); printf("Text argument is <%s>\n", text); return 37;
}
void _init()
{printf("\033[1m\033[34m""Initialize Dll...\033[0m\n"); return;
}
void _fini()
{printf("\033[1m\033[34m""Finitize Dll ...\033[0m\n"); return;
}
Рис. 8.5.3. Программа upd4.c для Linux.
Включение процедур инициализации и завершения работы разделяемой библиотеки, написанных на языке Си, требует дополнительных "ухищрений". Дело в том, что при простейшем использовании компилятора с помощью вызова gcc, этап автоматически выполняемой компоновки для разделяемой библиотеки использует стартовый специальный объектный файл (с именем crti.o), который содержит уже построенные (хотя ничего и не делающие) процедуры с именами _init и _fini.
Поэтому имеется две возможности - явно использовать иной стартовый файл, или же явно задать имена процедур инициализации и завершения, отличные от стандартных. Первый вариант приводит к использованию для построения библиотеки двух командных строк (вместо одной с прямым вызовом gcc)
gcc -c -o upd4.o upd4.c
ld -shared -o myd.so /usr/lib/crtn.o upd4.o
В общем случае - произвольных имен исходного файла и разделяемой библиотеки - задаваемые команды имеют вид
gcc -c -o имяобъектного.o имяисходного.c
ld -shared -o имябиблиотеки /usr/lib/crtn.o имяобъектного.o
Второй вариант требует переименования процедур инициализации и завершения в исходном файле, например на имена, соответственно, myinit и myfini и задание прямого указание для gcc в виде
212
gcc -shared -Wl,-init=myinit,-fini=myfini -o имябиблиотеки имяисходного.c
В пояснение заметим, что опция -Wl, аргументы которой должны перечисляться через запятые без пробелов, задает режимы работы компоновщика ld. С учетом возможности ошибок из-за неправильного прочтения символа подчеркнем, что второй символ этой опции является строчной латинской буквой, соответствующая прописная которой записывается как L. Этот компоновщик всегда вызывается для формирования исполняемых файлов, явно или неявно. Аргументы компоновщика задают переименования процедур инициализации и завершения в виде указаний
-init=имяпроцедурыинициализации -fini=имяпроцедурызавершения
Служебная процедура инициализации и завершения может отсутствовать в программе на языке Си для операционных систем OS/2 и Windows. Тогда эта отсутствующая процедура подменяется компоновщиком для этих систем на стандартную, которая в дальнейшем вызывается, но "ничего не делает", сообщая только возвращаемым значением, что инициализация библиотеки прошла нормально. Написание же содержимого DLL исключительно на ассемблере неизбежно влечет необходимость включения явных служебных процедур.
Важным в приложениях вопросом является - откуда вызываются библиотеки динамической компоновки. В операционной системе OS/2 для указания каталогов автоматического вызова DLL служит специальная переменная LIBPATH, устанавливаемая при загрузке системы в файле конфигурации, но которую, при большом желании можно модифицировать и в ходе работы.
ВWindows для поиска загружаемых DLL принята следующая дисциплина. Вначале такая библиотека ищется в том каталоге, где находится EXE файл, запрашивающий ее подключение (неявно при загрузке или явно в ходе выполнения функции GetProcAddress). Далее при не обнаружении просматривается текущий каталог, затем системный каталог Windows (имеющий имя SYSTEM), потом просматривается основной каталог Windows(обычно это каталог WINDOWS для Win9x и каталог WINNT для Windows типа NT). В завершение просматриваются каталоги, указанные в системной переменной PATH. (Тем самым в этой части нет разницы между поиском EXE файла и DLL файла).
ВОС Linux перечень каталогов, автоматически просматриваемых для загрузки разделяемой библиотеки, находится в конфигурационном файле /etc/ld.so.conf. Дополнительно к нему может использоваться переменная командной оболочки с именем LD_LIBRARY_LD, которая должна содержать перечисленные через двоеточия имена каталогов такого поиска. Этот вариант был кратко рассмотрен в разделе 9.2 для использования разделяемых библиотек из текущего каталога.
Для изучения содержимого разделяемых библиотек в Windows служат многофункциональные утилиты DUMPBIN.EXE из комплекта средств разработки Microsoft и TDUMP.EXE из комплекта разработки Borland/Inprise.
213
В частности запрос вывода экспортируемых имен на экран с помощью DUMPBIN будет иметь вид
DUMPBIN -export имябиблиотеки
а вывод импортируемых имен из исполняемого файла достигается вызовом DUMPBIN -import имяEXEфайла
Служебная утилита TDUMP единообразно решает обе задачи и, в простейшем случае, вызывается с единственным аргументом - именем исполняемого файла, в качестве которого может использоваться как разделяемая библиотека, так и обычный исполняемый файл.
Очень важные особенности связаны с использованием DLL при программировании на языке C++. Непосредственное использование функций (называемых для классов методами) в этом объектно-ориентированном варианте влечет неявное построение действительных их имен, существенно отличных от записанных программистом. Самое неприятное, что построение таких действительных имен существенно зависит от транслятора и никаких стандартов на этот случай не выработано. Получается, что DLL, построенная из программ C++, разработанных в одной системе программирования, совершенно не годится для программ другой системы программирования. Поэтому в большинстве случаев, даже программируя на С++, функции для DLL описывают как отвечающие строгому соответствию обычного Си. Для этих целей служит вспомогательных модификатор объявления С++ функций, записываемый как
extern "C" дальнейшее описание функции или ее прототипа
В общем случае следует избегать экспорта C++-классов, если программист не уверен, что разработчики EXE-модулей будут пользоваться тем же транслятором.
Упражнения.
1.Построить DLL библиотеку Windows, функции которой позволяют запомнить внутри нее текст данных, а затем - в другой функции - прочитать этот текст в вызывающую программу. Использовать эту библиотеку в двух параллельно работающий процессах и наблюдать получаемые результаты.
2.Построить разделяемую библиотеку в Linux, функции которой позволяют запомнить внутри нее текст данных, а затем - в другой функции - прочитать этот текст в вызывающую программу. Использовать эту библиотеку в двух параллельно работающий процессах и наблюдать получаемые результаты.
9.ИСПОЛЬЗОВАНИЕ ОТЛАДЧИКОВ ДЛЯ АССЕМБЛЕРНЫХ ПРОГРАММ
9.1. Особенности отладчика gdb для программ в Linux
Разработка досрочно сложных программ практически невозможна без использования отладчиков. К сожалению для начинающих, современные широко распространенные отладчики ориентированы на отладку программ, написанных на языках
214
высокого уровня, и, в первую очередь, на языке Си. Такая ситуация обусловлена тем, что использование ассемблера остается к настоящему времени уделом профессионалов и даже стандартный интерфейс доступа к программным функциям современной операционной системы (API ОС) задается теперь исключительно на языке Си.
Тем ни менее, имеющиеся отладчики могут использоваться и для отладки ассемблерных программ, хотя множество наиболее мощных средств этих отладчиков нацелены на более высокоуровневые языки.
В ОС Linux стандартным отладчиком, являющимся как и вся ОС Linux, свободно распространяемым продуктом, является отладчик с программным именем gdb (GNU debugger). Согласно документации, это так называемый символический отладчик. Его возможности очень многообразны, но в данном изложении будут рассмотрены лишь простейшие из них и те, без которых трудно обойтись даже в простейшей отладке.
Запустить программу для отладки с помощью gdb чрезвычайно просто: следует в командной строки консоли набрать вызов отладчика в виде
gdb имя_исполняемой_программы
Если в разработке не указывалась соответствующими опция, что должна быть вставлена отладочная информация, то непосредственно после вызова отладчика для программы появится сообщение
(no debugging symbol found) . . .
и будет выведено приглашение (prompt) для команд отладчика в виде (gdb)
Заметим, что текущие версии NASM не позволяют формировать отладочную информацию в процессе трансляции, а стандартный ассемблер Unix'а с именем as не удобен для начального изучения предмета. Поэтому мы ограничимся изучением использования отладчика gdb при отсутствии отладочной информации от транслятора. В то же время оказывается, что минимальная и достаточная информация для отладчика поступает от соответствующего исходного файла. В частности, оказываются доступными обычные (не обязательно глобальные) метки и именованные области данных, а поэтому, фактически и имена подпрограмм. (А больше никакой символьной информации программисту и не надо.) Заметим, что отсутствие отладочной информации от транслятора делает очень неудобной отладку программ, написанных на языке высоко уровня, так как в этом случае отладчик может предоставить отладочную информацию на уровне ассемблера, а нам больше "ничего и не надо". Поэтому в нашем случае замечание об отсутствии отладочной информации не может быть предметом неудовольствия.
Отладчику gdb присуще одно специфическое свойство, кажущееся неудобным для тех, кто привык к отладке простейших программ под MS-DOS. Именно, оказывается практически невозможным остановиться перед первой исполняемой командой программы, а можно только на угодно другой. Такое поведение - косвенный результат строгой ориентации отладчика на структуры программ, построенных компиляторами с языков высокого уровня. Такая структура обязательно строится
215
как структура систематически использующая кадры (фреймы) подпрограмм, где все подпрограммы, начиная с программной функции main, создаются по этим соглашениям.
При работе отладчика gdb и его использовании широчайшим образом применяется понятие текущего кадра, вложенных кадров вызова и переключения между кадрами для доступа к локальным данных процедуры любого более высокого уровня вызова. Расширенные возможности, вытекающие из этого подхода, влекут необходимость вхождения для отладки хотя бы в какой-то кадр и, поэтому начального выполнения хотя бы одной команды. Заметим, что при отладке любых программ, отличных от ассемблерных, указанное ограничение никак не заметно программисту, так как все программы с языков высоко уровня используют процедуру начального вызова главной подпрограммы разрабатываемой программы. Так для программы с языка Си используется (часто незаметно для малоквалифицированного программиста) вспомогательная программа запуска главной подпрограммы.
Поэтому придется удовлетвориться тем, что фактически отладчик gdb можно будет использовать для ассемблерных программ только с их второй команды. Отсюда следует два практических варианта: либо на веру принять правильность предполагаемых действия первой команды (тем более, что ее результат с помощью отладчика нетрудно увидеть), либо (для наиболее сомневающихся) вставить в качестве первой команды какую-то вспомогательную, от которой не зависят дальнейшие результаты (например, команду CLD). Можно для успокоения просто считать, что приостановиться можно только после первой команды, действия которой мы можем наблюдать.
Собственно отладка начинается с выдачи отладчику команды run (выполнить). Но читателю не следует торопится сразу же ее использовать. Если задать команду run сразу после вызова отладчика, то все выполнения будет происходить в автоматическом режиме (без отображения и приостановок отладчиком) и результат окажется во многих случаях таким же, как и при запуске той же программы без отладчика.
Мощнейшие средства, которые предоставляет отладчик - это задаваемые приостановки в автоматическом выполнении и пошаговое выполнение. Обычно разработчик использует и то, и другое. Заметим дополнительно, что нельзя задавать пошаговое выполнение, пока не выдана команда run.
Команда отладчика "установка точки приостанова" задается именем breakpoint, которое можно сокращать до двух начальных символов. Эта команда может использоваться в одной из двух следующих основных форм
br имя_метки_в_программе
или
br *адрес
Проще всего для наших целей воспользоваться этой командой для начальной приостановки автоматического выполнения, если в исходной программе на ассемблере поставить метку у второй ее команды и эту метку использовать в команде
216
приостановки, выдаваемой программистом сразу после команды запуска отладчика с отлаживаемой программой.
Для удобства демонстрировать конкретные средства отладки возьмем в качестве примера программу, исходный текст которой приведен на рис. 9.1.1.
GLOBAL _start
SEGMENT .text
_start: |
mov eax, 1000h |
|
beg: |
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 ; digit into array for text value |
inc ebx
loop izv ; izv,ecx call outa
mov eax, 1 ; N function = exit int 80h ;
outa:
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
ret SEGMENT .data
numtxt times 10 db 0 cntdd 0
Рис. 9.1.1. Пример программы для отладки
Для нашего примера пусть исполняемый файл формата ELF, который получается из программы с рис. 9.1.1, называется primer.exe. Тогда начало использования отладчика дают следующие строки, вводимые с консоли и выводимые на нее.
gdb primer.exe
(no debugging symbol found) . . .
217
(gdb) br beg
Breakpoint 1 at 0x8048085 (gdb) run
Starting program /home/student/primer.exe Breakpoint 1, 0x8048085 in beg ()
(gdb)
Здесь отладчик выводит всю изображенную информацию, кроме находящейся в строках, которые начинаются с приглашения (gdb). Текст в строках после этого приглашения всегда вводиться пользователем (и завершается нажатием клавиши Enter). В данном примере предполагается, что программа primer.exe была помещена в базовом каталоге пользователя student. В общем случае после слов "Starting program" идет полное имя исполняемого файла, запущенного "под отладчиком".
Далее следует решить какой вариант отображения команд желает использовать пользователь. Дело в том, что по умолчанию отображаются команды в форме универсального ассемблера ATT, которая для пользователей, привыкших к соглашениям Intel и Microsoft, может показаться непривычной. Мы кратко касались форм записи команд в этом ассемблере в главе 5.5. Напомним еще раз, что в этом ассемблере все операнды получатели записываются справа, а операнды исходной информации - слева (обратно принятому в Intel). Кроме того, обозначения всех регистров предваряются служебным символом %, а константы - служебным символом $.
Если пользователю кажется неудобным такая форма обозначения команд, то перейти к более привычной он может с помощью вспомогательной команды
set disassembly-flavor intel
Обратный переход осуществляется командой set disassembly-flavor intel
Заметим, что даже в режиме отображения Intel обозначениям регистров при выводе обязательно предшествуют служебные символы %.
Теперь (как и в любой последующий момент) мы можем воспользоваться командой disassemble, которую проще сокращать до пяти первых букв. Эта команда без аргументов выдает ассемблерную запись команд текущего кадра.
Для ассемблерных программ при отсутствии более детальной информации отладчик считает кадром последовательность команд от одной метки до другой. Поэтому задание команды disas отладчику в нашем примере (после приостановки на первой точке приостановки) даст следующий вывод.
0x8048085 <beg>: mov %esi,0xa 0x804808a <beg+5>: mov %ecx,0x0 (gdb)
Для ассемблерных программ эту команду удобней всего использовать с двумя аргументами, задающими начало и конец отображаемой области команд. Причем эти аргументы можно задавать именами меток в программе. Например, задав отладчику команду в виде
disas _start izv
218
мы получим в качестве отображаемого на экране следующий текст рассматриваемого примера
Dump of assembler code from 0x8048080 to 0x80480ac:
0x8048080 |
<_start>: |
mov |
%eax,0x1000 |
0x8048085 |
<beg>: |
mov |
%esi,0xa |
0x804808a |
<beg+5>: |
mov |
%ecx,0x0 |
0x804808f |
<pov>: |
mov |
%edx,0x0 |
0x8048094 |
<pov+5>: |
div |
%eax,%esi |
0x8048096 |
<pov+7>: |
add |
%dl,0x30 |
0x8048099 |
<pov+10>: |
push %edx |
|
0x804809a |
<pov+11>: |
inc |
%ecx |
0x804809b |
<pov+12>: |
cmp |
%eax,0x0 |
0x80480a0 |
<pov+17>: |
jne |
0x804808f <pov> |
0x80480a2 |
<pov+19>: |
mov |
%ds: 0x80490e2, %ecx |
0x80480a8 |
<pov+25>: |
mov |
%ebx,0x80490d8 |
End of assembler dump. (gdb)
Этот вариант появится, если было произведено переключение на ассемблер типа intel. В противоположном случае появится текст
Dump of assembler code from 0x8048080 to 0x80480ac:
0x8048080 |
<_start>: |
mov |
$0x1000, %eax |
0x8048085 |
<beg>: |
mov |
$0xa, %esi |
0x804808a |
<beg+5>: |
mov |
$0x0, %ecx |
0x804808f |
<pov>: |
mov |
$0x0, %edx |
0x8048094 |
<pov+5>: |
div |
%esi, %eax |
0x8048096 |
<pov+7>: |
add |
$0x30, %dl |
0x8048099 |
<pov+10>: |
push |
%edx |
0x804809a |
<pov+11>: |
inc |
%ecx |
0x804809b |
<pov+12>: |
cmp |
$0x0, %eax |
0x80480a0 |
<pov+17>: |
jne |
0x804808f <pov> |
0x80480a2 |
<pov+19>: |
mov |
%ecx, 0x80490e2 |
0x80480a8 |
<pov+25>: |
mov |
$0x80490d8, %ebx |
End of assembler dump. (gdb)
По умолчанию стандартный режим отладчика gdb не отображает информацию о текущей команде в пошаговом режиме этого отладчика. (Это вполне целесообразно, так как он обычно используется для отладки программ, написанных на языке высокого уровня.) Для установки режима автоматического отображения следующей команды следует передать отладчику приказ в виде директивы
display/i $eip
после чего в нашем примере автоматически выдается (с учетом предыдущей остановки на первой точки приостановки)
1: x/i $eip 0x8048085 <beg> mov %esi, 0xa
219
(если текущим режимом отображения команд будет режим intel).
Само приказание на выполнение одной следующий команды задается в виде команды отладчика
stepi
В частности, в нашем примере после полученной выше остановки на первой точке приостановки, выдача команды stepi приводит к следующей реакции отладчика в виде
(gdb) stepi 0x804808a in beg()
1: x/i $eip 0x804808a <beg+5> mov %ecx, 0x0
Дальнейшие выдачи команды stepi (в частности, после извлечения ее в командную строку с помощью клавиши "стрелка вверх") будут давать следующие выводы
(gdb) stepi |
|
|
0x804808f in pov() |
|
|
1: x/i $eip 0x804808f <pov>: |
mov |
%edx,0x0 |
(gdb) stepi |
|
|
0x804808a in pov() |
|
|
1: x/i $eip 0x8048094 <pov+5>: |
div |
%esi, %eax |
(gdb) stepi |
|
|
0x804808a in pov() |
|
|
1: x/i $eip 0x8048096 <pov+7>: |
add |
$0x30, %dl |
Для отображения информации служит команда с именем, восходящим к слову print, но задаваемая обычно единственным начальным символом p. В частности, выдача содержимого регистра в виде десятичного числа требует задания команды в виде p $имя_регистра, для вывода в виде шестнадцатеричного числа - команда задается в виде p/x $имя_регистра. Причем имя регистра в этой команде допускается только для 32-битных регистров (регистры меньшего размера все равно являются частью некоторого 32-битного регистра).
При отладки рассматриваемой программы-примера в качестве первой цифры должен появиться символ '6', и, действительно, в ситуации остановки перед строкой <pov+7>, т.е. после вывода последней из изображенных выше строк информации от отладчика, запрос
p $edx
приводит к выдачи информации в виде $1 = 0x6
а запрос в виде p/x $eax
к выводу результата $2 = 0x199
Аналогичный запрос в этом же месте отлаживаемой программы в виде p/x $eax
дает результат $3 = 409
220