Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Свитки
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

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

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  18:09[Войти] | [Зарегистрироваться]

ЯП, ОПП и т.д. и т.п. в свете безопасности программирования. III

Дмитрий Логинов
дата публикации 16-10-2000 14:02

ЯП, ОПП и т.д. и т.п. в свете безопасности программирования. III

Я специально сделал небольшой скачок во времени, чтобы уделить внимание моей любимой кампании. Теперь я хотел бы вернуться в 70ые. Подведем под всем этим большую жирную черту. Конец 40ых начало 70ых - это эпоха(можно даже назвать ее "золотой") процедурных языков. Эпоха господства АЛГОЛоподобных языков. Проблема декомпозиции программы считается как бы решенной. При разработке языков делается упор на простоту в понимании и безопасность в применении. Точнее выражаясь, выявление большинства ошибок на этапе компиляции. В этом плане АЛГОЛоподобные языки со строгой типизацией и отсутствием операций с указателями или вообще отсутствием оных занимали одно из лидирующих мест. Но давайте посмотрим на причину появления проблем безопасности программирования. Вернемся в далекие тридцатые.

Численные математические методы начали переходить на этап реализации где-то как раз в это время. Было три таких гениальных математика: Алан Тьюринг, Гедель и Черч(имя не помню). Все они работали над проблемой "построения машины" занимающейся вычислениями. Существовало и существует три способа решения этой задачи.

Первый: Машина работает по правилам логики предикатов первого порядка. Не булевская алгебра, нет. Ну, по правилам этой логики, считается, мыслит человек. Обрисую приблизительно. Заранее прошу прощения у математиков или людей лучше знакомых с этой проблемой. Если это не интересно, то можете пропустить этот абзац. Итак, программа или задача представляет собой Формальную или Аксиоматическую Систему. Она состоит из АЛФАВИТА(например цифры), ФОРМУЛ(правила использования алфавита - СЛОВА, в случае с цифрами это будут числа) и АКСИОМ, сочетание ФОРМУЛ. Последнее легко увидеть на цифрах, как правила сложения, вычитания и т.д. Все это дело, т.е. Формальная Система, доказывает или опровергает ЦЕЛЕВОЕ УТВЕРЖДЕНИЕ. ЛОГИКА ПРЕДИКАТОВ ПЕРВОГО ПОРЯДКА для таких систем - это когда предикаты(логические функции, аксиомы) принимают значение либо "TRUE", либо "FALSE". Я не буду дальше описывать, но там есть АТОМЫ, КОНСТАНТЫ, ПЕРЕМЕННЫЕ, и т.д. Смысл - мы при помощи всех этих дел записываем свою логику, свои рассуждения. Никаких типов, никаких указателей или зависимостей от архитектуры. Человек, программируя, просто записывает доказательство.

Ноги всего этого дела(интуиционистской математики, от слова интуиция) растут от теории доказательств, зародившейся в начале века. С панталыку тогда всех сбил Гильберт со своей программой о том, что если собрать все математические знания, то можно доказать или опровергнуть любую теорию. Так вот, а Гедель и Тьюринг доказали, что задача поставленная Гильбертом НЕПОЛНАЯ, говоря по-нашему не разрешаемая. Поэтому-то Тьюринг пошел искать другое решение в "построении вычислителя", что привело к опубликованию в 1936 году его труда "On Computable Numbers". Это дало почву к появлению принципа Фон Неймана(обновляемая память) и началу компьютеризации.

И лишь в 1965 году Робинс спас дело Гильберта. Робинс опубликовал алгоритм унификации и правило резолюции. Последнее означает, что доказательство описывается формулами, соединенными по правилу дизъюнкции(ЛОГИЧЕСКОЕ ИЛИ). Надо свести все записи к этой операции. Когда это получится, то набор соединенных т.о. литер называется предложением. А вот предложения соединяются ЛОГИЧЕСКИМ И. Алгоритм Унификации - это обход всех предложений, т.е. построение дерева решений - поиска соответствий. Вот и все! Начались первые попытки построения машин, "мыслящих логически". Все эти попытки не увенчались успехом. Только спустя шесть с лишним лет в Университете Марсель-Экс(Франция) профессор Колмероэ и его группа пишут программу на Фортране, которая "мыслит логически", т.е. доказывает или опровергает что-то. К программе приложили интерпретатор Ковальского(небольшой PARSING, используя DC-грамматику - definite clause grammar, разновидность Контекстно-Свободной Грамматики) и назвали это чудо Programmation en Logique, кратко Prolog.

hello :- printstring("HELLO WORLD!!!!").
   printstring([]).
   printstring([H|T]) :- put(H), printstring(T).

Напоминает РБНФ форму записи грамматик. Как видите никаких типов. Данные интерпретируются так, как нужно программе. Почему-то считается некоторыми, что Пролог произошел от Форта(forth). Но последний является детищем Charles H. Moore, появившемся в 1960 г(по некоторым данным 1970). Классический процедурный язык предназначенный для работы с массивами, строками, стеками и т.д. Функциональные операторы языка представляют собой знаки препинания(точка, двоеточие и т.д.). Транслятор переводил программу в байтовый код, а внешний интерпретатор выполнял этот код. Язык очень маленький, программы компактные и трудно читаемые.

: hello
        begin
          true
        while
          ." Hello World "
        repeat
;
hello

Выглядит просто, т.к. "ТОЧЕЧКА" одна, а если приводится сочетание нескольких операторов, то прощай крыша. Вот, например, прога реализующая код MD5:

        \ arcfour implementation
        needs core-ext

        decimal
        : carray ( n "name" -- )
          create  chars allot
          does> ( n -- addr )  swap chars + ;

        256 carray S

        0 value j
        : j+! ( n -- )  j +  255 and  to j ;
        : init-S ( -- )  256 0 do  i dup S c!  loop ;
        : mix-S ( K-addr length -- )
          256 0 do
        \ j=j+S[i]+K[i modulo length]
            2dup  i swap mod chars +  c@
            i S c@  +  j+!
        \ swap S[i] and S[j]
            j S c@  i S c@  j S c!  i S c!
          loop  2drop ;

        : arcfourkey ( K-addr length -- )
          init-S  0 to j  mix-S ;
        : arcfour ( M-addr length -- )
          0 to j
          swap 1- swap
          1+ 1 do
        \ j=j+S[i]
            i S c@ j+!
        \ swap S[i] and S[j]
            j S c@  i S c@  2dup  j S c!  i S c!
        \ xor M[i-1] with S[j]+S[i]
            + 255 and S  c@  over i chars +  dup
            c@ rot xor  swap c!
          loop  drop ;

        create MD5digest 16 chars allot
        : MD5 ( c-addr u -- c-addr2 16 )
          MD5digest >abs  2swap  swap >abs
          EncDigestMD5  2drop ( * fix * )
          MD5digest 16

Си по сравнению с этим - просто самый читабельный язык! Моя любимая строчка из проги:

      j S c@  i S c@  2dup  j S c!  i S c!

Заранее прошу прощения у поклонников ФОРТА, я уверен и к этому синтаксису можно привыкнуть. Для меня синтаксис языка в его оценке стоит далеко не на первом месте. Увлекся. Вернемся к задаче "О ВЫЧИСЛИТЕЛЕ".

Второй способ: Некий Черч, тоже математик, предложил компромисс. Не заниматься доказательством утверждений, а опуститься чуть пониже. Он предложил использовать математическое понятие функции. Т.е. существует некое множество данных, и существует преобразование, которое переводит это множество в другое. Если плясать не от аргументов, то функция - это зависимость между множеством одних данных и множеством других данных. Гы! Слышали бы меня сейчас мои бывшие препады. Они бы все мои четверки и пятерки сожгли бы в адском пламени. Это решение(не про оценки!) можно легко пронаблюдать на языках функционального программирования: LISP и производные от ML. Также нет типа, нет операторов присваивания, указателей и прочее. Типизация данных происходит на этапе попадания их в функтор, т.е. функцию. Это называется, по-моему, "лямбда-исчисление". По смыслу напоминает ООП, где вроде бы класс - это структура структурой, но данные без методов не воспринимаются. Правда тот же LISP поощрял процедурное программирование в себе. Поэтому классическим функциональным языком считается ML и его разновидности.

     val it = () : unit
         - print("hello world!\n");
           hello world!
     val it = () : unit

Преимуществом перед логическим программированием было "понимание" арифметики. Т.е. в случае с "чистым" логическим программированием, оное предусматривало булевский подход к операции с числами. Т.е извольте определить, что такое число и как его складывать и сравнивать. Правда, позже для удобства придумали стандартные арифметические предикаты, как и предикаты ввода/вывода и импорта/экспорта. Несомненным преимуществом данных языков является отсутствие ветвлений и итераций. Было "всего лишь" рекурсивное описание возможных вариантов развития программы("ход мыслей"). Именно тут впервые были ощутимы победы в "ДИНАМИЧЕСКОМ ПРОГРАММИРОВАНИИ", т.е. замены рекурсии итерацией. И хотя казалось, что программа на Прологе или ЛИСПе работает рекурсивно, на самом деле транслятор старался заменить рекурсии итерацией. Выигрыш в скорости был ощутимый.

И, наконец, третий способ построения ВЫЧИСЛИТЕЛЯ: Как я уже упоминал и как вы все уже знаете, Алан Тьюринг придумывает следующую модель ВЫЧИСЛИТЕЛЯ, которая ложится в основу архитектуры Фон Неймана. Есть внешняя память и процессор. Последний имеет локальную память, иначе говоря регистры. Чтобы выполнить операцию, процессор закачивает данные из памяти в регистры, осуществляет с ними вычисления и отправляет результат обратно в память. Т.е. так называемый принцип "обновляемой памяти".

Из-за того что есть регистры и операции над ними вытекает типизация. Из-за многочисленных обменов данными появляется операция присваивания, которая может выполняться многократно по одному и тому же адресу и с разными значениями. Получается некоторая зависимость от архитектуры процессора. Появляется необходимость в указателях. Вот мы и получили полный набор возможностей, делающих программирование небезопасным. Т.е. не выявление ошибок на этапе компиляции или динамически. Эта модель, кстати, формирует определенную "стилистику" первых программ на процедурных языках. Я бы ее назвал РЕКУРСИВНОЙ ДЕКОМПОЗИЦИЕЙ. Мы все уже знаем, что модуль - есть "глобальная" декомпозиция программы. Но внутри модуля тоже есть декомпозиция - это процедурная декомпозиция. Дело в том, что ранее код напоминал поток. Он лился вниз экрана водопадом символов. Это также способствует ошибкам. Поэтому листинг разбивается на повторяемые части - подпрограммы. Кстати, именно этот метод декомпозиции появился первым, а уж за ним более верхний - модульность, когда процедурки выносились в отдельные файлы. Это сильно уменьшало код, время компиляции и количество ошибок. Но программерам часто приходилось писать вспомогательные и локальные процедуры, а это тоже своего рода декомпозиция. Если приглядеться, то становиться четко видно, что процедурка - это маленькая машина Тьюринга. "Регистрами" в ней служат локальные переменные, а "внешней памятью" - выходные параметры и глобальные переменные. Да, не всегда удавалось обходиться лишь параметрами, т.к. это осложняло типизацию. Поэтому частенько появлялись глобальные переменные. Иногда они заводились в качестве экономии памяти. Заводится такой большой массив байт, а в разных процедурках к нему обращаются, то как к массиву целых, то как к строке. И чем "ценнее" была переменная, тем большая область видимости ей была нужна. Поэтому она становилась все глобальней и глобальней. Данные в проге протекали от самого низу(локальных процедур) к самому верху - результирующим глобальным переменным. Отсюда между прочим и "секционность" процедурных языков, пришедшая еще с Алгола-58. Выделялась некая секция, где можно было бы описывать переменные и далее шел код. Есть даже правило. В интерфейсной части программы описываешь типы, а потом переменные этих типов. Потом опять другая группа типов и их переменные. Раньше по другому было нельзя - не было ООП. Раньше было много "войн", например, война с goto-шниками. В некоторых языках специально убирали этот оператор. Суть споров состояла в том, что goto-шники твердили об оптимальности кода при наличии "экономных" переходов. А их оппоненты отстаивали "кристальную" декомпозицию. Goto нарушал мерное течение листинга и заставлял взгляд метаться по экрану.

Ну самую главную проблему тогдашнего программирования - ДЕКОМПОЗИЦИЮ - Вы уяснили. Другая "радость" свалившаяся на головы тогдашних программеров - это оптимизация. Этим болели все. Это было нужно. Временно, тогда требования к задачам опередили развитие аппаратных средств. Произошло это в основном потому, что языки высокого уровня не могли работать в "жалких" условиях, когда у тебя есть от силы 1К. Но коммерческий дух уже захватил компьютерщиков. Запомните ЭТОТ МОМЕНТ, мы к нему еще вернемся. Начинается ПОПУЛЯРИЗАЦИЯ программирования. Появляются Бейсики, Паскали и т.д. и т.п. Все это создается, чтобы "работать на дому". Вот они грабли, на которые мы наступаем опять уже на закате века. Язык сам по себе не представляет ЗАКОНЧЕНОЙ СИСТЕМЫ и не может обеспечить качества программы. Это инструмент чьего-то производства и без человека он ничто. И вот человек, который до этого не разу не програмил, садится за Васик, берет книжку "Программирование за 3 дня" и лабает код. Сможет он построить эффективный код? Нет! А таких людей больше и кода такого они наструляют ОГОГО! Дальше сами фантазируйте, но заканчивается все вопросами: "А почему не запускается?", "А почему повисло?", "А почему медленно?", "А кто виноват?". Человек частенько ища ответ сваливает ответственность с себя, единственной причины собственных неурядиц. Книжка прочитана, прога написана - почему медленно работает? Медленный компьютер. Я немного утрирую, но думаю вы догадываетесь о чем я.

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

"САМАЯ СТРАШНАЯ" ошибка в такой ситуации - это то, что указатель на память указывает на область, где помимо всего прочего есть другой указатель. И при "затирании" памяти иным значением этот "другой" указатель теряется. Это проблема "мусора".

   var
     PFirst : PMyType;
     //...
     PSecond : PMyType;
   begin
      // инициализация PFirst и PSecond
      // ...
      PFirst := PSecond; // атэросклероз 
   end; 

Операцию присваивания ведь не запретишь. Хотя некоторые так и делали, запрещая все операции над указателями. НО программеры все равно находили дыры - работали через ЦЕЛОЕ(int, Integer) а потом конвертировали его в указатель. Есть два решения:
  • 1) автоматический сборщик мусора
  • 2) "безопасный" указатель
Но последнее требует либо переопределения операторов, либо шаблонов. Поэтому остановились на первом. Благо это никак не зависит от юзера. Правда, программы становиться труднее понимать. Например, в предыдущем примере компилятор не ругнется, а при выполнении освободит память, на которую указывал PFirst.

Другая сторона этой же проблемы - это два указателя на одну и ту же переменную. Случается, что мы в программе освобождаем память через один из этих указателей. Но второй-то указатель об этом не знает. Да и мы о нем подзабыли. Вот тогда и начинается самое интересное! Через этот второй указатель можно попасть в ТАКИЕ места, о существовании которых вы и не подозревали! А если вы начнете туда писать...

Есть еще одна проблема безопасности, непосредственно касающаяся оптимизации - это проблема контроля типов. Т.е. когда компилятор разрешает доступ к области памяти по разным правилам, как я говорил три абзаца назад. Т.е., например, ЦЕЛОЕ можно интерпретировать, как массив байт. Или указатель на строку можно преобразовать в указатель на QUAD WORD. Вы спросите, зачем это нужно? Например, для оптимизированной пересылки. Копирование массива из 4 байт, можно представить как:

      mov eax, dword ptr [src_array_bytes]
      mov dword ptr [dst_array_byte], eax

Это будет быстрее, чем организация счетчика(загрузка еще одного регистра) и условный переход. Ах да, вы можете сказать, что при частоте дешевого процессора в 300МГц и выше эта разница будет не заметна для человека. И я скажу, ДА вы правы! Более того вы можете смело уменьшить частоту до 100МГц и эта разница не будет заметна. Но я привел простой пример. Надуманный пример. НО тогда, на тех машинах, это было бы хорошей оптимизацией. Это считалось бы "правильным" стилем. Но страшнее всего в этой ситуации, что тогда не было ООП и "событийного" вызова методов. Программа "текла" относительно линейно, поэтому нам самим нужно было бы отлавливать вышеупомянутые "затирания" памяти с указателем, чтобы не потерять данные.

Сейчас такими оптимизациями уже никто не занимается. Данные компилятором(как С++ так и Паскаль) выравниваются(ALIGN) по границе слов, двойных или четверных слов. Как поставите. И sizeof(type) равный 1 говорит вам неправду. Ну, т.е. он, конечно, с точки зрения RTTI не врет, но на самом деле границы статических глобальных и локальных переменных давно выравниваются. И эта ЕДИНИЦА может оказаться единицей, но чаще она оказывается четверкой или реже восьмеркой. Случай же с динамическими переменными - отдельная песня. Там все зависит от внутреннего менеджера кучи и менеджера памяти ОС. Так, например, выньДоУс работает с 4К страницами, если процессор АЛЬФА, и 2К если другой камень. Борландовский менеджер кучи это естественно знает и у него на это свой CHUNK_SIZE стоит ;-). Но к этому мы еще вернемся, потому что это тоже касается безопасности программы.

Вернемся к памяти фон Неймановских машин, построенных по принципу Тьюринга. Давайте на время представим, что в 30х годах 20го века стали доступны все три решения, о которых я говорил выше. Повторила бы история свой заковыристый путь? Я думаю, что да. Но это только мое мнение. Вот мой рассуждизм на этот утопический фантазм.

Преимущества логического и функционального программирования состоят в удобстве описания архитектурно-независимых решений задач. Правда принцип таких языков не так уж НЕЗАВИСИМ. Он несет в себе отпечаток той экспертной области, из которой он вышел. Логика предикатов первого порядка хороша для экспертных систем, искусственного интеллекта и смежных областей. Не так уж много. Лямбда- -исчисление хорошо для задач работы со списками, деревьями и прочая. Оба принципа программирования легко понимаются и программы на таких языках сравнительно легко читаются. Еще одно преимущество - возможность самим экспертам писать программы, не вникая в особенности архитектуры компьютера и ОС. У нас в САМАРЕ есть школа, где информатику преподавали на ПРОЛОГЕ. Не завидую школьникам! Мне приносили их задачки, чтоб я написал их. Ну проги, конечно, простые, но это для меня. Я писал на ПРОЛОГЕ в институте и программировал на других языках. Короче, у того препада крыша поехала. Хоть профессора и утверждают, что так думает человек (попытка микросхемы понять закон ОМА), логика предикатов и лямбда-исчисление не понимается с лету. Прочитав определение поэзии, еще никто не начинал сразу же писать стихи. Нужен какой-то опыт разработки, свой подход к рассмотрению области. А если вы смотрите на задачку не так как "ЛОГИКА ПРЕДИКАТОВ", то вы потерянный для Пролога человек - вы думаете не по научному. У вас НЕЧЕЛОВЕЧЕСКОЕ мышление, потому что человек думает предикатами (доказано профессурой).

Идиотский, конечно, подход. Я не предлагаю запретить такие языки или сказать, что они умирающие. Нет. Никто не может решить или вынести вердикт о языке, даже его автор. После того как язык выходит из-под пера автора - он начинает жить своей жизнью, наживая себе поклонников и врагов. Я, например, ненавижу попсу(в смысле музыка). Ну и что. Мало ли, что вам не нравятся бразильские сериалы, зато тех кому они нравятся значительно больше. Когда-то, между прочим, большинство было уверено, что ЗЕМЛЯ плоская.

К чему это я? А к тому, что все в этой вселенной(даже ее законы) не могут быть рассмотрены отдельно от человека. И языки программирования тому не исключение. Логические и функциональные языки имеют заметное преимущество перед процедурными языками в плане безопасности разработки. Я имею ввиду:
  • 1) Отсутствие типов
  • 2) Нет операций присваивания
  • 3) Из (1) следует, что нет указателей и массивов. Т.е. нет ошибок доступа, мусора и нарушения границ.
  • 4) Большая способность к портированию. Именно из-за 1,2,3 и из-за встроенных обьектов, использующих особенности API ОС.
Но эти же преимущества оборачиваются для них недостатками. Существуют целые классы задач (CAD,игры), которые не решаются достаточно эффективно этими языками. Вообще-то самое интересное в этих языках - это то, что они создавались не как другие языки, т.е. для решения какой-то задачи. Эти языки похожи на фундаментальное исследование программирования. Они появились просто как возможность. К примеру Пролог не вызвал интереса до середины 80-х, когда Япония заявила о начале крупномасштабных исследований по построению компьютера, работающего на основе логики предикатов. Ходили слухи, что они даже начали тестировать ОС написанную на Прологе. Но эта попытка была неудачной, хоть и метила не на замену ПК, а на создание специального компьютера для Экспертных Систем и Искусственного Интеллекта.

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

Ну что ж, вроде как с истоками "опасного" программирования мы ознакомились. Теперь, зная что безопасных процедурных языков не бывает, вернемся к началу 70-ых. Гм, вдогонку. Конечно, можно создать язык без указателей и без работы с кучей. Но таким языкам отведена узкая область. Действительно, есть такие языки и, на примере Явы, мы можем наблюдать "судьбу" таких языков. Нельзя сказать, что они умерли. Но остаются и другие области применения программирования. Тогда, в начале 70-х, для языков, подобных Яве, была только одна сфера - запросы к БД. Это почетное место тут же занял очередное детище IBM - SQL. Они впервые использовали его в System R. Надо понять тогдашнее состояние девелоперов. API как таковых не было - прямой недостаток изобилия ОС и архитектур компьютеров. Хочется отметить, что в 1968 году выходит последний стандарт самого плодовитого языка АЛГОЛ. В Алголе-68 впервые применена идея перегрузки операторов. Но стандарт не пошел. Даже в Старом Свете, где был популярен Алгол-60, не обратили особого внимания на появление сего стандарта - там смело утвердились и закрепили свои позиции разные переделки АЛГОЛ-60. Я могу понять тогдашних разработчиков - сама по себе идея перегрузки операторов НИЧТО без принципов ООП.

Продолжение...

Дмитрий Логинов
Специально для Королевства Delphi




Смотрите также материалы по темам:
[Средства разработки. Языки программирования.]

 Обсуждение материала нет сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования