Низкоуровневое программирование для Дzenствующих

Вид материалаДокументы

Содержание


IDE для больших проектов на ассемблере
2. Требования в IDE
3. Некоторые примеры IDE
Negatory Assembly Studio
Поиск адресов API в win95-XP Введение
API ф-ий, то сохраним указатель стека в ebp
/\ & () (алгоритмы и оптимизация) Циклические счетчики
Socket vs Socket
WinMain возвращаемся в Windows. А теперь разберем, что происходит в третьем пункте. В объявлении функции WinMain
IP адрес компьютера по имени: invoke gethostbyname, addr buffer Для этого есть в API
AF_INET – семейство, в версии 1.1 только AF_INET; SOCK_STREAM
НЕТ датаграммам (SOCK_DGRAM
WM_USER+… Цитата из CyberManiac
Крис Касперски ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ (избранное)
Подобный материал:
1   ...   12   13   14   15   16   17   18   19   ...   42

#Послесловие





###########################################################################
Masquer

IDE для больших проектов на ассемблере

  1. Введение
  2. Требования в IDE
  3. Некоторые примеры IDE
    1. RadASM
    2. WinASM и Negatory
    3. Visual SlickEdit 8.0
    4. Source Insight 3.5
  4. Заключение

1. Введение


Итак, сначала небольшое введение, посвященное тому, зачем мне (в частности) вообще нужно IDE. Дело в том, что наличие IDE практически необязательно для проектов с небольшим размером. Под проектами с небольшим размером я подразумеваю проекты с количеством строк, меньшим 1000. Как правило такая программа состоит из одного файла (или один .asm и один .inc) и содержит 10-15 процедур, столько же структур, макросов, глобальных переменных и констант. Для работы над таким проектом вполне достаточно даже Notepad-а, а компиляцию запускать из bat файла. Все эти элементы программы достаточно легко запоминаются в голове и их применение практически не вызывает никаких трудностей, в худшем случае придется прокрутить текст, посмотреть название или элемент структуры и вернутся к коду. Насчет NotePad-а, это, конечно, утрированно, и подразумевается, что с этим заданием справится любой текстовый редактор. Выбор такого рода редакторов достаточно широкий. Все они, как правило, поддерживают настраиваемую подсветку синтаксиса, возможность запуска внешних приложений и захват текста, возвращаемого компилятором. Для таких целей я предпочитаю использовать UltraEdit (ссылка скрыта). Почему? Кроме всего вышеперечисленного меня устраивает его компактность, скорость работы, удобные поиск и замена, возможность показа названий процедур (настраиваемая). На этом работы с небольшими проектами я касаться не буду.

2. Требования в IDE


Перейдем к более интересному, а именно большим проектам. Традиционно считается, что на ассемблере сейчас большие проекты не пишутся (или вообще ничего не пишется). А мне, например, нравится и у меня есть несколько проектов с количеством строк, большим 10 000 у каждого. Причем менять язык я не планирую, а число проектов - только увеличивать. Кроме этого каждый мой большой проект (незавершенный, кстати) имеет порядка 300 процедур, примерно такое же количество структур и глобальных переменных и дюжина макросов. При этом каждая процедура имеет 5-10 локальных переменных и 1-3 структуры. Все это разбросано по 5-6 asm-файлам и такому же количеству inc-файлов. Ну, и как прикажете этим хозяйством управлять? Самое время подумать о выборе IDE для управления такого ассорти.

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

1. Редактор текста.

1.1 Возможность настроить размер, стиль и имя шрифта. Я, например, предпочитаю все писать шрифтом Courier New, 14, bold;

1.2 Настраиваемая подсветка синтаксиса;

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

1.4 При наведении (клике) на переменную/константу/структуру/макрос/процедуру хотелось бы видеть ее во вспомогательном/всплывающем окне.

1.5 Желательно отделять цветом/стилем шрифта локальные переменные, глобальные и пр.

1.6 Неплохо (хотя и не критично для меня) было бы "сворачивать" процедуры и структуры.

1.6 Про прочие св-ва, присутствующие редакторам, типа настраиваемой табуляции и пр. даже неудобно напоминать.

2. Работа с проектами.

2.1 Удобство при добавлении и удалении файлов проекта (пофайловое добавление - это неудобство);

2.2 Возможность индивидуальных настроек у каждого проекта;

2.3 Копирование св-в одного проекта другому, наследование св-в

3. Непосредственно IDE

3.1 Отображение дерева из глобальных процедур, переменных, структур и констант с удобным поиском не только по первым символам (у меня многие процедуры начинаются одинаково);

3.2 Т.к. компилятор и линкер у masm консольный, то IDE должно захватывать результат компиляции и парсить его для того, чтобы в случае ошибки я мог перейти на нее одним кликом;

3.3 Удобная настройка запуска внешних приложений (отладчика например) с присвоением кнопки на тулбаре и сочетания клавиш;

3.4 Удобное создание и использование репозитария кода (хотя мне не очень важно);

3.5 Поддержка средств контроля версий;

3.6 Интерфейс для написания плагинов;

3.7 В качестве общего требования - как можно больше кастомизации (я люблю все под себя настроить).

Как это ни странно прозвучит, но я считаю редактор ресурсов абсолютно не нужным компонентом IDE, в качестве внешнего приложения - сколько угодно. Не стоит изобретать велосипед.

3. Некоторые примеры IDE


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

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

1. RadASM. Автор - Ketil Olsen. На этот продукт стоит однозначно обратить свое внимание, т.к. он сделан программистом на ассемблере для программистов на ассемблере. Кроме того, сам проект полностью написан на ассемблере. Один этот факт уже достоин уважения. Итак, мы имеем быстрый, маленький по объему и бесплатный IDE написанный энтузиастом для таких же энтузиастов. Среди возможностей - настраиваемая подсветка синтаксиса, автоподсказка при впечатывании функций WinApi, поддержка нескольких ассемблеров, плюс HLA, удобный выбор цветов и составление битовой маски, в панели свойств отображаются имена элементов языка с выпадающим списком либо аргументов (для процедур и макросов), либо элементов структуры (для структур соотв.), либо значений для констант. Есть возможность создавать ресурсы. Можно сворачивать процедуры и if-else a la MS Visual Studio .NET (collapsing). Есть интерфейс для плагинов. Вкратце - в нем есть все для работы со средними и малыми проектами. Почему он не пригоден для работы с большими? Допустим, у меня в проекте 300-400 процедур. Как мне в них ориентироваться, если нет абсолютно никакого поиска, простая сортировка по имени. Хорошо, хоть при впечатывании первых букв можно в выпадающем списке выбирать. При впечатывании имен структур и глобальных переменных меню с предложением вариантов возникает, только если нажать Ctrl-Space, а для этого я слишком ленив :) Локальные переменные и аргументы вообще не индексируются. Меню программы не продумано и неудобно (мне, например), для того, чтобы что-нибудь настроить, нужно хорошо полазить по меню. Нет возможности добавлять свои группы при подсветке синтаксиса.

2. Вторым пунктом хочу упомянуть небольшие, но (кажется?) развивающиеся проекты. Negatory Assembly Studio (ссылка скрыта) хочу упомянуть лишь как яркий пример того, что автор увлекся интерфейсом в ущерб функциональности. В результате получился симпатичный (хоть и спорный) интерфейс при практически полном отсутствии функциональности. По крайней я решил сохранить свое зрение, а не изучать функционал программы с жестко закодированным шрифтом размером в 6 pt. Еще одним продуктом, достойным упоминания (уже с лучшей стороны) является также индивидуальная разработка WinAsm (Antonis Kyprianou) (ссылка скрыта). Есть collapsing для процедур, при наборе понимает локальные переменные (после моей просьбы :)) Выпадающий список из процедур все с тем же прокручиванием или поиском по первым буквам. Поддержка AddIn У этого проекта определенно есть перспективы, но и сейчас его можно использовать для небольших проектов.

Самое интересное я оставил на конец. Следующие 2 программы относятся к многофункциональным IDE, в которых можно разрабатывать не только программы на ассмеблере (точнее - не столько). Это - дорогие коммерческие приложения, (в отличие от первых - бесплатных) и серьезные игроки - по функциональным возможностям они не хуже (а иногда и лучше чем Visual Studio от Microsoft). Начнем с самого дорогого...

3. Visual SlickEdit. (www.slickedit.com) Цена - $269. И по размеру, и по спартанскому в общем интерфейсу (ну, по сравнению с VS, например) сразу видно, что продукт очень серьезный. И после продолжительного тестирования я все больше и больше убеждался в этом. Вначале вкратце о пакете вообще - одним словом - предназначен для программиста, причем неважно на каком языке писать, SlickEdit имеет поддержку таких языков, о которых я даже и не слышал :). К своему немалому удивлению и радости ассемблер оказался в их числе, но об этом попозже. Пакет содержит мощный макроязык, синтаксически напоминающий С, с большим количеством примеров. Есть средство для сравнения файлов, причем все это с подсветкой и возможностью редактирования сравниваемых файлов. Для всех языков есть поддержка соответствующего синтаксиса, настраивается абсолютно все, что можно себе представить (и чего нельзя - тоже).

 



 

Имеется поддержка нескольких систем контроля версий (включая CVS). Возможностей пакета вполне хватает для конкуренции на равных с VS, а по некоторым параметрам - и превосходить. Что же мы имеем для работы с ассемблером? Легко настраивается подсветка синтаксиса, формата чисел. Возможность запуска внешних приложений с захватом и очень быстрым парсингом вывода - компилятора, например. При этом в случае ошибки к ней можно добраться одним щелчком. Есть поиск и замена. Для проекта на ассемблере есть дерево процедур в стиле MS VS, высвечиваются также глобальные переменные, макросы и структуры, правда, элементы структуры выводятся рядом, как независимые переменные (хотя для структур на C collapsing работает), и как это настроить для ассемблера, я так и не понял (возможно, скрипт переделывать).

4. Итак, последним по списку, но не по значению у меня Source Insight 3.5 (Source Dynamics, Inc.) Сайт - ссылка скрыта (Цена - $269).

 



 

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

 



 

Еще понравилась одна вещь, которой нет ни в одном IDE - Source Insight индексирует все мои структурные элементы программы и при наведении на них курсора, показывается нужный кусок кода внизу на панели. Для этой панели можно также задать ее размер и св-ва шрифта, но подсветка синтаксиса сохранится!

 



 

О подсветке синтаксиса тоже спою дифирамб - так, как ее можно настроить в Source Insight, я больше нигде не увидел в других продуктах. Очень удобно настраивается навигация по коду - я легко настроил ее как в броузере. Можно вывести статистику об используемых структурных элементах - об их встречаемости в проекте. Есть возможность вывести Relationship для процедур (надо сказать что для ассемблера эта функция работает, но несколько странновато результаты показаны). Для настройки парсинга локальных переменных пришлось написать регулярное выражение, правда, при этом включаются все локальные переменные файла, а не процедуры - не очень удобно, но терпимо. Зато при наборе любого текста появляется всплывающее меню autocompletion, при этом я могу пробежаться по предложенному списку, а все то же окошко внизу мне показывает первый десяток строк процедуры, например. Во время работы делается backup проекта, так что я практически застрахован от всяких неожиданностей, при аварийном завершении мне будет предложено восстановить работу на том месте, где ее прервал сбой. Кнопки сборки и запуска проекта идентичны кнопкам в VS. Из минусов, хоть и не критичных - долгий парсинг вывода, но зато я опять же могу не переходя сразу к коду, просмотреть первые строчки, при парсинге сразу же идет переход к первой ошибке (если они есть). Еще одна приятная мелочь - при печатании выражения, в котором есть несколько вложенных скобок, внешние становятся больше внутренних и сразу видна вложенность (хотя для ассемблера это не часто встречается). Еще одна немаловажная особенность, которой я больше нигде не видел, это подсвечивание строк, в которых произошли изменения - т.е. сразу можно увидеть, что и где я за этот сеанс изменил. Фактически, мне Source Insight напомнил улучшенный вариант VS + Visual Assist, и пока я свой выбор остановил на этом продукте.

4. Заключение


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

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


Sars

Поиск адресов API в win95-XP

Введение


На эту "избитую" тему написано уже много статей, но представлю на ваше обозрение еще одну.
Кстати, в этой статье вы не найдете подробного описания полей PE заголовка и технологии поиска API, предполагается, что в этом вы уже разбираетесь. Так же не буду останавливаться на очевидных моментах, это уже тысячу раз перетиралось.

Итак, особенности этой статьи заключаются в следующем:
  1. Код, рассмотренный в данной статье, будет работать на платформах win95-XP, за счет получения Image Base кернела не со стека, а через анализ SEH.
  2. Для поиска имени API-функции, код использует хеш от имени этой ф-ии. Это не бросается в глаза при рассмотрении зараженного файла в HEX Editor'е, в случае незашифрованного вируса, а так же сокращает размер кода.
  3. В коде нет переменных, адреса API-функций и другие нужные нам значения помещаются в стек, что бывает полезным в том случае, когда ваш вирус не должен производить запись в переменные до извлечения адресов API.

Итак поехали... (сначала код, потом комментарии)


Start:

Call _Delta

_Delta:

sub dword ptr [esp], offset _Delta

Теперь в стеке находится дельта смещение кода, можно в этом примере не использовать, но мне так захотелось.

_ReadSEH:

xor edx,edx

mov eax,fs:[edx]

dec edx


_SearchK32:


cmp [eax],edx


je _CheckK32


mov eax,dword [eax]


jmp _SearchK32


_CheckK32:


mov eax,[eax+4]


xor ax,ax

По адресу fs:0, находится seh, цепочка адресов на обработчики исключений. Формат одной записи таков:

next_handler dd ? ; указатель на следующую такую же запись
seh_handler dd ? ; адрес обработчика исключения

Последний указатель на следующую запись имеет маркировку 0FFFFFFFFh, а адрес последнего обработчика находится где-то в kernel. В общем, глядите в отладчик, мы нашли адрес последнего обработчика, а значит и адрес внутри kernel. Дальше выравним полученный адрес на 64 Кбайта, т.к. kernel грузится по адресу кратному этому значению. Теперь нам осталось найти Image Base пресловутого и небезызвестного кернела. Делается это путем поиска сигнатуры MZ и проверки на PE формат...

_SearchMZ:
cmp word ptr [eax],5A4Dh
je _CheckMZ
sub eax,10000h
jmp _SearchMZ
_CheckMZ:
mov edx,[eax+3ch]
cmp word ptr [eax+edx],4550h
jne _Exit

Так, теперь сравним слово по полученному адресу с 'MZ', если не совпало, то отнимем 64Кбайта, и повторим, если совпало, то проверим это заголовок PE или нет. Если да, то можно утверждать, что Image Base Kernel найден, если нет, то выйдем. Существует ли вероятность не найти Kernel? При использовании seh, навряд ли, по крайней мере, я этого не наблюдал при тестировании. В случае, когда адрес внутри Kernel берется со стека, заводится счетчик, чтоб не вылезти черт знает куда, но это описано в др. статьях. Для перестраховки можно завести свой обработчик исключений.

_SearchAPI:
mov esi,[eax+edx+78h] ;Export Table RVA
add esi,eax ;Export Teble VA
add esi,18h
xchg eax,ebx
lodsd ;Num of Name Pointers
push eax
lodsd ;Address Table RVA
push eax
lodsd ;Name Pointers RVA
push eax
add eax,ebx
push eax ;Index
lodsd ;Ordinal Table RVA
push eax
mov edi,[esp+4*5] ;Delta offset
lea edi,[edi+HeshTable]
mov ebp,esp

Здесь я не буду заострять особого внимания, почему, читайте выше. (см. документацию по PE формату).

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

В edi у нас смещение таблицы хешей искомых функций.

Т.к. дальше мы будем помещать в стек адреса найденных API ф-ий, то сохраним указатель стека в ebp, через ebp потом и будем обращаться к найденным адресам. Несколько слов, что такое index, первоначально он равен Name Pointers и указывает на таблицу адресов имен экспорта, каждый элемент таблицы равен двойному слову, и указывает на начало ASCII строки с именем API функции, если к Index прибавить 4, то он будет указывать на следующий адрес в таблице адресов имен... Конечно много непонятного, но поглядите в отладчик и половина вопросов отпадет.

_BeginSearch:
mov ecx,[ebp+4*4] ;NumOfNamePointers
xor edx,edx
_SearchAPIName:
mov esi,[ebp+4*1] ;Index
mov esi,[esi]
add esi,ebx

В ecx кол-во экспортируемых функций используем как счетчик, чтоб не найти какую-нибудь муть.

Обнулим edx, там потом будет порядковый номер найденной функции, начиная с нуля. Это понадобится для нахождения адреса. В esi адрес ACSII имени API функции, первоначально указывает на первую.

_GetHash:
xor eax,eax
push eax
_CalcHash:
ror eax,7
xor [esp],eax
lodsb
test al,al
jnz _CalcHash
pop eax

Далее считаем хеш от имени функции, чтобы потом сравнить с хешем от требуемой функции, помните, что на имя у нас указывает esi. На выходе в eax будет хеш.

OkHash:
cmp eax,dword ptr [edi]
je _OkAPI
add dword ptr [ebp+4*1],4 ;I=I+4 (I--Index)
inc edx
loop _SearchAPIName
jmp _Exit

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

_OkAPI:
shl edx,1
mov ecx,[ebp] ;OrdinalTableRVA
add ecx,ebx
add ecx,edx
mov ecx,[ecx]
and ecx,0FFFFh
mov edx,[ebp+4*3] ;AddressTableRVA
add edx,ebx
shl ecx,2
add edx,ecx
mov edx,[edx]
add edx,ebx

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

push edx

cmp word ptr [edi+4],0FFFFh ;0FFFFh-End of HeshTable

je _FindFirstFile

add edi,4


_NextName:

mov ecx,[ebp+4*2] ;NamePointersRVA

add ecx,ebx

mov [ebp+4*1],ecx ;Index

jmp short _BeginSearch

Адрес найден!!! Что и требовалось доказать, помещаем его в стек, смотрим последняя ли это требуемая функция из таблицы хешей, если нет, то устанавливаем edi на следующий хеш. Возвращаем Index в первозданное состояние, т.е. что бы он указывал на адрес имени первой функции в таблице экспорта кернела, и повторяемся...

Полностью рабочий пример можно найти ссылка скрыта.
Пример тестировался на различных платформах Win95-XP, за что отдельное спасибо Ingrem'у.
Т.к. я не обладаю творческими изысками, в тексте могут содержаться неточности, о коих прошу сообщать на sars@ukrtop.com Исправлю...

Если кому-нибудь поможет данная статья, не сочтите за труд черкануть пару строк автору, тогда возможно продолжу эту тему. Все вопросы по коду туда же, кроме таких как: "Что такое стек?" и т.д.

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

Ссылки:


ссылка скрыта – есть неплохая обучалка по Win32 VX от Billy Belcebu
ссылка скрыта – имеется хороший FAQ для начинающих вирмейкеров и еще много чего


/\ & ()

(алгоритмы и оптимизация)

Циклические счетчики


Несколько реализаций алгоритмов циклического счётчика, который при достижении верхней границы переходят в нижнию, и наоборот, предложили masquer, Fixer, and Black_mirror:

by masquer


mov eax, A; initial value
mov edx, X+1 ; upper limit value + 1
mov ecx, -1 ; forward = -1 backward = 1
@@: sub eax, ecx
mov edi, eax
add edi, 1
sbb ebx, ebx
add edi, edx
and edi, ebx
add eax, edi
mov edi, eax
sub edi, 1
sbb ebx, ebx
not ebx
and eax, ebx
mov edi, eax
sub edi, edx
sbb ebx, ebx
and eax, ebx
cmp eax, X
jz @F
; 1 cycle takes 11 clocks
; here we can process the value calculated before
jmp @B
@@:


----------------------------------------------------------------


by Fixer


Вероятно самый компактный алгоритм

mov eax, A ; initial value

mov ebx, X+1 ; upper limit value + 1

mov ecx, step


sub eax, ecx

_beg_loop:

add eax, ecx

xor edx, edx

add eax, ebx

div ebx

mov eax, edx

;

jmp _beg_loop


----------------------------------------------------------------


by Black_mirror


inc eax

cmp eax,X+1

sbb ebx,ebx

and eax,ebx


dec eax

sbb ebx,ebx

and ebx,X+1

add eax,ebx


 



По следам FORUM.WASM.RU

И пробуждается поэзия во мне....

KiNDeR


Я заглянул в себя и ужаснулся!...
Увидев там лишь пустоту и пыль.
Как сделать так, чтобы мой дух проснулся,
И суету мирскую подавил.

Я попытался разогнать сомненье
И на минуту даже счаслив стал.
Подумал я, что дух мой пробудился,
А сердце стало чистым, как кристал.

Но это все иллюзия, не больше...
Не знаю я, как с ней бороться мне.
Но не хочу, чтоб жизнь моя казалась-
Малевича квадратом на стене!...


MSDN

Aquila



В душе кипит бешенная яpость,
Тело pазpывает безумная стpасть,
Вопящее тело может упасть,
В падении на пол сбавляя скоpость.

Стоны и кpики заполняют уши,
От злобы пеpекошенно pот pычит,
Втоpя ему живот pычит,
Банда воpвалась в pайские кущи.

Сознанья усталого иссушенный залив
Пеною мудpости вдpуг обмочился,
Пpиступ интеллекта внезапно случился,
Здpавого смысла буpный отлив.

Мягкие, мелкие волны,
Солнцем ночным освещаются,
Достанный хелпом пpогpаммеp
Hагpетым пивком упивается

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

Суббота это такой день, когда можно развалится перед экраном, почитать какую-нибудь «лабулабуду», вроде этой рассылки :), на что оно в принципе и рассчитано. Можно почитать что-то из классики, как, например эта рассылка (что тоже продумано), или немного утреннего юмора, (можно и здесь), или мистики, что тоже предусмотрено.

Те, кто не умеет читать, могут просто долго смотреть на заставку, и медитировать с дымящим кофе.

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

Кстати нас наконец-то затопили. Это приятно. Неприятно, то, что затопили сверху. Вообще сплошная чёрная полоса. Вчера уборщица Соня, пытаясь довести корпус компьютера до блеска стёрла наклейку Win98 ОEM, и довела нашего начальника до инфаркта. Теперь она убирает не выше плинтуса.

Ещё немного новостей из Underworld.

Намедни нашему товарищу букинисту попалось замечательное издание из 5 корпуса общежития некого факультета. Книга называется: «Учёт посещений общежития #5». Я бы и не рассказывал об этом случае, и не рассказывал о как обычно присутствующих записях: «Шварцнегер к Терминатору», «Чебурашка к Гене», если бы не интересный факт. Из 10634 записей, более 4000 – были сделаны на 626 комнату. Что ещё более странно, что в комнате живут обычные студенты, и пока никаких аномалий не было выявлено.

Вообще говоря, Мистика – замечательная вещь в быту. Кажется, в неё никто не верит, но все плюют, через плечо, стучат по дереву, посылают к чёрту, не бреются, например, перед казнью, и так далее.

Сегодня мне вспомнилась замечательная история, не менее загадочная, чем с 626 комнатой.



* * *

В одном местечке, где теперь провисает потрёпанный плакат «Выставка», раньше было село. Как звалось оно, мало кто помнит. Вообще солнечный край – тут сады, там поля, да и город рядом. Вот - 17-ый год пришел. Террор, реки крови по великой России. Добежало все это и до нашего местечка. Но, Семнадцатый – это потом, а пока… Хозяин – Барин.

Был камердинер один - старичок, лет 60-ти. Как соберутся гости у барина в бильярд играть, он навытяжку стоит, в руке поднос. На подносе: выпивка, закуски, сигары дорогие, фрукты разные. Компания раззадорится, до утра гуляет, а он, бедный, навытяжку стоит, с подносом. Это в 60-то лет то! И так он этот биллирад возненавидел, что даже во сне кричал.

Думал и умрёт так, стоя за бильярдом. Но судьба дура, не знаешь, где порадует, а где огорчит. Пришёл семнадцатый год, власть поменялась, мужики шабаш подняли.

Радуются – барина на фонарный столб повесили, и петуха красного запускают.

Камердинер тоже ошалел: сукно на столе рвет, кии ломает, табуретом тяжелым зеркала все раскрошил. Всё разгромил, только одни шары бильярдные остались, а они были слоновой кости отличной работы. И камердинер то ли устал уже, толи работу мастерскую пожалел, толи разбить их не смог. Собрал он их в обрывки сукна и закинул в болото. Туда подальше, в самую трясину, чтоб наверняка уже.

А потом в селе чекисты объявились. Бильярд оказался работы одного очень известного итальянского мастера, барин за него 1000 рублей золотом отвалил, да скакуна лучшего из конюшен, крови арабской. Чекисты хотели Самому его подарить. Но пропал Бильярд. Мужички не видали, а камердинер, хоть и перепужался до заикания, а по дурости не пропал. Не видал и все тут. Пожар был, сгорело все. Да, стол обгоревший так и стоял там, а еще что в доказательство? Вот так покрутились, поискали ну и пошли ни с чем. Старичок поволновался-поволновался, да все так и устаканилось.

Потом Гражданская, потом репрессии и голодомор, неспокойное время. А там и Великая Отечественная грянула. Камердинера сын, Александр, Сашка, всю войну прошагал до самого Берлина, без ранений, ни царапины, домой героем пришел, в медалях весь, даже орден был! Потом - город восстанавливать - в войну здесь просто мясорубка была, два артбатальона так вспахали плавни, что железо военное до сих пор откапывается. А потом - в слесаря на завод. Первый инструментальщик был на заводе. Там и сын родился, Санька, Сан Саныч значит...

(продолжение следует...)



DirectOr

Socket vs Socket

или использование сокетов MS Windows в ассемблерных программах

Теория применения сокетов совсем недавно была описана в статье “Сокеты M$ Windows”, которую вы можете легко найти в разделе “Статьи/Сеть”. Не будем повторяться, но вкратце скажем, что сокеты определяют логику для программирования сети аналогичную работе с файлами. В разделе “Исходники/Сеть и коммуникации” уже давно (со времен wasm.zite.ru) имеется рабочая небольшая программа “ledilog.zip\connect.asm”, в которой на основе использования сокетов реализован обмен текстовыми сообщениями между двумя компьютерами в сети. Таким образом, накопилась критическая масса, состоящая из некоторого минимума теории и кое-каких практических материалов. Все это и подтолкнуло автора к использованию в своем проекте (детали в данном случае не важны) связи по локальной сети между компьютерами на основе сокетов MS Windows. Однако все оказалось не так просто, как хотелось. Впрочем, как и всегда...

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

Для впервые заинтересовавшихся сетевым программированием есть смысл пояснить, что же из себя представляют сокеты. Сокет можно рассматривать, как конечный пункт передачи данных по сети. Сетевое соединение - это процесс передачи данных по сети между двумя компьютерами или процессами. Тогда сокет - конечный пункт такой передачи данных. Другими словами, когда программы используют сокет, для них он является абстракцией, представляющий одно из окончаний сетевого соединения. Для установления соединения в абстрактной модели сокетов необходимо, чтобы каждая из сетевых программ имела свой собственный сокет. Недаром слово socket переводится с английского как гнездо или разъем!

Связь между двумя сокетами может быть ориентирована на соединение, а может быть и нет. С чем это едят? Все дело в том, в сетевом протоколе TCP/IP (а на сегодня это “родной” протокол интернета, да и большинства локальных сетей) предусмотрено два режима: ориентированный и не ориентированный на соединение. В ориентированных на соединение протоколах данные перемещаются как единый, последовательный поток байт без какого либо деления на блоки. Конечно, имеется в виду логика процесса, а не то, что физически происходит в среде передачи. В не ориентированных на соединение протоколах сетевые данные перемещаются в виде отдельных пакетов, называемых датаграммами. Как мы уже говорили, сокеты могут работать как с одними, так и с другими. В дальнейшем при создании сокета мы с этим столкнемся. А сейчас достаточно запомнить, что датаграммы могут приходить к получателю не подряд, а в непpедсказуемой последовательности. Так что датаграммы мы использовать в данном примере не будем, а только режим, ориентированный на соединение!

С аналогией между сокетами и файлами тоже не все просто. Конечно, для начала тоже нужно создать сокет, получить его дескриптор, а потом, используя этот дескриптор, писать или читать… Но отличие в том, что дескриптор файла указывает на уже существующий или только что созданный файл или на устройство, дескриптор сокета не содержит каких-либо определенных адресов или пунктов назначения сетевого соединения. Этот факт существенно отличает его от любого другого дескриптора стандартной системы ввода-вывода. Программа, работающая с сокетами, сначала образует сокет и только потом соединяет его с точкой назначения на другом конце сетевого соединения. Если бы файловый ввод-вывод состоял из таких же шагов, приложение сначала получало бы дескриптор файла, а затем привязывало бы его к имени определенного файла на диске.

Несмотря на то, что первоначально сокеты появились в системе UNIX (т.н. сокеты Беркли), на данный момент разработчики Windows давно модифицировали и расширили интерфейс для работы сокетов. Таким образом, сейчас мы имеем многочисленные функции API, так или иначе связанные с сокетами:

WSAStartup()*

инициализирует Windows Socket dll

WSACleanup()*

прекращает использование этой dll

socket()*

функция создает сокет с заданными параметрами

WSAAsyncSelect()*

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

bind()*

ассоциирует локальный адрес с сокетом

listen()*

устанавливает сокет в состояние, в котором он
слушает порт на предмет входящих соединений

accept()*

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

connect()*

функция подключает созданный сокет к
указанному адресу

select()

функция определяет статус одного или более
сокетов

shutdown()

функция запрещает посылать и/или принимать
данные от сокета

ioctlsocket()

функция управляет режимом сокета

getsockopt()

функция возвращает установки сокета

recv()*

функция получает данные от сокета

send()*

функция посылает данные в ранее
подключенный сокет

sendto()

функция посылает данные по указанному адресу

recvfrom()

функция получает датаграммы от сокета

Звездочками отмечены те функции, которые будут встречаться в тестовой программе. Учтите, что кроме перечисленных существуют и многие другие… А теперь приступим к более подробному рассмотрению того, как этим хозяйством пользоваться. Скачайте пример ссылка скрыта. Программа начинается как обычно с подключения необходимых библиотек, имеет секцию инициализированных и неинициализированных данных (.data и .data?). Обратите внимание на задание констант. Все константы (определение использованных ресурсов и др.) вынесены в отдельный файл SocSoc.inc и они нам в этот раз не интересны. Другое дело строка:

WM_SOCKET equ WM_USER + 100

Это задание численного значения сообщения WM_SOCKET, того самого сообщения, которое нам в дальнейшем будет посылать Windows при работе с сокетами. Дело в том, что Windows не использует для своих стандартных сообщений значения выше WM_USER, поэтому мы легко можем использовать этот диапазон для нужд своего приложения. Отметим этот важный момент, в дальнейшем мы еще раз вернемся к обсуждению сообщений типа WM_USER+...

Секция кода начинается с определений макросов, используемых при анализе сообщений нашим окном. Конечно, надо бы вынести макросы в отдельный файл .inc – файл и подключить его с помощью include. Но в данном случае их всего два и хотелось максимально упростить для понимания начинающими этот проект... Только поэтому макросы помещены в начало секции .code. Ну не данные же это в самом деле! Так что не делайте, как я.

Cначала опишем вкратце общий алгоритм работы нашей программы, а затем приступим к подробному анализу кода:

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

2) инициализируем dll, ответственную за использование сокетов Windows;

3) если получена ошибка инициализации, то делать дальше нечего – выходим с ошибкой из программы! Если же вызов dll прошел успешно, то продолжим работу: получаем адрес командной строки, (если таковая была при запуске – в данном примере не используется). Вызываем функцию WinMain – которая, собственно и определяет логику работы приложения и которая будет подробно рассмотрена ниже;

4) после выхода из WinMain возвращаемся в Windows.

А теперь разберем, что происходит в третьем пункте. В объявлении функции WinMain все достаточно традиционно и подробный анализ пропускаем. Для нас интерес составляет процедура WndProc главного окна программы, которая получает и реагирует на сообщения Windows. Сразу же после создания нашего окна процедура WndProc получит соответствующее сообщение: WM_CREATE. Это удобный момент для приведения в исходное состояние всего нашего хозяйства. Раз мы собираемся обмениваться с другими компьютерами, хорошо бы узнать кое-что и о себе, вернее о том компьютере, где запущен экземпляр нашей программы. Можно получить имя компьютера в виде текстовой строки в buffer соответствующей функцией, а затем оттуда скопировать в выходной буфер:

invoke gethostname, addr buffer, sizeof buffer
invoke wsprintf, addr buf_out, addr szName, addr buffer

Аналогично получаем IP адрес компьютера по имени:

invoke gethostbyname, addr buffer

Для этого есть в API специальная функции gethostbyname, которая возвращает информацию о компьютере по его имени, заполняя специальную структуру hostent. Приведем ее для лучшего понимания дальнейших действий:

hostent STRUCT
h_name DWORD ? ; char FAR *
h_alias DWORD ?
h_addr WORD ?
h_len WORD ?
h_list DWORD ? ; char FAR * FAR *
hostent ENDS

Элемент структуры h_name – не что иное, как указатель на строку с именем нашего компьютера. А вот элемент h_list более интересен! Это указатель на список IP-адресов компьютера. Их может быть несколько, по числу сетевых интерфейсных карт, установленных в компьютере. Причем адреса представлены в сетевом порядке байт. Нас интересует первый из них. Вот как добраться до этого адреса:

mov eax, [eax+12] ; получаем указатель на элемент h_list в HOSTENT

mov eax, [eax] ; получаем указатель на указатель на IP
mov eax, [eax] ; получаем указатель на строку IP в сетевом порядке байт

После преобразования функцией inet_ntoa, сохраняем полученный строковый формат IP адреса (вида 127.0.0.1) в еще один выходной буфер. Во всем этом есть один тонкий момент, непонимание которого может привести к дальнейшему использованию неправильных данных! Те функции Windows Sockets API, которые возвращают указатели на различные данные (а это часто так и есть) гарантирует их (данных) сохранность только до следующего вызова функций Sockets API. Поэтому необходимо сразу же копировать все необходимые нам значения в отведенные нашей программой для этого переменные! Это в полной мере относится и к функции gethostbyname.

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

А вот дальше начинается собственно работа с сокетами. Тут сразу оговоримся, что в программе будет использоваться два сокета, один открываем на прием сразу же при инициализации главного окна, другой в дальнейшем будет открыт на передачу. Картина сильно напоминает дуплексный режим связи, кто понимает...

Итак. Сначала нам нужно открыть сокет:

invoke socket, AF_INET, SOCK_STREAM, 0;

Параметры следующие:

AF_INET – семейство, в версии 1.1 только AF_INET;

SOCK_STREAM - тип сокета, который вы желаете использовать.

Помните, мы выбираем связь, ориентированную на соединение, и говорим НЕТ датаграммам (SOCK_DGRAM);

0 – протокол, не устанавливать никакого протокола.

И если нет ошибки, сохранить его дескриптор для дальнейшего использования:

mov hSocket2, eax

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

invoke WSAAsyncSelect, hSocket2, hWnd, WM_SOCKET, \
FD_ACCEPT+FD_READ

где hSocket2 - дескриптор сокета (вот сразу и пригодился)

hWnd - дескриптор главного окна приложения

WM_SOCKET - сообщение, нами же определенное

FD_ACCEPT+FD_READ – маска, задающая интересующие нас сообщения от этого сокета (в данном случае мы хотим получать уведомление о попытке подключения и уведомление о готовности данных для чтения).

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

invoke htons, Port
mov sin.sin_port, ax
mov sin.sin_family, AF_INET
mov sin.sin_addr, INADDR_ANY

Далее необходимо сопоставить локальный адрес, представленный в структуре с ранее открытым сокетом:

invoke bind, hSocket2, addr sin, sizeof sin

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

invoke listen, hSocket2, 5

Где указываем, естественно, дескриптор сокета и некое число, определяющее максимальную длину очереди ожидаемых подключений. Так что число пять просто перекочевало из программы – оригинала. Поставьте десять, если это нужно. На этом, собственно, и заканчивается настройка сокета на прием, т.е. слушающего указанный порт.

Затем, если пользователь отважится и выберет пункт меню “Подключить”, то создаем диалоговое окно выбора IP адреса подключения:

invoke DialogBoxParam, hInstance, addr DlgNameZ, hWnd, addr DlgProcZ, 0

После выхода из которого (если адрес был указан) приходит пора создавать сокет на передачу:

invoke socket, AF_INET, SOCK_STREAM, 0

.if eax != INVALID_SOCKET

mov hSocket, eax

.else

invoke ERROR, addr ErrorCrSocket, 1

.endif

invoke WSAAsyncSelect, hSocket, hWnd, WM_SOCKET,\

FD_CONNECT+FD_CLOSE

Здесь пока все тоже, что и ранее с первым сокетом, только теперь нас интересуют сообщение о состоявшемся подключении и уведомление о закрытии сокета (FD_CONNECTи FD_CLOSE).

Далее по аналогии формируем структуру , содержащую адрес подключения:


invoke htons, Port

mov sin.sin_port, ax

mov sin.sin_family, AF_INET

invoke inet_addr, addr AdresIP

mov sin.sin_addr, eax


Тут немного пояснений. После выхода из диалогового окна выбора адреса в буфере AdresIP содержится IP-адрес в привычном для нас виде a.b.c.d. Для удобства отладки приложения указан и адрес “самого себя”. Это - 127.0.0.1. Дело в том, что согласно соглашениям TCP гарантируется, что все пакеты на все адреса, начинающиеся со 127.ххх в физическую линию передаваться не будут. Таким образом, это сильно облегчает жизнь тем людям, у которых нет возможности все время сидеть на двух компьютерах в сети сразу. Пример полностью работает и на ОДНОМ компьютере. Только адрес надо предварительно преобразовать функцией inet_addr из строкового формата с точками, а потом уже заносить в .

И апофеоз, надо подключить созданный сокет к указанному в IP- адресу:

invoke connect, hSocket, addr sin, sizeof sin

Вот после этих строк процедура главного окна получит сообщение от Windows: WM_SOCKET с lParam = FD_CONNECT. Тут самое время сообщить о состоявшемся (или нет) соединении с помощью простого MessageBox-а. Ничего дополнительно делать не надо.

Что же произойдет, когда слушающий сокет обнаруживает попытку соединения? Не забывайте, что мы можем коннектиться к самим себе, но это существа дела не меняет. Все просто. При наступлении какого-либо события, связанного с сокетом, Windows шлет сообщение процедуре главного окна, в данном случае пошлет тот же WM_SOCKET, теперь уже с параметром FD_ACCEPT. Делать нечего, нужно вызвать специальную функцию API, чтобы разрешить входящее соединение:

invoke accept, hSocket2, 0, 0
mov hClient, eax ; сохранить дескриптор сокета

Будьте бдительны! Это еще один важный момент, который в свое время попортил мне немало нервов. Конечно, входящее соединение надо сначала разрешить, а если мы не прореагируем на попытку соединения, то следующего такого сообщения от сокета больше не получим. Но более того, возвращаемое функцией accept значение представляет из себя дескриптор НОВОГО сокета, который мы должны будем далее использовать для приема данных. Его, конечно, надо сохранить. И именно его использовать в функции приема данных! Первоначальный слушающий сокет остается открытым.

Теперь о том, как передать данные, если уж связь налажена. Специально в программе есть два места, откуда можно передавать. Это и главное окно, пункт меню “Передать…”, и диалоговое окно приема/передачи. Рассмотрим второе. Это участок процедуры DlgProcZ1 диалогового окна:

.if eax == IDC_BUTT6; нажата кнопка "отослать"

.if Connected == 1

invoke GetDlgItemText, hDlg, IDC_EDIT02, addr BytSend, 64

invoke lstrlen, addr BytSend

.if eax < 64

invoke send, hSocket, addr BytSend, 64, 0

.if eax == SOCKET_ERROR

invoke ERROR, addr ErrorReseau, 0

.endif

.endif

.endif

При нажатии кнопки “Отослать” проверяем, установлено ли уже соединение и если да, то получаем с элемента редактирования текста IDC_EDIT02 собственно текст (до 64 байт) в буфер BytSend. Проверяем длину строки. И передаем весь буфер в сокет. Функция send имеет следующие параметры:

hSocket – дескриптор (раннее подключенного функцией connect) сокета

addr BytSend – указатель на передаваемый буфер

64 – длина буфера

0 - флаг, определяющий поведение функции (для нас только такой).

Сразу же не забудем проверить признак ошибки при передаче. Мало ли чего!

А что же прием? Если мы (или какой-нибудь другой компьютер) прислали данные на слушающий сокет, для которого была выполнена функция accept (помните, мы сохраняли дескриптор в hClient), опять процедуре главного окна летит сообщение WM_SOCKET уже c параметром FD_READ. Вот кусок соответствующего кода:

.elseif ax == FD_READ

HIWORD lParam

.if ax == NULL ; отсутствует ошибка

invoke recv, hClient, addr BytRecu, 64, 0

mov eax, 1 ; установить признак, что в буфер чтения получено...

invoke SendMessage, HWND_BROADCAST, MessageIPC, eax, 0

Ну, с функцией собственно приема recv, надеюсь ясно. Параметры аналогичны функции send:


hClient – дескриптор сокета (ранее полученный функцией accept!)
addr BytRecu – указатель на приемный буфер
64 – длина этого буфера
0 - флаг (для нас такой).

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

Картинка у меня долго не складывалась, пока не появилась статья от CyberManiac (см. “Статьи/Секреты Win32/IPC”). Посвящена она механизму обмена данными между приложениями (Interprocess communication, сокращенно – IPC). Почитайте. Но у нас ситуация несколько иная. Приложение у нас одно и в данном случае не надо пересылать данные. Пусть они себе лежат в буфере. Надо всего лишь подать сигнал процедуре диалогового окна приема/передачи о факте приема от сокета. А как? Сокет–то шлет сообщения главному окну, а индицируем принятые данные в другом, диалоговом, которое ничего не знает... Да и самого диалогового окна приема/передачи в момент приема данных может не быть на экране. Но нас это не сильно беспокоит, не хотите – не надо. Наше дело сообщить. Посылаем широковещательное сообщение всем окнам функцией SendMessage. Параметры следующие:

HWND_BROADCAST – идентификатор окна, процедура которого получит

сообщение, или HWND_BROADCAST,

тогда сообщение посылается всем окнам верхнего уровня в системе,

в том числе и невидимым,…но сообщение не посылается дочерним окнам;
MessageIPC – номер зарегистрированного ранее сообщения
eax – wParam, дополнительный параметр сообщения (DWORD)
0 – lParam, аналогично.

А вот и часть кода в процедуре окна DlgProcZ1, ответственная за прием такого сообщения:

; если получено зарегистрированное нами сообщение

.elseif eax == MessageIPC

; вывести полученные данные из буфера приема в контрол IDC_EDIT03

invoke SetDlgItemText, hDlg, IDC_EDIT03, addr BytRecu

Можно заметить, что не анализируется wParam и lParam. Нам достаточно только факта самого сообщения. А теперь рассмотрим, как все-таки регистрировалось сообщение MessageIPC при входе в программу. Для этого надо было придумать уникальную текстовую строку:

MsgString db "MessageSocDirectOr", 0

Это своего рода пароль для разных приложений, по которому они могут узнавать “свое” сообщение. Такой себе “у вас продается славянский шкаф?”. Далее надо провести собственно регистрацию:

invoke RegisterWindowMessage, addr MsgString

И на выходе, если eax не равно нулю, значит ошибки нет и там содержится код сообщения. Какой он? Но мы же не хотим, знать больше, чем положено. Надо просто запомнить его и использовать по мере надобности:

mov MessageIPC, eax ; сохранить присвоенный сообщению номер

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

Вот и все, что касается азов работы с сокетами. Конечно, не забывайте закрывать сокеты, когда они уже не нужны. Вот кусок кода при закрытии главного окна:

; закрываем сокеты и сообщаем о том, что dll нам больше уже не нужна

invoke closesocket, hSocket

invoke closesocket, hSocket2

invoke WSACleanup ; dll больше не нужна

invoke PostQuitMessage, NULL


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

.elseif ax == FD_CLOSE

HIWORD lParam

.if ax == NULL ; отсутствует ошибка

invoke closesocket, hSocket

mov hSocket, 0

И еще. Возвращаемся к обсуждению сообщений типа WM_USER+… Цитата из CyberManiac-а: “…в некоторых программах для обмена информацией используются сообщения WM_USER+N, в частности, именно так реализован механизм IPC в WinAmp. Однако Microsoft имеет по этому поводу свое особое мнение - согласно MSDN, сообщения от WM_USER+0 до WM_USER+3FFFh включительно используются только для передачи данных внутри или между окнами одного класса.” Так что одно дело получать сообщения в процедуре окна от сокетов Windows, и совсем другое, слать IPC широковещательные сообщения с WM_USER+… в качестве параметра.

Все происходящее при открытии/закрытии сокетов очень удобно наблюдать при помощи утилиты Tcpview.exe (Mark Russinovich - ссылка скрыта). Рекомендую! Хорошо видны порты, а также протоколы, их использующие… В частности, хорошо отслеживается эффект “расщепления” сокета на два после выполнения функции accept. Кстати, появляется еще один (кроме первоначального 3030) используемый порт, что-нибудь типа 30хх, в зависимости от наличия свободных.

Подведем итог. Для того, чтобы организовать обмен информацией между двумя компьютерами в сети, в программе создается два сокета, используемые раздельно для приема и для передачи. Имеется главное окно приложения и в разных местах программы могут создаваться два диалоговых окна. Сокеты создаются в процедуре главного окна, но это не принципиально. Существенно важно то, что оба они при наступлении соответствующих событий посылают предопределенное нами самими сообщение WM_SOCKET именно главному окну. Где в цикле разбора сообщений от Windows уже анализируются и обрабатываются… Таким образом, ВСЯ работа с сокетами сосредоточена в процедуре главного окна. Существует затруднение, состоящее в том, что данные, полученные с сокета на прием, могут быть нужны нам в диалоговом окне приема/передачи, которое у нас не получает соответствующего сообщения от сокетов Windows. Вопрос решается с помощью посылки главным окном широковещательного сообщения о факте приема данных всем другим окнам. Диалоговое окно (или другое приложение, если захотите) может легко получить такое сообщение и обработать… Конечно, это не единственный способ взаимодействия между главным окном и диалоговым. Но соль в данном случае в том, что при поступлении данных от сокета диалогового окна-то может и не быть. Поэтому был выбран путь, когда программа просто сигнализирует о факте приема данных от сокета, а далее уже не важно, нужны они кому-нибудь, или нет.

В описываемом примере было рассмотрено использование только некоторых основных функции API, ответственных за работу с сокетами Windows, в основном разобраны их параметры, кроме тех, значение которых не критично. Но! Возникает (у меня, по крайней мере) несколько вопросов. Ну, во-первых, номер порта. Какой он может быть? А должен? Какие правила на этот счет. Кроме тех, которые широко известны: 80 порт для HTTP и др. Конечно, Windows при попытке создать сокет с уже используемым портом вернет код ошибки. Попробуйте запустить два экземпляра тестовой программы! Но разве от этого легче? Можно, конечно, в программе перебирать номера, но как тот, другой компьютер узнает, чем сердце успокоилось?
Во-вторых, понятно, почему используется два раздельных сокета. Чтобы можно было независимо передавать данные и читать их тогда, когда они поступают извне. А как можно обойтись одним сокетом? Если уж действительно сокеты задают логику работы с сетью, аналогичную работе с файлами, то хорошо было бы и писать, и читать, работая с одним “файлом”. И более того. Что делать, когда надо посылать сообщения нескольким компьютерам, например, десяти… Создавать десять сокетов или перебирать один с разными IP–адресами по очереди? Что-нибудь типа широковещательного сообщения? Вообще, тот же пример, расширенный на n-компьютеров, где каждый может обмениваться с каждым, выглядит пока туманно. Более или менее ясно, как на один слушающий сокет принимать входящие с разных адресов. Не зря же мы писали: invoke listen, hSocket2, 5. 5! Хотя программа все равно могла сохранить только один дескриптор сокета после invoke accept…

Подводя итог итогам, можно сказать, что вопросы остаются. Надеюсь, что новичкам в работе с сокетами пример пригодится, а если появятся работающие ответы на вопросы (эти и другие), то будет и продолжение…

(c) DirectOr октябрь 2003г.


Крис Касперски

ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

(избранное)